mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-11 20:08:37 +00:00
refactor: split storybook (#2706)
This commit is contained in:
@@ -1,62 +0,0 @@
|
||||
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';
|
||||
|
||||
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: ['../../../apps/web/public'],
|
||||
addons: [
|
||||
'@storybook/addon-links',
|
||||
'@storybook/addon-essentials',
|
||||
'@storybook/addon-interactions',
|
||||
'@storybook/addon-storysource',
|
||||
'@storybook/addon-coverage',
|
||||
'storybook-dark-mode',
|
||||
],
|
||||
framework: {
|
||||
name: '@storybook/react-vite',
|
||||
},
|
||||
async viteFinal(config, { configType }) {
|
||||
return mergeConfig(config, {
|
||||
assetsInclude: ['**/*.md'],
|
||||
plugins: [
|
||||
vanillaExtractPlugin(),
|
||||
tsconfigPaths({
|
||||
root: fileURLToPath(new URL('../../../', import.meta.url)),
|
||||
}),
|
||||
],
|
||||
define: {
|
||||
'process.env': {},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'dotenv/config': fileURLToPath(
|
||||
new URL('../../../scripts/vitest/dotenv-config.ts', import.meta.url)
|
||||
),
|
||||
'next/config': fileURLToPath(
|
||||
new URL(
|
||||
'../../../scripts/vitest/next-config-mock.ts',
|
||||
import.meta.url
|
||||
)
|
||||
),
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
} as StorybookConfig;
|
||||
@@ -1,3 +0,0 @@
|
||||
<script>
|
||||
window.global = window;
|
||||
</script>
|
||||
@@ -1,66 +0,0 @@
|
||||
import '@affine/component/theme/global.css';
|
||||
import '@affine/component/theme/theme.css';
|
||||
import { LOCALES, createI18n } from '@affine/i18n';
|
||||
import { ThemeProvider, useTheme } from 'next-themes';
|
||||
import { ComponentType, useEffect } from 'react';
|
||||
import { useDarkMode } from 'storybook-dark-mode';
|
||||
|
||||
export const parameters = {
|
||||
backgrounds: { disable: true },
|
||||
actions: { argTypesRegex: '^on[A-Z].*' },
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const globalTypes = {
|
||||
locale: {
|
||||
name: 'Locale',
|
||||
description: 'Internationalization locale',
|
||||
defaultValue: 'en',
|
||||
toolbar: {
|
||||
icon: 'globe',
|
||||
items: LOCALES.map(locale => ({
|
||||
title: locale.originalName,
|
||||
value: locale.tag,
|
||||
right: locale.flagEmoji,
|
||||
})),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const createI18nDecorator = () => {
|
||||
const i18n = createI18n();
|
||||
const withI18n = (Story: any, context: any) => {
|
||||
const locale = context.globals.locale;
|
||||
useEffect(() => {
|
||||
i18n.changeLanguage(locale);
|
||||
}, [locale]);
|
||||
return <Story {...context} />;
|
||||
};
|
||||
return withI18n;
|
||||
};
|
||||
|
||||
const Component = () => {
|
||||
const isDark = useDarkMode();
|
||||
const theme = useTheme();
|
||||
useEffect(() => {
|
||||
theme.setTheme(isDark ? 'dark' : 'light');
|
||||
}, [isDark]);
|
||||
return null;
|
||||
};
|
||||
|
||||
export const decorators = [
|
||||
(Story: ComponentType) => {
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<Component />
|
||||
<Story />
|
||||
</ThemeProvider>
|
||||
);
|
||||
},
|
||||
createI18nDecorator(),
|
||||
];
|
||||
@@ -2,11 +2,6 @@
|
||||
"name": "@affine/component",
|
||||
"private": true,
|
||||
"main": "./src/index.ts",
|
||||
"scripts": {
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"build-storybook": "NODE_OPTIONS=--max_old_space_size=4096 storybook build",
|
||||
"test-storybook": "test-storybook"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./theme/*": "./src/theme/*",
|
||||
@@ -60,32 +55,12 @@
|
||||
"@blocksuite/icons": "^2.1.19",
|
||||
"@blocksuite/lit": "0.0.0-20230601122821-16196c35-nightly",
|
||||
"@blocksuite/store": "0.0.0-20230601122821-16196c35-nightly",
|
||||
"@storybook/addon-actions": "^7.0.18",
|
||||
"@storybook/addon-coverage": "^0.0.8",
|
||||
"@storybook/addon-essentials": "^7.0.18",
|
||||
"@storybook/addon-interactions": "^7.0.18",
|
||||
"@storybook/addon-links": "^7.0.18",
|
||||
"@storybook/addon-storysource": "^7.0.18",
|
||||
"@storybook/blocks": "^7.0.18",
|
||||
"@storybook/builder-vite": "^7.0.18",
|
||||
"@storybook/jest": "^0.1.0",
|
||||
"@storybook/react": "^7.0.18",
|
||||
"@storybook/react-vite": "^7.0.18",
|
||||
"@storybook/test-runner": "^0.10.0",
|
||||
"@storybook/testing-library": "^0.1.0",
|
||||
"@types/react": "^18.2.6",
|
||||
"@types/react-dnd": "^3.0.2",
|
||||
"@types/react-dom": "18.2.4",
|
||||
"@vanilla-extract/css": "^1.11.0",
|
||||
"@vitejs/plugin-react": "^4.0.0",
|
||||
"concurrently": "^8.1.0",
|
||||
"jest-mock": "^29.5.0",
|
||||
"serve": "^14.2.0",
|
||||
"storybook": "^7.0.18",
|
||||
"storybook-dark-mode": "^3.0.0",
|
||||
"typescript": "^5.0.4",
|
||||
"vite": "^4.3.9",
|
||||
"wait-on": "^7.0.1",
|
||||
"yjs": "^13.6.1"
|
||||
},
|
||||
"version": "0.7.0-canary.8"
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
import {
|
||||
DeleteTemporarilyIcon,
|
||||
SettingsIcon,
|
||||
SidebarIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import type { Meta, StoryFn } from '@storybook/react';
|
||||
import { useAtom } from 'jotai';
|
||||
import { type PropsWithChildren, useState } from 'react';
|
||||
|
||||
import { IconButton } from '../..';
|
||||
import { AppSidebar, AppSidebarFallback, appSidebarOpenAtom } from '.';
|
||||
import { AddPageButton } from './add-page-button';
|
||||
import { CategoryDivider } from './category-divider';
|
||||
import { navHeaderStyle, sidebarButtonStyle } from './index.css';
|
||||
import { MenuLinkItem } from './menu-item';
|
||||
import { QuickSearchInput } from './quick-search-input';
|
||||
import {
|
||||
SidebarContainer,
|
||||
SidebarScrollableContainer,
|
||||
} from './sidebar-containers';
|
||||
|
||||
export default {
|
||||
title: 'Components/AppSidebar',
|
||||
component: AppSidebar,
|
||||
} satisfies Meta;
|
||||
|
||||
const Container = ({ children }: PropsWithChildren) => (
|
||||
<main
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: '100vw',
|
||||
height: 'calc(100vh - 40px)',
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</main>
|
||||
);
|
||||
const Main = () => {
|
||||
const [open, setOpen] = useAtom(appSidebarOpenAtom);
|
||||
return (
|
||||
<div>
|
||||
<div className={navHeaderStyle}>
|
||||
{!open && (
|
||||
<IconButton
|
||||
className={sidebarButtonStyle}
|
||||
onClick={() => {
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
<SidebarIcon width={24} height={24} />
|
||||
</IconButton>
|
||||
)}
|
||||
</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 />}
|
||||
href="/test"
|
||||
onClick={() => alert('opened')}
|
||||
>
|
||||
Settings
|
||||
</MenuLinkItem>
|
||||
<MenuLinkItem
|
||||
icon={<SettingsIcon />}
|
||||
href="/test"
|
||||
onClick={() => alert('opened')}
|
||||
>
|
||||
Settings
|
||||
</MenuLinkItem>
|
||||
<MenuLinkItem
|
||||
icon={<SettingsIcon />}
|
||||
href="/test"
|
||||
onClick={() => alert('opened')}
|
||||
>
|
||||
Settings
|
||||
</MenuLinkItem>
|
||||
</SidebarContainer>
|
||||
|
||||
<SidebarScrollableContainer>
|
||||
<CategoryDivider label="Favorites" />
|
||||
<MenuLinkItem
|
||||
collapsed={collapsed}
|
||||
onCollapsedChange={setCollapsed}
|
||||
icon={<SettingsIcon />}
|
||||
href="/test"
|
||||
onClick={() => alert('opened')}
|
||||
>
|
||||
Collapsible Item
|
||||
</MenuLinkItem>
|
||||
<MenuLinkItem
|
||||
collapsed={!collapsed}
|
||||
onCollapsedChange={setCollapsed}
|
||||
icon={<SettingsIcon />}
|
||||
href="/test"
|
||||
onClick={() => alert('opened')}
|
||||
>
|
||||
Collapsible Item
|
||||
</MenuLinkItem>
|
||||
<MenuLinkItem
|
||||
icon={<SettingsIcon />}
|
||||
href="/test"
|
||||
onClick={() => alert('opened')}
|
||||
>
|
||||
Settings
|
||||
</MenuLinkItem>
|
||||
|
||||
<CategoryDivider label="Others" />
|
||||
<MenuLinkItem
|
||||
icon={<DeleteTemporarilyIcon />}
|
||||
href="/test"
|
||||
onClick={() => alert('opened')}
|
||||
>
|
||||
Trash
|
||||
</MenuLinkItem>
|
||||
</SidebarScrollableContainer>
|
||||
<SidebarContainer>
|
||||
<AddPageButton />
|
||||
</SidebarContainer>
|
||||
</AppSidebar>
|
||||
<Main />
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
@@ -131,7 +131,9 @@ export const AppSidebarFallback = (): ReactElement | null => {
|
||||
export * from './add-page-button';
|
||||
export * from './app-updater-button';
|
||||
export * from './category-divider';
|
||||
export * from './index.css';
|
||||
export * from './menu-item';
|
||||
export * from './quick-search-input';
|
||||
export * from './sidebar-containers';
|
||||
export * from './sidebar-header';
|
||||
export { appSidebarFloatingAtom, appSidebarOpenAtom, appSidebarResizingAtom };
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
/* deepscan-disable USELESS_ARROW_FUNC_BIND */
|
||||
import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models';
|
||||
import type { EditorContainer } from '@blocksuite/editor';
|
||||
import type { Page } from '@blocksuite/store';
|
||||
import { createMemoryStorage, Workspace } from '@blocksuite/store';
|
||||
import { expect } from '@storybook/jest';
|
||||
import type { Meta, StoryFn } from '@storybook/react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import type { EditorProps } from '.';
|
||||
import { BlockSuiteEditor } from '.';
|
||||
|
||||
const blockSuiteWorkspace = new Workspace({
|
||||
id: 'test',
|
||||
blobStorages: [createMemoryStorage],
|
||||
});
|
||||
|
||||
function initPage(page: Page): void {
|
||||
// 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:frame', {}, pageBlockId);
|
||||
page.addBlock(
|
||||
'affine:paragraph',
|
||||
{
|
||||
text: new page.Text('This is a paragraph.'),
|
||||
},
|
||||
frameId
|
||||
);
|
||||
page.resetHistory();
|
||||
}
|
||||
|
||||
blockSuiteWorkspace.register(AffineSchemas).register(__unstableSchemas);
|
||||
const page = blockSuiteWorkspace.createPage('page0');
|
||||
initPage(page);
|
||||
|
||||
type BlockSuiteMeta = Meta<typeof BlockSuiteEditor>;
|
||||
export default {
|
||||
title: 'BlockSuite/Editor',
|
||||
component: BlockSuiteEditor,
|
||||
} satisfies BlockSuiteMeta;
|
||||
|
||||
const Template: StoryFn<EditorProps> = (props: Partial<EditorProps>) => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
height: '100vh',
|
||||
width: '100vw',
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
<BlockSuiteEditor onInit={initPage} page={page} mode="page" {...props} />
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: 12,
|
||||
bottom: 12,
|
||||
}}
|
||||
id="toolWrapper"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Empty = Template.bind({});
|
||||
Empty.play = async ({ canvasElement }) => {
|
||||
await new Promise<void>(resolve => {
|
||||
setTimeout(() => resolve(), 500);
|
||||
});
|
||||
const editorContainer = canvasElement.querySelector(
|
||||
'[data-testid="editor-page0"]'
|
||||
) as HTMLDivElement;
|
||||
expect(editorContainer).not.toBeNull();
|
||||
const editor = editorContainer.querySelector(
|
||||
'editor-container'
|
||||
) as EditorContainer;
|
||||
expect(editor).not.toBeNull();
|
||||
};
|
||||
|
||||
Empty.args = {
|
||||
mode: 'page',
|
||||
};
|
||||
|
||||
export const Error: StoryFn = () => {
|
||||
const [props, setProps] = useState<Pick<EditorProps, 'page' | 'onInit'>>({
|
||||
page: null!,
|
||||
onInit: null!,
|
||||
});
|
||||
return (
|
||||
<BlockSuiteEditor
|
||||
{...props}
|
||||
mode="page"
|
||||
onReset={() => {
|
||||
setProps({
|
||||
page,
|
||||
onInit: initPage,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
Error.play = async ({ canvasElement }) => {
|
||||
{
|
||||
const editorContainer = canvasElement.querySelector(
|
||||
'[data-testid="editor-page0"]'
|
||||
);
|
||||
expect(editorContainer).toBeNull();
|
||||
}
|
||||
{
|
||||
const button = canvasElement.querySelector(
|
||||
'[data-testid="error-fallback-reset-button"]'
|
||||
) as HTMLButtonElement;
|
||||
expect(button).not.toBeNull();
|
||||
button.click();
|
||||
await new Promise<void>(resolve => setTimeout(() => resolve(), 50));
|
||||
}
|
||||
{
|
||||
const editorContainer = canvasElement.querySelector(
|
||||
'[data-testid="editor-page0"]'
|
||||
);
|
||||
expect(editorContainer).not.toBeNull();
|
||||
}
|
||||
};
|
||||
@@ -1,65 +0,0 @@
|
||||
import { initPage } from '@affine/env/blocksuite';
|
||||
import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
|
||||
import type { Meta } from '@storybook/react';
|
||||
|
||||
import { BlockSuiteEditor } from '../block-suite-editor';
|
||||
import { ImagePreviewModal } from '.';
|
||||
|
||||
export default {
|
||||
title: 'Component/ImagePreviewModal',
|
||||
component: ImagePreviewModal,
|
||||
} satisfies Meta;
|
||||
|
||||
const workspace = createEmptyBlockSuiteWorkspace(
|
||||
'test',
|
||||
WorkspaceFlavour.LOCAL
|
||||
);
|
||||
const page = workspace.createPage('page0');
|
||||
initPage(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:frame')[0].id;
|
||||
page.addBlock(
|
||||
'affine:paragraph',
|
||||
{
|
||||
text: new page.Text('Please double click the image to preview it.'),
|
||||
},
|
||||
frameId
|
||||
);
|
||||
page.addBlock(
|
||||
'affine:embed',
|
||||
{
|
||||
sourceId: id,
|
||||
},
|
||||
frameId
|
||||
);
|
||||
});
|
||||
|
||||
export const Default = () => {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
height: '100vh',
|
||||
width: '100vw',
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
<BlockSuiteEditor mode="page" page={page} onInit={initPage} />
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: 12,
|
||||
bottom: 12,
|
||||
}}
|
||||
id="toolWrapper"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,200 +0,0 @@
|
||||
import type { Meta } from '@storybook/react';
|
||||
import { useAtomValue, useSetAtom } from 'jotai';
|
||||
|
||||
import { NotificationCenter, pushNotificationAtom } from '.';
|
||||
import { expandNotificationCenterAtom } from './index.jotai';
|
||||
|
||||
export default {
|
||||
title: 'AFFiNE/NotificationCenter',
|
||||
component: NotificationCenter,
|
||||
} satisfies Meta<typeof NotificationCenter>;
|
||||
|
||||
let id = 0;
|
||||
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>
|
||||
<NotificationCenter />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -26,8 +26,11 @@ import {
|
||||
removeNotificationAtom,
|
||||
} from './index.jotai';
|
||||
|
||||
// only expose necessary function atom to avoid misuse
|
||||
export { pushNotificationAtom, removeNotificationAtom };
|
||||
export {
|
||||
expandNotificationCenterAtom,
|
||||
pushNotificationAtom,
|
||||
removeNotificationAtom,
|
||||
};
|
||||
type Height = {
|
||||
height: number;
|
||||
notificationKey: number | string;
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import type { Meta } from '@storybook/react';
|
||||
|
||||
import { PageDetailSkeleton } from '.';
|
||||
|
||||
export default {
|
||||
title: 'AFFiNE/PageDetailSkeleton',
|
||||
component: PageDetailSkeleton,
|
||||
} satisfies Meta<typeof PageDetailSkeleton>;
|
||||
|
||||
export const Basic = () => {
|
||||
return <PageDetailSkeleton />;
|
||||
};
|
||||
@@ -1,73 +0,0 @@
|
||||
import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
|
||||
import { arrayMove } from '@dnd-kit/sortable';
|
||||
import type { Meta } from '@storybook/react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import type { WorkspaceListProps } from './index';
|
||||
import { WorkspaceList } from './index';
|
||||
|
||||
export default {
|
||||
title: 'AFFiNE/WorkspaceList',
|
||||
component: WorkspaceList,
|
||||
} satisfies Meta<WorkspaceListProps>;
|
||||
|
||||
export const Default = () => {
|
||||
const [items, setItems] = useState(() => {
|
||||
const items = [
|
||||
{
|
||||
id: '1',
|
||||
flavour: WorkspaceFlavour.LOCAL,
|
||||
blockSuiteWorkspace: createEmptyBlockSuiteWorkspace(
|
||||
'1',
|
||||
WorkspaceFlavour.LOCAL
|
||||
),
|
||||
providers: [],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
flavour: WorkspaceFlavour.LOCAL,
|
||||
blockSuiteWorkspace: createEmptyBlockSuiteWorkspace(
|
||||
'2',
|
||||
WorkspaceFlavour.LOCAL
|
||||
),
|
||||
providers: [],
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
flavour: WorkspaceFlavour.LOCAL,
|
||||
blockSuiteWorkspace: createEmptyBlockSuiteWorkspace(
|
||||
'3',
|
||||
WorkspaceFlavour.LOCAL
|
||||
),
|
||||
providers: [],
|
||||
},
|
||||
] 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);
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,37 +0,0 @@
|
||||
import type { StoryFn } from '@storybook/react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { BrowserWarning, DownloadTips } from '../components/affine-banner';
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,25 +0,0 @@
|
||||
import type { StoryFn } from '@storybook/react';
|
||||
|
||||
import { AffineLoading } from '../components/affine-loading';
|
||||
|
||||
export default {
|
||||
title: 'AFFiNE/Loading',
|
||||
component: AffineLoading,
|
||||
};
|
||||
|
||||
export const Default: StoryFn = ({ width, loop, autoplay, autoReverse }) => (
|
||||
<div
|
||||
style={{
|
||||
width: width,
|
||||
height: width,
|
||||
}}
|
||||
>
|
||||
<AffineLoading loop={loop} autoplay={autoplay} autoReverse={autoReverse} />
|
||||
</div>
|
||||
);
|
||||
Default.args = {
|
||||
width: 100,
|
||||
loop: true,
|
||||
autoplay: true,
|
||||
autoReverse: true,
|
||||
};
|
||||
@@ -1,40 +0,0 @@
|
||||
/* deepscan-disable USELESS_ARROW_FUNC_BIND */
|
||||
import { Link, Typography } from '@mui/material';
|
||||
import { expect } from '@storybook/jest';
|
||||
import type { Meta, StoryFn } from '@storybook/react';
|
||||
import { within } from '@storybook/testing-library';
|
||||
|
||||
import { Breadcrumbs } from '..';
|
||||
|
||||
export default {
|
||||
title: 'AFFiNE/Breadcrumbs',
|
||||
component: Breadcrumbs,
|
||||
} 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>,
|
||||
],
|
||||
};
|
||||
@@ -1,138 +0,0 @@
|
||||
/* deepscan-disable USELESS_ARROW_FUNC_BIND */
|
||||
import type { Meta, StoryFn } from '@storybook/react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Button } from '../ui/button/button';
|
||||
import { DropdownButton } from '../ui/button/dropdown';
|
||||
import type { ButtonProps } from '../ui/button/interface';
|
||||
import { RadioButton, RadioButtonGroup } from '../ui/button/radio';
|
||||
import { Menu } from '../ui/menu/menu';
|
||||
import { toast } from '../ui/toast/toast';
|
||||
|
||||
export default {
|
||||
title: 'AFFiNE/Button',
|
||||
component: Button,
|
||||
argTypes: {
|
||||
hoverBackground: { control: 'color' },
|
||||
hoverColor: { control: 'color' },
|
||||
},
|
||||
} as Meta<ButtonProps>;
|
||||
|
||||
const Template: StoryFn<ButtonProps> = args => <Button {...args} />;
|
||||
|
||||
export const Primary = Template.bind(undefined);
|
||||
Primary.args = {
|
||||
type: 'primary',
|
||||
children: 'This is a primary button',
|
||||
onClick: () => toast('Click button'),
|
||||
};
|
||||
|
||||
export const Default = Template.bind(undefined);
|
||||
Default.args = {
|
||||
type: 'default',
|
||||
children: 'This is a default button',
|
||||
onClick: () => toast('Click button'),
|
||||
};
|
||||
|
||||
export const Light = Template.bind(undefined);
|
||||
Light.args = {
|
||||
type: 'light',
|
||||
children: 'This is a light button',
|
||||
onClick: () => toast('Click button'),
|
||||
};
|
||||
|
||||
export const Warning = Template.bind(undefined);
|
||||
Warning.args = {
|
||||
type: 'warning',
|
||||
children: 'This is a warning button',
|
||||
onClick: () => toast('Click button'),
|
||||
};
|
||||
|
||||
export const Danger = Template.bind(undefined);
|
||||
Danger.args = {
|
||||
type: 'danger',
|
||||
children: 'This is a danger button',
|
||||
onClick: () => toast('Click button'),
|
||||
};
|
||||
|
||||
export const Dropdown: StoryFn = ({ onClickDropDown, ...props }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<DropdownButton onClickDropDown={onClickDropDown} {...props}>
|
||||
Dropdown Button
|
||||
</DropdownButton>
|
||||
|
||||
<Menu
|
||||
visible={open}
|
||||
placement="bottom-end"
|
||||
trigger={['click']}
|
||||
width={235}
|
||||
disablePortal={true}
|
||||
onClickAway={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
content={<>Dropdown Menu</>}
|
||||
>
|
||||
<DropdownButton
|
||||
onClick={() => {
|
||||
toast('Click button');
|
||||
setOpen(false);
|
||||
}}
|
||||
onClickDropDown={() => setOpen(!open)}
|
||||
>
|
||||
Dropdown with Menu
|
||||
</DropdownButton>
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
Dropdown.args = {
|
||||
onClick: () => toast('Click button'),
|
||||
onClickDropDown: () => toast('Click dropdown'),
|
||||
};
|
||||
|
||||
export const RadioGroup: StoryFn = ({ ...props }) => {
|
||||
return (
|
||||
<>
|
||||
<RadioButtonGroup {...props}>
|
||||
<RadioButton value="all">All</RadioButton>
|
||||
<RadioButton value="page">Page</RadioButton>
|
||||
<RadioButton value="edgeless">Edgeless</RadioButton>
|
||||
</RadioButtonGroup>
|
||||
<RadioButtonGroup {...props}>
|
||||
<RadioButton value="all">Long long text and some more</RadioButton>
|
||||
<RadioButton value="page">Lorem ipsum dolor sit amet</RadioButton>
|
||||
<RadioButton value="edgeless">Long long text</RadioButton>
|
||||
</RadioButtonGroup>
|
||||
</>
|
||||
);
|
||||
};
|
||||
RadioGroup.args = {
|
||||
defaultValue: 'all',
|
||||
onValueChange: (value: string) => toast(`Radio value: ${value}`),
|
||||
};
|
||||
|
||||
export const Test: StoryFn<ButtonProps> = () => {
|
||||
const [input, setInput] = useState('');
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
type="text"
|
||||
data-testid="test-input"
|
||||
value={input}
|
||||
onChange={e => setInput(e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setInput('');
|
||||
}}
|
||||
data-testid="clear-button"
|
||||
>
|
||||
clear
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Test.storyName = 'Click Test';
|
||||
@@ -1,55 +0,0 @@
|
||||
import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
|
||||
import { Workspace } from '@blocksuite/store';
|
||||
|
||||
import { BlockCard } from '../components/card/block-card';
|
||||
import { WorkspaceCard } from '../components/card/workspace-card';
|
||||
import { toast } from '../ui/toast';
|
||||
|
||||
export default {
|
||||
title: 'AFFiNE/Card',
|
||||
component: WorkspaceCard,
|
||||
};
|
||||
|
||||
const blockSuiteWorkspace = new Workspace({
|
||||
id: 'blocksuite-local',
|
||||
});
|
||||
|
||||
blockSuiteWorkspace.meta.setName('Hello World');
|
||||
|
||||
export const AffineWorkspaceCard = () => {
|
||||
return (
|
||||
<WorkspaceCard
|
||||
workspace={{
|
||||
flavour: WorkspaceFlavour.LOCAL,
|
||||
id: 'local',
|
||||
blockSuiteWorkspace,
|
||||
providers: [],
|
||||
}}
|
||||
onClick={() => {}}
|
||||
onSettingClick={() => {}}
|
||||
currentWorkspaceId={null}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
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')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,48 +0,0 @@
|
||||
import type { StoryFn } from '@storybook/react';
|
||||
import { within } from '@storybook/testing-library';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { ChangeLog } from '../components/changeLog';
|
||||
|
||||
export default {
|
||||
title: 'AFFiNE/ChangeLog',
|
||||
component: ChangeLog,
|
||||
};
|
||||
|
||||
export const Default: StoryFn = () => (
|
||||
<div
|
||||
style={{
|
||||
width: '256px',
|
||||
height: '100vh',
|
||||
}}
|
||||
>
|
||||
<ChangeLog onCloseWhatsNew={() => {}} />
|
||||
</div>
|
||||
);
|
||||
|
||||
export const Close: StoryFn = () => {
|
||||
const [closed, setIsClosed] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<div>Closed: {closed ? 'true' : 'false'}</div>
|
||||
<div
|
||||
style={{
|
||||
width: '256px',
|
||||
height: '100vh',
|
||||
}}
|
||||
>
|
||||
<ChangeLog
|
||||
onCloseWhatsNew={() => {
|
||||
setIsClosed(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Close.play = async ({ canvasElement }) => {
|
||||
const element = within(canvasElement);
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
element.getByTestId('change-log-close-button').click();
|
||||
};
|
||||
@@ -1,36 +0,0 @@
|
||||
import type { StoryFn } from '@storybook/react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import type { ContactModalProps } from '../components/contact-modal';
|
||||
import { ContactModal } from '../components/contact-modal';
|
||||
import { Button } from '../ui/button';
|
||||
|
||||
export default {
|
||||
title: 'AFFiNE/ContactModal',
|
||||
component: ContactModal,
|
||||
};
|
||||
|
||||
export const Basic: StoryFn<ContactModalProps> = args => {
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
Open
|
||||
</Button>
|
||||
<ContactModal
|
||||
{...args}
|
||||
open={open}
|
||||
onClose={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
Basic.args = {
|
||||
logoSrc: '/imgs/affine-text-logo.png',
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
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)
|
||||
@@ -1,16 +0,0 @@
|
||||
/* deepscan-disable USELESS_ARROW_FUNC_BIND */
|
||||
import type { StoryFn } from '@storybook/react';
|
||||
|
||||
import { TourModal } from '../components/tour-modal';
|
||||
|
||||
export default {
|
||||
title: 'AFFiNE/TourModal',
|
||||
component: TourModal,
|
||||
};
|
||||
|
||||
export const Basic: StoryFn = () => {
|
||||
return <TourModal open={true} onClose={() => {}} />;
|
||||
};
|
||||
Basic.args = {
|
||||
logoSrc: '/imgs/affine-text-logo.png',
|
||||
};
|
||||
@@ -1,191 +0,0 @@
|
||||
import { PageIcon } from '@blocksuite/icons';
|
||||
import { expect } from '@storybook/jest';
|
||||
import type { StoryFn } from '@storybook/react';
|
||||
import { userEvent } from '@storybook/testing-library';
|
||||
|
||||
import { AffineLoading } from '../components/affine-loading';
|
||||
import { PageListTrashView } from '../components/page-list/all-page';
|
||||
import { PageList } from '../components/page-list/all-page';
|
||||
import { NewPageButton } from '../components/page-list/components/new-page-buttton';
|
||||
import type { OperationCellProps } from '../components/page-list/operation-cell';
|
||||
import { OperationCell } from '../components/page-list/operation-cell';
|
||||
import Empty from '../ui/empty/empty';
|
||||
import { toast } from '../ui/toast';
|
||||
|
||||
export default {
|
||||
title: 'AFFiNE/PageList',
|
||||
component: AffineLoading,
|
||||
};
|
||||
|
||||
export const AffineOperationCell: StoryFn<OperationCellProps> = ({
|
||||
...props
|
||||
}) => <OperationCell {...props} />;
|
||||
|
||||
AffineOperationCell.args = {
|
||||
title: 'Example Page',
|
||||
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();
|
||||
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();
|
||||
userEvent.click(dropdown);
|
||||
};
|
||||
|
||||
export const AffineAllPageList: StoryFn<typeof PageList> = ({ ...props }) => (
|
||||
<PageList {...props} />
|
||||
);
|
||||
|
||||
AffineAllPageList.args = {
|
||||
isPublicWorkspace: false,
|
||||
onCreateNewPage: () => toast('Create new page'),
|
||||
onCreateNewEdgeless: () => toast('Create new edgeless'),
|
||||
onImportFile: () => toast('Import file'),
|
||||
list: [
|
||||
{
|
||||
pageId: '1',
|
||||
favorite: false,
|
||||
icon: <PageIcon />,
|
||||
isPublicPage: true,
|
||||
title: 'Today Page',
|
||||
preview: 'this is page preview',
|
||||
createDate: new Date(),
|
||||
updatedDate: new Date(),
|
||||
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',
|
||||
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',
|
||||
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',
|
||||
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 description="empty description" />,
|
||||
};
|
||||
|
||||
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'),
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,130 +0,0 @@
|
||||
import { PermissionType, WorkspaceType } from '@affine/workspace/affine/api';
|
||||
import type {
|
||||
AffineLegacyCloudWorkspace,
|
||||
LocalWorkspace,
|
||||
} from '@affine/workspace/type';
|
||||
import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
|
||||
import type { Page } from '@blocksuite/store';
|
||||
import { expect } from '@storybook/jest';
|
||||
import type { StoryFn } from '@storybook/react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { PublicLinkDisableModal } from '../components/share-menu/disable-public-link';
|
||||
import { ShareMenu } from '../components/share-menu/share-menu';
|
||||
import { StyledDisableButton } from '../components/share-menu/styles';
|
||||
import toast from '../ui/toast/toast';
|
||||
|
||||
export default {
|
||||
title: 'AFFiNE/ShareMenu',
|
||||
component: ShareMenu,
|
||||
};
|
||||
|
||||
function initPage(page: Page): void {
|
||||
// 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:frame', {}, pageBlockId);
|
||||
page.addBlock(
|
||||
'affine:paragraph',
|
||||
{
|
||||
text: new page.Text('This is a paragraph.'),
|
||||
},
|
||||
frameId
|
||||
);
|
||||
page.resetHistory();
|
||||
}
|
||||
|
||||
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
|
||||
'test-workspace',
|
||||
WorkspaceFlavour.LOCAL
|
||||
);
|
||||
|
||||
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,
|
||||
providers: [],
|
||||
};
|
||||
|
||||
const affineWorkspace: AffineLegacyCloudWorkspace = {
|
||||
id: 'test-workspace',
|
||||
flavour: WorkspaceFlavour.AFFINE,
|
||||
blockSuiteWorkspace,
|
||||
providers: [],
|
||||
public: false,
|
||||
type: WorkspaceType.Normal,
|
||||
permission: PermissionType.Owner,
|
||||
};
|
||||
|
||||
async function unimplemented() {
|
||||
toast('work in progress');
|
||||
}
|
||||
|
||||
export const Basic: StoryFn = () => {
|
||||
return (
|
||||
<ShareMenu
|
||||
currentPage={blockSuiteWorkspace.getPage('page0') as Page}
|
||||
workspace={localWorkspace}
|
||||
onEnableAffineCloud={unimplemented}
|
||||
onOpenWorkspaceSettings={unimplemented}
|
||||
togglePagePublic={unimplemented}
|
||||
toggleWorkspacePublish={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 => setTimeout(resolve, 100));
|
||||
{
|
||||
const button = canvasElement.querySelector(
|
||||
'[data-testid="share-menu-enable-affine-cloud-button"]'
|
||||
);
|
||||
expect(button).not.toBeNull();
|
||||
}
|
||||
};
|
||||
|
||||
export const AffineBasic: StoryFn = () => {
|
||||
return (
|
||||
<ShareMenu
|
||||
currentPage={blockSuiteWorkspace.getPage('page0') as Page}
|
||||
workspace={affineWorkspace}
|
||||
onEnableAffineCloud={unimplemented}
|
||||
onOpenWorkspaceSettings={unimplemented}
|
||||
togglePagePublic={unimplemented}
|
||||
toggleWorkspacePublish={unimplemented}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const DisableModal: StoryFn = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<StyledDisableButton onClick={() => setOpen(!open)}>
|
||||
Disable Public Link
|
||||
</StyledDisableButton>
|
||||
<PublicLinkDisableModal
|
||||
open={open}
|
||||
onConfirmDisable={() => {
|
||||
toast('Disabled');
|
||||
setOpen(false);
|
||||
}}
|
||||
onClose={() => setOpen(false)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,13 +0,0 @@
|
||||
/* deepscan-disable USELESS_ARROW_FUNC_BIND */
|
||||
import type { StoryFn } from '@storybook/react';
|
||||
|
||||
import { Switch } from '..';
|
||||
|
||||
export default {
|
||||
title: 'AFFiNE/Switch',
|
||||
component: Switch,
|
||||
};
|
||||
|
||||
export const Basic: StoryFn = () => {
|
||||
return <Switch />;
|
||||
};
|
||||
@@ -1,84 +0,0 @@
|
||||
import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||
import { Workspace } from '@blocksuite/store';
|
||||
import type { Meta, StoryFn } from '@storybook/react';
|
||||
|
||||
import type { WorkspaceAvatarProps } from '../components/workspace-avatar';
|
||||
import { WorkspaceAvatar } from '../components/workspace-avatar';
|
||||
|
||||
export default {
|
||||
title: 'AFFiNE/WorkspaceAvatar',
|
||||
component: WorkspaceAvatar,
|
||||
argTypes: {
|
||||
size: {
|
||||
control: {
|
||||
type: 'range',
|
||||
min: 20,
|
||||
max: 100,
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies Meta<WorkspaceAvatarProps>;
|
||||
|
||||
const basicBlockSuiteWorkspace = new Workspace({
|
||||
id: 'blocksuite-local',
|
||||
});
|
||||
|
||||
basicBlockSuiteWorkspace.meta.setName('Hello World');
|
||||
|
||||
export const Basic: StoryFn<WorkspaceAvatarProps> = props => {
|
||||
return (
|
||||
<WorkspaceAvatar
|
||||
{...props}
|
||||
workspace={{
|
||||
flavour: WorkspaceFlavour.LOCAL,
|
||||
id: 'local',
|
||||
blockSuiteWorkspace: basicBlockSuiteWorkspace,
|
||||
providers: [],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
Basic.args = {
|
||||
size: 40,
|
||||
};
|
||||
|
||||
const avatarBlockSuiteWorkspace = new Workspace({
|
||||
id: 'blocksuite-local',
|
||||
});
|
||||
|
||||
avatarBlockSuiteWorkspace.meta.setName('Hello World');
|
||||
fetch(new URL('@affine-test/fixtures/smile.png', import.meta.url))
|
||||
.then(res => res.arrayBuffer())
|
||||
.then(async buffer => {
|
||||
const id = await avatarBlockSuiteWorkspace.blobs.set(
|
||||
new Blob([buffer], { type: 'image/png' })
|
||||
);
|
||||
avatarBlockSuiteWorkspace.meta.setAvatar(id);
|
||||
});
|
||||
|
||||
export const BlobExample: StoryFn<WorkspaceAvatarProps> = props => {
|
||||
return (
|
||||
<WorkspaceAvatar
|
||||
{...props}
|
||||
workspace={{
|
||||
flavour: WorkspaceFlavour.LOCAL,
|
||||
id: 'local',
|
||||
blockSuiteWorkspace: avatarBlockSuiteWorkspace,
|
||||
providers: [],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
BlobExample.args = {
|
||||
size: 40,
|
||||
};
|
||||
|
||||
export const Empty: StoryFn<WorkspaceAvatarProps> = props => {
|
||||
return <WorkspaceAvatar {...props} workspace={null} />;
|
||||
};
|
||||
|
||||
Empty.args = {
|
||||
size: 40,
|
||||
};
|
||||
@@ -4,6 +4,9 @@ import type { ButtonProps } from './interface';
|
||||
import { Loading } from './loading';
|
||||
import { StyledButton } from './styles';
|
||||
import { getSize } from './utils';
|
||||
|
||||
export type { ButtonProps };
|
||||
|
||||
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
(
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './button';
|
||||
export * from './dropdown';
|
||||
export * from './icon-button';
|
||||
export * from './radio';
|
||||
export * from './text-button';
|
||||
|
||||
@@ -30,9 +30,6 @@
|
||||
{
|
||||
"path": "../../apps/electron"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
},
|
||||
{ "path": "./tsconfig.workspace.json" },
|
||||
{ "path": "../../tests/fixtures" }
|
||||
]
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"jsx": "react-jsx",
|
||||
"moduleResolution": "Node",
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"noEmit": false,
|
||||
"outDir": "lib/.storybook"
|
||||
},
|
||||
"include": [".storybook/**/*"],
|
||||
"exclude": ["lib"],
|
||||
"references": [{ "path": "../i18n" }]
|
||||
}
|
||||
Reference in New Issue
Block a user