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:
@@ -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>
|
||||
@@ -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]
|
||||
@@ -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();
|
||||
@@ -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 it’s 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>
|
||||
@@ -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}`} />;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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")}
|
||||
@@ -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>
|
||||
)}
|
||||
@@ -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={
|
||||
@@ -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}
|
||||
@@ -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();
|
||||
@@ -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={
|
||||
@@ -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);
|
||||
@@ -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 = ({
|
||||
@@ -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} />;
|
||||
}
|
||||
|
||||
@@ -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 && (
|
||||
<>
|
||||
{" "}
|
||||
@@ -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")}
|
||||
@@ -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 (
|
||||
@@ -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;
|
||||
52
app/scenes/Settings/components/UserListItem.tsx
Normal file
52
app/scenes/Settings/components/UserListItem.tsx
Normal 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;
|
||||
@@ -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(
|
||||
() => [
|
||||
{
|
||||
Reference in New Issue
Block a user