mirror of
https://github.com/outline/outline.git
synced 2026-06-13 03:14:59 +03:00
0139b91b5d
* chore: Replace lodash with es-toolkit Migrate all direct lodash imports to es-toolkit/compat for a smaller, faster, lodash-compatible utility library. Transitive lodash usage from other packages remains unchanged. * fix: Restore isPlainObject semantics in CanCan policy The lodash migration aliased `isObject` to `lodash/isPlainObject` and the codemod incorrectly mapped the local name to es-toolkit's `isObject`, which also returns true for arrays and functions. This caused condition objects in policy definitions to be skipped, breaking authorization checks across the codebase. * fix: Restore unicode-aware length counting in validators es-toolkit/compat's size() returns string.length, while lodash's _.size() counts unicode code points. Switch to [...value].length to preserve the previous behavior so multi-byte characters like emoji count as one.
160 lines
4.6 KiB
TypeScript
160 lines
4.6 KiB
TypeScript
import { capitalize } from "es-toolkit/compat";
|
|
import { observer } from "mobx-react";
|
|
import { CrossIcon, DoneIcon, WarningIcon } from "outline-icons";
|
|
import { useMemo, useCallback } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { toast } from "sonner";
|
|
import { useTheme } from "styled-components";
|
|
import Spinner from "@shared/components/Spinner";
|
|
import { ImportState } from "@shared/types";
|
|
import type Import from "~/models/Import";
|
|
import { Action } from "~/components/Actions";
|
|
import ConfirmationDialog from "~/components/ConfirmationDialog";
|
|
import ListItem from "~/components/List/Item";
|
|
import Time from "~/components/Time";
|
|
import useCurrentUser from "~/hooks/useCurrentUser";
|
|
import useStores from "~/hooks/useStores";
|
|
import { ImportMenu } from "~/menus/ImportMenu";
|
|
import isCloudHosted from "~/utils/isCloudHosted";
|
|
|
|
type Props = {
|
|
/** Import that's displayed as list item. */
|
|
importModel: Import;
|
|
};
|
|
|
|
export const ImportListItem = observer(({ importModel }: Props) => {
|
|
const { t } = useTranslation();
|
|
const { dialogs } = useStores();
|
|
const user = useCurrentUser();
|
|
const theme = useTheme();
|
|
const showProgress =
|
|
importModel.state !== ImportState.Canceled &&
|
|
importModel.state !== ImportState.Errored;
|
|
|
|
const stateMap = useMemo(
|
|
() => ({
|
|
[ImportState.Created]: t("Processing"),
|
|
[ImportState.InProgress]: t("Processing"),
|
|
[ImportState.Processed]: t("Processing"),
|
|
[ImportState.Completed]: t("Completed"),
|
|
[ImportState.Errored]: t("Failed"),
|
|
[ImportState.Canceled]: t("Canceled"),
|
|
}),
|
|
[t]
|
|
);
|
|
|
|
const iconMap = useMemo(
|
|
() => ({
|
|
[ImportState.Created]: <Spinner />,
|
|
[ImportState.InProgress]: <Spinner />,
|
|
[ImportState.Processed]: <Spinner />,
|
|
[ImportState.Completed]: <DoneIcon color={theme.accent} />,
|
|
[ImportState.Errored]: <WarningIcon color={theme.danger} />,
|
|
[ImportState.Canceled]: <CrossIcon color={theme.textTertiary} />,
|
|
}),
|
|
[theme]
|
|
);
|
|
|
|
const handleCancel = useCallback(async () => {
|
|
const onCancel = async () => {
|
|
try {
|
|
await importModel.cancel();
|
|
toast.success(t("Import canceled"));
|
|
} catch (err) {
|
|
toast.error(err.message);
|
|
}
|
|
};
|
|
|
|
dialogs.openModal({
|
|
title: t("Are you sure you want to cancel this import?"),
|
|
content: (
|
|
<ConfirmationDialog
|
|
onSubmit={onCancel}
|
|
submitText={t("Cancel")}
|
|
savingText={`${t("Canceling")}…`}
|
|
danger
|
|
>
|
|
{t(
|
|
"Canceling this import will discard any progress made. This cannot be undone."
|
|
)}
|
|
</ConfirmationDialog>
|
|
),
|
|
});
|
|
}, [t, dialogs, importModel]);
|
|
|
|
const handleDelete = useCallback(async () => {
|
|
const onDelete = async () => {
|
|
try {
|
|
await importModel.delete();
|
|
toast.success(t("Import deleted"));
|
|
} catch (err) {
|
|
toast.error(err.message);
|
|
}
|
|
};
|
|
|
|
dialogs.openModal({
|
|
title: t("Are you sure you want to delete this import?"),
|
|
content: (
|
|
<ConfirmationDialog
|
|
onSubmit={onDelete}
|
|
savingText={`${t("Deleting")}…`}
|
|
danger
|
|
>
|
|
{t(
|
|
"Deleting this import will also delete all collections and documents that were created from it. This cannot be undone."
|
|
)}
|
|
</ConfirmationDialog>
|
|
),
|
|
});
|
|
}, [t, dialogs, importModel]);
|
|
|
|
const selfHostedHelp = isCloudHosted
|
|
? ""
|
|
: `. ${t("Check server logs for more details.")}`;
|
|
|
|
return (
|
|
<ListItem
|
|
title={importModel.name}
|
|
image={iconMap[importModel.state]}
|
|
subtitle={
|
|
<>
|
|
{stateMap[importModel.state]} •
|
|
{importModel.error && (
|
|
<>
|
|
{importModel.error}
|
|
{selfHostedHelp} •
|
|
</>
|
|
)}
|
|
{t(`{{userName}} requested`, {
|
|
userName:
|
|
user.id === importModel.createdBy.id
|
|
? t("You")
|
|
: importModel.createdBy.name,
|
|
})}
|
|
|
|
<Time dateTime={importModel.createdAt} addSuffix shorten />
|
|
•
|
|
{capitalize(importModel.service)}
|
|
{showProgress && (
|
|
<>
|
|
•
|
|
{t("{{ count }} document imported", {
|
|
count: importModel.documentCount,
|
|
})}
|
|
</>
|
|
)}
|
|
</>
|
|
}
|
|
actions={
|
|
<Action>
|
|
<ImportMenu
|
|
importModel={importModel}
|
|
onCancel={handleCancel}
|
|
onDelete={handleDelete}
|
|
/>
|
|
</Action>
|
|
}
|
|
/>
|
|
);
|
|
});
|