Files
outline/app/scenes/Settings/components/ImportListItem.tsx
T
Tom Moor 0139b91b5d chore: Replace lodash with es-toolkit (#12281)
* 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.
2026-05-06 21:03:47 -04:00

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]}&nbsp;&nbsp;
{importModel.error && (
<>
{importModel.error}
{selfHostedHelp}&nbsp;&nbsp;
</>
)}
{t(`{{userName}} requested`, {
userName:
user.id === importModel.createdBy.id
? t("You")
: importModel.createdBy.name,
})}
&nbsp;
<Time dateTime={importModel.createdAt} addSuffix shorten />
&nbsp;&nbsp;
{capitalize(importModel.service)}
{showProgress && (
<>
&nbsp;&nbsp;
{t("{{ count }} document imported", {
count: importModel.documentCount,
})}
</>
)}
</>
}
actions={
<Action>
<ImportMenu
importModel={importModel}
onCancel={handleCancel}
onDelete={handleDelete}
/>
</Action>
}
/>
);
});