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.
79 lines
2.3 KiB
TypeScript
79 lines
2.3 KiB
TypeScript
import { groupBy } from "es-toolkit/compat";
|
|
import * as React from "react";
|
|
import { Trans, useTranslation } from "react-i18next";
|
|
import styled from "styled-components";
|
|
import Flex from "@shared/components/Flex";
|
|
import Heading from "~/components/Heading";
|
|
import InputSearch from "~/components/InputSearch";
|
|
import Scene from "~/components/Scene";
|
|
import Text from "~/components/Text";
|
|
import useSettingsConfig from "~/hooks/useSettingsConfig";
|
|
import useStores from "~/hooks/useStores";
|
|
import { settingsPath } from "~/utils/routeHelpers";
|
|
import IntegrationCard, { Card } from "./components/IntegrationCard";
|
|
import { StickyFilters } from "./components/StickyFilters";
|
|
import { observer } from "mobx-react";
|
|
|
|
function Integrations() {
|
|
const { t } = useTranslation();
|
|
const { integrations } = useStores();
|
|
const items = useSettingsConfig();
|
|
const [query, setQuery] = React.useState("");
|
|
|
|
const handleQuery = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
setQuery(event.target.value);
|
|
};
|
|
|
|
const groupedItems = groupBy(
|
|
items.filter(
|
|
(item) =>
|
|
item.group === t("Integrations") &&
|
|
item.enabled &&
|
|
item.path !== settingsPath("integrations") &&
|
|
item.name.toLowerCase().includes(query.toLowerCase())
|
|
),
|
|
(item) =>
|
|
item.pluginId && integrations.findByService(item.pluginId)
|
|
? "connected"
|
|
: "available"
|
|
);
|
|
|
|
return (
|
|
<Scene title={t("Integrations")}>
|
|
<Heading>{t("Integrations")}</Heading>
|
|
<Text as="p" type="secondary">
|
|
<Trans>
|
|
Configure a variety of integrations with third-party services.
|
|
</Trans>
|
|
</Text>
|
|
<StickyFilters>
|
|
<InputSearch
|
|
short
|
|
value={query}
|
|
placeholder={`${t("Filter")}…`}
|
|
onChange={handleQuery}
|
|
/>
|
|
</StickyFilters>
|
|
|
|
<Cards gap={30} wrap>
|
|
{groupedItems.connected?.map((item) => (
|
|
<IntegrationCard key={item.path} integration={item} isConnected />
|
|
))}
|
|
{groupedItems.available?.map((item) => (
|
|
<IntegrationCard key={item.path} integration={item} />
|
|
))}
|
|
{groupedItems.available?.length % 2 === 1 && (
|
|
<Card style={{ visibility: "hidden" }} />
|
|
)}
|
|
</Cards>
|
|
</Scene>
|
|
);
|
|
}
|
|
|
|
const Cards = styled(Flex)`
|
|
margin-top: 20px;
|
|
width: "100%";
|
|
`;
|
|
|
|
export default observer(Integrations);
|