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.
99 lines
2.8 KiB
TypeScript
99 lines
2.8 KiB
TypeScript
import { IconTitleWrapper } from "@shared/components/Icon";
|
|
import breakpoint from "styled-components-breakpoint";
|
|
import { first } from "es-toolkit/compat";
|
|
import { Suspense, useCallback } from "react";
|
|
import styled from "styled-components";
|
|
import { CollectionValidation } from "@shared/validations";
|
|
import { isRTL } from "@shared/utils/rtl";
|
|
import Heading from "~/components/Heading";
|
|
import ContentEditable from "~/components/ContentEditable";
|
|
import CollectionIcon from "~/components/Icons/CollectionIcon";
|
|
import type Collection from "~/models/Collection";
|
|
import { colorPalette } from "@shared/utils/collections";
|
|
import usePolicy from "~/hooks/usePolicy";
|
|
import { observer } from "mobx-react";
|
|
import lazyWithRetry from "~/utils/lazyWithRetry";
|
|
|
|
const IconPicker = lazyWithRetry(() => import("~/components/IconPicker"));
|
|
|
|
type Props = {
|
|
/** The collection for which to render a header */
|
|
collection: Collection;
|
|
/** Whether the header is in editing mode */
|
|
isEditing?: boolean;
|
|
};
|
|
|
|
export const Header = observer(function Header_({
|
|
collection,
|
|
isEditing,
|
|
}: Props) {
|
|
const can = usePolicy(collection);
|
|
const canEdit = can.update && isEditing;
|
|
const handleIconChange = useCallback(
|
|
(icon: string | null, color: string | null) =>
|
|
collection?.save({ icon, color }),
|
|
[collection]
|
|
);
|
|
|
|
const handleTitleChange = useCallback(
|
|
(text: string) => {
|
|
const trimmed = text.trim();
|
|
if (trimmed.length > 0 && trimmed !== collection.name) {
|
|
void collection.save({ name: trimmed });
|
|
}
|
|
},
|
|
[collection]
|
|
);
|
|
|
|
const fallbackIcon = collection ? (
|
|
<CollectionIcon collection={collection} size={40} expanded />
|
|
) : null;
|
|
|
|
const dir = isRTL(collection.name) ? "rtl" : "ltr";
|
|
|
|
return (
|
|
<StyledHeading dir={dir}>
|
|
<IconTitleWrapper dir={dir}>
|
|
{canEdit ? (
|
|
<Suspense fallback={fallbackIcon}>
|
|
<IconPicker
|
|
icon={collection.icon ?? "collection"}
|
|
color={collection.color ?? (first(colorPalette) as string)}
|
|
initial={collection.initial}
|
|
size={40}
|
|
popoverPosition="bottom-start"
|
|
onChange={handleIconChange}
|
|
borderOnHover
|
|
>
|
|
{fallbackIcon}
|
|
</IconPicker>
|
|
</Suspense>
|
|
) : (
|
|
fallbackIcon
|
|
)}
|
|
</IconTitleWrapper>
|
|
{canEdit ? (
|
|
<ContentEditable
|
|
value={collection.name}
|
|
onChange={handleTitleChange}
|
|
maxLength={CollectionValidation.maxNameLength}
|
|
dir="auto"
|
|
/>
|
|
) : (
|
|
collection.name
|
|
)}
|
|
</StyledHeading>
|
|
);
|
|
});
|
|
|
|
const StyledHeading = styled(Heading)`
|
|
display: flex;
|
|
align-items: center;
|
|
position: relative;
|
|
margin-left: 40px;
|
|
|
|
${breakpoint("tablet")`
|
|
margin-left: 0;
|
|
`}
|
|
`;
|