chore: Move to Typescript (#2783)

This PR moves the entire project to Typescript. Due to the ~1000 ignores this will lead to a messy codebase for a while, but the churn is worth it – all of those ignore comments are places that were never type-safe previously.

closes #1282
This commit is contained in:
Tom Moor
2021-11-29 06:40:55 -08:00
committed by GitHub
parent 25ccfb5d04
commit 15b1069bcc
1017 changed files with 17410 additions and 54942 deletions

View File

@@ -1,34 +1,33 @@
// @flow
import { observer } from "mobx-react";
import { TeamIcon } from "outline-icons";
import * as React from "react";
import { useRef, useState } from "react";
import * as React from "react";
import { useTranslation, Trans } from "react-i18next";
import styled from "styled-components";
import Button from "components/Button";
import Flex from "components/Flex";
import Heading from "components/Heading";
import HelpText from "components/HelpText";
import Input, { LabelText } from "components/Input";
import Scene from "components/Scene";
import Button from "~/components/Button";
import Flex from "~/components/Flex";
import Heading from "~/components/Heading";
import HelpText from "~/components/HelpText";
import Input, { LabelText } from "~/components/Input";
import Scene from "~/components/Scene";
import env from "~/env";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import useStores from "~/hooks/useStores";
import useToasts from "~/hooks/useToasts";
import ImageUpload from "./components/ImageUpload";
import env from "env";
import useCurrentTeam from "hooks/useCurrentTeam";
import useStores from "hooks/useStores";
import useToasts from "hooks/useToasts";
function Details() {
const { auth } = useStores();
const { showToast } = useToasts();
const team = useCurrentTeam();
const { t } = useTranslation();
const form = useRef<?HTMLFormElement>();
const form = useRef<HTMLFormElement>(null);
const [name, setName] = useState(team.name);
const [subdomain, setSubdomain] = useState(team.subdomain);
const [avatarUrl, setAvatarUrl] = useState();
const [avatarUrl, setAvatarUrl] = useState<string>();
const handleSubmit = React.useCallback(
async (event: ?SyntheticEvent<>) => {
async (event?: React.SyntheticEvent) => {
if (event) {
event.preventDefault();
}
@@ -39,20 +38,27 @@ function Details() {
avatarUrl,
subdomain,
});
showToast(t("Settings saved"), { type: "success" });
showToast(t("Settings saved"), {
type: "success",
});
} catch (err) {
showToast(err.message, { type: "error" });
showToast(err.message, {
type: "error",
});
}
},
[auth, showToast, name, avatarUrl, subdomain, t]
);
const handleNameChange = React.useCallback((ev: SyntheticInputEvent<*>) => {
setName(ev.target.value);
}, []);
const handleNameChange = React.useCallback(
(ev: React.ChangeEvent<HTMLInputElement>) => {
setName(ev.target.value);
},
[]
);
const handleSubdomainChange = React.useCallback(
(ev: SyntheticInputEvent<*>) => {
(ev: React.ChangeEvent<HTMLInputElement>) => {
setSubdomain(ev.target.value.toLowerCase());
},
[]
@@ -67,14 +73,13 @@ function Details() {
);
const handleAvatarError = React.useCallback(
(error: ?string) => {
(error: string | null | undefined) => {
showToast(error || t("Unable to upload new logo"));
},
[showToast, t]
);
const isValid = form.current && form.current.checkValidity();
return (
<Scene title={t("Details")} icon={<TeamIcon color="currentColor" />}>
<Heading>{t("Details")}</Heading>

View File

@@ -1,17 +1,16 @@
// @flow
import { debounce } from "lodash";
import { observer } from "mobx-react";
import { BeakerIcon } from "outline-icons";
import * as React from "react";
import { useState } from "react";
import * as React from "react";
import { useTranslation, Trans } from "react-i18next";
import Checkbox from "components/Checkbox";
import Heading from "components/Heading";
import HelpText from "components/HelpText";
import Scene from "components/Scene";
import useCurrentTeam from "hooks/useCurrentTeam";
import useStores from "hooks/useStores";
import useToasts from "hooks/useToasts";
import Checkbox from "~/components/Checkbox";
import Heading from "~/components/Heading";
import HelpText from "~/components/HelpText";
import Scene from "~/components/Scene";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import useStores from "~/hooks/useStores";
import useToasts from "~/hooks/useToasts";
function Features() {
const { auth } = useStores();
@@ -24,18 +23,19 @@ function Features() {
const showSuccessMessage = React.useCallback(
debounce(() => {
showToast(t("Settings saved"), { type: "success" });
showToast(t("Settings saved"), {
type: "success",
});
}, 250),
[t, showToast]
);
const handleChange = React.useCallback(
async (ev: SyntheticInputEvent<*>) => {
async (ev: React.ChangeEvent<HTMLInputElement>) => {
const newData = { ...data, [ev.target.name]: ev.target.checked };
setData(newData);
await auth.updateTeam(newData);
showSuccessMessage();
},
[auth, data, showSuccessMessage]

View File

@@ -1,23 +1,22 @@
// @flow
import { observer } from "mobx-react";
import { PlusIcon, GroupIcon } from "outline-icons";
import * as React from "react";
import { useTranslation, Trans } from "react-i18next";
import GroupNew from "scenes/GroupNew";
import { Action } from "components/Actions";
import Button from "components/Button";
import Empty from "components/Empty";
import GroupListItem from "components/GroupListItem";
import Heading from "components/Heading";
import HelpText from "components/HelpText";
import Modal from "components/Modal";
import PaginatedList from "components/PaginatedList";
import Scene from "components/Scene";
import Subheading from "components/Subheading";
import useBoolean from "hooks/useBoolean";
import useCurrentTeam from "hooks/useCurrentTeam";
import useStores from "hooks/useStores";
import GroupMenu from "menus/GroupMenu";
import GroupNew from "~/scenes/GroupNew";
import { Action } from "~/components/Actions";
import Button from "~/components/Button";
import Empty from "~/components/Empty";
import GroupListItem from "~/components/GroupListItem";
import Heading from "~/components/Heading";
import HelpText from "~/components/HelpText";
import Modal from "~/components/Modal";
import PaginatedList from "~/components/PaginatedList";
import Scene from "~/components/Scene";
import Subheading from "~/components/Subheading";
import useBoolean from "~/hooks/useBoolean";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import useStores from "~/hooks/useStores";
import GroupMenu from "~/menus/GroupMenu";
function Groups() {
const { t } = useTranslation();

View File

@@ -1,4 +1,3 @@
// @flow
import invariant from "invariant";
import { observer } from "mobx-react";
import { CollectionIcon, DocumentIcon } from "outline-icons";
@@ -6,65 +5,64 @@ import * as React from "react";
import { useTranslation, Trans } from "react-i18next";
import { VisuallyHidden } from "reakit/VisuallyHidden";
import styled from "styled-components";
import { parseOutlineExport } from "shared/utils/zip";
import FileOperation from "models/FileOperation";
import Button from "components/Button";
import Heading from "components/Heading";
import HelpText from "components/HelpText";
import Notice from "components/Notice";
import PaginatedList from "components/PaginatedList";
import Scene from "components/Scene";
import Subheading from "components/Subheading";
import { parseOutlineExport, Item } from "@shared/utils/zip";
import FileOperation from "~/models/FileOperation";
import Button from "~/components/Button";
import Heading from "~/components/Heading";
import HelpText from "~/components/HelpText";
import Notice from "~/components/Notice";
import PaginatedList from "~/components/PaginatedList";
import Scene from "~/components/Scene";
import Subheading from "~/components/Subheading";
import useCurrentUser from "~/hooks/useCurrentUser";
import useStores from "~/hooks/useStores";
import useToasts from "~/hooks/useToasts";
import getDataTransferFiles from "~/utils/getDataTransferFiles";
import { uploadFile } from "~/utils/uploadFile";
import FileOperationListItem from "./components/FileOperationListItem";
import useCurrentUser from "hooks/useCurrentUser";
import useStores from "hooks/useStores";
import useToasts from "hooks/useToasts";
import getDataTransferFiles from "utils/getDataTransferFiles";
import { uploadFile } from "utils/uploadFile";
function ImportExport() {
const { t } = useTranslation();
const user = useCurrentUser();
const fileRef = React.useRef();
const fileRef = React.useRef<HTMLInputElement>(null);
const { fileOperations, collections } = useStores();
const { showToast } = useToasts();
const [isLoading, setLoading] = React.useState(false);
const [isImporting, setImporting] = React.useState(false);
const [isImported, setImported] = React.useState(false);
const [isExporting, setExporting] = React.useState(false);
const [file, setFile] = React.useState();
const [importDetails, setImportDetails] = React.useState();
const [file, setFile] = React.useState<File>();
const [importDetails, setImportDetails] = React.useState<
Item[] | undefined
>();
const handleImport = React.useCallback(
async (ev) => {
setImported(undefined);
setImporting(true);
const handleImport = React.useCallback(async () => {
setImported(false);
setImporting(true);
try {
invariant(file, "File must exist to upload");
const attachment = await uploadFile(file, {
name: file.name,
});
await collections.import(attachment.id);
showToast(t("Import started"));
setImported(true);
} catch (err) {
showToast(err.message);
} finally {
if (fileRef.current) {
fileRef.current.value = "";
}
setImporting(false);
setFile(undefined);
setImportDetails(undefined);
try {
invariant(file, "File must exist to upload");
const attachment = await uploadFile(file, {
name: file.name,
});
await collections.import(attachment.id);
showToast(t("Import started"));
setImported(true);
} catch (err) {
showToast(err.message);
} finally {
if (fileRef.current) {
fileRef.current.value = "";
}
},
[t, file, collections, showToast]
);
setImporting(false);
setFile(undefined);
setImportDetails(undefined);
}
}, [t, file, collections, showToast]);
const handleFilePicked = React.useCallback(async (ev) => {
ev.preventDefault();
const files = getDataTransferFiles(ev);
const file = files[0];
setFile(file);
@@ -88,7 +86,7 @@ function ImportExport() {
);
const handleExport = React.useCallback(
async (ev: SyntheticEvent<>) => {
async (ev: React.SyntheticEvent) => {
ev.preventDefault();
setLoading(true);
@@ -109,7 +107,9 @@ function ImportExport() {
await fileOperations.delete(fileOperation);
showToast(t("Export deleted"));
} catch (err) {
showToast(err.message, { type: "error" });
showToast(err.message, {
type: "error",
});
}
},
[fileOperations, showToast, t]
@@ -156,8 +156,12 @@ function ImportExport() {
<ImportPreview>
<Trans
defaults="Sorry, the file <em>{{ fileName }}</em> is missing valid collections or documents."
values={{ fileName: file.name }}
components={{ em: <strong /> }}
values={{
fileName: file.name,
}}
components={{
em: <strong />,
}}
/>
</ImportPreview>
)}
@@ -166,8 +170,12 @@ function ImportExport() {
<ImportPreview as="div">
<Trans
defaults="<em>{{ fileName }}</em> looks good, the following collections and their documents will be imported:"
values={{ fileName: file.name }}
components={{ em: <strong /> }}
values={{
fileName: file.name,
}}
components={{
em: <strong />,
}}
/>
<List>
{importDetails
@@ -198,8 +206,12 @@ function ImportExport() {
<HelpText>
<Trans
defaults="A full export might take some time, consider exporting a single document or collection. The exported data is a zip of your documents in Markdown format. You may leave this page once the export has started we will email a link to <em>{{ userEmail }}</em> when its complete."
values={{ userEmail: user.email }}
components={{ em: <strong /> }}
values={{
userEmail: user.email,
}}
components={{
em: <strong />,
}}
/>
</HelpText>
<Button
@@ -219,7 +231,9 @@ function ImportExport() {
<PaginatedList
items={fileOperations.orderedDataExports}
fetch={fileOperations.fetchPage}
options={{ type: "export" }}
options={{
type: "export",
}}
heading={
<Subheading>
<Trans>Recent exports</Trans>

View File

@@ -1,21 +1,20 @@
// @flow
import { debounce } from "lodash";
import { observer } from "mobx-react";
import { EmailIcon } from "outline-icons";
import * as React from "react";
import { useTranslation, Trans } from "react-i18next";
import styled from "styled-components";
import Heading from "components/Heading";
import HelpText from "components/HelpText";
import Input from "components/Input";
import Notice from "components/Notice";
import Scene from "components/Scene";
import Subheading from "components/Subheading";
import Heading from "~/components/Heading";
import HelpText from "~/components/HelpText";
import Input from "~/components/Input";
import Notice from "~/components/Notice";
import Scene from "~/components/Scene";
import Subheading from "~/components/Subheading";
import env from "~/env";
import useCurrentUser from "~/hooks/useCurrentUser";
import useStores from "~/hooks/useStores";
import useToasts from "~/hooks/useToasts";
import NotificationListItem from "./components/NotificationListItem";
import env from "env";
import useCurrentUser from "hooks/useCurrentUser";
import useStores from "hooks/useStores";
import useToasts from "hooks/useToasts";
function Notifications() {
const { notificationSettings } = useStores();
@@ -63,15 +62,17 @@ function Notifications() {
];
React.useEffect(() => {
notificationSettings.fetchPage();
notificationSettings.fetchPage({});
}, [notificationSettings]);
const showSuccessMessage = debounce(() => {
showToast(t("Notifications saved"), { type: "success" });
showToast(t("Notifications saved"), {
type: "success",
});
}, 500);
const handleChange = React.useCallback(
async (ev: SyntheticInputEvent<>) => {
async (ev: React.ChangeEvent<HTMLInputElement>) => {
const setting = notificationSettings.getByEvent(ev.target.name);
if (ev.target.checked) {
@@ -86,7 +87,6 @@ function Notifications() {
},
[notificationSettings, showSuccessMessage]
);
const showSuccessNotice = window.location.search === "?success";
return (
@@ -120,7 +120,7 @@ function Notifications() {
<Subheading>{t("Notifications")}</Subheading>
{options.map((option, index) => {
if (option.separator) {
if (option.separator || !option.event) {
return <Separator key={`separator-${index}`} />;
}

View File

@@ -1,4 +1,3 @@
// @flow
import { sortBy } from "lodash";
import { observer } from "mobx-react";
import { PlusIcon, UserIcon } from "outline-icons";
@@ -6,24 +5,25 @@ import * as React from "react";
import { Trans, useTranslation } from "react-i18next";
import { useHistory, useLocation } from "react-router-dom";
import scrollIntoView from "smooth-scroll-into-view-if-needed";
import { PAGINATION_SYMBOL } from "stores/BaseStore";
import Invite from "scenes/Invite";
import { Action } from "components/Actions";
import Button from "components/Button";
import Flex from "components/Flex";
import Heading from "components/Heading";
import HelpText from "components/HelpText";
import InputSearch from "components/InputSearch";
import Modal from "components/Modal";
import Scene from "components/Scene";
import { PAGINATION_SYMBOL } from "~/stores/BaseStore";
import User from "~/models/User";
import Invite from "~/scenes/Invite";
import { Action } from "~/components/Actions";
import Button from "~/components/Button";
import Flex from "~/components/Flex";
import Heading from "~/components/Heading";
import HelpText from "~/components/HelpText";
import InputSearch from "~/components/InputSearch";
import Modal from "~/components/Modal";
import Scene from "~/components/Scene";
import useBoolean from "~/hooks/useBoolean";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import useQuery from "~/hooks/useQuery";
import useStores from "~/hooks/useStores";
import PeopleTable from "./components/PeopleTable";
import UserStatusFilter from "./components/UserStatusFilter";
import useBoolean from "hooks/useBoolean";
import useCurrentTeam from "hooks/useCurrentTeam";
import useQuery from "hooks/useQuery";
import useStores from "hooks/useStores";
function People(props) {
function People() {
const topRef = React.useRef();
const location = useLocation();
const history = useHistory();
@@ -37,15 +37,17 @@ function People(props) {
const { t } = useTranslation();
const params = useQuery();
const [isLoading, setIsLoading] = React.useState(false);
const [data, setData] = React.useState([]);
const [data, setData] = React.useState<User[]>([]);
const [totalPages, setTotalPages] = React.useState(0);
const [userIds, setUserIds] = React.useState([]);
const [userIds, setUserIds] = React.useState<string[]>([]);
const can = policies.abilities(team.id);
const query = params.get("query") || "";
const filter = params.get("filter") || "";
const sort = params.get("sort") || "name";
const direction = (params.get("direction") || "asc").toUpperCase();
const page = parseInt(params.get("page") || 0, 10);
const direction =
params.get("direction")?.toUpperCase() === "ASC" ? "ASC" : "DESC";
const page = parseInt(params.get("page") || "0", 10);
const limit = 25;
React.useEffect(() => {
@@ -61,9 +63,8 @@ function People(props) {
query,
filter,
});
setTotalPages(Math.ceil(response[PAGINATION_SYMBOL].total / limit));
setUserIds(response.map((u) => u.id));
setUserIds(response.map((u: User) => u.id));
} finally {
setIsLoading(false);
}
@@ -74,6 +75,7 @@ function People(props) {
React.useEffect(() => {
let filtered = users.orderedData;
if (!filter) {
filtered = users.active.filter((u) => userIds.includes(u.id));
} else if (filter === "all") {
@@ -109,6 +111,7 @@ function People(props) {
} else {
params.delete("filter");
}
history.replace({
pathname: location.pathname,
search: params.toString(),
@@ -120,12 +123,14 @@ function People(props) {
const handleSearch = React.useCallback(
(event) => {
const { value } = event.target;
if (value) {
params.set("query", event.target.value);
params.delete("page");
} else {
params.delete("query");
}
history.replace({
pathname: location.pathname,
search: params.toString(),
@@ -141,11 +146,13 @@ function People(props) {
} else {
params.delete("sort");
}
if (direction === "DESC") {
params.set("direction", direction.toLowerCase());
} else {
params.delete("direction");
}
history.replace({
pathname: location.pathname,
search: params.toString(),
@@ -161,6 +168,7 @@ function People(props) {
} else {
params.delete("page");
}
history.replace({
pathname: location.pathname,
search: params.toString(),
@@ -169,7 +177,7 @@ function People(props) {
if (topRef.current) {
scrollIntoView(topRef.current, {
scrollMode: "if-needed",
behavior: "instant",
behavior: "auto",
block: "start",
});
}

View File

@@ -1,52 +1,49 @@
// @flow
import { observer } from "mobx-react";
import { ProfileIcon } from "outline-icons";
import * as React from "react";
import { Trans, useTranslation } from "react-i18next";
import styled from "styled-components";
import { languageOptions } from "shared/i18n";
import UserDelete from "scenes/UserDelete";
import Button from "components/Button";
import Flex from "components/Flex";
import Heading from "components/Heading";
import HelpText from "components/HelpText";
import Input, { LabelText } from "components/Input";
import InputSelect from "components/InputSelect";
import Scene from "components/Scene";
import { languageOptions } from "@shared/i18n";
import UserDelete from "~/scenes/UserDelete";
import Button from "~/components/Button";
import Flex from "~/components/Flex";
import Heading from "~/components/Heading";
import HelpText from "~/components/HelpText";
import Input, { LabelText } from "~/components/Input";
import InputSelect from "~/components/InputSelect";
import Scene from "~/components/Scene";
import useStores from "~/hooks/useStores";
import useToasts from "~/hooks/useToasts";
import ImageUpload from "./components/ImageUpload";
import useStores from "hooks/useStores";
import useToasts from "hooks/useToasts";
const Profile = () => {
const { auth } = useStores();
const form = React.useRef<?HTMLFormElement>();
const form = React.useRef<HTMLFormElement>(null);
const [name, setName] = React.useState<string>(auth.user?.name || "");
const [avatarUrl, setAvatarUrl] = React.useState<?string>();
const [avatarUrl, setAvatarUrl] = React.useState<string | null | undefined>();
const [showDeleteModal, setShowDeleteModal] = React.useState(false);
const [language, setLanguage] = React.useState(auth.user?.language);
const { showToast } = useToasts();
const { t } = useTranslation();
const handleSubmit = async (ev: SyntheticEvent<>) => {
const handleSubmit = async (ev: React.SyntheticEvent) => {
ev.preventDefault();
await auth.updateUser({
name,
avatarUrl,
language,
});
showToast(t("Profile saved"), { type: "success" });
showToast(t("Profile saved"), {
type: "success",
});
};
const handleNameChange = (ev: SyntheticInputEvent<*>) => {
const handleNameChange = (ev: React.ChangeEvent<HTMLInputElement>) => {
setName(ev.target.value);
};
const handleAvatarUpload = async (avatarUrl: string) => {
setAvatarUrl(avatarUrl);
await auth.updateUser({
avatarUrl,
});
@@ -55,7 +52,7 @@ const Profile = () => {
});
};
const handleAvatarError = (error: ?string) => {
const handleAvatarError = (error: string | null | undefined) => {
showToast(error || t("Unable to upload new profile picture"), {
type: "error",
});
@@ -70,7 +67,6 @@ const Profile = () => {
};
const isValid = form.current && form.current.checkValidity();
const { user, isSaving } = auth;
if (!user) return null;

View File

@@ -1,19 +1,18 @@
// @flow
import { debounce } from "lodash";
import { observer } from "mobx-react";
import { PadlockIcon } from "outline-icons";
import * as React from "react";
import { useState } from "react";
import * as React from "react";
import { useTranslation, Trans } from "react-i18next";
import Checkbox from "components/Checkbox";
import Heading from "components/Heading";
import HelpText from "components/HelpText";
import InputSelect from "components/InputSelect";
import Scene from "components/Scene";
import env from "env";
import useCurrentTeam from "hooks/useCurrentTeam";
import useStores from "hooks/useStores";
import useToasts from "hooks/useToasts";
import Checkbox from "~/components/Checkbox";
import Heading from "~/components/Heading";
import HelpText from "~/components/HelpText";
import InputSelect from "~/components/InputSelect";
import Scene from "~/components/Scene";
import env from "~/env";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import useStores from "~/hooks/useStores";
import useToasts from "~/hooks/useToasts";
function Security() {
const { auth } = useStores();
@@ -26,7 +25,6 @@ function Security() {
guestSignin: team.guestSignin,
defaultUserRole: team.defaultUserRole,
});
const notes = {
member: t("New user accounts will be given member permissions by default"),
viewer: t("New user accounts will be given viewer permissions by default"),
@@ -35,18 +33,18 @@ function Security() {
const showSuccessMessage = React.useMemo(
() =>
debounce(() => {
showToast(t("Settings saved"), { type: "success" });
showToast(t("Settings saved"), {
type: "success",
});
}, 250),
[showToast, t]
);
const handleChange = React.useCallback(
async (ev: SyntheticInputEvent<*>) => {
async (ev: React.ChangeEvent<HTMLInputElement>) => {
const newData = { ...data, [ev.target.name]: ev.target.checked };
setData(newData);
await auth.updateTeam(newData);
showSuccessMessage();
},
[auth, data, showSuccessMessage]
@@ -55,9 +53,7 @@ function Security() {
const handleDefaultRoleChange = async (newDefaultRole: string) => {
const newData = { ...data, defaultUserRole: newDefaultRole };
setData(newData);
await auth.updateTeam(newData);
showSuccessMessage();
};
@@ -107,8 +103,14 @@ function Security() {
value={data.defaultUserRole}
label="Default role"
options={[
{ label: t("Member"), value: "member" },
{ label: t("Viewer"), value: "viewer" },
{
label: t("Member"),
value: "member",
},
{
label: t("Viewer"),
value: "viewer",
},
]}
onChange={handleDefaultRoleChange}
ariaLabel={t("Default role")}

View File

@@ -1,18 +1,17 @@
// @flow
import { observer } from "mobx-react";
import { LinkIcon } from "outline-icons";
import * as React from "react";
import { useTranslation, Trans } from "react-i18next";
import { Link } from "react-router-dom";
import Empty from "components/Empty";
import Heading from "components/Heading";
import HelpText from "components/HelpText";
import PaginatedList from "components/PaginatedList";
import Scene from "components/Scene";
import Subheading from "components/Subheading";
import Empty from "~/components/Empty";
import Heading from "~/components/Heading";
import HelpText from "~/components/HelpText";
import PaginatedList from "~/components/PaginatedList";
import Scene from "~/components/Scene";
import Subheading from "~/components/Subheading";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import useStores from "~/hooks/useStores";
import ShareListItem from "./components/ShareListItem";
import useCurrentTeam from "hooks/useCurrentTeam";
import useStores from "hooks/useStores";
function Shares() {
const team = useCurrentTeam();
@@ -38,7 +37,9 @@ function Shares() {
)}{" "}
<Trans
defaults="You can globally enable and disable public document sharing in the <em>security settings</em>."
components={{ em: <Link to="/settings/security" /> }}
components={{
em: <Link to="/settings/security" />,
}}
/>
</HelpText>
)}

View File

@@ -1,24 +1,23 @@
// @flow
import { find } from "lodash";
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation, Trans } from "react-i18next";
import styled from "styled-components";
import Button from "components/Button";
import CollectionIcon from "components/CollectionIcon";
import Heading from "components/Heading";
import HelpText from "components/HelpText";
import List from "components/List";
import ListItem from "components/List/Item";
import Notice from "components/Notice";
import Scene from "components/Scene";
import SlackIcon from "components/SlackIcon";
import Collection from "~/models/Collection";
import Button from "~/components/Button";
import CollectionIcon from "~/components/CollectionIcon";
import Heading from "~/components/Heading";
import HelpText from "~/components/HelpText";
import List from "~/components/List";
import ListItem from "~/components/List/Item";
import Notice from "~/components/Notice";
import Scene from "~/components/Scene";
import SlackIcon from "~/components/SlackIcon";
import env from "~/env";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import useQuery from "~/hooks/useQuery";
import useStores from "~/hooks/useStores";
import SlackButton from "./components/SlackButton";
import env from "env";
import useCurrentTeam from "hooks/useCurrentTeam";
import useQuery from "hooks/useQuery";
import useStores from "hooks/useStores";
function Slack() {
const team = useCurrentTeam();
@@ -28,13 +27,18 @@ function Slack() {
const error = query.get("error");
React.useEffect(() => {
collections.fetchPage({ limit: 100 });
integrations.fetchPage({ limit: 100 });
collections.fetchPage({
limit: 100,
});
integrations.fetchPage({
limit: 100,
});
}, [collections, integrations]);
const commandIntegration = find(integrations.slackIntegrations, {
type: "command",
});
const commandIntegration = find(
integrations.slackIntegrations,
(i) => i.type === "command"
);
return (
<Scene title="Slack" icon={<SlackIcon color="currentColor" />}>
@@ -59,15 +63,19 @@ function Slack() {
<HelpText>
<Trans
defaults="Get rich previews of Outline links shared in Slack and use the <em>{{ command }}</em> slash command to search for documents without leaving your chat."
values={{ command: "/outline" }}
components={{ em: <Code /> }}
values={{
command: "/outline",
}}
components={{
em: <Code />,
}}
/>
</HelpText>
{env.SLACK_KEY ? (
<>
<p>
{commandIntegration ? (
<Button onClick={commandIntegration.delete}>
<Button onClick={() => commandIntegration.delete()}>
{t("Disconnect")}
</Button>
) : (
@@ -90,7 +98,7 @@ function Slack() {
</HelpText>
<List>
{collections.orderedData.map((collection) => {
{collections.orderedData.map((collection: Collection) => {
const integration = find(integrations.slackIntegrations, {
collectionId: collection.id,
});
@@ -104,8 +112,12 @@ function Slack() {
subtitle={
<Trans
defaults={`Connected to the <em>{{ channelName }}</em> channel`}
values={{ channelName: integration.settings.channel }}
components={{ em: <strong /> }}
values={{
channelName: integration.settings.channel,
}}
components={{
em: <strong />,
}}
/>
}
actions={

View File

@@ -1,21 +1,20 @@
// @flow
import { observer } from "mobx-react";
import { CodeIcon } from "outline-icons";
import * as React from "react";
import { useTranslation, Trans } from "react-i18next";
import APITokenNew from "scenes/APITokenNew";
import { Action } from "components/Actions";
import Button from "components/Button";
import Heading from "components/Heading";
import HelpText from "components/HelpText";
import Modal from "components/Modal";
import PaginatedList from "components/PaginatedList";
import Scene from "components/Scene";
import Subheading from "components/Subheading";
import APITokenNew from "~/scenes/APITokenNew";
import { Action } from "~/components/Actions";
import Button from "~/components/Button";
import Heading from "~/components/Heading";
import HelpText from "~/components/HelpText";
import Modal from "~/components/Modal";
import PaginatedList from "~/components/PaginatedList";
import Scene from "~/components/Scene";
import Subheading from "~/components/Subheading";
import useBoolean from "~/hooks/useBoolean";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import useStores from "~/hooks/useStores";
import TokenListItem from "./components/TokenListItem";
import useBoolean from "hooks/useBoolean";
import useCurrentTeam from "hooks/useCurrentTeam";
import useStores from "hooks/useStores";
function Tokens() {
const team = useCurrentTeam();
@@ -55,7 +54,6 @@ function Tokens() {
}}
/>
</HelpText>
<PaginatedList
fetch={apiKeys.fetchPage}
items={apiKeys.orderedData}
@@ -64,7 +62,6 @@ function Tokens() {
<TokenListItem key={token.id} token={token} onDelete={token.delete} />
)}
/>
<Modal
title={t("Create a token")}
onRequestClose={handleNewModalClose}

View File

@@ -1,11 +1,10 @@
// @flow
import * as React from "react";
import { useTranslation, Trans } from "react-i18next";
import Button from "components/Button";
import Heading from "components/Heading";
import HelpText from "components/HelpText";
import Scene from "components/Scene";
import ZapierIcon from "components/ZapierIcon";
import Button from "~/components/Button";
import Heading from "~/components/Heading";
import HelpText from "~/components/HelpText";
import Scene from "~/components/Scene";
import ZapierIcon from "~/components/ZapierIcon";
function Zapier() {
const { t } = useTranslation();

View File

@@ -1,28 +1,26 @@
// @flow
import * as React from "react";
import { useTranslation } from "react-i18next";
import FileOperation from "models/FileOperation";
import { Action } from "components/Actions";
import ListItem from "components/List/Item";
import Time from "components/Time";
import useCurrentUser from "hooks/useCurrentUser";
import FileOperationMenu from "menus/FileOperationMenu";
type Props = {|
fileOperation: FileOperation,
handleDelete: (FileOperation) => Promise<void>,
|};
import FileOperation from "~/models/FileOperation";
import { Action } from "~/components/Actions";
import ListItem from "~/components/List/Item";
import Time from "~/components/Time";
import useCurrentUser from "~/hooks/useCurrentUser";
import FileOperationMenu from "~/menus/FileOperationMenu";
type Props = {
fileOperation: FileOperation;
handleDelete: (arg0: FileOperation) => Promise<void>;
};
const FileOperationListItem = ({ fileOperation, handleDelete }: Props) => {
const { t } = useTranslation();
const user = useCurrentUser();
const stateMapping = {
creating: t("Processing"),
expired: t("Expired"),
uploading: t("Processing"),
error: t("Error"),
};
return (
<ListItem
title={

View File

@@ -1,36 +1,44 @@
// @flow
import invariant from "invariant";
import { observable } from "mobx";
import { observer, inject } from "mobx-react";
import { observer } from "mobx-react";
import * as React from "react";
import AvatarEditor from "react-avatar-editor";
import Dropzone from "react-dropzone";
import styled from "styled-components";
import UiStore from "stores/UiStore";
import Button from "components/Button";
import Flex from "components/Flex";
import LoadingIndicator from "components/LoadingIndicator";
import Modal from "components/Modal";
import { compressImage } from "utils/compressImage";
import { uploadFile, dataUrlToBlob } from "utils/uploadFile";
import RootStore from "~/stores/RootStore";
import Button from "~/components/Button";
import Flex from "~/components/Flex";
import LoadingIndicator from "~/components/LoadingIndicator";
import Modal from "~/components/Modal";
import withStores from "~/components/withStores";
import { compressImage } from "~/utils/compressImage";
import { uploadFile, dataUrlToBlob } from "~/utils/uploadFile";
const EMPTY_OBJECT = {};
type Props = {
children?: React.Node,
onSuccess: (string) => void | Promise<void>,
onError: (string) => void,
submitText: string,
borderRadius: number,
ui: UiStore,
type Props = RootStore & {
children?: React.ReactNode;
onSuccess: (arg0: string) => void | Promise<void>;
onError: (arg0: string) => void;
submitText?: string;
borderRadius?: number;
};
@observer
class ImageUpload extends React.Component<Props> {
@observable isUploading: boolean = false;
@observable isCropping: boolean = false;
@observable zoom: number = 1;
@observable file: File;
avatarEditorRef: AvatarEditor;
@observable
isUploading = false;
@observable
isCropping = false;
@observable
zoom = 1;
@observable
file: File;
avatarEditorRef = React.createRef<AvatarEditor>();
static defaultProps = {
submitText: "Crop Picture",
@@ -44,15 +52,16 @@ class ImageUpload extends React.Component<Props> {
handleCrop = () => {
this.isUploading = true;
// allow the UI to update before converting the canvas to a Blob
// for large images this can cause the page rendering to hang.
setImmediate(this.uploadImage);
};
uploadImage = async () => {
const canvas = this.avatarEditorRef.getImage();
const canvas = this.avatarEditorRef.current?.getImage();
invariant(canvas, "canvas is not defined");
const imageBlob = dataUrlToBlob(canvas.toDataURL());
try {
const compressed = await compressImage(imageBlob, {
maxHeight: 512,
@@ -76,8 +85,9 @@ class ImageUpload extends React.Component<Props> {
this.isCropping = false;
};
handleZoom = (event: SyntheticDragEvent<*>) => {
let target = event.target;
handleZoom = (event: React.DragEvent<any>) => {
const target = event.target;
if (target instanceof HTMLInputElement) {
this.zoom = parseFloat(target.value);
}
@@ -85,14 +95,13 @@ class ImageUpload extends React.Component<Props> {
renderCropping() {
const { ui, submitText } = this.props;
return (
<Modal isOpen onRequestClose={this.handleClose} title="">
<Flex auto column align="center" justify="center">
{this.isUploading && <LoadingIndicator />}
<AvatarEditorContainer>
<AvatarEditor
ref={(ref) => (this.avatarEditorRef = ref)}
ref={this.avatarEditorRef}
image={this.file}
width={250}
height={250}
@@ -111,6 +120,7 @@ class ImageUpload extends React.Component<Props> {
max="2"
step="0.01"
defaultValue="1"
// @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
onChange={this.handleZoom}
/>
<CropButton onClick={this.handleCrop} disabled={this.isUploading}>
@@ -130,6 +140,7 @@ class ImageUpload extends React.Component<Props> {
<Dropzone
accept="image/png, image/jpeg"
onDropAccepted={this.onDropAccepted}
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ children: ({ getRootProps, getInputProps }... Remove this comment to see the full error message
style={EMPTY_OBJECT}
disablePreview
>
@@ -177,4 +188,4 @@ const CropButton = styled(Button)`
width: 300px;
`;
export default inject("ui")(ImageUpload);
export default withStores(ImageUpload);

View File

@@ -1,15 +1,14 @@
// @flow
import * as React from "react";
import NotificationSetting from "models/NotificationSetting";
import Checkbox from "components/Checkbox";
import NotificationSetting from "~/models/NotificationSetting";
import Checkbox from "~/components/Checkbox";
type Props = {
setting?: NotificationSetting,
title: string,
event: string,
description: string,
disabled: boolean,
onChange: (ev: SyntheticInputEvent<>) => void | Promise<void>,
setting?: NotificationSetting;
title: string;
event: string;
description: string;
disabled: boolean;
onChange: (ev: React.SyntheticEvent) => void | Promise<void>;
};
const NotificationListItem = ({

View File

@@ -1,31 +1,38 @@
// @flow
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import User from "models/User";
import Avatar from "components/Avatar";
import Badge from "components/Badge";
import Flex from "components/Flex";
import { type Props as TableProps } from "components/Table";
import Time from "components/Time";
import useCurrentUser from "hooks/useCurrentUser";
import UserMenu from "menus/UserMenu";
import { $Diff } from "utility-types";
import User from "~/models/User";
import Avatar from "~/components/Avatar";
import Badge from "~/components/Badge";
import Flex from "~/components/Flex";
import { Props as TableProps } from "~/components/Table";
import Time from "~/components/Time";
import useCurrentUser from "~/hooks/useCurrentUser";
import UserMenu from "~/menus/UserMenu";
const Table = React.lazy<TableProps>(() =>
import(/* webpackChunkName: "table" */ "components/Table")
// @ts-expect-error ts-migrate(2344) FIXME: Type 'Props' does not satisfy the constraint 'Comp... Remove this comment to see the full error message
const Table = React.lazy<TableProps>(
() =>
import(
/* webpackChunkName: "table" */
"~/components/Table"
)
);
type Props = {|
...$Diff<TableProps, { columns: any }>,
data: User[],
canManage: boolean,
|};
type Props = $Diff<
TableProps,
{
columns: any;
}
> & {
data: User[];
canManage: boolean;
};
function PeopleTable({ canManage, ...rest }: Props) {
const { t } = useTranslation();
const currentUser = useCurrentUser();
const columns = React.useMemo(
() =>
[
@@ -33,6 +40,7 @@ function PeopleTable({ canManage, ...rest }: Props) {
id: "name",
Header: t("Name"),
accessor: "name",
// @ts-expect-error ts-migrate(7031) FIXME: Binding element 'value' implicitly has an 'any' ty... Remove this comment to see the full error message
Cell: observer(({ value, row }) => (
<Flex align="center" gap={8}>
<Avatar src={row.original.avatarUrl} size={32} /> {value}{" "}
@@ -45,6 +53,7 @@ function PeopleTable({ canManage, ...rest }: Props) {
id: "email",
Header: t("Email"),
accessor: "email",
// @ts-expect-error ts-migrate(7031) FIXME: Binding element 'value' implicitly has an 'any' ty... Remove this comment to see the full error message
Cell: observer(({ value }) => value),
}
: undefined,
@@ -53,6 +62,7 @@ function PeopleTable({ canManage, ...rest }: Props) {
Header: t("Last active"),
accessor: "lastActiveAt",
Cell: observer(
// @ts-expect-error ts-migrate(7031) FIXME: Binding element 'value' implicitly has an 'any' ty... Remove this comment to see the full error message
({ value }) => value && <Time dateTime={value} addSuffix />
),
},
@@ -60,6 +70,7 @@ function PeopleTable({ canManage, ...rest }: Props) {
id: "isAdmin",
Header: t("Role"),
accessor: "rank",
// @ts-expect-error ts-migrate(7031) FIXME: Binding element 'row' implicitly has an 'any' type... Remove this comment to see the full error message
Cell: observer(({ row }) => (
<Badges>
{!row.original.lastActiveAt && <Badge>{t("Invited")}</Badge>}
@@ -75,6 +86,7 @@ function PeopleTable({ canManage, ...rest }: Props) {
accessor: "id",
className: "actions",
Cell: observer(
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type '({ row, value }: { row: any; val... Remove this comment to see the full error message
({ row, value }) =>
currentUser.id !== value && <UserMenu user={row.original} />
),
@@ -83,7 +95,7 @@ function PeopleTable({ canManage, ...rest }: Props) {
].filter((i) => i),
[t, canManage, currentUser]
);
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ data: any[] & User[]; offset?: number | un... Remove this comment to see the full error message
return <Table columns={columns} {...rest} />;
}

View File

@@ -1,14 +1,13 @@
// @flow
import * as React from "react";
import { useTranslation } from "react-i18next";
import Share from "models/Share";
import ListItem from "components/List/Item";
import Time from "components/Time";
import ShareMenu from "menus/ShareMenu";
import Share from "~/models/Share";
import ListItem from "~/components/List/Item";
import Time from "~/components/Time";
import ShareMenu from "~/menus/ShareMenu";
type Props = {|
share: Share,
|};
type Props = {
share: Share;
};
const ShareListItem = ({ share }: Props) => {
const { t } = useTranslation();
@@ -20,7 +19,9 @@ const ShareListItem = ({ share }: Props) => {
subtitle={
<>
{t("Shared")} <Time dateTime={share.createdAt} addSuffix />{" "}
{t("by {{ name }}", { name: share.createdBy.name })}{" "}
{t("by {{ name }}", {
name: share.createdBy.name,
})}{" "}
{lastAccessedAt && (
<>
{" "}

View File

@@ -1,20 +1,20 @@
// @flow
import * as React from "react";
import { useTranslation } from "react-i18next";
import { slackAuth } from "shared/utils/routeHelpers";
import Button from "components/Button";
import SlackIcon from "components/SlackIcon";
import env from "env";
import { slackAuth } from "@shared/utils/routeHelpers";
import Button from "~/components/Button";
import SlackIcon from "~/components/SlackIcon";
import env from "~/env";
type Props = {|
scopes?: string[],
redirectUri: string,
state?: string,
label?: string,
|};
type Props = {
scopes?: string[];
redirectUri: string;
state?: string;
label?: string;
};
function SlackButton({ state = "", scopes, redirectUri, label }: Props) {
const { t } = useTranslation();
const handleClick = () =>
(window.location.href = slackAuth(
state,
@@ -26,7 +26,7 @@ function SlackButton({ state = "", scopes, redirectUri, label }: Props) {
return (
<Button
onClick={handleClick}
icon={<SlackIcon fill="currentColor" />}
icon={<SlackIcon color="currentColor" />}
neutral
>
{label || t("Add to Slack")}

View File

@@ -1,13 +1,12 @@
// @flow
import * as React from "react";
import ApiKey from "models/ApiKey";
import Button from "components/Button";
import ListItem from "components/List/Item";
import ApiKey from "~/models/ApiKey";
import Button from "~/components/Button";
import ListItem from "~/components/List/Item";
type Props = {|
token: ApiKey,
onDelete: (tokenId: string) => Promise<void>,
|};
type Props = {
token: ApiKey;
onDelete: (tokenId: string) => Promise<void>;
};
const TokenListItem = ({ token, onDelete }: Props) => {
return (

View File

@@ -1,78 +0,0 @@
// @flow
import { observable } from "mobx";
import { observer } from "mobx-react";
import * as React from "react";
import styled from "styled-components";
import User from "models/User";
import UserProfile from "scenes/UserProfile";
import Avatar from "components/Avatar";
import Badge from "components/Badge";
import ListItem from "components/List/Item";
import Time from "components/Time";
import UserMenu from "menus/UserMenu";
type Props = {
user: User,
showMenu: boolean,
};
@observer
class UserListItem extends React.Component<Props> {
@observable profileOpen: boolean = false;
handleOpenProfile = () => {
this.profileOpen = true;
};
handleCloseProfile = () => {
this.profileOpen = false;
};
render() {
const { user, showMenu } = this.props;
return (
<ListItem
title={<Title onClick={this.handleOpenProfile}>{user.name}</Title>}
image={
<>
<Avatar
src={user.avatarUrl}
size={32}
onClick={this.handleOpenProfile}
/>
<UserProfile
user={user}
isOpen={this.profileOpen}
onRequestClose={this.handleCloseProfile}
/>
</>
}
subtitle={
<>
{user.email ? `${user.email} · ` : undefined}
{user.lastActiveAt ? (
<>
Active <Time dateTime={user.lastActiveAt} /> ago
</>
) : (
"Invited"
)}
{user.isAdmin && <Badge primary={user.isAdmin}>Admin</Badge>}
{user.isSuspended && <Badge>Suspended</Badge>}
</>
}
actions={showMenu ? <UserMenu user={user} /> : undefined}
/>
);
}
}
const Title = styled.span`
&:hover {
text-decoration: underline;
cursor: pointer;
}
`;
export default UserListItem;

View File

@@ -0,0 +1,52 @@
import { observer } from "mobx-react";
import * as React from "react";
import styled from "styled-components";
import User from "~/models/User";
import Avatar from "~/components/Avatar";
import Badge from "~/components/Badge";
import ListItem from "~/components/List/Item";
import Time from "~/components/Time";
import UserMenu from "~/menus/UserMenu";
type Props = {
user: User;
showMenu: boolean;
};
@observer
class UserListItem extends React.Component<Props> {
render() {
const { user, showMenu } = this.props;
return (
<ListItem
title={<Title>{user.name}</Title>}
image={<Avatar src={user.avatarUrl} size={32} />}
subtitle={
<>
{user.email ? `${user.email} · ` : undefined}
{user.lastActiveAt ? (
<>
Active <Time dateTime={user.lastActiveAt} /> ago
</>
) : (
"Invited"
)}
{user.isAdmin && <Badge primary={user.isAdmin}>Admin</Badge>}
{user.isSuspended && <Badge>Suspended</Badge>}
</>
}
actions={showMenu ? <UserMenu user={user} /> : undefined}
/>
);
}
}
const Title = styled.span`
&:hover {
text-decoration: underline;
cursor: pointer;
}
`;
export default UserListItem;

View File

@@ -1,16 +1,14 @@
// @flow
import * as React from "react";
import { useTranslation } from "react-i18next";
import FilterOptions from "components/FilterOptions";
import FilterOptions from "~/components/FilterOptions";
type Props = {|
activeKey: string,
onSelect: (key: ?string) => void,
|};
type Props = {
activeKey: string;
onSelect: (key: string | null | undefined) => void;
};
const UserStatusFilter = ({ activeKey, onSelect }: Props) => {
const { t } = useTranslation();
const options = React.useMemo(
() => [
{