mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 05:14:54 +00:00
feat: support enable/disable plugin (#3605)
This commit is contained in:
@@ -1,55 +1,96 @@
|
||||
import { Switch } from '@affine/component';
|
||||
import { SettingHeader } from '@affine/component/setting-components';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import type { CallbackMap } from '@affine/sdk/entry';
|
||||
import {
|
||||
registeredPluginAtom,
|
||||
settingItemsAtom,
|
||||
} from '@toeverything/infra/atom';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import type { FC, ReactNode } from 'react';
|
||||
import { useRef } from 'react';
|
||||
addCleanup,
|
||||
enabledPluginAtom,
|
||||
pluginPackageJson,
|
||||
pluginSettingAtom,
|
||||
} from '@toeverything/infra/__internal__/plugin';
|
||||
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<{
|
||||
id: string;
|
||||
title?: ReactNode;
|
||||
}> = ({ title, id }) => {
|
||||
const Setting = useAtomValue(settingItemsAtom)[id];
|
||||
const disposeRef = useRef<(() => void) | null>(null);
|
||||
type PluginItemProps = {
|
||||
json: z.infer<typeof packageJsonOutputSchema>;
|
||||
};
|
||||
|
||||
type PluginSettingDetailProps = {
|
||||
pluginName: string;
|
||||
create: CallbackMap['setting'];
|
||||
};
|
||||
|
||||
const PluginSettingDetail = ({
|
||||
pluginName,
|
||||
create,
|
||||
}: PluginSettingDetailProps) => {
|
||||
return (
|
||||
<div>
|
||||
{title ? <div className="title">{title}</div> : null}
|
||||
<div
|
||||
ref={ref => {
|
||||
if (ref && Setting) {
|
||||
setTimeout(() => {
|
||||
disposeRef.current = Setting(ref);
|
||||
});
|
||||
} else if (ref === null) {
|
||||
setTimeout(() => {
|
||||
disposeRef.current?.();
|
||||
});
|
||||
<div
|
||||
ref={useCallback(
|
||||
(ref: HTMLDivElement | null) => {
|
||||
if (ref) {
|
||||
const cleanup = create(ref);
|
||||
addCleanup(pluginName, cleanup);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
},
|
||||
[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>
|
||||
);
|
||||
};
|
||||
|
||||
export const Plugins = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const allowedPlugins = useAtomValue(registeredPluginAtom);
|
||||
const loadedPlugins = useAtomValue(loadedPluginNameAtom);
|
||||
return (
|
||||
<>
|
||||
<SettingHeader
|
||||
title={'Plugins'}
|
||||
subtitle={allowedPlugins.length === 0 && t['None yet']()}
|
||||
subtitle={loadedPlugins.length === 0 && t['None yet']()}
|
||||
data-testid="plugins-title"
|
||||
/>
|
||||
{allowedPlugins.map(plugin => (
|
||||
<div className={pluginItem} key={plugin}>
|
||||
<PluginSettingWrapper key={plugin} id={plugin} title={plugin} />
|
||||
</div>
|
||||
{useAtomValue(pluginPackageJson).map(json => (
|
||||
<PluginItem json={json} key={json.name} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const settingWrapper = style({
|
||||
export const settingWrapperStyle = style({
|
||||
flexGrow: 1,
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
@@ -8,7 +8,7 @@ export const settingWrapper = style({
|
||||
maxWidth: '250px',
|
||||
});
|
||||
|
||||
export const pluginItem = style({
|
||||
export const pluginItemStyle = style({
|
||||
borderBottom: '1px solid var(--affine-border-color)',
|
||||
transition: '0.3s',
|
||||
padding: '24px 8px',
|
||||
|
||||
@@ -7,12 +7,16 @@ import { SidebarSwitch } from '@affine/component/app-sidebar/sidebar-header';
|
||||
import { isDesktop } from '@affine/env/constant';
|
||||
import { CloseIcon, MinusIcon, RoundedRectangleIcon } from '@blocksuite/icons';
|
||||
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 { useAtom, useAtomValue } from 'jotai';
|
||||
import type { FC, HTMLAttributes, PropsWithChildren, ReactNode } from 'react';
|
||||
import {
|
||||
forwardRef,
|
||||
startTransition,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
@@ -112,37 +116,39 @@ const WindowsAppControls = () => {
|
||||
};
|
||||
|
||||
const PluginHeader = () => {
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
const headerItems = useAtomValue(headerItemsAtom);
|
||||
useEffect(() => {
|
||||
const root = rootRef.current;
|
||||
if (!root) {
|
||||
return;
|
||||
}
|
||||
let disposes: (() => void)[] = [];
|
||||
const renderTimeout = setTimeout(() => {
|
||||
disposes = Object.entries(headerItems).map(([id, headerItem]) => {
|
||||
const div = document.createElement('div');
|
||||
div.setAttribute('plugin-id', id);
|
||||
div.style.display = 'flex';
|
||||
const cleanup = headerItem(div);
|
||||
root.appendChild(div);
|
||||
return () => {
|
||||
cleanup();
|
||||
root.removeChild(div);
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
clearTimeout(renderTimeout);
|
||||
setTimeout(() => {
|
||||
disposes.forEach(dispose => dispose());
|
||||
});
|
||||
};
|
||||
}, [headerItems]);
|
||||
|
||||
return <div className={styles.pluginHeaderItems} ref={rootRef} />;
|
||||
const headerItem = useAtomValue(pluginHeaderItemAtom);
|
||||
const pluginsRef = useRef<string[]>([]);
|
||||
return (
|
||||
<div
|
||||
className={styles.pluginHeaderItems}
|
||||
ref={useCallback(
|
||||
(root: HTMLDivElement | null) => {
|
||||
if (root) {
|
||||
Object.entries(headerItem).forEach(([pluginName, create]) => {
|
||||
if (pluginsRef.current.includes(pluginName)) {
|
||||
return;
|
||||
}
|
||||
pluginsRef.current.push(pluginName);
|
||||
const div = document.createElement('div');
|
||||
div.setAttribute('plugin-id', pluginName);
|
||||
startTransition(() => {
|
||||
const cleanup = create(div);
|
||||
root.appendChild(div);
|
||||
addCleanup(pluginName, () => {
|
||||
pluginsRef.current = pluginsRef.current.filter(
|
||||
name => name !== pluginName
|
||||
);
|
||||
root.removeChild(div);
|
||||
cleanup();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
[headerItem]
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const Header = forwardRef<
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import './page-detail-editor.css';
|
||||
|
||||
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 type { EditorContainer } from '@blocksuite/editor';
|
||||
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 { useBlockSuiteWorkspacePage } from '@toeverything/hooks/use-block-suite-workspace-page';
|
||||
import {
|
||||
contentLayoutAtom,
|
||||
editorItemsAtom,
|
||||
rootStore,
|
||||
windowItemsAtom,
|
||||
} from '@toeverything/infra/atom';
|
||||
addCleanup,
|
||||
pluginEditorAtom,
|
||||
pluginWindowAtom,
|
||||
} from '@toeverything/infra/__internal__/plugin';
|
||||
import { contentLayoutAtom, rootStore } from '@toeverything/infra/atom';
|
||||
import clsx from 'clsx';
|
||||
import { useAtomValue, useSetAtom } from 'jotai';
|
||||
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 { pageSettingFamily } from '../atoms';
|
||||
@@ -96,7 +96,7 @@ const EditorWrapper = memo(function EditorWrapper({
|
||||
if (onLoad) {
|
||||
dispose = onLoad(page, editor);
|
||||
}
|
||||
const editorItems = rootStore.get(editorItemsAtom);
|
||||
const editorItems = rootStore.get(pluginEditorAtom);
|
||||
let disposes: (() => void)[] = [];
|
||||
const renderTimeout = setTimeout(() => {
|
||||
disposes = Object.entries(editorItems).map(([id, editorItem]) => {
|
||||
@@ -129,34 +129,28 @@ const EditorWrapper = memo(function EditorWrapper({
|
||||
});
|
||||
|
||||
const PluginContentAdapter = memo<{
|
||||
windowItem: CallbackMap['window'];
|
||||
}>(function PluginContentAdapter({ windowItem }) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
useEffect(() => {
|
||||
const root = ref.current;
|
||||
if (!root) {
|
||||
return;
|
||||
}
|
||||
let cleanup: () => void = () => {};
|
||||
let childDiv: HTMLDivElement | null = null;
|
||||
const renderTimeout = setTimeout(() => {
|
||||
const div = document.createElement('div');
|
||||
cleanup = windowItem(div);
|
||||
root.appendChild(div);
|
||||
childDiv = div;
|
||||
});
|
||||
|
||||
return () => {
|
||||
clearTimeout(renderTimeout);
|
||||
setTimeout(() => {
|
||||
cleanup();
|
||||
if (childDiv) {
|
||||
root.removeChild(childDiv);
|
||||
}
|
||||
});
|
||||
};
|
||||
}, [windowItem]);
|
||||
return <div className={pluginContainer} ref={ref} />;
|
||||
windowItem: (div: HTMLDivElement) => () => void;
|
||||
pluginName: string;
|
||||
}>(function PluginContentAdapter({ windowItem, pluginName }) {
|
||||
return (
|
||||
<div
|
||||
className={pluginContainer}
|
||||
ref={useCallback(
|
||||
(ref: HTMLDivElement | null) => {
|
||||
if (ref) {
|
||||
const div = document.createElement('div');
|
||||
const cleanup = windowItem(div);
|
||||
ref.appendChild(div);
|
||||
addCleanup(pluginName, () => {
|
||||
cleanup();
|
||||
ref.removeChild(div);
|
||||
});
|
||||
}
|
||||
},
|
||||
[pluginName, windowItem]
|
||||
)}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
type LayoutPanelProps = {
|
||||
@@ -168,13 +162,13 @@ const LayoutPanel = memo(function LayoutPanel(
|
||||
props: LayoutPanelProps
|
||||
): ReactElement {
|
||||
const node = props.node;
|
||||
const windowItems = useAtomValue(windowItemsAtom);
|
||||
const windowItems = useAtomValue(pluginWindowAtom);
|
||||
if (typeof node === 'string') {
|
||||
if (node === 'editor') {
|
||||
return <EditorWrapper {...props.editorProps} />;
|
||||
} else {
|
||||
const windowItem = windowItems[node];
|
||||
return <PluginContentAdapter windowItem={windowItem} />;
|
||||
return <PluginContentAdapter pluginName={node} windowItem={windowItem} />;
|
||||
}
|
||||
} else {
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user