refactor(infra): directory structure (#4615)

This commit is contained in:
Joooye_34
2023-10-18 23:30:08 +08:00
committed by GitHub
parent 814d552be8
commit bed9310519
1150 changed files with 539 additions and 584 deletions

View File

@@ -7,10 +7,10 @@
"include": ["e2e"],
"references": [
{
"path": "../kit"
"path": "../../tests/kit"
},
{
"path": "../fixtures"
"path": "../../tests/fixtures"
}
]
}

View File

@@ -7,10 +7,10 @@
"include": ["e2e"],
"references": [
{
"path": "../kit"
"path": "../../tests/kit"
},
{
"path": "../fixtures"
"path": "../../tests/fixtures"
}
]
}

View File

@@ -7,10 +7,10 @@
"include": ["e2e"],
"references": [
{
"path": "../kit"
"path": "../../tests/kit"
},
{
"path": "../fixtures"
"path": "../../tests/fixtures"
}
]
}

View File

@@ -7,10 +7,10 @@
"include": ["e2e"],
"references": [
{
"path": "../../fixtures"
"path": "../../../tests/kit"
},
{
"path": "../../kit"
"path": "../../../tests/fixtures"
}
]
}

View File

@@ -7,10 +7,10 @@
"include": ["e2e"],
"references": [
{
"path": "../../fixtures"
"path": "../../../tests/kit"
},
{
"path": "../../kit"
"path": "../../../tests/fixtures"
}
]
}

View File

@@ -7,10 +7,10 @@
"include": ["e2e"],
"references": [
{
"path": "../../fixtures"
"path": "../../../tests/kit"
},
{
"path": "../../kit"
"path": "../../../tests/fixtures"
}
]
}

View File

@@ -7,10 +7,10 @@
"include": ["e2e"],
"references": [
{
"path": "../kit"
"path": "../../tests/kit"
},
{
"path": "../fixtures"
"path": "../../tests/fixtures"
}
]
}

View File

@@ -8,10 +8,10 @@
"include": ["e2e"],
"references": [
{
"path": "../fixtures"
"path": "../../tests/kit"
},
{
"path": "../kit"
"path": "../../tests/fixtures"
}
]
}

View File

@@ -7,10 +7,10 @@
"include": ["e2e"],
"references": [
{
"path": "../kit"
"path": "../../tests/kit"
},
{
"path": "../fixtures"
"path": "../../tests/fixtures"
}
]
}

View File

