feat: support enable/disable plugin (#3605)
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { assertExists } from '@blocksuite/global/utils';
|
import { assertExists } from '@blocksuite/global/utils';
|
||||||
import { registeredPluginAtom, rootStore } from '@toeverything/infra/atom';
|
import { loadedPluginNameAtom, rootStore } from '@toeverything/infra/atom';
|
||||||
import { use } from 'foxact/use';
|
import { use } from 'foxact/use';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
import { Provider } from 'jotai/react';
|
import { Provider } from 'jotai/react';
|
||||||
@@ -17,7 +17,7 @@ async function main() {
|
|||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
use(pluginRegisterPromise);
|
use(pluginRegisterPromise);
|
||||||
const plugins = useAtomValue(registeredPluginAtom);
|
const plugins = useAtomValue(loadedPluginNameAtom);
|
||||||
_pluginNestedImportsMap.forEach(value => {
|
_pluginNestedImportsMap.forEach(value => {
|
||||||
const exports = value.get('index.js');
|
const exports = value.get('index.js');
|
||||||
assertExists(exports);
|
assertExists(exports);
|
||||||
|
|||||||
@@ -1,18 +1,26 @@
|
|||||||
import { DebugLogger } from '@affine/debug';
|
import { DebugLogger } from '@affine/debug';
|
||||||
import type { CallbackMap, PluginContext } from '@affine/sdk/entry';
|
import type {
|
||||||
|
CallbackMap,
|
||||||
|
ExpectedLayout,
|
||||||
|
LayoutNode,
|
||||||
|
PluginContext,
|
||||||
|
} from '@affine/sdk/entry';
|
||||||
import { FormatQuickBar } from '@blocksuite/blocks';
|
import { FormatQuickBar } from '@blocksuite/blocks';
|
||||||
import { assertExists } from '@blocksuite/global/utils';
|
import { assertExists } from '@blocksuite/global/utils';
|
||||||
import { DisposableGroup } from '@blocksuite/global/utils';
|
import {
|
||||||
|
addCleanup,
|
||||||
|
pluginEditorAtom,
|
||||||
|
pluginHeaderItemAtom,
|
||||||
|
pluginSettingAtom,
|
||||||
|
pluginWindowAtom,
|
||||||
|
} from '@toeverything/infra/__internal__/plugin';
|
||||||
import {
|
import {
|
||||||
contentLayoutAtom,
|
contentLayoutAtom,
|
||||||
currentPageAtom,
|
currentPageAtom,
|
||||||
currentWorkspaceAtom,
|
currentWorkspaceAtom,
|
||||||
editorItemsAtom,
|
|
||||||
headerItemsAtom,
|
|
||||||
rootStore,
|
rootStore,
|
||||||
settingItemsAtom,
|
|
||||||
windowItemsAtom,
|
|
||||||
} from '@toeverything/infra/atom';
|
} from '@toeverything/infra/atom';
|
||||||
|
import { atom } from 'jotai';
|
||||||
import { Provider } from 'jotai/react';
|
import { Provider } from 'jotai/react';
|
||||||
import { createElement, type PropsWithChildren } from 'react';
|
import { createElement, type PropsWithChildren } from 'react';
|
||||||
|
|
||||||
@@ -25,6 +33,84 @@ const dynamicImportKey = '$h_import';
|
|||||||
const permissionLogger = new DebugLogger('plugins:permission');
|
const permissionLogger = new DebugLogger('plugins:permission');
|
||||||
const importLogger = new DebugLogger('plugins:import');
|
const importLogger = new DebugLogger('plugins:import');
|
||||||
|
|
||||||
|
const pushLayoutAtom = atom<
|
||||||
|
null,
|
||||||
|
// fixme: check plugin name here
|
||||||
|
[pluginName: string, create: (root: HTMLElement) => () => void],
|
||||||
|
void
|
||||||
|
>(null, (_, set, pluginName, callback) => {
|
||||||
|
set(pluginWindowAtom, items => ({
|
||||||
|
...items,
|
||||||
|
[pluginName]: callback,
|
||||||
|
}));
|
||||||
|
set(contentLayoutAtom, layout => {
|
||||||
|
if (layout === 'editor') {
|
||||||
|
return {
|
||||||
|
direction: 'horizontal',
|
||||||
|
first: 'editor',
|
||||||
|
second: pluginName,
|
||||||
|
splitPercentage: 70,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
...layout,
|
||||||
|
direction: 'horizontal',
|
||||||
|
first: 'editor',
|
||||||
|
second: {
|
||||||
|
direction: 'horizontal',
|
||||||
|
// fixme: incorrect type here
|
||||||
|
first: layout.second,
|
||||||
|
second: pluginName,
|
||||||
|
splitPercentage: 70,
|
||||||
|
},
|
||||||
|
} as ExpectedLayout;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
addCleanup(pluginName, () => {
|
||||||
|
set(deleteLayoutAtom, pluginName);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteLayoutAtom = atom<null, [string], void>(null, (_, set, id) => {
|
||||||
|
set(pluginWindowAtom, items => {
|
||||||
|
const newItems = { ...items };
|
||||||
|
delete newItems[id];
|
||||||
|
return newItems;
|
||||||
|
});
|
||||||
|
const removeLayout = (layout: LayoutNode): LayoutNode => {
|
||||||
|
if (layout === 'editor') {
|
||||||
|
return 'editor';
|
||||||
|
} else {
|
||||||
|
if (typeof layout === 'string') {
|
||||||
|
return layout as ExpectedLayout;
|
||||||
|
}
|
||||||
|
if (layout.first === id) {
|
||||||
|
return layout.second;
|
||||||
|
} else if (layout.second === id) {
|
||||||
|
return layout.first;
|
||||||
|
} else {
|
||||||
|
return removeLayout(layout.second);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
set(contentLayoutAtom, layout => {
|
||||||
|
if (layout === 'editor') {
|
||||||
|
return 'editor';
|
||||||
|
} else {
|
||||||
|
if (typeof layout === 'string') {
|
||||||
|
return layout as ExpectedLayout;
|
||||||
|
}
|
||||||
|
if (layout.first === id) {
|
||||||
|
return layout.second as ExpectedLayout;
|
||||||
|
} else if (layout.second === id) {
|
||||||
|
return layout.first as ExpectedLayout;
|
||||||
|
} else {
|
||||||
|
return removeLayout(layout.second) as ExpectedLayout;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// module -> importName -> updater[]
|
// module -> importName -> updater[]
|
||||||
export const _rootImportsMap = new Map<string, Map<string, any>>();
|
export const _rootImportsMap = new Map<string, Map<string, any>>();
|
||||||
const rootImportsMapSetupPromise = setupImportsMap(_rootImportsMap, {
|
const rootImportsMapSetupPromise = setupImportsMap(_rootImportsMap, {
|
||||||
@@ -41,7 +127,8 @@ const rootImportsMapSetupPromise = setupImportsMap(_rootImportsMap, {
|
|||||||
rootStore: rootStore,
|
rootStore: rootStore,
|
||||||
currentWorkspaceAtom: currentWorkspaceAtom,
|
currentWorkspaceAtom: currentWorkspaceAtom,
|
||||||
currentPageAtom: currentPageAtom,
|
currentPageAtom: currentPageAtom,
|
||||||
contentLayoutAtom: contentLayoutAtom,
|
pushLayoutAtom: pushLayoutAtom,
|
||||||
|
deleteLayoutAtom: deleteLayoutAtom,
|
||||||
},
|
},
|
||||||
'@blocksuite/blocks/std': import('@blocksuite/blocks/std'),
|
'@blocksuite/blocks/std': import('@blocksuite/blocks/std'),
|
||||||
'@blocksuite/global/utils': import('@blocksuite/global/utils'),
|
'@blocksuite/global/utils': import('@blocksuite/global/utils'),
|
||||||
@@ -379,7 +466,6 @@ const PluginProvider = ({ children }: PropsWithChildren) =>
|
|||||||
children
|
children
|
||||||
);
|
);
|
||||||
|
|
||||||
const group = new DisposableGroup();
|
|
||||||
const entryLogger = new DebugLogger('plugin:entry');
|
const entryLogger = new DebugLogger('plugin:entry');
|
||||||
|
|
||||||
export const evaluatePluginEntry = (pluginName: string) => {
|
export const evaluatePluginEntry = (pluginName: string) => {
|
||||||
@@ -392,29 +478,50 @@ export const evaluatePluginEntry = (pluginName: string) => {
|
|||||||
register: (part, callback) => {
|
register: (part, callback) => {
|
||||||
entryLogger.info(`Registering ${pluginName} to ${part}`);
|
entryLogger.info(`Registering ${pluginName} to ${part}`);
|
||||||
if (part === 'headerItem') {
|
if (part === 'headerItem') {
|
||||||
rootStore.set(headerItemsAtom, items => ({
|
rootStore.set(pluginHeaderItemAtom, items => ({
|
||||||
...items,
|
...items,
|
||||||
[pluginName]: callback as CallbackMap['headerItem'],
|
[pluginName]: callback as CallbackMap['headerItem'],
|
||||||
}));
|
}));
|
||||||
|
addCleanup(pluginName, () => {
|
||||||
|
rootStore.set(pluginHeaderItemAtom, items => {
|
||||||
|
const newItems = { ...items };
|
||||||
|
delete newItems[pluginName];
|
||||||
|
return newItems;
|
||||||
|
});
|
||||||
|
});
|
||||||
} else if (part === 'editor') {
|
} else if (part === 'editor') {
|
||||||
rootStore.set(editorItemsAtom, items => ({
|
rootStore.set(pluginEditorAtom, items => ({
|
||||||
...items,
|
...items,
|
||||||
[pluginName]: callback as CallbackMap['editor'],
|
[pluginName]: callback as CallbackMap['editor'],
|
||||||
}));
|
}));
|
||||||
} else if (part === 'window') {
|
addCleanup(pluginName, () => {
|
||||||
rootStore.set(windowItemsAtom, items => ({
|
rootStore.set(pluginEditorAtom, items => {
|
||||||
...items,
|
const newItems = { ...items };
|
||||||
[pluginName]: callback as CallbackMap['window'],
|
delete newItems[pluginName];
|
||||||
}));
|
return newItems;
|
||||||
|
});
|
||||||
|
});
|
||||||
} else if (part === 'setting') {
|
} else if (part === 'setting') {
|
||||||
rootStore.set(settingItemsAtom, items => ({
|
rootStore.set(pluginSettingAtom, items => ({
|
||||||
...items,
|
...items,
|
||||||
[pluginName]: callback as CallbackMap['setting'],
|
[pluginName]: callback as CallbackMap['setting'],
|
||||||
}));
|
}));
|
||||||
|
addCleanup(pluginName, () => {
|
||||||
|
rootStore.set(pluginSettingAtom, items => {
|
||||||
|
const newItems = { ...items };
|
||||||
|
delete newItems[pluginName];
|
||||||
|
return newItems;
|
||||||
|
});
|
||||||
|
});
|
||||||
} else if (part === 'formatBar') {
|
} else if (part === 'formatBar') {
|
||||||
FormatQuickBar.customElements.push((page, getBlockRange) => {
|
FormatQuickBar.customElements.push((page, getBlockRange) => {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
(callback as CallbackMap['formatBar'])(div, page, getBlockRange);
|
const cleanup = (callback as CallbackMap['formatBar'])(
|
||||||
|
div,
|
||||||
|
page,
|
||||||
|
getBlockRange
|
||||||
|
);
|
||||||
|
addCleanup(pluginName, cleanup);
|
||||||
return div;
|
return div;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -428,5 +535,5 @@ export const evaluatePluginEntry = (pluginName: string) => {
|
|||||||
if (typeof cleanup !== 'function') {
|
if (typeof cleanup !== 'function') {
|
||||||
throw new Error('Plugin entry must return a function');
|
throw new Error('Plugin entry must return a function');
|
||||||
}
|
}
|
||||||
group.add(cleanup);
|
addCleanup(pluginName, cleanup);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,18 +1,16 @@
|
|||||||
import { DebugLogger } from '@affine/debug';
|
import { DebugLogger } from '@affine/debug';
|
||||||
import { registeredPluginAtom, rootStore } from '@toeverything/infra/atom';
|
import {
|
||||||
|
builtinPluginPaths,
|
||||||
|
enabledPluginAtom,
|
||||||
|
invokeCleanup,
|
||||||
|
pluginPackageJson,
|
||||||
|
} from '@toeverything/infra/__internal__/plugin';
|
||||||
|
import { loadedPluginNameAtom, rootStore } from '@toeverything/infra/atom';
|
||||||
import { packageJsonOutputSchema } from '@toeverything/infra/type';
|
import { packageJsonOutputSchema } from '@toeverything/infra/type';
|
||||||
import type { z } from 'zod';
|
import type { z } from 'zod';
|
||||||
|
|
||||||
import { evaluatePluginEntry, setupPluginCode } from './plugins/setup';
|
import { evaluatePluginEntry, setupPluginCode } from './plugins/setup';
|
||||||
|
|
||||||
const builtinPluginUrl = new Set([
|
|
||||||
'/plugins/bookmark',
|
|
||||||
'/plugins/copilot',
|
|
||||||
'/plugins/hello-world',
|
|
||||||
'/plugins/image-preview',
|
|
||||||
'/plugins/vue-hello-world',
|
|
||||||
]);
|
|
||||||
|
|
||||||
const logger = new DebugLogger('register-plugins');
|
const logger = new DebugLogger('register-plugins');
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
@@ -20,10 +18,44 @@ declare global {
|
|||||||
var __pluginPackageJson__: unknown[];
|
var __pluginPackageJson__: unknown[];
|
||||||
}
|
}
|
||||||
|
|
||||||
globalThis.__pluginPackageJson__ = [];
|
Object.defineProperty(globalThis, '__pluginPackageJson__', {
|
||||||
|
get() {
|
||||||
|
return rootStore.get(pluginPackageJson);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
rootStore.sub(enabledPluginAtom, () => {
|
||||||
|
const added = new Set<string>();
|
||||||
|
const removed = new Set<string>();
|
||||||
|
const enabledPlugin = new Set(rootStore.get(enabledPluginAtom));
|
||||||
|
enabledPlugin.forEach(pluginName => {
|
||||||
|
if (!enabledPluginSet.has(pluginName)) {
|
||||||
|
added.add(pluginName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
enabledPluginSet.forEach(pluginName => {
|
||||||
|
if (!enabledPlugin.has(pluginName)) {
|
||||||
|
removed.add(pluginName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// update plugins
|
||||||
|
enabledPluginSet.clear();
|
||||||
|
enabledPlugin.forEach(pluginName => {
|
||||||
|
enabledPluginSet.add(pluginName);
|
||||||
|
});
|
||||||
|
added.forEach(pluginName => {
|
||||||
|
evaluatePluginEntry(pluginName);
|
||||||
|
});
|
||||||
|
removed.forEach(pluginName => {
|
||||||
|
invokeCleanup(pluginName);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const enabledPluginSet = new Set(rootStore.get(enabledPluginAtom));
|
||||||
|
const loadedAssets = new Set<string>();
|
||||||
|
|
||||||
|
// we will load all plugins in parallel from builtinPlugins
|
||||||
export const pluginRegisterPromise = Promise.all(
|
export const pluginRegisterPromise = Promise.all(
|
||||||
[...builtinPluginUrl].map(url => {
|
[...builtinPluginPaths].map(url => {
|
||||||
return fetch(`${url}/package.json`)
|
return fetch(`${url}/package.json`)
|
||||||
.then(async res => {
|
.then(async res => {
|
||||||
const packageJson = (await res.json()) as z.infer<
|
const packageJson = (await res.json()) as z.infer<
|
||||||
@@ -38,28 +70,27 @@ export const pluginRegisterPromise = Promise.all(
|
|||||||
assets,
|
assets,
|
||||||
},
|
},
|
||||||
} = packageJson;
|
} = packageJson;
|
||||||
globalThis.__pluginPackageJson__.push(packageJson);
|
rootStore.set(pluginPackageJson, json => [...json, packageJson]);
|
||||||
logger.debug(`registering plugin ${pluginName}`);
|
logger.debug(`registering plugin ${pluginName}`);
|
||||||
logger.debug(`package.json: ${packageJson}`);
|
logger.debug(`package.json: ${packageJson}`);
|
||||||
if (!release && !runtimeConfig.enablePlugin) {
|
if (!release && !runtimeConfig.enablePlugin) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
if (
|
|
||||||
release === 'development' &&
|
|
||||||
process.env.NODE_ENV !== 'development'
|
|
||||||
) {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
const baseURL = url;
|
const baseURL = url;
|
||||||
const entryURL = `${baseURL}/${core}`;
|
const entryURL = `${baseURL}/${core}`;
|
||||||
rootStore.set(registeredPluginAtom, prev => [...prev, pluginName]);
|
rootStore.set(loadedPluginNameAtom, prev => [...prev, pluginName]);
|
||||||
await setupPluginCode(baseURL, pluginName, core);
|
await setupPluginCode(baseURL, pluginName, core);
|
||||||
console.log(`prepareImports for ${pluginName} done`);
|
console.log(`prepareImports for ${pluginName} done`);
|
||||||
await fetch(entryURL).then(async () => {
|
await fetch(entryURL).then(async () => {
|
||||||
if (assets.length > 0) {
|
if (assets.length > 0) {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
assets.map(async (asset: string) => {
|
assets.map(async (asset: string) => {
|
||||||
|
// todo(himself65): add assets into shadow dom
|
||||||
|
if (loadedAssets.has(asset)) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
if (asset.endsWith('.css')) {
|
if (asset.endsWith('.css')) {
|
||||||
|
loadedAssets.add(asset);
|
||||||
const res = await fetch(`${baseURL}/${asset}`);
|
const res = await fetch(`${baseURL}/${asset}`);
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
// todo: how to put css file into sandbox?
|
// todo: how to put css file into sandbox?
|
||||||
@@ -77,7 +108,12 @@ export const pluginRegisterPromise = Promise.all(
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
evaluatePluginEntry(pluginName);
|
if (!enabledPluginSet.has(pluginName)) {
|
||||||
|
logger.debug(`plugin ${pluginName} is not enabled`);
|
||||||
|
} else {
|
||||||
|
logger.debug(`plugin ${pluginName} is enabled`);
|
||||||
|
evaluatePluginEntry(pluginName);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
|
|||||||
@@ -1,55 +1,96 @@
|
|||||||
|
import { Switch } from '@affine/component';
|
||||||
import { SettingHeader } from '@affine/component/setting-components';
|
import { SettingHeader } from '@affine/component/setting-components';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
|
import type { CallbackMap } from '@affine/sdk/entry';
|
||||||
import {
|
import {
|
||||||
registeredPluginAtom,
|
addCleanup,
|
||||||
settingItemsAtom,
|
enabledPluginAtom,
|
||||||
} from '@toeverything/infra/atom';
|
pluginPackageJson,
|
||||||
import { useAtomValue } from 'jotai';
|
pluginSettingAtom,
|
||||||
import type { FC, ReactNode } from 'react';
|
} from '@toeverything/infra/__internal__/plugin';
|
||||||
import { useRef } from 'react';
|
import { loadedPluginNameAtom } from '@toeverything/infra/atom';
|
||||||
|
import type { packageJsonOutputSchema } from '@toeverything/infra/type';
|
||||||
|
import { useAtom, useAtomValue } from 'jotai/react';
|
||||||
|
import { startTransition, useCallback, useMemo } from 'react';
|
||||||
|
import type { z } from 'zod';
|
||||||
|
|
||||||
import { pluginItem } from './style.css';
|
import { pluginItemStyle } from './style.css';
|
||||||
|
|
||||||
const PluginSettingWrapper: FC<{
|
type PluginItemProps = {
|
||||||
id: string;
|
json: z.infer<typeof packageJsonOutputSchema>;
|
||||||
title?: ReactNode;
|
};
|
||||||
}> = ({ title, id }) => {
|
|
||||||
const Setting = useAtomValue(settingItemsAtom)[id];
|
type PluginSettingDetailProps = {
|
||||||
const disposeRef = useRef<(() => void) | null>(null);
|
pluginName: string;
|
||||||
|
create: CallbackMap['setting'];
|
||||||
|
};
|
||||||
|
|
||||||
|
const PluginSettingDetail = ({
|
||||||
|
pluginName,
|
||||||
|
create,
|
||||||
|
}: PluginSettingDetailProps) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div
|
||||||
{title ? <div className="title">{title}</div> : null}
|
ref={useCallback(
|
||||||
<div
|
(ref: HTMLDivElement | null) => {
|
||||||
ref={ref => {
|
if (ref) {
|
||||||
if (ref && Setting) {
|
const cleanup = create(ref);
|
||||||
setTimeout(() => {
|
addCleanup(pluginName, cleanup);
|
||||||
disposeRef.current = Setting(ref);
|
|
||||||
});
|
|
||||||
} else if (ref === null) {
|
|
||||||
setTimeout(() => {
|
|
||||||
disposeRef.current?.();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}}
|
},
|
||||||
/>
|
[pluginName, create]
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const PluginItem = ({ json }: PluginItemProps) => {
|
||||||
|
const [plugins, setEnabledPlugins] = useAtom(enabledPluginAtom);
|
||||||
|
const checked = useMemo(
|
||||||
|
() => plugins.includes(json.name),
|
||||||
|
[json.name, plugins]
|
||||||
|
);
|
||||||
|
const create = useAtomValue(pluginSettingAtom)[json.name];
|
||||||
|
return (
|
||||||
|
<div className={pluginItemStyle} key={json.name}>
|
||||||
|
<div>
|
||||||
|
{json.name}
|
||||||
|
<Switch
|
||||||
|
checked={checked}
|
||||||
|
onChange={useCallback(
|
||||||
|
(checked: boolean) => {
|
||||||
|
startTransition(() => {
|
||||||
|
setEnabledPlugins(plugins => {
|
||||||
|
if (checked) {
|
||||||
|
return [...plugins, json.name];
|
||||||
|
} else {
|
||||||
|
return plugins.filter(plugin => plugin !== json.name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[json.name, setEnabledPlugins]
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>{json.description}</div>
|
||||||
|
{create && <PluginSettingDetail pluginName={json.name} create={create} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Plugins = () => {
|
export const Plugins = () => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const allowedPlugins = useAtomValue(registeredPluginAtom);
|
const loadedPlugins = useAtomValue(loadedPluginNameAtom);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SettingHeader
|
<SettingHeader
|
||||||
title={'Plugins'}
|
title={'Plugins'}
|
||||||
subtitle={allowedPlugins.length === 0 && t['None yet']()}
|
subtitle={loadedPlugins.length === 0 && t['None yet']()}
|
||||||
data-testid="plugins-title"
|
data-testid="plugins-title"
|
||||||
/>
|
/>
|
||||||
{allowedPlugins.map(plugin => (
|
{useAtomValue(pluginPackageJson).map(json => (
|
||||||
<div className={pluginItem} key={plugin}>
|
<PluginItem json={json} key={json.name} />
|
||||||
<PluginSettingWrapper key={plugin} id={plugin} title={plugin} />
|
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { style } from '@vanilla-extract/css';
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
export const settingWrapper = style({
|
export const settingWrapperStyle = style({
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'flex-end',
|
justifyContent: 'flex-end',
|
||||||
@@ -8,7 +8,7 @@ export const settingWrapper = style({
|
|||||||
maxWidth: '250px',
|
maxWidth: '250px',
|
||||||
});
|
});
|
||||||
|
|
||||||
export const pluginItem = style({
|
export const pluginItemStyle = style({
|
||||||
borderBottom: '1px solid var(--affine-border-color)',
|
borderBottom: '1px solid var(--affine-border-color)',
|
||||||
transition: '0.3s',
|
transition: '0.3s',
|
||||||
padding: '24px 8px',
|
padding: '24px 8px',
|
||||||
|
|||||||
@@ -7,12 +7,16 @@ import { SidebarSwitch } from '@affine/component/app-sidebar/sidebar-header';
|
|||||||
import { isDesktop } from '@affine/env/constant';
|
import { isDesktop } from '@affine/env/constant';
|
||||||
import { CloseIcon, MinusIcon, RoundedRectangleIcon } from '@blocksuite/icons';
|
import { CloseIcon, MinusIcon, RoundedRectangleIcon } from '@blocksuite/icons';
|
||||||
import type { Page } from '@blocksuite/store';
|
import type { Page } from '@blocksuite/store';
|
||||||
import { headerItemsAtom } from '@toeverything/infra/atom';
|
import {
|
||||||
|
addCleanup,
|
||||||
|
pluginHeaderItemAtom,
|
||||||
|
} from '@toeverything/infra/__internal__/plugin';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useAtom, useAtomValue } from 'jotai';
|
import { useAtom, useAtomValue } from 'jotai';
|
||||||
import type { FC, HTMLAttributes, PropsWithChildren, ReactNode } from 'react';
|
import type { FC, HTMLAttributes, PropsWithChildren, ReactNode } from 'react';
|
||||||
import {
|
import {
|
||||||
forwardRef,
|
forwardRef,
|
||||||
|
startTransition,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
@@ -112,37 +116,39 @@ const WindowsAppControls = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const PluginHeader = () => {
|
const PluginHeader = () => {
|
||||||
const rootRef = useRef<HTMLDivElement>(null);
|
const headerItem = useAtomValue(pluginHeaderItemAtom);
|
||||||
const headerItems = useAtomValue(headerItemsAtom);
|
const pluginsRef = useRef<string[]>([]);
|
||||||
useEffect(() => {
|
return (
|
||||||
const root = rootRef.current;
|
<div
|
||||||
if (!root) {
|
className={styles.pluginHeaderItems}
|
||||||
return;
|
ref={useCallback(
|
||||||
}
|
(root: HTMLDivElement | null) => {
|
||||||
let disposes: (() => void)[] = [];
|
if (root) {
|
||||||
const renderTimeout = setTimeout(() => {
|
Object.entries(headerItem).forEach(([pluginName, create]) => {
|
||||||
disposes = Object.entries(headerItems).map(([id, headerItem]) => {
|
if (pluginsRef.current.includes(pluginName)) {
|
||||||
const div = document.createElement('div');
|
return;
|
||||||
div.setAttribute('plugin-id', id);
|
}
|
||||||
div.style.display = 'flex';
|
pluginsRef.current.push(pluginName);
|
||||||
const cleanup = headerItem(div);
|
const div = document.createElement('div');
|
||||||
root.appendChild(div);
|
div.setAttribute('plugin-id', pluginName);
|
||||||
return () => {
|
startTransition(() => {
|
||||||
cleanup();
|
const cleanup = create(div);
|
||||||
root.removeChild(div);
|
root.appendChild(div);
|
||||||
};
|
addCleanup(pluginName, () => {
|
||||||
});
|
pluginsRef.current = pluginsRef.current.filter(
|
||||||
});
|
name => name !== pluginName
|
||||||
|
);
|
||||||
return () => {
|
root.removeChild(div);
|
||||||
clearTimeout(renderTimeout);
|
cleanup();
|
||||||
setTimeout(() => {
|
});
|
||||||
disposes.forEach(dispose => dispose());
|
});
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
}, [headerItems]);
|
},
|
||||||
|
[headerItem]
|
||||||
return <div className={styles.pluginHeaderItems} ref={rootRef} />;
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Header = forwardRef<
|
export const Header = forwardRef<
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import './page-detail-editor.css';
|
import './page-detail-editor.css';
|
||||||
|
|
||||||
import { PageNotFoundError } from '@affine/env/constant';
|
import { PageNotFoundError } from '@affine/env/constant';
|
||||||
import type { CallbackMap, LayoutNode } from '@affine/sdk//entry';
|
import type { LayoutNode } from '@affine/sdk//entry';
|
||||||
import { rootBlockHubAtom } from '@affine/workspace/atom';
|
import { rootBlockHubAtom } from '@affine/workspace/atom';
|
||||||
import type { EditorContainer } from '@blocksuite/editor';
|
import type { EditorContainer } from '@blocksuite/editor';
|
||||||
import { assertExists } from '@blocksuite/global/utils';
|
import { assertExists } from '@blocksuite/global/utils';
|
||||||
@@ -9,15 +9,15 @@ import type { Page, Workspace } from '@blocksuite/store';
|
|||||||
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
|
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
|
||||||
import { useBlockSuiteWorkspacePage } from '@toeverything/hooks/use-block-suite-workspace-page';
|
import { useBlockSuiteWorkspacePage } from '@toeverything/hooks/use-block-suite-workspace-page';
|
||||||
import {
|
import {
|
||||||
contentLayoutAtom,
|
addCleanup,
|
||||||
editorItemsAtom,
|
pluginEditorAtom,
|
||||||
rootStore,
|
pluginWindowAtom,
|
||||||
windowItemsAtom,
|
} from '@toeverything/infra/__internal__/plugin';
|
||||||
} from '@toeverything/infra/atom';
|
import { contentLayoutAtom, rootStore } from '@toeverything/infra/atom';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useAtomValue, useSetAtom } from 'jotai';
|
import { useAtomValue, useSetAtom } from 'jotai';
|
||||||
import type { CSSProperties, FC, ReactElement } from 'react';
|
import type { CSSProperties, FC, ReactElement } from 'react';
|
||||||
import { memo, Suspense, useCallback, useEffect, useMemo, useRef } from 'react';
|
import { memo, Suspense, useCallback, useMemo } from 'react';
|
||||||
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
|
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
|
||||||
|
|
||||||
import { pageSettingFamily } from '../atoms';
|
import { pageSettingFamily } from '../atoms';
|
||||||
@@ -96,7 +96,7 @@ const EditorWrapper = memo(function EditorWrapper({
|
|||||||
if (onLoad) {
|
if (onLoad) {
|
||||||
dispose = onLoad(page, editor);
|
dispose = onLoad(page, editor);
|
||||||
}
|
}
|
||||||
const editorItems = rootStore.get(editorItemsAtom);
|
const editorItems = rootStore.get(pluginEditorAtom);
|
||||||
let disposes: (() => void)[] = [];
|
let disposes: (() => void)[] = [];
|
||||||
const renderTimeout = setTimeout(() => {
|
const renderTimeout = setTimeout(() => {
|
||||||
disposes = Object.entries(editorItems).map(([id, editorItem]) => {
|
disposes = Object.entries(editorItems).map(([id, editorItem]) => {
|
||||||
@@ -129,34 +129,28 @@ const EditorWrapper = memo(function EditorWrapper({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const PluginContentAdapter = memo<{
|
const PluginContentAdapter = memo<{
|
||||||
windowItem: CallbackMap['window'];
|
windowItem: (div: HTMLDivElement) => () => void;
|
||||||
}>(function PluginContentAdapter({ windowItem }) {
|
pluginName: string;
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
}>(function PluginContentAdapter({ windowItem, pluginName }) {
|
||||||
useEffect(() => {
|
return (
|
||||||
const root = ref.current;
|
<div
|
||||||
if (!root) {
|
className={pluginContainer}
|
||||||
return;
|
ref={useCallback(
|
||||||
}
|
(ref: HTMLDivElement | null) => {
|
||||||
let cleanup: () => void = () => {};
|
if (ref) {
|
||||||
let childDiv: HTMLDivElement | null = null;
|
const div = document.createElement('div');
|
||||||
const renderTimeout = setTimeout(() => {
|
const cleanup = windowItem(div);
|
||||||
const div = document.createElement('div');
|
ref.appendChild(div);
|
||||||
cleanup = windowItem(div);
|
addCleanup(pluginName, () => {
|
||||||
root.appendChild(div);
|
cleanup();
|
||||||
childDiv = div;
|
ref.removeChild(div);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
return () => {
|
},
|
||||||
clearTimeout(renderTimeout);
|
[pluginName, windowItem]
|
||||||
setTimeout(() => {
|
)}
|
||||||
cleanup();
|
/>
|
||||||
if (childDiv) {
|
);
|
||||||
root.removeChild(childDiv);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}, [windowItem]);
|
|
||||||
return <div className={pluginContainer} ref={ref} />;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
type LayoutPanelProps = {
|
type LayoutPanelProps = {
|
||||||
@@ -168,13 +162,13 @@ const LayoutPanel = memo(function LayoutPanel(
|
|||||||
props: LayoutPanelProps
|
props: LayoutPanelProps
|
||||||
): ReactElement {
|
): ReactElement {
|
||||||
const node = props.node;
|
const node = props.node;
|
||||||
const windowItems = useAtomValue(windowItemsAtom);
|
const windowItems = useAtomValue(pluginWindowAtom);
|
||||||
if (typeof node === 'string') {
|
if (typeof node === 'string') {
|
||||||
if (node === 'editor') {
|
if (node === 'editor') {
|
||||||
return <EditorWrapper {...props.editorProps} />;
|
return <EditorWrapper {...props.editorProps} />;
|
||||||
} else {
|
} else {
|
||||||
const windowItem = windowItems[node];
|
const windowItem = windowItems[node];
|
||||||
return <PluginContentAdapter windowItem={windowItem} />;
|
return <PluginContentAdapter pluginName={node} windowItem={windowItem} />;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -30,15 +30,10 @@
|
|||||||
"import": "./dist/type.js",
|
"import": "./dist/type.js",
|
||||||
"require": "./dist/type.cjs"
|
"require": "./dist/type.cjs"
|
||||||
},
|
},
|
||||||
"./__internal__/workspace": {
|
"./__internal__/*": {
|
||||||
"type": "./dist/src/__internal__/workspace.d.ts",
|
"type": "./dist/src/__internal__/*.d.ts",
|
||||||
"import": "./dist/__internal__/workspace.js",
|
"import": "./dist/__internal__/*.js",
|
||||||
"require": "./dist/__internal__/workspace.cjs"
|
"require": "./dist/__internal__/*.cjs"
|
||||||
},
|
|
||||||
"./__internal__/react": {
|
|
||||||
"type": "./dist/src/__internal__/react.d.ts",
|
|
||||||
"import": "./dist/__internal__/react.js",
|
|
||||||
"require": "./dist/__internal__/react.cjs"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
|
|||||||
51
packages/infra/src/__internal__/plugin.ts
Normal file
51
packages/infra/src/__internal__/plugin.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import type { CallbackMap } from '@affine/sdk/entry';
|
||||||
|
import { atomWithStorage } from 'jotai/utils';
|
||||||
|
import { atom } from 'jotai/vanilla';
|
||||||
|
import type { z } from 'zod';
|
||||||
|
|
||||||
|
import type { packageJsonOutputSchema } from '../type.js';
|
||||||
|
|
||||||
|
export const builtinPluginPaths = new Set([
|
||||||
|
'/plugins/bookmark',
|
||||||
|
'/plugins/copilot',
|
||||||
|
'/plugins/hello-world',
|
||||||
|
'/plugins/image-preview',
|
||||||
|
'/plugins/vue-hello-world',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const pluginCleanupMap = new Map<string, (() => void)[]>();
|
||||||
|
|
||||||
|
export function addCleanup(pluginName: string, cleanup: () => void) {
|
||||||
|
if (!pluginCleanupMap.has(pluginName)) {
|
||||||
|
pluginCleanupMap.set(pluginName, []);
|
||||||
|
}
|
||||||
|
pluginCleanupMap.get(pluginName)?.push(cleanup);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function invokeCleanup(pluginName: string) {
|
||||||
|
pluginCleanupMap.get(pluginName)?.forEach(cleanup => cleanup());
|
||||||
|
pluginCleanupMap.delete(pluginName);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const pluginPackageJson = atom<
|
||||||
|
z.infer<typeof packageJsonOutputSchema>[]
|
||||||
|
>([]);
|
||||||
|
|
||||||
|
export const enabledPluginAtom = atomWithStorage('affine-enabled-plugin', [
|
||||||
|
'@affine/bookmark-plugin',
|
||||||
|
'@affine/image-preview-plugin',
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const pluginHeaderItemAtom = atom<
|
||||||
|
Record<string, CallbackMap['headerItem']>
|
||||||
|
>({});
|
||||||
|
|
||||||
|
export const pluginSettingAtom = atom<Record<string, CallbackMap['setting']>>(
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const pluginEditorAtom = atom<Record<string, CallbackMap['editor']>>({});
|
||||||
|
|
||||||
|
export const pluginWindowAtom = atom<
|
||||||
|
Record<string, (root: HTMLElement) => () => void>
|
||||||
|
>({});
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { CallbackMap, ExpectedLayout } from '@affine/sdk/entry';
|
import type { ExpectedLayout } from '@affine/sdk/entry';
|
||||||
import { assertExists } from '@blocksuite/global/utils';
|
import { assertExists } from '@blocksuite/global/utils';
|
||||||
import type { Page, Workspace } from '@blocksuite/store';
|
import type { Page, Workspace } from '@blocksuite/store';
|
||||||
import { atom, createStore } from 'jotai/vanilla';
|
import { atom, createStore } from 'jotai/vanilla';
|
||||||
@@ -7,20 +7,7 @@ import { getWorkspace, waitForWorkspace } from './__internal__/workspace.js';
|
|||||||
|
|
||||||
// global store
|
// global store
|
||||||
export const rootStore = createStore();
|
export const rootStore = createStore();
|
||||||
|
export const loadedPluginNameAtom = atom<string[]>([]);
|
||||||
// id -> HTML element
|
|
||||||
export const headerItemsAtom = atom<Record<string, CallbackMap['headerItem']>>(
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
export const editorItemsAtom = atom<Record<string, CallbackMap['editor']>>({});
|
|
||||||
export const registeredPluginAtom = atom<string[]>([]);
|
|
||||||
export const windowItemsAtom = atom<Record<string, CallbackMap['window']>>({});
|
|
||||||
export const settingItemsAtom = atom<Record<string, CallbackMap['setting']>>(
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
export const formatBarItemsAtom = atom<
|
|
||||||
Record<string, CallbackMap['formatBar']>
|
|
||||||
>({});
|
|
||||||
|
|
||||||
export const currentWorkspaceIdAtom = atom<string | null>(null);
|
export const currentWorkspaceIdAtom = atom<string | null>(null);
|
||||||
export const currentPageIdAtom = atom<string | null>(null);
|
export const currentPageIdAtom = atom<string | null>(null);
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export default defineConfig({
|
|||||||
'src/__internal__/workspace.ts'
|
'src/__internal__/workspace.ts'
|
||||||
),
|
),
|
||||||
'__internal__/react': resolve(root, 'src/__internal__/react.ts'),
|
'__internal__/react': resolve(root, 'src/__internal__/react.ts'),
|
||||||
|
'__internal__/plugin': resolve(root, 'src/__internal__/plugin.ts'),
|
||||||
},
|
},
|
||||||
formats: ['es', 'cjs'],
|
formats: ['es', 'cjs'],
|
||||||
name: 'AffineInfra',
|
name: 'AffineInfra',
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ import type { getCurrentBlockRange } from '@blocksuite/blocks';
|
|||||||
import type { EditorContainer } from '@blocksuite/editor';
|
import type { EditorContainer } from '@blocksuite/editor';
|
||||||
import type { Page } from '@blocksuite/store';
|
import type { Page } from '@blocksuite/store';
|
||||||
import type { Workspace } from '@blocksuite/store';
|
import type { Workspace } from '@blocksuite/store';
|
||||||
import type { Atom, getDefaultStore, PrimitiveAtom } from 'jotai/vanilla';
|
import type { Atom, getDefaultStore } from 'jotai/vanilla';
|
||||||
|
import type { WritableAtom } from 'jotai/vanilla/atom';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
|
|
||||||
export type Part = 'headerItem' | 'editor' | 'window' | 'setting' | 'formatBar';
|
export type Part = 'headerItem' | 'editor' | 'setting' | 'formatBar';
|
||||||
|
|
||||||
export type CallbackMap = {
|
export type CallbackMap = {
|
||||||
headerItem: (root: HTMLElement) => () => void;
|
headerItem: (root: HTMLElement) => () => void;
|
||||||
window: (root: HTMLElement) => () => void;
|
|
||||||
editor: (root: HTMLElement, editor: EditorContainer) => () => void;
|
editor: (root: HTMLElement, editor: EditorContainer) => () => void;
|
||||||
setting: (root: HTMLElement) => () => void;
|
setting: (root: HTMLElement) => () => void;
|
||||||
formatBar: (
|
formatBar: (
|
||||||
@@ -31,13 +31,13 @@ export type LayoutNode = LayoutParentNode | string;
|
|||||||
export type LayoutParentNode = {
|
export type LayoutParentNode = {
|
||||||
direction: LayoutDirection;
|
direction: LayoutDirection;
|
||||||
splitPercentage: number; // 0 - 100
|
splitPercentage: number; // 0 - 100
|
||||||
first: LayoutNode;
|
first: string;
|
||||||
second: LayoutNode;
|
second: LayoutNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ExpectedLayout =
|
export type ExpectedLayout =
|
||||||
| {
|
| {
|
||||||
direction: LayoutDirection;
|
direction: 'horizontal';
|
||||||
// the first element is always the editor
|
// the first element is always the editor
|
||||||
first: 'editor';
|
first: 'editor';
|
||||||
second: LayoutNode;
|
second: LayoutNode;
|
||||||
@@ -46,7 +46,12 @@ export type ExpectedLayout =
|
|||||||
}
|
}
|
||||||
| 'editor';
|
| 'editor';
|
||||||
|
|
||||||
export declare const contentLayoutAtom: PrimitiveAtom<ExpectedLayout>;
|
export declare const pushLayoutAtom: WritableAtom<
|
||||||
|
null,
|
||||||
|
[string, (div: HTMLDivElement) => () => void],
|
||||||
|
void
|
||||||
|
>;
|
||||||
|
export declare const deleteLayoutAtom: WritableAtom<null, [string], void>;
|
||||||
export declare const currentPageAtom: Atom<Promise<Page>>;
|
export declare const currentPageAtom: Atom<Promise<Page>>;
|
||||||
export declare const currentWorkspaceAtom: Atom<Promise<Workspace>>;
|
export declare const currentWorkspaceAtom: Atom<Promise<Workspace>>;
|
||||||
export declare const rootStore: ReturnType<typeof getDefaultStore>;
|
export declare const rootStore: ReturnType<typeof getDefaultStore>;
|
||||||
|
|||||||
@@ -1,32 +1,43 @@
|
|||||||
import { IconButton, Tooltip } from '@affine/component';
|
import { IconButton, Tooltip } from '@affine/component';
|
||||||
import { contentLayoutAtom } from '@affine/sdk/entry';
|
import { deleteLayoutAtom, pushLayoutAtom } from '@affine/sdk/entry';
|
||||||
import { AiIcon } from '@blocksuite/icons';
|
import { AiIcon } from '@blocksuite/icons';
|
||||||
import { useSetAtom } from 'jotai';
|
import { useSetAtom } from 'jotai';
|
||||||
import type { ReactElement } from 'react';
|
import type { ComponentType, PropsWithChildren, ReactElement } from 'react';
|
||||||
import { useCallback } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
|
||||||
export const HeaderItem = (): ReactElement => {
|
import { DetailContent } from './detail-content';
|
||||||
const setLayout = useSetAtom(contentLayoutAtom);
|
|
||||||
|
export const HeaderItem = ({
|
||||||
|
Provider,
|
||||||
|
}: {
|
||||||
|
Provider: ComponentType<PropsWithChildren>;
|
||||||
|
}): ReactElement => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const pushLayout = useSetAtom(pushLayoutAtom);
|
||||||
|
const deleteLayout = useSetAtom(deleteLayoutAtom);
|
||||||
return (
|
return (
|
||||||
<Tooltip content="Chat with AI" placement="bottom-end">
|
<Tooltip content="Chat with AI" placement="bottom-end">
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={useCallback(
|
onClick={useCallback(() => {
|
||||||
() =>
|
if (!open) {
|
||||||
// todo: abstract a context function to open a new tab
|
setOpen(true);
|
||||||
setLayout(layout => {
|
pushLayout('@affine/copilot-plugin', div => {
|
||||||
if (layout === 'editor') {
|
const root = createRoot(div);
|
||||||
return {
|
root.render(
|
||||||
direction: 'horizontal',
|
<Provider>
|
||||||
first: 'editor',
|
<DetailContent />
|
||||||
second: '@affine/copilot-plugin',
|
</Provider>
|
||||||
splitPercentage: 70,
|
);
|
||||||
};
|
return () => {
|
||||||
} else {
|
root.unmount();
|
||||||
return 'editor';
|
};
|
||||||
}
|
});
|
||||||
}),
|
} else {
|
||||||
[setLayout]
|
setOpen(false);
|
||||||
)}
|
deleteLayout('@affine/copilot-plugin');
|
||||||
|
}
|
||||||
|
}, [Provider, deleteLayout, open, pushLayout])}
|
||||||
>
|
>
|
||||||
<AiIcon />
|
<AiIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|||||||
@@ -3,28 +3,19 @@ import { createElement } from 'react';
|
|||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
|
|
||||||
import { DebugContent } from './UI/debug-content';
|
import { DebugContent } from './UI/debug-content';
|
||||||
import { DetailContent } from './UI/detail-content';
|
|
||||||
import { HeaderItem } from './UI/header-item';
|
import { HeaderItem } from './UI/header-item';
|
||||||
|
|
||||||
export const entry = (context: PluginContext) => {
|
export const entry = (context: PluginContext) => {
|
||||||
console.log('copilot entry');
|
console.log('copilot entry');
|
||||||
context.register('headerItem', div => {
|
context.register('headerItem', div => {
|
||||||
const root = createRoot(div);
|
|
||||||
root.render(
|
|
||||||
createElement(context.utils.PluginProvider, {}, createElement(HeaderItem))
|
|
||||||
);
|
|
||||||
return () => {
|
|
||||||
root.unmount();
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
context.register('window', div => {
|
|
||||||
const root = createRoot(div);
|
const root = createRoot(div);
|
||||||
root.render(
|
root.render(
|
||||||
createElement(
|
createElement(
|
||||||
context.utils.PluginProvider,
|
context.utils.PluginProvider,
|
||||||
{},
|
{},
|
||||||
createElement(DetailContent)
|
createElement(HeaderItem, {
|
||||||
|
Provider: context.utils.PluginProvider,
|
||||||
|
})
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
return () => {
|
return () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user