feat: init @affine/copilot (#2511)

This commit is contained in:
Himself65
2023-05-30 18:02:49 +08:00
committed by GitHub
parent f669164674
commit 6648fe4dcc
49 changed files with 2963 additions and 1331 deletions
@@ -1,7 +0,0 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`ProviderComposer 1`] = `
<DocumentFragment>
test1
</DocumentFragment>
`;
@@ -1,7 +0,0 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`ProviderComposer 1`] = `
<DocumentFragment>
test1
</DocumentFragment>
`;
@@ -1,31 +0,0 @@
/**
* @vitest-environment happy-dom
*/
import { render } from '@testing-library/react';
import type React from 'react';
import { createContext, useContext } from 'react';
import { expect, test } from 'vitest';
import { ProviderComposer } from '../provider-composer';
test('ProviderComposer', async () => {
const Context = createContext('null');
const Provider: React.FC<React.PropsWithChildren> = ({ children }) => {
return <Context.Provider value="test1">{children}</Context.Provider>;
};
const ConsumerComponent = () => {
const value = useContext(Context);
return <>{value}</>;
};
const Component = () => {
return (
<ProviderComposer contexts={[<Provider key={1} />]}>
<ConsumerComponent />
</ProviderComposer>
);
};
const result = render(<Component />);
await result.findByText('test1');
expect(result.asFragment()).toMatchSnapshot();
});
@@ -7,11 +7,14 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { WorkspaceFlavour } from '@affine/workspace/type';
import { CloseIcon, MinusIcon, RoundedRectangleIcon } from '@blocksuite/icons';
import type { Page } from '@blocksuite/store';
import { affinePluginsAtom } from '@toeverything/plugin-infra/manager';
import type { PluginUIAdapter } from '@toeverything/plugin-infra/type';
import { useAtom, useAtomValue } from 'jotai';
import type { FC, HTMLAttributes, PropsWithChildren, ReactNode } from 'react';
import {
forwardRef,
lazy,
memo,
Suspense,
useEffect,
useMemo,
@@ -19,6 +22,7 @@ import {
} from 'react';
import { guideDownloadClientTipAtom } from '../../../atoms/guide';
import { contentLayoutAtom } from '../../../atoms/layout';
import { useCurrentMode } from '../../../hooks/current/use-current-mode';
import type { AffineOfficialWorkspace } from '../../../shared';
import { DownloadClientTip } from './download-tips';
@@ -149,6 +153,43 @@ const HeaderRightItems: Record<HeaderRightItemName, HeaderItem> = {
export type HeaderProps = BaseHeaderProps;
const PluginHeaderItemAdapter = memo<{
headerItem: PluginUIAdapter['headerItem'];
}>(function PluginHeaderItemAdapter({ headerItem }) {
return (
<div>
{headerItem({
contentLayoutAtom,
})}
</div>
);
});
const PluginHeader = () => {
const affinePluginsMap = useAtomValue(affinePluginsAtom);
const plugins = useMemo(
() => Object.values(affinePluginsMap),
[affinePluginsMap]
);
return (
<div>
{plugins
.filter(plugin => plugin.uiAdapter.headerItem != null)
.map(plugin => {
const headerItem = plugin.uiAdapter
.headerItem as PluginUIAdapter['headerItem'];
return (
<PluginHeaderItemAdapter
key={plugin.definition.id}
headerItem={headerItem}
/>
);
})}
</div>
);
};
export const Header = forwardRef<
HTMLDivElement,
PropsWithChildren<HeaderProps> & HTMLAttributes<HTMLDivElement>
@@ -169,6 +210,7 @@ export const Header = forwardRef<
const appSidebarFloating = useAtomValue(appSidebarFloatingAtom);
const mode = useCurrentMode();
return (
<div
className={styles.headerContainer}
@@ -209,6 +251,7 @@ export const Header = forwardRef<
{props.children}
<div className={styles.headerRightSide}>
<PluginHeader />
{useMemo(() => {
return Object.entries(HeaderRightItems).map(
([name, { availableWhen, Component }]) => {
+67 -8
View File
@@ -7,12 +7,24 @@ import { assertExists } from '@blocksuite/store';
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
import { useBlockSuiteWorkspacePage } from '@toeverything/hooks/use-block-suite-workspace-page';
import { useBlockSuiteWorkspacePageTitle } from '@toeverything/hooks/use-block-suite-workspace-page-title';
import { useAtomValue, useSetAtom } from 'jotai';
import { affinePluginsAtom } from '@toeverything/plugin-infra/manager';
import type { PluginUIAdapter } from '@toeverything/plugin-infra/type';
import type { ExpectedLayout } from '@toeverything/plugin-infra/type';
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import Head from 'next/head';
import type React from 'react';
import { lazy, memo, startTransition, useCallback } from 'react';
import type { FC } from 'react';
import React, {
lazy,
memo,
startTransition,
Suspense,
useCallback,
useMemo,
} from 'react';
import type { MosaicNode } from 'react-mosaic-component';
import { currentEditorAtom, workspacePreferredModeAtom } from '../atoms';
import { contentLayoutAtom } from '../atoms/layout';
import type { AffineOfficialWorkspace } from '../shared';
import { BlockSuiteEditor as Editor } from './blocksuite/block-suite-editor';
@@ -86,7 +98,19 @@ const EditorWrapper = memo(function EditorWrapper({
);
});
export const PageDetailEditor: React.FC<PageDetailEditorProps> = props => {
const PluginContentAdapter = memo<{
detailContent: PluginUIAdapter['detailContent'];
}>(function PluginContentAdapter({ detailContent }) {
return (
<div>
{detailContent({
contentLayoutAtom,
})}
</div>
);
});
export const PageDetailEditor: FC<PageDetailEditorProps> = props => {
const { workspace, pageId } = props;
const blockSuiteWorkspace = workspace.blockSuiteWorkspace;
const page = useBlockSuiteWorkspacePage(blockSuiteWorkspace, pageId);
@@ -94,22 +118,57 @@ export const PageDetailEditor: React.FC<PageDetailEditorProps> = props => {
throw new PageNotFoundError(blockSuiteWorkspace, pageId);
}
const title = useBlockSuiteWorkspacePageTitle(blockSuiteWorkspace, pageId);
const affinePluginsMap = useAtomValue(affinePluginsAtom);
const plugins = useMemo(
() => Object.values(affinePluginsMap),
[affinePluginsMap]
);
const [layout, setLayout] = useAtom(contentLayoutAtom);
return (
<>
<Head>
<title>{title}</title>
</Head>
<Mosaic
onChange={useCallback(() => {}, [])}
onChange={useCallback(
(_: MosaicNode<string | number> | null) => {
// type cast
const node = _ as MosaicNode<string> | null;
if (node) {
if (typeof node === 'string') {
console.error('unexpected layout');
} else {
if (node.splitPercentage && node.splitPercentage < 70) {
return;
} else if (node.first !== 'editor') {
return;
}
setLayout(node as ExpectedLayout);
}
}
},
[setLayout]
)}
renderTile={id => {
if (id === 'editor') {
return <EditorWrapper {...props} />;
} else {
// @affine/copilot and other plugins will be added in the future
throw new Unreachable();
const plugin = plugins.find(plugin => plugin.definition.id === id);
if (plugin && plugin.uiAdapter.detailContent) {
return (
<Suspense>
<PluginContentAdapter
detailContent={plugin.uiAdapter.detailContent}
/>
</Suspense>
);
}
}
throw new Unreachable();
}}
value="editor"
value={layout}
/>
</>
);
@@ -1,15 +0,0 @@
import type { FC, PropsWithChildren, ReactNode } from 'react';
import { cloneElement } from 'react';
export const ProviderComposer: FC<
PropsWithChildren<{
contexts: any;
}>
> = ({ contexts, children }) =>
contexts.reduceRight(
(kids: ReactNode, parent: any) =>
cloneElement(parent, {
children: kids,
}),
children
);