mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 04:18:54 +00:00
feat(infra): add ability to mount nodes to nearest FrameworkScope root (#7551)

This commit is contained in:
@@ -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<FrameworkProvider[]>([
|
||||
Framework.EMPTY.provider(),
|
||||
@@ -126,7 +129,7 @@ export const FrameworkScope = ({
|
||||
|
||||
return (
|
||||
<FrameworkStackContext.Provider value={nextStack}>
|
||||
{children}
|
||||
<MountPoint>{children}</MountPoint>
|
||||
</FrameworkStackContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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<React.SetStateAction<NodesMap>>;
|
||||
}>({ 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(<div>Node</div>);
|
||||
* return unmount;
|
||||
* }, [])
|
||||
* ```
|
||||
* @return A function to unmount the added node.
|
||||
*/
|
||||
mount,
|
||||
};
|
||||
}, [mount]);
|
||||
};
|
||||
|
||||
export const MountPoint = ({ children }: React.PropsWithChildren) => {
|
||||
const [nodes, setNodes] = React.useState<NodesMap>(new Map());
|
||||
|
||||
return (
|
||||
<ScopeRootComponentsContext.Provider value={{ nodes, setNodes }}>
|
||||
{children}
|
||||
{Array.from(nodes.entries()).map(([id, { node, debugKey }]) => (
|
||||
<div data-testid={debugKey} key={id} style={{ display: 'contents' }}>
|
||||
{node}
|
||||
</div>
|
||||
))}
|
||||
</ScopeRootComponentsContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -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(
|
||||
<Modal
|
||||
open={!!value}
|
||||
onOpenChange={close}
|
||||
@@ -47,16 +51,22 @@ export const useSelectPage = ({
|
||||
/>
|
||||
) : null}
|
||||
</Modal>
|
||||
);
|
||||
}, [allPageListConfig, close, handleCancel, mount, value]);
|
||||
|
||||
return {
|
||||
open: useCallback(
|
||||
(init: string[]): Promise<string[]> =>
|
||||
new Promise<string[]>(res => {
|
||||
onChange({
|
||||
init,
|
||||
onConfirm: list => {
|
||||
close(false);
|
||||
res(list);
|
||||
},
|
||||
});
|
||||
}),
|
||||
[close]
|
||||
),
|
||||
open: (init: string[]): Promise<string[]> =>
|
||||
new Promise<string[]>(res => {
|
||||
onChange({
|
||||
init,
|
||||
onConfirm: list => {
|
||||
close(false);
|
||||
res(list);
|
||||
},
|
||||
});
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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 = ({
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>{buttons}</div>
|
||||
</div>
|
||||
{selectPageNode}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user