mirror of
https://github.com/outline/outline.git
synced 2026-06-13 11:25:03 +03:00
fix: Render-per-model type, 4x improvement on perf (#10465)
* fix: Render-per-model type, 4x improvement on perf * fix: Sidebar CollectionLinkChildren render when @mention changes
This commit is contained in:
@@ -7,18 +7,28 @@ export default function useCollectionDocuments(
|
||||
collection: Collection | undefined,
|
||||
activeDocument: Document | undefined
|
||||
) {
|
||||
const insertDraftDocument = useMemo(
|
||||
() =>
|
||||
activeDocument &&
|
||||
activeDocument.isActive &&
|
||||
activeDocument.isDraft &&
|
||||
activeDocument.collectionId === collection?.id &&
|
||||
!activeDocument.parentDocumentId,
|
||||
[
|
||||
activeDocument?.isActive,
|
||||
activeDocument?.isDraft,
|
||||
activeDocument?.collectionId,
|
||||
activeDocument?.parentDocumentId,
|
||||
collection?.id,
|
||||
]
|
||||
);
|
||||
|
||||
return useMemo(() => {
|
||||
if (!collection?.sortedDocuments) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const insertDraftDocument =
|
||||
activeDocument?.isActive &&
|
||||
activeDocument?.isDraft &&
|
||||
activeDocument?.collectionId === collection.id &&
|
||||
!activeDocument?.parentDocumentId;
|
||||
|
||||
return insertDraftDocument
|
||||
return insertDraftDocument && activeDocument
|
||||
? sortNavigationNodes(
|
||||
[activeDocument.asNavigationNode, ...collection.sortedDocuments],
|
||||
collection.sort,
|
||||
@@ -26,14 +36,9 @@ export default function useCollectionDocuments(
|
||||
)
|
||||
: collection.sortedDocuments;
|
||||
}, [
|
||||
activeDocument?.isActive,
|
||||
activeDocument?.isDraft,
|
||||
activeDocument?.collectionId,
|
||||
activeDocument?.parentDocumentId,
|
||||
insertDraftDocument,
|
||||
activeDocument?.asNavigationNode,
|
||||
collection,
|
||||
collection?.sortedDocuments,
|
||||
collection?.id,
|
||||
collection?.sort,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useCallback } from "react";
|
||||
import useDictionary from "~/hooks/useDictionary";
|
||||
import getMenuItems from "../menus/block";
|
||||
import { useEditor } from "./EditorContext";
|
||||
@@ -13,20 +14,25 @@ function BlockMenu(props: Props) {
|
||||
const dictionary = useDictionary();
|
||||
const { elementRef } = useEditor();
|
||||
|
||||
const renderMenuItem = useCallback(
|
||||
(item, _index, options) => (
|
||||
<SuggestionsMenuItem
|
||||
onClick={options.onClick}
|
||||
selected={options.selected}
|
||||
icon={item.icon}
|
||||
title={item.title}
|
||||
shortcut={item.shortcut}
|
||||
/>
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<SuggestionsMenu
|
||||
{...props}
|
||||
filterable
|
||||
trigger="/"
|
||||
renderMenuItem={(item, _index, options) => (
|
||||
<SuggestionsMenuItem
|
||||
onClick={options.onClick}
|
||||
selected={options.selected}
|
||||
icon={item.icon}
|
||||
title={item.title}
|
||||
shortcut={item.shortcut}
|
||||
/>
|
||||
)}
|
||||
renderMenuItem={renderMenuItem}
|
||||
items={getMenuItems(dictionary, elementRef)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import capitalize from "lodash/capitalize";
|
||||
import { useMemo } from "react";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { emojiMartToGemoji, snakeCase } from "@shared/editor/lib/emoji";
|
||||
import { search as emojiSearch } from "@shared/utils/emoji";
|
||||
import EmojiMenuItem from "./EmojiMenuItem";
|
||||
@@ -45,18 +45,23 @@ const EmojiMenu = (props: Props) => {
|
||||
[search]
|
||||
);
|
||||
|
||||
const renderMenuItem = useCallback(
|
||||
(item, _index, options) => (
|
||||
<EmojiMenuItem
|
||||
onClick={options.onClick}
|
||||
selected={options.selected}
|
||||
title={item.description}
|
||||
emoji={item.emoji}
|
||||
/>
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<SuggestionsMenu
|
||||
{...props}
|
||||
filterable={false}
|
||||
renderMenuItem={(item, _index, options) => (
|
||||
<EmojiMenuItem
|
||||
onClick={options.onClick}
|
||||
selected={options.selected}
|
||||
title={item.description}
|
||||
emoji={item.emoji}
|
||||
/>
|
||||
)}
|
||||
renderMenuItem={renderMenuItem}
|
||||
items={items}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -25,6 +25,7 @@ import SuggestionsMenu, {
|
||||
Props as SuggestionsMenuProps,
|
||||
} from "./SuggestionsMenu";
|
||||
import SuggestionsMenuItem from "./SuggestionsMenuItem";
|
||||
import { runInAction } from "mobx";
|
||||
|
||||
interface MentionItem extends MenuItem {
|
||||
attrs: {
|
||||
@@ -53,12 +54,17 @@ function MentionMenu({ search, isActive, ...rest }: Props) {
|
||||
|
||||
const { loading, request } = useRequest(
|
||||
useCallback(async () => {
|
||||
const res = await client.post("/suggestions.mention", { query: search });
|
||||
const res = await client.post("/suggestions.mention", {
|
||||
query: search,
|
||||
limit: maxResultsInSection,
|
||||
});
|
||||
|
||||
res.data.documents.map(documents.add);
|
||||
res.data.users.map(users.add);
|
||||
res.data.collections.map(collections.add);
|
||||
res.data.groups.map(groups.add);
|
||||
runInAction(() => {
|
||||
res.data.documents.map(documents.add);
|
||||
res.data.users.map(users.add);
|
||||
res.data.collections.map(collections.add);
|
||||
res.data.groups.map(groups.add);
|
||||
});
|
||||
}, [search, documents, users, collections])
|
||||
);
|
||||
|
||||
@@ -274,6 +280,19 @@ function MentionMenu({ search, isActive, ...rest }: Props) {
|
||||
[t, users, documentId, groups]
|
||||
);
|
||||
|
||||
const renderMenuItem = useCallback(
|
||||
(item, _index, options) => (
|
||||
<SuggestionsMenuItem
|
||||
onClick={options.onClick}
|
||||
selected={options.selected}
|
||||
subtitle={item.subtitle}
|
||||
title={item.title}
|
||||
icon={item.icon}
|
||||
/>
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
// Prevent showing the menu until we have data otherwise it will be positioned
|
||||
// incorrectly due to the height being unknown.
|
||||
if (!loaded) {
|
||||
@@ -287,15 +306,7 @@ function MentionMenu({ search, isActive, ...rest }: Props) {
|
||||
filterable={false}
|
||||
search={search}
|
||||
onSelect={handleSelect}
|
||||
renderMenuItem={(item, _index, options) => (
|
||||
<SuggestionsMenuItem
|
||||
onClick={options.onClick}
|
||||
selected={options.selected}
|
||||
subtitle={item.subtitle}
|
||||
title={item.title}
|
||||
icon={item.icon}
|
||||
/>
|
||||
)}
|
||||
renderMenuItem={renderMenuItem}
|
||||
items={items}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { observer } from "mobx-react";
|
||||
import { EmailIcon, LinkIcon } from "outline-icons";
|
||||
import React from "react";
|
||||
import React, { useCallback } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { EmbedDescriptor } from "@shared/editor/embeds";
|
||||
import { MenuItem } from "@shared/editor/types";
|
||||
@@ -26,6 +26,18 @@ type Props = Omit<
|
||||
export const PasteMenu = observer(({ pastedText, embeds, ...props }: Props) => {
|
||||
const items = useItems({ pastedText, embeds });
|
||||
|
||||
const renderMenuItem = useCallback(
|
||||
(item, _index, options) => (
|
||||
<SuggestionsMenuItem
|
||||
onClick={options.onClick}
|
||||
selected={options.selected}
|
||||
title={item.title}
|
||||
icon={item.icon}
|
||||
/>
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
if (!items) {
|
||||
props.onClose();
|
||||
return null;
|
||||
@@ -36,14 +48,7 @@ export const PasteMenu = observer(({ pastedText, embeds, ...props }: Props) => {
|
||||
{...props}
|
||||
trigger=""
|
||||
filterable={false}
|
||||
renderMenuItem={(item, _index, options) => (
|
||||
<SuggestionsMenuItem
|
||||
onClick={options.onClick}
|
||||
selected={options.selected}
|
||||
title={item.title}
|
||||
icon={item.icon}
|
||||
/>
|
||||
)}
|
||||
renderMenuItem={renderMenuItem}
|
||||
items={items}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -641,6 +641,10 @@ function SuggestionsMenu<T extends MenuItem>(props: Props<T>) {
|
||||
}
|
||||
};
|
||||
|
||||
const handleOnClick = () => {
|
||||
handleClickItem(item);
|
||||
};
|
||||
|
||||
const currentHeading =
|
||||
"section" in item ? item.section?.({ t }) : undefined;
|
||||
|
||||
@@ -657,7 +661,7 @@ function SuggestionsMenu<T extends MenuItem>(props: Props<T>) {
|
||||
>
|
||||
{props.renderMenuItem(item as any, index, {
|
||||
selected: index === selectedIndex,
|
||||
onClick: () => handleClickItem(item),
|
||||
onClick: handleOnClick,
|
||||
})}
|
||||
</ListItem>
|
||||
</React.Fragment>
|
||||
|
||||
@@ -92,4 +92,4 @@ const Shortcut = styled.span<{ $active?: boolean }>`
|
||||
text-align: right;
|
||||
`;
|
||||
|
||||
export default SuggestionsMenuItem;
|
||||
export default React.memo(SuggestionsMenuItem);
|
||||
|
||||
@@ -16,6 +16,7 @@ import { client } from "~/utils/ApiClient";
|
||||
import User from "./User";
|
||||
import Field from "./decorators/Field";
|
||||
import { AfterChange } from "./decorators/Lifecycle";
|
||||
import isEqual from "lodash/isEqual";
|
||||
|
||||
export default class Collection extends ParanoidModel {
|
||||
static modelName = "Collection";
|
||||
@@ -156,7 +157,7 @@ export default class Collection extends ParanoidModel {
|
||||
return this.sort.field === "index";
|
||||
}
|
||||
|
||||
@computed
|
||||
@computed({ equals: isEqual })
|
||||
get sortedDocuments(): NavigationNode[] | undefined {
|
||||
if (!this.documents) {
|
||||
return undefined;
|
||||
|
||||
@@ -6,6 +6,7 @@ import Logger from "~/utils/Logger";
|
||||
import { getFieldsForModel } from "../decorators/Field";
|
||||
import { LifecycleManager } from "../decorators/Lifecycle";
|
||||
import { getRelationsForModelClass } from "../decorators/Relation";
|
||||
import { isEqual } from "lodash";
|
||||
|
||||
export default abstract class Model {
|
||||
static modelName: string;
|
||||
@@ -147,6 +148,10 @@ export default abstract class Model {
|
||||
continue;
|
||||
}
|
||||
// @ts-expect-error TODO
|
||||
if (isEqual(this[key], data[key])) {
|
||||
continue;
|
||||
}
|
||||
// @ts-expect-error TODO
|
||||
this[key] = data[key];
|
||||
} catch (error) {
|
||||
Logger.warn(`Error setting ${key} on model`, error);
|
||||
|
||||
Reference in New Issue
Block a user