Compare commits

...

7 Commits

21 changed files with 138 additions and 62 deletions
+8 -2
View File
@@ -15,6 +15,7 @@ import Button from "~/components/Button";
import Flex from "~/components/Flex";
import Input from "~/components/Input";
import InputSelectPermission from "~/components/InputSelectPermission";
import { createLazyComponent } from "~/components/LazyLoad";
import Switch from "~/components/Switch";
import Text from "~/components/Text";
import useBoolean from "~/hooks/useBoolean";
@@ -22,7 +23,7 @@ import useCurrentTeam from "~/hooks/useCurrentTeam";
import useStores from "~/hooks/useStores";
import { EmptySelectValue } from "~/types";
const IconPicker = React.lazy(() => import("~/components/IconPicker"));
const IconPicker = createLazyComponent(() => import("~/components/IconPicker"));
export interface FormData {
name: string;
@@ -88,6 +89,11 @@ export const CollectionForm = observer(function CollectionForm_({
const values = watch();
// Preload the IconPicker component on mount
React.useEffect(() => {
void IconPicker.preload();
}, []);
React.useEffect(() => {
// If the user hasn't picked an icon yet, go ahead and suggest one based on
// the name of the collection. It's the little things sometimes.
@@ -202,7 +208,7 @@ export const CollectionForm = observer(function CollectionForm_({
);
});
const StyledIconPicker = styled(IconPicker)`
const StyledIconPicker = styled(IconPicker.Component)`
margin-left: 4px;
margin-right: 4px;
`;
+47
View File
@@ -0,0 +1,47 @@
import * as React from "react";
import lazyWithRetry from "~/utils/lazyWithRetry";
export interface LazyComponent<T extends React.ComponentType<any>> {
Component: React.LazyExoticComponent<T>;
preload: () => Promise<{ default: T }>;
}
interface LazyLoadOptions {
retries?: number;
interval?: number;
}
/**
* Creates a lazy-loaded component with preloading capability and automatic retries on failure.
*
* @param factory A function that returns a promise of a component (eg: () => import('./MyComponent'))
* @param options Optional configuration for retry behavior
* @returns An object containing the lazy Component and a preload function
*
* @example
* ```typescript
* const MyComponent = createLazyComponent(() => import('./MyComponent'));
*
* function App() {
* return (
* <Suspense fallback={<div>Loading...</div>}>
* <MyComponent.Component />
* </Suspense>
* );
* }
*
* // Preload when needed:
* MyComponent.preload();
* ```
*/
export function createLazyComponent<T extends React.ComponentType<any>>(
factory: () => Promise<{ default: T }>,
options: LazyLoadOptions = {}
): LazyComponent<T> {
const { retries, interval } = options;
return {
Component: lazyWithRetry(factory, retries, interval),
preload: factory,
};
}
+4 -2
View File
@@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next";
import { PopoverDisclosure, usePopoverState } from "reakit";
import EventBoundary from "@shared/components/EventBoundary";
import Flex from "~/components/Flex";
import { createLazyComponent } from "~/components/LazyLoad";
import NudeButton from "~/components/NudeButton";
import PlaceholderText from "~/components/PlaceholderText";
import Popover from "~/components/Popover";
@@ -12,7 +13,7 @@ import useOnClickOutside from "~/hooks/useOnClickOutside";
import useWindowSize from "~/hooks/useWindowSize";
import Tooltip from "../Tooltip";
const EmojiPanel = React.lazy(
const EmojiPanel = createLazyComponent(
() => import("~/components/IconPicker/components/EmojiPanel")
);
@@ -104,6 +105,7 @@ const ReactionPicker: React.FC<Props> = ({
aria-label={t("Reaction picker")}
className={className}
onClick={handlePopoverButtonClick}
onMouseEnter={() => EmojiPanel.preload()}
size={size}
>
<ReactionIcon size={22} />
@@ -123,7 +125,7 @@ const ReactionPicker: React.FC<Props> = ({
{popover.visible && (
<React.Suspense fallback={<Placeholder />}>
<EventBoundary>
<EmojiPanel
<EmojiPanel.Component
height={300}
panelWidth={panelWidth}
query={query}
@@ -93,11 +93,13 @@ export const Suggestions = observer(
const suggestions = React.useMemo(() => {
const filtered: Suggestion[] = (
document
? users.notInDocument(document.id, query)
? users
.notInDocument(document.id, query)
.filter((u) => u.id !== user.id)
: collection
? users.notInCollection(collection.id, query)
: users.activeOrInvited
).filter((u) => !u.isSuspended && u.id !== user.id);
).filter((u) => !u.isSuspended);
if (isEmail(query)) {
filtered.push(getSuggestionForEmail(query));
+1
View File
@@ -63,6 +63,7 @@ function SettingsSidebar() {
<SidebarLink
key={item.path}
to={item.path}
onClickIntent={item.preload}
active={
item.path !== settingsPath()
? location.pathname.startsWith(item.path)
+1
View File
@@ -41,6 +41,7 @@ function useKeyboardShortcuts({
useKeyDown(
(ev) =>
isModKey(ev) &&
!popover.visible &&
ev.code === "KeyF" &&
// Keyboard handler is through the AppMenu on Desktop v1.2.0+
!(Desktop.bridge && "onFindInPage" in Desktop.bridge),
+36 -18
View File
@@ -19,9 +19,9 @@ import React, { ComponentProps } from "react";
import { useTranslation } from "react-i18next";
import { integrationSettingsPath } from "@shared/utils/routeHelpers";
import ZapierIcon from "~/components/Icons/ZapierIcon";
import { createLazyComponent as lazy } from "~/components/LazyLoad";
import { Hook, PluginManager } from "~/utils/PluginManager";
import isCloudHosted from "~/utils/isCloudHosted";
import lazy from "~/utils/lazyWithRetry";
import { settingsPath } from "~/utils/routeHelpers";
import { useComputed } from "./useComputed";
import useCurrentTeam from "./useCurrentTeam";
@@ -50,6 +50,7 @@ export type ConfigItem = {
path: string;
icon: React.FC<ComponentProps<typeof Icon>>;
component: React.ComponentType;
preload?: () => void;
enabled: boolean;
group: string;
};
@@ -66,7 +67,8 @@ const useSettingsConfig = () => {
{
name: t("Profile"),
path: settingsPath(),
component: Profile,
component: Profile.Component,
preload: Profile.preload,
enabled: true,
group: t("Account"),
icon: ProfileIcon,
@@ -74,7 +76,8 @@ const useSettingsConfig = () => {
{
name: t("Preferences"),
path: settingsPath("preferences"),
component: Preferences,
component: Preferences.Component,
preload: Preferences.preload,
enabled: true,
group: t("Account"),
icon: SettingsIcon,
@@ -82,7 +85,8 @@ const useSettingsConfig = () => {
{
name: t("Notifications"),
path: settingsPath("notifications"),
component: Notifications,
component: Notifications.Component,
preload: Notifications.preload,
enabled: true,
group: t("Account"),
icon: EmailIcon,
@@ -90,7 +94,8 @@ const useSettingsConfig = () => {
{
name: t("API & Apps"),
path: settingsPath("api-and-apps"),
component: APIAndApps,
component: APIAndApps.Component,
preload: APIAndApps.preload,
enabled: true,
group: t("Account"),
icon: PadlockIcon,
@@ -99,7 +104,8 @@ const useSettingsConfig = () => {
{
name: t("Details"),
path: settingsPath("details"),
component: Details,
component: Details.Component,
preload: Details.preload,
enabled: can.update,
group: t("Workspace"),
icon: TeamIcon,
@@ -107,7 +113,8 @@ const useSettingsConfig = () => {
{
name: t("Security"),
path: settingsPath("security"),
component: Security,
component: Security.Component,
preload: Security.preload,
enabled: can.update,
group: t("Workspace"),
icon: PadlockIcon,
@@ -115,7 +122,8 @@ const useSettingsConfig = () => {
{
name: t("Features"),
path: settingsPath("features"),
component: Features,
component: Features.Component,
preload: Features.preload,
enabled: can.update,
group: t("Workspace"),
icon: BeakerIcon,
@@ -123,7 +131,8 @@ const useSettingsConfig = () => {
{
name: t("Members"),
path: settingsPath("members"),
component: Members,
component: Members.Component,
preload: Members.preload,
enabled: can.listUsers,
group: t("Workspace"),
icon: UserIcon,
@@ -131,7 +140,8 @@ const useSettingsConfig = () => {
{
name: t("Groups"),
path: settingsPath("groups"),
component: Groups,
component: Groups.Component,
preload: Groups.preload,
enabled: can.listGroups,
group: t("Workspace"),
icon: GroupIcon,
@@ -139,7 +149,8 @@ const useSettingsConfig = () => {
{
name: t("Templates"),
path: settingsPath("templates"),
component: Templates,
component: Templates.Component,
preload: Templates.preload,
enabled: can.readTemplate,
group: t("Workspace"),
icon: ShapesIcon,
@@ -147,7 +158,8 @@ const useSettingsConfig = () => {
{
name: t("API Keys"),
path: settingsPath("api-keys"),
component: ApiKeys,
component: ApiKeys.Component,
preload: ApiKeys.preload,
enabled: can.listApiKeys,
group: t("Workspace"),
icon: CodeIcon,
@@ -155,7 +167,8 @@ const useSettingsConfig = () => {
{
name: t("Applications"),
path: settingsPath("applications"),
component: Applications,
component: Applications.Component,
preload: Applications.preload,
enabled: can.listOAuthClients,
group: t("Workspace"),
icon: InternetIcon,
@@ -163,7 +176,8 @@ const useSettingsConfig = () => {
{
name: t("Shared Links"),
path: settingsPath("shares"),
component: Shares,
component: Shares.Component,
preload: Shares.preload,
enabled: can.listShares,
group: t("Workspace"),
icon: GlobeIcon,
@@ -171,7 +185,8 @@ const useSettingsConfig = () => {
{
name: t("Import"),
path: settingsPath("import"),
component: Import,
component: Import.Component,
preload: Import.preload,
enabled: can.createImport,
group: t("Workspace"),
icon: ImportIcon,
@@ -179,7 +194,8 @@ const useSettingsConfig = () => {
{
name: t("Export"),
path: settingsPath("export"),
component: Export,
component: Export.Component,
preload: Export.preload,
enabled: can.createExport,
group: t("Workspace"),
icon: ExportIcon,
@@ -188,7 +204,8 @@ const useSettingsConfig = () => {
{
name: "Zapier",
path: integrationSettingsPath("zapier"),
component: Zapier,
component: Zapier.Component,
preload: Zapier.preload,
enabled: can.update && isCloudHosted,
group: t("Integrations"),
icon: ZapierIcon,
@@ -208,7 +225,8 @@ const useSettingsConfig = () => {
? integrationSettingsPath(plugin.id)
: settingsPath(plugin.id),
group: t(group),
component: plugin.value.component,
component: plugin.value.component.Component,
preload: plugin.value.component.preload,
enabled: plugin.value.enabled
? plugin.value.enabled(team, user)
: can.update,
+2 -7
View File
@@ -279,19 +279,14 @@ export default class DocumentsStore extends Store<Document> {
@action
fetchBacklinks = async (documentId: string): Promise<void> => {
const res = await client.post(`/documents.list`, {
const documents = await this.fetchAll({
backlinkDocumentId: documentId,
});
invariant(res?.data, "Document list not available");
const { data } = res;
runInAction("DocumentsStore#fetchBacklinks", () => {
data.forEach(this.add);
this.addPolicies(res.policies);
this.backlinks.set(
documentId,
data.map((doc: Partial<Document>) => doc.id)
documents.map((doc) => doc.id)
);
});
};
+2 -1
View File
@@ -3,6 +3,7 @@ import sortBy from "lodash/sortBy";
import { action, observable } from "mobx";
import Team from "~/models/Team";
import User from "~/models/User";
import { LazyComponent } from "~/components/LazyLoad";
import { useComputed } from "~/hooks/useComputed";
import Logger from "./Logger";
import isCloudHosted from "./isCloudHosted";
@@ -28,7 +29,7 @@ type PluginValueMap = {
/** The displayed icon of the plugin. */
icon: React.ElementType;
/** The settings screen somponent, should be lazy loaded. */
component: React.LazyExoticComponent<React.ComponentType>;
component: LazyComponent<React.ComponentType>;
/** Whether the plugin is enabled in the current context. */
enabled?: (team: Team, user: User) => boolean;
};
+1 -1
View File
@@ -141,7 +141,7 @@
"jsdom": "^22.1.0",
"jsonwebtoken": "^9.0.0",
"jszip": "^3.10.1",
"katex": "^0.16.21",
"katex": "^0.16.22",
"kbar": "0.1.0-beta.41",
"koa": "^2.16.1",
"koa-body": "^6.0.1",
+2 -2
View File
@@ -1,4 +1,4 @@
import * as React from "react";
import { createLazyComponent } from "~/components/LazyLoad";
import { Hook, PluginManager } from "~/utils/PluginManager";
import config from "../plugin.json";
import Icon from "./Icon";
@@ -10,7 +10,7 @@ PluginManager.add([
value: {
group: "Integrations",
icon: Icon,
component: React.lazy(() => import("./Settings")),
component: createLazyComponent(() => import("./Settings")),
},
},
]);
+2 -2
View File
@@ -1,4 +1,4 @@
import * as React from "react";
import { createLazyComponent } from "~/components/LazyLoad";
import { Hook, PluginManager } from "~/utils/PluginManager";
import config from "../plugin.json";
import Icon from "./Icon";
@@ -10,7 +10,7 @@ PluginManager.add([
value: {
group: "Integrations",
icon: Icon,
component: React.lazy(() => import("./Settings")),
component: createLazyComponent(() => import("./Settings")),
},
},
]);
+2 -2
View File
@@ -1,4 +1,4 @@
import * as React from "react";
import { createLazyComponent } from "~/components/LazyLoad";
import { Hook, PluginManager } from "~/utils/PluginManager";
import config from "../plugin.json";
import Icon from "./Icon";
@@ -10,7 +10,7 @@ PluginManager.add([
value: {
group: "Integrations",
icon: Icon,
component: React.lazy(() => import("./Settings")),
component: createLazyComponent(() => import("./Settings")),
},
},
]);
+2 -2
View File
@@ -1,5 +1,5 @@
import * as React from "react";
import { UserRole } from "@shared/types";
import { createLazyComponent } from "~/components/LazyLoad";
import { Hook, PluginManager } from "~/utils/PluginManager";
import config from "../plugin.json";
import Icon from "./Icon";
@@ -11,7 +11,7 @@ PluginManager.add([
value: {
group: "Integrations",
icon: Icon,
component: React.lazy(() => import("./Settings")),
component: createLazyComponent(() => import("./Settings")),
enabled: (_, user) => user.role === UserRole.Admin,
},
},
+2 -2
View File
@@ -1,5 +1,5 @@
import * as React from "react";
import { UserRole } from "@shared/types";
import { createLazyComponent } from "~/components/LazyLoad";
import { Hook, PluginManager } from "~/utils/PluginManager";
import config from "../plugin.json";
import Icon from "./Icon";
@@ -11,7 +11,7 @@ PluginManager.add([
value: {
group: "Integrations",
icon: Icon,
component: React.lazy(() => import("./Settings")),
component: createLazyComponent(() => import("./Settings")),
enabled: (_, user) =>
[UserRole.Member, UserRole.Admin].includes(user.role),
},
+2 -2
View File
@@ -1,5 +1,5 @@
import * as React from "react";
import { UserRole } from "@shared/types";
import { createLazyComponent } from "~/components/LazyLoad";
import { Hook, PluginManager } from "~/utils/PluginManager";
import config from "../plugin.json";
import Icon from "./Icon";
@@ -11,7 +11,7 @@ PluginManager.add([
value: {
group: "Integrations",
icon: Icon,
component: React.lazy(() => import("./Settings")),
component: createLazyComponent(() => import("./Settings")),
enabled: (_, user) => user.role === UserRole.Admin,
},
},
+2 -2
View File
@@ -1,4 +1,4 @@
import * as React from "react";
import { createLazyComponent } from "~/components/LazyLoad";
import { Hook, PluginManager } from "~/utils/PluginManager";
import config from "../plugin.json";
import Icon from "./Icon";
@@ -10,7 +10,7 @@ PluginManager.add([
value: {
group: "Integrations",
icon: Icon,
component: React.lazy(() => import("./Settings")),
component: createLazyComponent(() => import("./Settings")),
},
},
]);
@@ -171,7 +171,8 @@ export default abstract class ExportDocumentTreeTask extends ExportTask {
/**
* Generates a map of document urls to their path in the zip file.
*
* @param collections
* @param collections The collections to generate the path map for.
* @param format The format of the exported documents.
*/
private createPathMap(
collections: Collection[],
+7 -5
View File
@@ -44,11 +44,13 @@ export default abstract class ExportTask extends BaseTask<Props> {
? [fileOperation.collectionId]
: await user.collectionIds();
const collections = await Collection.findAll({
where: {
id: collectionIds,
},
});
const collections = await Collection.scope("withDocumentStructure").findAll(
{
where: {
id: collectionIds,
},
}
);
let filePath: string | undefined;
+1 -1
View File
@@ -55,7 +55,7 @@ const mathStyle = (props: Props) => css`
cursor: auto;
white-space: pre-wrap;
overflow-x: auto;
overflow-y: none;
overflow-y: hidden;
}
.math-node.empty-math .math-render::before {
+8 -8
View File
@@ -737,7 +737,7 @@
"@jridgewell/trace-mapping" "^0.3.25"
jsesc "^3.0.2"
"@babel/helper-annotate-as-pure@^7.22.5", "@babel/helper-annotate-as-pure@^7.25.9", "@babel/helper-annotate-as-pure@^7.27.1":
"@babel/helper-annotate-as-pure@^7.22.5", "@babel/helper-annotate-as-pure@^7.27.1":
version "7.27.1"
resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.1.tgz#4345d81a9a46a6486e24d069469f13e60445c05d"
integrity sha512-WnuuDILl9oOBbKnb4L+DyODx7iC47XfzmNCpTttFsSp6hTG7XZxu60+4IO+2/hPfcGOoKbFiwoI/+zwARbNQow==
@@ -834,7 +834,7 @@
dependencies:
"@babel/types" "^7.27.1"
"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.13.0", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.19.0", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.25.9", "@babel/helper-plugin-utils@^7.27.1", "@babel/helper-plugin-utils@^7.8.0":
"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.13.0", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.19.0", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.27.1", "@babel/helper-plugin-utils@^7.8.0":
version "7.27.1"
resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz#ddb2f876534ff8013e6c2b299bf4d39b3c51d44c"
integrity sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==
@@ -1663,7 +1663,7 @@
"@babel/parser" "^7.27.1"
"@babel/types" "^7.27.1"
"@babel/traverse@^7.13.0", "@babel/traverse@^7.25.9", "@babel/traverse@^7.27.1", "@babel/traverse@^7.4.5", "@babel/traverse@^7.7.0":
"@babel/traverse@^7.13.0", "@babel/traverse@^7.27.1", "@babel/traverse@^7.4.5", "@babel/traverse@^7.7.0":
version "7.27.1"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.27.1.tgz#4db772902b133bbddd1c4f7a7ee47761c1b9f291"
integrity sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==
@@ -1676,7 +1676,7 @@
debug "^4.3.1"
globals "^11.1.0"
"@babel/types@^7.0.0", "@babel/types@^7.25.9", "@babel/types@^7.27.1", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.7.0":
"@babel/types@^7.0.0", "@babel/types@^7.27.1", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4", "@babel/types@^7.7.0":
version "7.27.1"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.27.1.tgz#9defc53c16fc899e46941fc6901a9eea1c9d8560"
integrity sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==
@@ -11162,10 +11162,10 @@ jws@^3.2.2:
jwa "^1.4.1"
safe-buffer "^5.0.1"
katex@^0.16.21, katex@^0.16.9:
version "0.16.21"
resolved "https://registry.yarnpkg.com/katex/-/katex-0.16.21.tgz#8f63c659e931b210139691f2cc7bb35166b792a3"
integrity sha512-XvqR7FgOHtWupfMiigNzmh+MgUVmDGU2kXZm899ZkPfcuoPuFxyHmXsgATDpFZDAXCI8tvinaVcDo8PIIJSo4A==
katex@^0.16.22, katex@^0.16.9:
version "0.16.22"
resolved "https://registry.yarnpkg.com/katex/-/katex-0.16.22.tgz#d2b3d66464b1e6d69e6463b28a86ced5a02c5ccd"
integrity sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg==
dependencies:
commander "^8.3.0"