@@ -15,7 +15,7 @@ import {
import { removeWithRetry } from './utils/utils';
const projectRoot = join(__dirname, '..', '..');
const electronRoot = join(projectRoot, 'apps', 'electron');
const electronRoot = join(projectRoot, 'packages/frontend/electron');
function generateUUID() {
return crypto.randomUUID();

View File

@@ -48,14 +48,14 @@ export const runPrisma = async <T>(
cb: (
prisma: InstanceType<
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
typeof import('../../../apps/server/node_modules/@prisma/client').PrismaClient
typeof import('../../../packages/backend/server/node_modules/@prisma/client').PrismaClient
>
) => Promise<T>
): Promise<T> => {
const {
PrismaClient,
// eslint-disable-next-line @typescript-eslint/no-var-requires
} = require('../../../apps/server/node_modules/@prisma/client');
} = require('../../../packages/backend/server/node_modules/@prisma/client');
const client = new PrismaClient();
await client.$connect();
try {

View File

@@ -0,0 +1,70 @@
import { runCli } from '@magic-works/i18n-codegen';
import type { StorybookConfig } from '@storybook/react-vite';
import { fileURLToPath } from 'node:url';
import { mergeConfig } from 'vite';
import tsconfigPaths from 'vite-tsconfig-paths';
import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin';
import { getRuntimeConfig } from '../../../packages/frontend/core/.webpack/runtime-config';
runCli(
{
config: fileURLToPath(
new URL('../../../.i18n-codegen.json', import.meta.url)
),
watch: false,
},
error => {
console.error(error);
process.exit(1);
}
);
export default {
stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
staticDirs: ['../../../packages/frontend/core/public'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-interactions',
'@storybook/addon-storysource',
'storybook-dark-mode',
'storybook-addon-react-router-v6',
],
framework: {
name: '@storybook/react-vite',
},
async viteFinal(config, _options) {
return mergeConfig(config, {
assetsInclude: ['**/*.md'],
resolve: {
alias: {
'@toeverything/infra': fileURLToPath(
new URL('../../../packages/common/infra/src', import.meta.url)
),
},
},
plugins: [
vanillaExtractPlugin(),
tsconfigPaths({
root: fileURLToPath(new URL('../../../', import.meta.url)),
ignoreConfigErrors: true,
}),
],
define: {
'process.env': {},
'process.env.COVERAGE': JSON.stringify(!!process.env.COVERAGE),
'process.env.SHOULD_REPORT_TRACE': `${Boolean(
process.env.SHOULD_REPORT_TRACE === 'true'
)}`,
'process.env.TRACE_REPORT_ENDPOINT': `"${process.env.TRACE_REPORT_ENDPOINT}"`,
'process.env.CAPTCHA_SITE_KEY': `"${process.env.CAPTCHA_SITE_KEY}"`,
runtimeConfig: getRuntimeConfig({
distribution: 'browser',
mode: 'development',
channel: 'canary',
coverage: false,
}),
},
});
},
} as StorybookConfig;

View File

@@ -0,0 +1,3 @@
<script>
window.global = window;
</script>

View File

@@ -0,0 +1,200 @@
import 'ses';
import '@affine/component/theme/global.css';
import '@affine/component/theme/theme.css';
import '@toeverything/components/style.css';
import { createI18n } from '@affine/i18n';
import MockSessionContext, {
mockAuthStates,
// @ts-ignore
} from '@tomfreudenberg/next-auth-mock';
import { ThemeProvider, useTheme } from 'next-themes';
import { useDarkMode } from 'storybook-dark-mode';
import { AffineContext } from '@affine/component/context';
import useSWR from 'swr';
import type { Decorator } from '@storybook/react';
import { createStore } from 'jotai/vanilla';
import { _setCurrentStore } from '@toeverything/infra/atom';
import { setupGlobal } from '@affine/env/global';
import type { Preview } from '@storybook/react';
import { useLayoutEffect, useRef } from 'react';
setupGlobal();
export const parameters = {
backgrounds: { disable: true },
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
};
const SB_PARAMETER_KEY = 'nextAuthMock';
export const mockAuthPreviewToolbarItem = ({
name = 'mockAuthState',
description = 'Set authentication state',
defaultValue = null,
icon = 'user',
items = mockAuthStates,
} = {}) => {
return {
mockAuthState: {
name,
description,
defaultValue,
toolbar: {
icon,
items: Object.keys(items).map(e => ({
value: e,
title: items[e].title,
})),
},
},
};
};
export const withMockAuth: Decorator = (Story, context) => {
// Set a session value for mocking
const session = (() => {
// Allow overwrite of session value by parameter in story
const paramValue = context?.parameters[SB_PARAMETER_KEY];
if (typeof paramValue?.session === 'string') {
return mockAuthStates[paramValue.session]?.session;
} else {
return paramValue?.session
? paramValue.session
: mockAuthStates[context.globals.mockAuthState]?.session;
}
})();
return (
<MockSessionContext session={session}>
<Story {...context} />
</MockSessionContext>
);
};
const i18n = createI18n();
const withI18n: Decorator = (Story, context) => {
const locale = context.globals.locale;
useSWR(
locale,
async () => {
await i18n.changeLanguage(locale);
},
{
suspense: true,
}
);
return <Story {...context} />;
};
const ThemeChange = () => {
const isDark = useDarkMode();
const theme = useTheme();
if (theme.resolvedTheme === 'dark' && !isDark) {
theme.setTheme('light');
} else if (theme.resolvedTheme === 'light' && isDark) {
theme.setTheme('dark');
}
return null;
};
const storeMap = new Map<string, ReturnType<typeof createStore>>();
const bootstrapPluginSystemPromise = import(
'@affine/core/bootstrap/register-plugins'
).then(({ bootstrapPluginSystem }) => bootstrapPluginSystem);
const setupPromise = import('@affine/core/bootstrap/setup').then(
({ setup }) => setup
);
const withContextDecorator: Decorator = (Story, context) => {
const { data: store } = useSWR(
context.id,
async () => {
if (storeMap.has(context.id)) {
return storeMap.get(context.id);
}
localStorage.clear();
const store = createStore();
_setCurrentStore(store);
const setup = await setupPromise;
await setup(store);
const bootstrapPluginSystem = await bootstrapPluginSystemPromise;
await bootstrapPluginSystem(store);
storeMap.set(context.id, store);
return store;
},
{
suspense: true,
}
);
return (
<ThemeProvider>
<AffineContext store={store}>
<ThemeChange />
<Story {...context} />
</AffineContext>
</ThemeProvider>
);
};
const platforms = ['web', 'desktop-macos', 'desktop-windows'] as const;
const withPlatformSelectionDecorator: Decorator = (Story, context) => {
const setupCounterRef = useRef(0);
useLayoutEffect(() => {
if (setupCounterRef.current++ === 0) {
return;
}
switch (context.globals.platform) {
case 'desktop-macos':
environment.isDesktop = true;
environment.isMacOs = true;
environment.isWindows = false;
break;
case 'desktop-windows':
environment.isDesktop = true;
environment.isMacOs = false;
environment.isWindows = true;
break;
default:
globalThis.$AFFINE_SETUP = false;
setupGlobal();
break;
}
}, [context.globals.platform]);
return <Story key={context.globals.platform} {...context} />;
};
const decorators = [
withContextDecorator,
withI18n,
withMockAuth,
withPlatformSelectionDecorator,
];
const preview: Preview = {
decorators,
globalTypes: {
platform: {
description: 'Rendering platform target',
defaultValue: 'web',
toolbar: {
// The label to show for this toolbar item
title: 'platform',
// Array of plain string values or MenuItem shape (see below)
items: platforms,
// Change title based on selected value
dynamicTitle: true,
},
},
},
};
export default preview;

View File

@@ -0,0 +1 @@
# Storybook

View File

@@ -0,0 +1,61 @@
{
"name": "@affine/storybook",
"private": true,
"scripts": {
"dev": "storybook dev -p 6006",
"build": "storybook build",
"test": "test-storybook"
},
"dependencies": {
"@affine/component": "workspace:*",
"@affine/i18n": "workspace:*",
"@mui/material": "^5.14.13",
"@storybook/addon-actions": "^7.4.6",
"@storybook/addon-essentials": "^7.4.6",
"@storybook/addon-interactions": "^7.4.6",
"@storybook/addon-links": "^7.4.6",
"@storybook/addon-storysource": "^7.4.6",
"@storybook/blocks": "^7.4.6",
"@storybook/builder-vite": "^7.4.6",
"@storybook/jest": "^0.2.3",
"@storybook/react": "^7.4.6",
"@storybook/react-vite": "^7.4.6",
"@storybook/test-runner": "^0.13.0",
"@storybook/testing-library": "^0.2.2",
"@vitejs/plugin-react": "^4.1.0",
"concurrently": "^8.2.1",
"jest-mock": "^29.7.0",
"serve": "^14.2.1",
"ses": "^0.18.8",
"storybook": "^7.4.6",
"storybook-dark-mode": "^3.0.1",
"wait-on": "^7.0.1"
},
"devDependencies": {
"@blocksuite/block-std": "0.0.0-20231018100009-361737d3-nightly",
"@blocksuite/blocks": "0.0.0-20231018100009-361737d3-nightly",
"@blocksuite/editor": "0.0.0-20231018100009-361737d3-nightly",
"@blocksuite/global": "0.0.0-20231018100009-361737d3-nightly",
"@blocksuite/icons": "2.1.34",
"@blocksuite/lit": "0.0.0-20231018100009-361737d3-nightly",
"@blocksuite/store": "0.0.0-20231018100009-361737d3-nightly",
"@dnd-kit/sortable": "^7.0.2",
"@tomfreudenberg/next-auth-mock": "^0.5.6",
"chromatic": "^7.4.0",
"foxact": "^0.2.20",
"jotai": "^2.4.3",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-router-dom": "^6.16.0",
"storybook-addon-react-router-v6": "^2.0.7"
},
"peerDependencies": {
"@blocksuite/blocks": "*",
"@blocksuite/editor": "*",
"@blocksuite/global": "*",
"@blocksuite/icons": "2.1.34",
"@blocksuite/lit": "*",
"@blocksuite/store": "*"
},
"version": "0.10.0-canary.1"
}

View File

@@ -0,0 +1,49 @@
{
"name": "@affine/storybook",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"targets": {
"build": {
"executor": "nx:run-script",
"dependsOn": ["^build"],
"inputs": [
"default",
"^production",
"{projectRoot}/.storybook/**/*",
"{workspaceRoot}/packages/frontend/core/src/**/*",
"{workspaceRoot}/packages/common/infra/**/*",
"{workspaceRoot}/packages/common/sdk/**/*",
{
"runtime": "node -v"
},
{
"env": "BUILD_TYPE"
},
{
"env": "PERFSEE_TOKEN"
},
{
"env": "SENTRY_ORG"
},
{
"env": "SENTRY_PROJECT"
},
{
"env": "SENTRY_AUTH_TOKEN"
},
{
"env": "NEXT_PUBLIC_SENTRY_DSN"
},
{
"env": "DISTRIBUTION"
},
{
"env": "COVERAGE"
}
],
"options": {
"script": "build"
},
"outputs": ["{projectRoot}/storybook-static"]
}
}
}

View File

@@ -0,0 +1,36 @@
import { BrowserWarning, DownloadTips } from '@affine/component/affine-banner';
import type { StoryFn } from '@storybook/react';
import { useState } from 'react';
export default {
title: 'AFFiNE/Banner',
component: BrowserWarning,
};
export const Default: StoryFn = () => {
const [closed, setIsClosed] = useState(true);
return (
<div>
<BrowserWarning
message={<span>test</span>}
show={closed}
onClose={() => {
setIsClosed(false);
}}
/>
</div>
);
};
export const Download: StoryFn = () => {
const [, setIsClosed] = useState(true);
return (
<div>
<DownloadTips
onClose={() => {
setIsClosed(false);
}}
/>
</div>
);
};

View File

@@ -0,0 +1,149 @@
import {
AppSidebar,
AppSidebarFallback,
appSidebarOpenAtom,
SidebarSwitch,
} from '@affine/component/app-sidebar';
import { AddPageButton } from '@affine/component/app-sidebar';
import { CategoryDivider } from '@affine/component/app-sidebar';
import { navHeaderStyle } from '@affine/component/app-sidebar';
import { MenuLinkItem } from '@affine/component/app-sidebar';
import { QuickSearchInput } from '@affine/component/app-sidebar';
import {
SidebarContainer,
SidebarScrollableContainer,
} from '@affine/component/app-sidebar';
import { DeleteTemporarilyIcon, SettingsIcon } from '@blocksuite/icons';
import type { Meta, StoryFn } from '@storybook/react';
import { useAtom } from 'jotai';
import { type PropsWithChildren, useState } from 'react';
import { MemoryRouter } from 'react-router-dom';
export default {
title: 'AFFiNE/AppSidebar',
component: AppSidebar,
} satisfies Meta;
const Container = ({ children }: PropsWithChildren) => (
<MemoryRouter>
<main
style={{
position: 'relative',
width: '100vw',
height: 'calc(100vh - 40px)',
overflow: 'hidden',
display: 'flex',
flexDirection: 'row',
}}
>
{children}
</main>
</MemoryRouter>
);
const Main = () => {
const [open] = useAtom(appSidebarOpenAtom);
return (
<div>
<div className={navHeaderStyle}>
<SidebarSwitch show={!open} />
</div>
</div>
);
};
export const Default: StoryFn = () => {
return (
<>
<Container>
<AppSidebar />
<Main />
</Container>
</>
);
};
export const Fallback = () => {
return (
<Container>
<AppSidebarFallback />
<Main />
</Container>
);
};
export const WithItems: StoryFn = () => {
const [collapsed, setCollapsed] = useState(false);
return (
<Container>
<AppSidebar>
<SidebarContainer>
<QuickSearchInput />
<div style={{ height: '20px' }} />
<MenuLinkItem
icon={<SettingsIcon />}
to="/test"
onClick={() => alert('opened')}
>
Settings
</MenuLinkItem>
<MenuLinkItem
icon={<SettingsIcon />}
to="/test"
onClick={() => alert('opened')}
>
Settings
</MenuLinkItem>
<MenuLinkItem
icon={<SettingsIcon />}
to="/test"
onClick={() => alert('opened')}
>
Settings
</MenuLinkItem>
</SidebarContainer>
<SidebarScrollableContainer>
<CategoryDivider label="Favorites" />
<MenuLinkItem
collapsed={collapsed}
onCollapsedChange={setCollapsed}
icon={<SettingsIcon />}
to="/test"
onClick={() => alert('opened')}
>
Collapsible Item
</MenuLinkItem>
<MenuLinkItem
collapsed={!collapsed}
onCollapsedChange={setCollapsed}
icon={<SettingsIcon />}
to="/test"
onClick={() => alert('opened')}
>
Collapsible Item
</MenuLinkItem>
<MenuLinkItem
icon={<SettingsIcon />}
to="/test"
onClick={() => alert('opened')}
>
Settings
</MenuLinkItem>
<CategoryDivider label="Others" />
<MenuLinkItem
icon={<DeleteTemporarilyIcon />}
to="/test"
onClick={() => alert('opened')}
>
Trash
</MenuLinkItem>
</SidebarScrollableContainer>
<SidebarContainer>
<AddPageButton />
</SidebarContainer>
</AppSidebar>
<Main />
</Container>
);
};

View File

@@ -0,0 +1,60 @@
import {
type AddPageButtonPureProps,
AppUpdaterButtonPure,
} from '@affine/component/app-sidebar';
import type { Meta, StoryFn } from '@storybook/react';
import type { PropsWithChildren } from 'react';
export default {
title: 'AFFiNE/AppUpdaterButton',
component: AppUpdaterButtonPure,
parameters: {
chromatic: { disableSnapshot: true },
},
} satisfies Meta<typeof AppUpdaterButtonPure>;
const Container = ({ children }: PropsWithChildren) => (
<main
style={{
position: 'relative',
width: '320px',
display: 'flex',
flexDirection: 'row',
backgroundColor: '#eee',
padding: '16px',
}}
>
{children}
</main>
);
export const Default: StoryFn<AddPageButtonPureProps> = props => {
return (
<Container>
<AppUpdaterButtonPure {...props} />
</Container>
);
};
Default.args = {
appQuitting: false,
updateReady: true,
updateAvailable: {
version: '1.0.0-beta.1',
allowAutoUpdate: true,
},
downloadProgress: 42,
currentChangelogUnread: true,
};
export const Updated: StoryFn<AddPageButtonPureProps> = props => {
return (
<Container>
<AppUpdaterButtonPure {...props} updateAvailable={null} />
</Container>
);
};
Updated.args = {
currentChangelogUnread: true,
};

View File

@@ -0,0 +1,41 @@
/* deepscan-disable USELESS_ARROW_FUNC_BIND */
import { Breadcrumbs } from '@affine/component';
import { Link, Typography } from '@mui/material';
import { expect } from '@storybook/jest';
import type { Meta, StoryFn } from '@storybook/react';
import { within } from '@storybook/testing-library';
export default {
title: 'AFFiNE/Breadcrumbs',
component: Breadcrumbs,
parameters: {
chromatic: { disableSnapshot: true },
},
} as Meta<typeof Breadcrumbs>;
const Template: StoryFn = args => <Breadcrumbs {...args} />;
export const Primary = Template.bind(undefined);
Primary.play = async ({ canvasElement }) => {
const canvas = within(canvasElement);
const text = canvas.getByText('AFFiNE');
expect(text.getAttribute('data-testid')).toBe('affine');
};
Primary.args = {
children: [
<Link
data-testid="affine"
key="1"
underline="hover"
color="inherit"
href="/"
>
AFFiNE
</Link>,
<Link key="2" underline="hover" color="inherit" href="/Docs/">
Docs
</Link>,
<Typography key="3" color="text.primary">
Introduction
</Typography>,
],
};

View File

@@ -0,0 +1,75 @@
import { toast } from '@affine/component';
import { BlockCard } from '@affine/component/card/block-card';
import { WorkspaceCard } from '@affine/component/card/workspace-card';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { getOrCreateWorkspace } from '@affine/workspace/manager';
import {
EdgelessIcon,
ExportToHtmlIcon,
HelpIcon,
PageIcon,
} from '@blocksuite/icons';
import type { Meta } from '@storybook/react';
import { Tooltip } from '@toeverything/components/tooltip';
export default {
title: 'AFFiNE/Card',
component: WorkspaceCard,
parameters: {
chromatic: { disableSnapshot: true },
},
} satisfies Meta;
const blockSuiteWorkspace = getOrCreateWorkspace(
'blocksuite-local',
WorkspaceFlavour.LOCAL
);
blockSuiteWorkspace.meta.setName('Hello World');
export const AffineWorkspaceCard = () => {
return (
<WorkspaceCard
meta={{
id: 'blocksuite-local',
flavour: WorkspaceFlavour.LOCAL,
}}
onClick={() => {}}
onSettingClick={() => {}}
currentWorkspaceId={null}
isOwner={true}
/>
);
};
export const AffineBlockCard = () => {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
<BlockCard title={'New Page'} onClick={() => toast('clicked')} />
<BlockCard
title={'New Page'}
desc={'Write with a blank page'}
right={<PageIcon width={20} height={20} />}
onClick={() => toast('clicked page')}
/>
<BlockCard
title={'New Edgeless'}
desc={'Draw with a blank whiteboard'}
left={<PageIcon width={20} height={20} />}
right={<EdgelessIcon width={20} height={20} />}
onClick={() => toast('clicked edgeless')}
/>
<BlockCard
left={<ExportToHtmlIcon width={20} height={20} />}
title="HTML"
disabled
right={
<Tooltip content={'Learn how to Import pages into AFFiNE.'}>
<HelpIcon />
</Tooltip>
}
onClick={() => toast('click HTML')}
/>
</div>
);
};

View File

@@ -0,0 +1,202 @@
import { routes } from '@affine/core/router';
import { assertExists } from '@blocksuite/global/utils';
import type { StoryFn } from '@storybook/react';
import { screen, userEvent, waitFor, within } from '@storybook/testing-library';
import { Outlet, useLocation } from 'react-router-dom';
import {
reactRouterOutlets,
reactRouterParameters,
withRouter,
} from 'storybook-addon-react-router-v6';
const FakeApp = () => {
const location = useLocation();
// fixme: `key` is a hack to force the storybook to re-render the outlet
return <Outlet key={location.pathname} />;
};
export default {
title: 'Preview/Core',
parameters: {
chromatic: { disableSnapshot: false },
},
};
export const Index: StoryFn = () => {
return <FakeApp />;
};
Index.decorators = [withRouter];
Index.parameters = {
reactRouter: reactRouterParameters({
routing: reactRouterOutlets(routes),
}),
};
export const SettingPage: StoryFn = () => {
return <FakeApp />;
};
SettingPage.play = async ({ canvasElement, step }) => {
const canvas = within(canvasElement);
await waitFor(
() => {
assertExists(canvasElement.querySelector('v-line'));
},
{
timeout: 10000,
}
);
await step('click setting modal button', async () => {
await userEvent.click(canvas.getByTestId('settings-modal-trigger'));
});
await waitFor(async () => {
assertExists(
document.body.querySelector('[data-testid="language-menu-button"]')
);
});
// Menu button may have "pointer-events: none" style, await 100ms to avoid this weird situation.
await new Promise(resolve => window.setTimeout(resolve, 100));
await step('click language menu button', async () => {
await userEvent.click(
document.body.querySelector(
'[data-testid="language-menu-button"]'
) as HTMLElement
);
});
};
SettingPage.decorators = [withRouter];
SettingPage.parameters = {
reactRouter: reactRouterParameters({
routing: reactRouterOutlets(routes),
}),
};
export const NotFoundPage: StoryFn = () => {
return <FakeApp />;
};
NotFoundPage.decorators = [withRouter];
NotFoundPage.parameters = {
reactRouter: reactRouterParameters({
routing: reactRouterOutlets(routes),
location: {
path: '/404',
},
}),
};
export const WorkspaceList: StoryFn = () => {
return <FakeApp />;
};
WorkspaceList.play = async ({ canvasElement }) => {
const canvas = within(canvasElement);
// click current-workspace
const currentWorkspace = await waitFor(
() => {
assertExists(canvas.getByTestId('current-workspace'));
return canvas.getByTestId('current-workspace');
},
{
timeout: 5000,
}
);
// todo: figure out why userEvent cannot click this element?
// await userEvent.click(currentWorkspace);
currentWorkspace.click();
};
WorkspaceList.decorators = [withRouter];
WorkspaceList.parameters = {
reactRouter: reactRouterParameters({
routing: reactRouterOutlets(routes),
location: {
path: '/',
},
}),
};
export const SearchPage: StoryFn = () => {
return <FakeApp />;
};
SearchPage.play = async ({ canvasElement }) => {
const canvas = within(canvasElement);
await waitFor(
() => {
assertExists(canvasElement.querySelector('v-line'));
},
{
timeout: 10000,
}
);
await userEvent.click(canvas.getByTestId('slider-bar-quick-search-button'));
await waitFor(
() => {
assertExists(screen.getByTestId('cmdk-quick-search'));
},
{
timeout: 3000,
}
);
};
SearchPage.decorators = [withRouter];
SearchPage.parameters = {
reactRouter: reactRouterParameters({
routing: reactRouterOutlets(routes),
location: {
path: '/',
},
}),
};
export const ImportPage: StoryFn = () => {
return <FakeApp />;
};
ImportPage.play = async ({ canvasElement }) => {
const canvas = within(canvasElement);
await waitFor(
() => {
assertExists(canvasElement.querySelector('v-line'));
},
{
timeout: 10000,
}
);
await waitFor(() => {
assertExists(
canvasElement.querySelector('[data-testid="header-dropDownButton"]')
);
});
await userEvent.click(canvas.getByTestId('header-dropDownButton'));
await waitFor(() => {
assertExists(
document.body.querySelector('[data-testid="editor-option-menu-import"]')
);
});
await userEvent.click(screen.getByTestId('editor-option-menu-import'));
};
ImportPage.decorators = [withRouter];
ImportPage.parameters = {
reactRouter: reactRouterParameters({
routing: reactRouterOutlets(routes),
location: {
path: '/',
},
}),
};
export const OpenAppPage: StoryFn = () => {
return <FakeApp />;
};
OpenAppPage.decorators = [withRouter];
OpenAppPage.parameters = {
reactRouter: reactRouterParameters({
routing: reactRouterOutlets(routes),
location: {
path: '/open-app/url',
searchParams: {
url: 'affine-beta://foo-bar.com',
open: 'false',
},
},
}),
};

View File

@@ -0,0 +1,16 @@
import { AFFiNEDatePicker } from '@affine/component/date-picker';
import type { Meta, StoryFn } from '@storybook/react';
import { useState } from 'react';
export default {
title: 'AFFiNE/AFFiNEDatePicker',
component: AFFiNEDatePicker,
parameters: {
chromatic: { disableSnapshot: true },
},
} satisfies Meta;
export const Default: StoryFn = () => {
const [value, setValue] = useState<string>(new Date().toString());
return <AFFiNEDatePicker value={value} onChange={setValue} />;
};

View File

@@ -0,0 +1,77 @@
import { BlockHubWrapper } from '@affine/component/block-hub';
import { BlockSuiteEditor } from '@affine/component/block-suite-editor';
import { WorkspaceFlavour } from '@affine/env/workspace';
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
import { ImagePreviewModal } from '@affine/image-preview-plugin/src/component';
import { rootBlockHubAtom } from '@affine/workspace/atom';
import { getOrCreateWorkspace } from '@affine/workspace/manager';
import type { Meta } from '@storybook/react';
import { initEmptyPage } from '@toeverything/infra/blocksuite';
import { useCallback } from 'react';
import { createPortal } from 'react-dom';
export default {
title: 'Component/ImagePreviewModal',
component: ImagePreviewModal,
} satisfies Meta;
const workspace = getOrCreateWorkspace('test', WorkspaceFlavour.LOCAL);
const page = workspace.createPage('page0');
initEmptyPage(page);
fetch(new URL('@affine-test/fixtures/large-image.png', import.meta.url))
.then(res => res.arrayBuffer())
.then(async buffer => {
const id = await workspace.blobs.set(
new Blob([buffer], { type: 'image/png' })
);
const frameId = page.getBlockByFlavour('affine:note')[0].id;
page.addBlock(
'affine:paragraph',
{
text: new page.Text('Please double click the image to preview it.'),
},
frameId
);
page.addBlock(
'affine:image',
{
sourceId: id,
},
frameId
);
})
.catch(err => {
console.error('Failed to load large-image.png', err);
});
export const Default = () => {
return (
<>
<div
style={{
height: '100vh',
width: '100vw',
overflow: 'auto',
}}
>
<BlockSuiteEditor
mode="page"
page={page}
onInit={useCallback(async page => initEmptyPage(page), [])}
/>
{createPortal(
<ImagePreviewModal pageId={page.id} workspace={page.workspace} />,
document.body
)}
</div>
<BlockHubWrapper
style={{
position: 'absolute',
right: 12,
bottom: 12,
}}
blockHubAtom={rootBlockHubAtom}
/>
</>
);
};

View File

@@ -0,0 +1,23 @@
/* deepscan-disable USELESS_ARROW_FUNC_BIND */
import { toast } from '@affine/component';
import { ImportPage } from '@affine/component/import-page';
import type { StoryFn } from '@storybook/react';
import type { Meta } from '@storybook/react';
export default {
title: 'AFFiNE/ImportPage',
component: ImportPage,
parameters: {
chromatic: { disableSnapshot: true },
},
} satisfies Meta;
const Template: StoryFn<typeof ImportPage> = args => <ImportPage {...args} />;
export const Basic = Template.bind(undefined);
Basic.args = {
importHtml: () => toast('Click importHtml'),
importMarkdown: () => toast('Click importMarkdown'),
importNotion: () => toast('Click importNotion'),
onClose: () => toast('Click onClose'),
};

View File

@@ -0,0 +1,18 @@
import { Meta } from '@storybook/blocks';
<Meta title="Introduction" />
# AFFiNE UI Storybook
This is a UI component dev environment for AFFiNE UI.
It allows you to browse the AFFiNE UI components,
view the different states of each component,
and interactively develop and test components.
## Bug Reporting
If you find a bug, please file an issue on [GitHub](https://github.com/toeverything/AFFiNE/issues)
## Contributing
We welcome contributions from the community! [Get started here](https://github.com/toeverything/AFFiNE/blob/master/docs/BUILDING.md)

View File

@@ -0,0 +1,249 @@
import {
expandNotificationCenterAtom,
NotificationCenter,
pushNotificationAtom,
} from '@affine/component/notification-center';
import type { Meta } from '@storybook/react';
import { useAtomValue, useSetAtom } from 'jotai';
export default {
title: 'AFFiNE/NotificationCenter',
component: NotificationCenter,
parameters: {
chromatic: { disableSnapshot: true },
},
} satisfies Meta<typeof NotificationCenter>;
let id = 0;
const image = (
<video autoPlay muted loop>
<source
src="http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"
type="video/mp4"
/>
</video>
);
export const Basic = () => {
const push = useSetAtom(pushNotificationAtom);
const expand = useAtomValue(expandNotificationCenterAtom);
return (
<>
<div>{expand ? 'expanded' : 'collapsed'}</div>
<div>
<button
onClick={() => {
const key = id++;
push({
key: `${key}`,
title: `${key} title`,
message: `${key} message`,
timeout: 3000,
progressingBar: true,
undo: async () => {
console.log('undo');
},
type: 'info',
});
}}
>
Push timeout notification
</button>
</div>
<div>
<button
onClick={() => {
const key = id++;
push({
key: `${key}`,
title: `${key} title`,
message: `${key} message`,
type: 'info',
});
}}
>
Push notification with no timeout
</button>
</div>
<div>
<button
onClick={() => {
const key = id++;
push({
key: `${key}`,
title: `${key} title`,
message: ``,
type: 'info',
});
}}
>
Push notification with no message
</button>
</div>
<div>
<button
onClick={() => {
const key = id++;
push({
key: `${key}`,
title: `${key} title`,
message: ``,
type: 'success',
theme: 'light',
});
}}
>
light success
</button>
</div>
<div>
<button
onClick={() => {
const key = id++;
push({
key: `${key}`,
title: `${key} title`,
message: ``,
type: 'success',
theme: 'dark',
});
}}
>
dark success
</button>
</div>
<div>
<button
onClick={() => {
const key = id++;
push({
key: `${key}`,
title: `${key} title`,
message: ``,
type: 'info',
theme: 'light',
});
}}
>
light info
</button>
</div>
<div>
<button
onClick={() => {
const key = id++;
push({
key: `${key}`,
title: `${key} title`,
message: ``,
type: 'info',
theme: 'dark',
});
}}
>
dark info
</button>
</div>
<div>
<button
onClick={() => {
const key = id++;
push({
key: `${key}`,
title: `${key} title`,
message: ``,
type: 'warning',
theme: 'light',
});
}}
>
light warning
</button>
</div>
<div>
<button
onClick={() => {
const key = id++;
push({
key: `${key}`,
title: `${key} title`,
message: ``,
type: 'warning',
theme: 'dark',
});
}}
>
dark warning
</button>
</div>
<div>
<button
onClick={() => {
const key = id++;
push({
key: `${key}`,
title: `${key} title`,
message: ``,
type: 'error',
theme: 'light',
});
}}
>
light error
</button>
</div>
<div>
<button
onClick={() => {
const key = id++;
push({
key: `${key}`,
title: `${key} title`,
message: ``,
type: 'error',
theme: 'dark',
});
}}
>
dark error
</button>
</div>
<div>
<button
onClick={() => {
const key = id++;
push({
key: `${key}`,
title: `${key} title`,
message: `gif test`,
type: 'info',
multimedia: image,
timeout: 3000,
undo: async () => {
console.log('undo');
},
progressingBar: true,
});
}}
>
gif
</button>
</div>
<div>
<button
onClick={() => {
const key = id++;
push({
title: `${key} title`,
type: 'info',
theme: 'default',
timeout: 3000,
});
}}
>
default message
</button>
</div>
<NotificationCenter />
</>
);
};

View File

@@ -0,0 +1,18 @@
/* deepscan-disable USELESS_ARROW_FUNC_BIND */
import { TourModal } from '@affine/component/tour-modal';
import type { Meta, StoryFn } from '@storybook/react';
export default {
title: 'AFFiNE/TourModal',
component: TourModal,
parameters: {
chromatic: { disableSnapshot: true },
},
} satisfies Meta;
export const Basic: StoryFn = () => {
return <TourModal open={true} />;
};
Basic.args = {
logoSrc: '/imgs/affine-text-logo.png',
};

View File

@@ -0,0 +1,14 @@
import { PageDetailSkeleton } from '@affine/component/page-detail-skeleton';
import type { Meta } from '@storybook/react';
export default {
title: 'AFFiNE/PageDetailSkeleton',
component: PageDetailSkeleton,
parameters: {
chromatic: { disableSnapshot: true },
},
} satisfies Meta<typeof PageDetailSkeleton>;
export const Basic = () => {
return <PageDetailSkeleton />;
};

View File

@@ -0,0 +1,221 @@
import { Empty, toast } from '@affine/component';
import type { OperationCellProps } from '@affine/component/page-list';
import {
NewPageButton,
OperationCell,
PageList,
PageListTrashView,
} from '@affine/component/page-list';
import type { Collection } from '@affine/env/filter';
import { PageIcon } from '@blocksuite/icons';
import { expect } from '@storybook/jest';
import type { Meta, StoryFn } from '@storybook/react';
import { userEvent } from '@storybook/testing-library';
import { atom } from 'jotai';
export default {
title: 'AFFiNE/PageList',
component: PageList,
parameters: {
chromatic: { disableSnapshot: true },
},
} satisfies Meta;
export const AffineOperationCell: StoryFn<OperationCellProps> = ({
...props
}) => <OperationCell {...props} />;
AffineOperationCell.args = {
favorite: false,
isPublic: true,
onToggleFavoritePage: () => toast('Toggle favorite page'),
onDisablePublicSharing: () => toast('Disable public sharing'),
onOpenPageInNewTab: () => toast('Open page in new tab'),
onRemoveToTrash: () => toast('Remove to trash'),
};
AffineOperationCell.play = async ({ canvasElement }) => {
{
const button = canvasElement.querySelector(
'[data-testid="page-list-operation-button"]'
) as HTMLButtonElement;
expect(button).not.toBeNull();
await userEvent.click(button);
}
};
export const AffineNewPageButton: StoryFn<typeof NewPageButton> = ({
...props
}) => <NewPageButton {...props} />;
AffineNewPageButton.args = {
createNewPage: () => toast('Create new page'),
createNewEdgeless: () => toast('Create new edgeless'),
};
AffineNewPageButton.play = async ({ canvasElement }) => {
const button = canvasElement.querySelector('button') as HTMLButtonElement;
expect(button).not.toBeNull();
const dropdown = button.querySelector('svg') as SVGSVGElement;
expect(dropdown).not.toBeNull();
await userEvent.click(dropdown);
};
export const AffineAllPageList: StoryFn<typeof PageList> = ({ ...props }) => (
<PageList {...props} />
);
const baseAtom = atom<Collection[]>([]);
AffineAllPageList.args = {
isPublicWorkspace: false,
onCreateNewPage: () => toast('Create new page'),
onCreateNewEdgeless: () => toast('Create new edgeless'),
onImportFile: () => toast('Import file'),
collectionsAtom: atom(
get => get(baseAtom),
async (_, set, update) => {
set(baseAtom, update);
}
),
list: [
{
pageId: '1',
favorite: false,
icon: <PageIcon />,
isPublicPage: true,
title: 'Last Page',
tags: [],
preview: 'this is page preview',
createDate: new Date('2021-01-01'),
updatedDate: new Date('2023-08-15'),
bookmarkPage: () => toast('Bookmark page'),
onClickPage: () => toast('Click page'),
onDisablePublicSharing: () => toast('Disable public sharing'),
onOpenPageInNewTab: () => toast('Open page in new tab'),
removeToTrash: () => toast('Remove to trash'),
},
{
pageId: '3',
favorite: false,
icon: <PageIcon />,
isPublicPage: true,
title:
'1 Example Public Page with long title that will be truncated because it is too too long',
tags: [],
preview:
'this is page preview and it is very long and will be truncated because it is too long and it is very long and will be truncated because it is too long',
createDate: new Date('2021-01-01'),
updatedDate: new Date('2021-01-02'),
bookmarkPage: () => toast('Bookmark page'),
onClickPage: () => toast('Click page'),
onDisablePublicSharing: () => toast('Disable public sharing'),
onOpenPageInNewTab: () => toast('Open page in new tab'),
removeToTrash: () => toast('Remove to trash'),
},
{
pageId: '2',
favorite: true,
isPublicPage: false,
icon: <PageIcon />,
title: '2 Favorited Page 2021',
tags: [],
createDate: new Date('2021-01-02'),
updatedDate: new Date('2021-01-01'),
bookmarkPage: () => toast('Bookmark page'),
onClickPage: () => toast('Click page'),
onDisablePublicSharing: () => toast('Disable public sharing'),
onOpenPageInNewTab: () => toast('Open page in new tab'),
removeToTrash: () => toast('Remove to trash'),
},
{
pageId: '4',
favorite: false,
isPublicPage: false,
icon: <PageIcon />,
title: 'page created in 2023-04-01',
tags: [],
createDate: new Date('2023-04-01'),
updatedDate: new Date('2023-04-01'),
bookmarkPage: () => toast('Bookmark page'),
onClickPage: () => toast('Click page'),
onDisablePublicSharing: () => toast('Disable public sharing'),
onOpenPageInNewTab: () => toast('Open page in new tab'),
removeToTrash: () => toast('Remove to trash'),
},
],
};
export const AffineEmptyAllPageList: StoryFn<typeof PageList> = ({
...props
}) => <PageList {...props} />;
AffineEmptyAllPageList.args = {
isPublicWorkspace: false,
onCreateNewPage: () => toast('Create new page'),
onCreateNewEdgeless: () => toast('Create new edgeless'),
onImportFile: () => toast('Import file'),
list: [],
fallback: (
<Empty
title="Empty"
description={
<div>
empty description, click{' '}
<button
onClick={() => {
toast('click');
}}
>
button
</button>
</div>
}
/>
),
};
export const AffinePublicPageList: StoryFn<typeof PageList> = ({
...props
}) => <PageList {...props} />;
AffinePublicPageList.args = {
...AffineAllPageList.args,
isPublicWorkspace: true,
};
export const AffineAllPageMobileList: StoryFn<typeof PageList> = ({
...props
}) => <PageList {...props} />;
AffineAllPageMobileList.args = AffineAllPageList.args;
AffineAllPageMobileList.parameters = {
viewport: {
defaultViewport: 'mobile2',
},
};
export const AffineTrashPageList: StoryFn<typeof PageListTrashView> = ({
...props
}) => <PageListTrashView {...props} />;
AffineTrashPageList.args = {
list: [
{
pageId: '1',
icon: <PageIcon />,
title: 'Example Page',
preview: 'this is trash page preview',
createDate: new Date('2021-01-01'),
trashDate: new Date('2021-01-01'),
onClickPage: () => toast('Click page'),
onPermanentlyDeletePage: () => toast('Permanently delete page'),
onRestorePage: () => toast('Restore page'),
},
{
pageId: '2',
icon: <PageIcon />,
title: 'Example Page with long title that will be truncated',
createDate: new Date('2021-01-01'),
onClickPage: () => toast('Click page'),
onPermanentlyDeletePage: () => toast('Permanently delete page'),
onRestorePage: () => toast('Restore page'),
},
],
};

View File

@@ -0,0 +1,100 @@
import {
registerAffineCreationCommands,
registerAffineLayoutCommands,
registerAffineSettingsCommands,
} from '@affine/core/commands';
import { CMDKQuickSearchModal } from '@affine/core/components/pure/cmdk';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import { getOrCreateWorkspace } from '@affine/workspace/manager';
import type { Page } from '@blocksuite/store';
import type { Meta, StoryFn } from '@storybook/react';
import { currentWorkspaceIdAtom } from '@toeverything/infra/atom';
import { useStore } from 'jotai';
import { useEffect, useLayoutEffect } from 'react';
import { withRouter } from 'storybook-addon-react-router-v6';
export default {
title: 'AFFiNE/QuickSearch',
parameters: {
chromatic: { disableSnapshot: true },
},
} satisfies Meta;
const createMockedPage = () => {
return {
id: 'test-page',
waitForLoaded: () => Promise.resolve(),
} as any as Page;
};
function useRegisterCommands() {
const t = useAFFiNEI18N();
const store = useStore();
useEffect(() => {
const unsubs = [
registerAffineSettingsCommands({
t,
store,
theme: {
setTheme: () => {},
theme: 'auto',
themes: ['auto', 'dark', 'light'],
},
languageHelper: {
onSelect: () => {},
languagesList: [
{ tag: 'en', name: 'English', originalName: 'English' },
{
tag: 'zh-Hans',
name: 'Simplified Chinese',
originalName: '简体中文',
},
],
currentLanguage: undefined,
},
}),
registerAffineCreationCommands({
t,
store,
pageHelper: {
createEdgeless: createMockedPage,
createPage: createMockedPage,
importFile: () => Promise.resolve(),
isPreferredEdgeless: () => false,
},
}),
registerAffineLayoutCommands({ t, store }),
];
return () => {
unsubs.forEach(unsub => unsub());
};
}, [store, t]);
}
function usePrepareWorkspace() {
const store = useStore();
useLayoutEffect(() => {
const workspaceId = 'test-workspace';
getOrCreateWorkspace(workspaceId, WorkspaceFlavour.LOCAL);
store.set(rootWorkspacesMetadataAtom, [
{
id: workspaceId,
flavour: WorkspaceFlavour.LOCAL,
version: 4,
},
]);
store.set(currentWorkspaceIdAtom, workspaceId);
}, [store]);
}
export const CMDKStoryWithCommands: StoryFn = () => {
usePrepareWorkspace();
useRegisterCommands();
return <CMDKQuickSearchModal open />;
};
CMDKStoryWithCommands.decorators = [withRouter];

View File

@@ -0,0 +1,37 @@
import { CMDKContainer, CMDKModal } from '@affine/core/components/pure/cmdk';
import type { Meta, StoryFn } from '@storybook/react';
import { Button } from '@toeverything/components/button';
import { useState } from 'react';
export default {
title: 'AFFiNE/QuickSearch',
parameters: {
chromatic: { disableSnapshot: true },
},
} satisfies Meta;
export const CMDKModalStory: StoryFn = () => {
const [open, setOpen] = useState(false);
const [counter, setCounter] = useState(0);
return (
<>
<Button onClick={() => setOpen(true)}>Open Modal</Button>
<CMDKModal key={counter} open={open} onOpenChange={setOpen}>
<Button onClick={() => setCounter(c => c + 1)}>
Trigger new modal
</Button>
</CMDKModal>
</>
);
};
export const CMDKPanelStory: StoryFn = () => {
const [query, setQuery] = useState('');
return (
<>
<CMDKModal open>
<CMDKContainer query={query} onQueryChange={setQuery} />
</CMDKModal>
</>
);
};

View File

@@ -0,0 +1,149 @@
import { toast } from '@affine/component';
import {
PublicLinkDisableModal,
StyledDisableButton,
} from '@affine/component/share-menu';
import { ShareMenu } from '@affine/component/share-menu';
import type {
AffineCloudWorkspace,
LocalWorkspace,
} from '@affine/env/workspace';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { getOrCreateWorkspace } from '@affine/workspace/manager';
import type { Page } from '@blocksuite/store';
import { expect } from '@storybook/jest';
import type { Meta, StoryFn } from '@storybook/react';
import { use } from 'foxact/use';
import { useState } from 'react';
export default {
title: 'AFFiNE/ShareMenu',
component: ShareMenu,
parameters: {
chromatic: { disableSnapshot: true },
},
} satisfies Meta;
const sharePageMap = new Map<string, boolean>([]);
// todo: use a real hook
const useIsSharedPage = (
_workspaceId: string,
pageId: string
): [isSharePage: boolean, setIsSharePage: (enable: boolean) => void] => {
const [isShared, setIsShared] = useState(sharePageMap.get(pageId) ?? false);
const togglePagePublic = (enable: boolean) => {
setIsShared(enable);
sharePageMap.set(pageId, enable);
};
return [isShared, togglePagePublic];
};
async function initPage(page: Page) {
await page.waitForLoaded();
// Add page block and surface block at root level
const pageBlockId = page.addBlock('affine:page', {
title: new page.Text('Hello, world!'),
});
page.addBlock('affine:surface', {}, pageBlockId);
const frameId = page.addBlock('affine:note', {}, pageBlockId);
page.addBlock(
'affine:paragraph',
{
text: new page.Text('This is a paragraph.'),
},
frameId
);
page.resetHistory();
}
const blockSuiteWorkspace = getOrCreateWorkspace(
'test-workspace',
WorkspaceFlavour.LOCAL
);
const promise = Promise.all([
initPage(blockSuiteWorkspace.createPage({ id: 'page0' })),
initPage(blockSuiteWorkspace.createPage({ id: 'page1' })),
initPage(blockSuiteWorkspace.createPage({ id: 'page2' })),
]);
const localWorkspace: LocalWorkspace = {
id: 'test-workspace',
flavour: WorkspaceFlavour.LOCAL,
blockSuiteWorkspace,
};
const affineWorkspace: AffineCloudWorkspace = {
id: 'test-workspace',
flavour: WorkspaceFlavour.AFFINE_CLOUD,
blockSuiteWorkspace,
};
async function unimplemented() {
toast('work in progress');
}
export const Basic: StoryFn = () => {
use(promise);
return (
<ShareMenu
currentPage={blockSuiteWorkspace.getPage('page0') as Page}
useIsSharedPage={useIsSharedPage}
workspace={localWorkspace}
onEnableAffineCloud={unimplemented}
togglePagePublic={unimplemented}
exportHandler={unimplemented}
/>
);
};
Basic.play = async ({ canvasElement }) => {
{
const button = canvasElement.querySelector(
'[data-testid="share-menu-button"]'
) as HTMLButtonElement;
expect(button).not.toBeNull();
button.click();
}
await new Promise(resolve => window.setTimeout(resolve, 100));
{
const button = canvasElement.querySelector(
'[data-testid="share-menu-enable-affine-cloud-button"]'
);
expect(button).not.toBeNull();
}
};
export const AffineBasic: StoryFn = () => {
use(promise);
return (
<ShareMenu
currentPage={blockSuiteWorkspace.getPage('page0') as Page}
useIsSharedPage={useIsSharedPage}
workspace={affineWorkspace}
onEnableAffineCloud={unimplemented}
togglePagePublic={unimplemented}
exportHandler={unimplemented}
/>
);
};
export const DisableModal: StoryFn = () => {
const [open, setOpen] = useState(false);
use(promise);
return (
<>
<StyledDisableButton onClick={() => setOpen(!open)}>
Disable Public Link
</StyledDisableButton>
<PublicLinkDisableModal
open={open}
onConfirm={() => {
toast('Disabled');
setOpen(false);
}}
onOpenChange={setOpen}
/>
</>
);
};

View File

@@ -0,0 +1,15 @@
/* deepscan-disable USELESS_ARROW_FUNC_BIND */
import { Switch } from '@affine/component';
import type { Meta, StoryFn } from '@storybook/react';
export default {
title: 'AFFiNE/Switch',
component: Switch,
parameters: {
chromatic: { disableSnapshot: true },
},
} satisfies Meta;
export const Basic: StoryFn = () => {
return <Switch>Switch</Switch>;
};

View File

@@ -0,0 +1,63 @@
import type { WorkspaceListProps } from '@affine/component/workspace-list';
import { WorkspaceList } from '@affine/component/workspace-list';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { getOrCreateWorkspace } from '@affine/workspace/manager';
import { arrayMove } from '@dnd-kit/sortable';
import type { Meta } from '@storybook/react';
import { useState } from 'react';
export default {
title: 'AFFiNE/WorkspaceList',
component: WorkspaceList,
parameters: {
chromatic: { disableSnapshot: true },
},
} satisfies Meta<WorkspaceListProps>;
export const Default = () => {
const [items, setItems] = useState(() => {
const items = [
{
id: '1',
flavour: WorkspaceFlavour.LOCAL,
blockSuiteWorkspace: getOrCreateWorkspace('1', WorkspaceFlavour.LOCAL),
},
{
id: '2',
flavour: WorkspaceFlavour.LOCAL,
blockSuiteWorkspace: getOrCreateWorkspace('2', WorkspaceFlavour.LOCAL),
},
{
id: '3',
flavour: WorkspaceFlavour.LOCAL,
blockSuiteWorkspace: getOrCreateWorkspace('3', WorkspaceFlavour.LOCAL),
},
] satisfies WorkspaceListProps['items'];
items.forEach(item => {
item.blockSuiteWorkspace.meta.setName(item.id);
});
return items;
});
return (
<WorkspaceList
currentWorkspaceId={null}
items={items}
onClick={() => {}}
onSettingClick={() => {}}
onDragEnd={event => {
const { active, over } = event;
if (active.id !== over?.id) {
setItems(items => {
const oldIndex = items.findIndex(item => item.id === active.id);
const newIndex = items.findIndex(item => item.id === over?.id);
return arrayMove(items, oldIndex, newIndex);
});
}
}}
/>
);
};

View File

@@ -0,0 +1,31 @@
{
"extends": "../../tsconfig.json",
"include": ["./src"],
"compilerOptions": {
// Workaround for storybook build
"baseUrl": "../..",
"composite": true,
"noEmit": false,
"outDir": "lib"
},
"references": [
{
"path": "../../packages/frontend/core"
},
{
"path": "../../packages/frontend/component"
},
{
"path": "../../packages/common/env"
},
{
"path": "../../packages/frontend/workspace"
},
{
"path": "../../packages/plugins/image-preview"
},
{
"path": "./tsconfig.node.json"
}
]
}

View File

@@ -0,0 +1,25 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": true,
"module": "ESNext",
"jsx": "react-jsx",
"moduleResolution": "bundler",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"noEmit": false,
"outDir": "lib/.storybook"
},
"include": [".storybook"],
"exclude": ["lib"],
"references": [
{ "path": "../../packages/frontend/core" },
{ "path": "../../packages/frontend/i18n" },
{
"path": "../../packages/common/env"
},
{
"path": "../../packages/frontend/core/tsconfig.node.json"
}
]
}