From 8646221ee8df2f74cc1085ac3358f1d94b417397 Mon Sep 17 00:00:00 2001 From: CatsJuice Date: Fri, 26 Jul 2024 03:19:27 +0000 Subject: [PATCH] feat(infra): add ability to mount nodes to nearest FrameworkScope root (#7551) ![CleanShot 2024-07-19 at 12.52.08.gif](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/LakojjjzZNf6ogjOVwKE/dc5b8dc6-b7b2-4db2-83d5-7601c3f966d8.gif) --- .../infra/src/framework/react/index.tsx | 5 +- .../framework/react/scope-root-components.tsx | 74 +++++++++++++++++++ .../page-list/view/edit-collection/hooks.tsx | 36 +++++---- .../view/edit-collection/rules-mode.tsx | 3 +- 4 files changed, 102 insertions(+), 16 deletions(-) create mode 100644 packages/common/infra/src/framework/react/scope-root-components.tsx diff --git a/packages/common/infra/src/framework/react/index.tsx b/packages/common/infra/src/framework/react/index.tsx index ab032aca5a..367b6e1189 100644 --- a/packages/common/infra/src/framework/react/index.tsx +++ b/packages/common/infra/src/framework/react/index.tsx @@ -4,6 +4,9 @@ import type { FrameworkProvider, Scope, Service } from '../core'; import { ComponentNotFoundError, Framework } from '../core'; import { parseIdentifier } from '../core/identifier'; import type { GeneralIdentifier, IdentifierType, Type } from '../core/types'; +import { MountPoint } from './scope-root-components'; + +export { useMount } from './scope-root-components'; export const FrameworkStackContext = React.createContext([ Framework.EMPTY.provider(), @@ -126,7 +129,7 @@ export const FrameworkScope = ({ return ( - {children} + {children} ); }; diff --git a/packages/common/infra/src/framework/react/scope-root-components.tsx b/packages/common/infra/src/framework/react/scope-root-components.tsx new file mode 100644 index 0000000000..04b51ebcc5 --- /dev/null +++ b/packages/common/infra/src/framework/react/scope-root-components.tsx @@ -0,0 +1,74 @@ +import React from 'react'; + +type NodesMap = Map< + number, + { + node: React.ReactNode; + debugKey?: string; + } +>; + +const ScopeRootComponentsContext = React.createContext<{ + nodes: NodesMap; + setNodes: React.Dispatch>; +}>({ nodes: new Map(), setNodes: () => {} }); + +let _id = 0; +/** + * A hook to add nodes to the nearest scope's root + */ +export const useMount = (debugKey?: string) => { + const [id] = React.useState(_id++); + const { setNodes } = React.useContext(ScopeRootComponentsContext); + + const unmount = React.useCallback(() => { + setNodes(prev => { + if (!prev.has(id)) { + return prev; + } + const next = new Map(prev); + next.delete(id); + return next; + }); + }, [id, setNodes]); + + const mount = React.useCallback( + (node: React.ReactNode) => { + setNodes(prev => new Map(prev).set(id, { node, debugKey })); + return unmount; + }, + [setNodes, id, debugKey, unmount] + ); + + return React.useMemo(() => { + return { + /** + * Add a node to the nearest scope root + * ```tsx + * const { mount } = useMount(); + * useEffect(() => { + * const unmount = mount(
Node
); + * return unmount; + * }, []) + * ``` + * @return A function to unmount the added node. + */ + mount, + }; + }, [mount]); +}; + +export const MountPoint = ({ children }: React.PropsWithChildren) => { + const [nodes, setNodes] = React.useState(new Map()); + + return ( + + {children} + {Array.from(nodes.entries()).map(([id, { node, debugKey }]) => ( +
+ {node} +
+ ))} +
+ ); +}; diff --git a/packages/frontend/core/src/components/page-list/view/edit-collection/hooks.tsx b/packages/frontend/core/src/components/page-list/view/edit-collection/hooks.tsx index a9f7f95887..ce2403c29f 100644 --- a/packages/frontend/core/src/components/page-list/view/edit-collection/hooks.tsx +++ b/packages/frontend/core/src/components/page-list/view/edit-collection/hooks.tsx @@ -1,5 +1,6 @@ import { Modal } from '@affine/component'; -import { useCallback, useState } from 'react'; +import { useMount } from '@toeverything/infra'; +import { useCallback, useEffect, useState } from 'react'; import type { AllPageListConfig } from './edit-collection'; import { SelectPage } from './select-page'; @@ -20,8 +21,11 @@ export const useSelectPage = ({ const handleCancel = useCallback(() => { close(false); }, [close]); - return { - node: ( + + const { mount } = useMount('select-page-modal'); + + useEffect(() => { + return mount( ) : null} + ); + }, [allPageListConfig, close, handleCancel, mount, value]); + + return { + open: useCallback( + (init: string[]): Promise => + new Promise(res => { + onChange({ + init, + onConfirm: list => { + close(false); + res(list); + }, + }); + }), + [close] ), - open: (init: string[]): Promise => - new Promise(res => { - onChange({ - init, - onConfirm: list => { - close(false); - res(list); - }, - }); - }), }; }; diff --git a/packages/frontend/core/src/components/page-list/view/edit-collection/rules-mode.tsx b/packages/frontend/core/src/components/page-list/view/edit-collection/rules-mode.tsx index cdf475bd16..eb02f0db3f 100644 --- a/packages/frontend/core/src/components/page-list/view/edit-collection/rules-mode.tsx +++ b/packages/frontend/core/src/components/page-list/view/edit-collection/rules-mode.tsx @@ -72,7 +72,7 @@ export const RulesMode = ({ allowListPages.push(meta); } }); - const { node: selectPageNode, open } = useSelectPage({ allPageListConfig }); + const { open } = useSelectPage({ allPageListConfig }); const openSelectPage = useCallback(() => { open(collection.allowList).then( ids => { @@ -343,7 +343,6 @@ export const RulesMode = ({
{buttons}
- {selectPageNode} ); };