mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-18 10:31:50 +08:00
refactor(infra): directory structure (#4615)
This commit is contained in:
@@ -7,10 +7,10 @@
|
||||
"include": ["e2e"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../kit"
|
||||
"path": "../../tests/kit"
|
||||
},
|
||||
{
|
||||
"path": "../fixtures"
|
||||
"path": "../../tests/fixtures"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
"include": ["e2e"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../kit"
|
||||
"path": "../../tests/kit"
|
||||
},
|
||||
{
|
||||
"path": "../fixtures"
|
||||
"path": "../../tests/fixtures"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
"include": ["e2e"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../kit"
|
||||
"path": "../../tests/kit"
|
||||
},
|
||||
{
|
||||
"path": "../fixtures"
|
||||
"path": "../../tests/fixtures"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
"include": ["e2e"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../fixtures"
|
||||
"path": "../../../tests/kit"
|
||||
},
|
||||
{
|
||||
"path": "../../kit"
|
||||
"path": "../../../tests/fixtures"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
"include": ["e2e"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../fixtures"
|
||||
"path": "../../../tests/kit"
|
||||
},
|
||||
{
|
||||
"path": "../../kit"
|
||||
"path": "../../../tests/fixtures"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
"include": ["e2e"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../fixtures"
|
||||
"path": "../../../tests/kit"
|
||||
},
|
||||
{
|
||||
"path": "../../kit"
|
||||
"path": "../../../tests/fixtures"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
"include": ["e2e"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../kit"
|
||||
"path": "../../tests/kit"
|
||||
},
|
||||
{
|
||||
"path": "../fixtures"
|
||||
"path": "../../tests/fixtures"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
"include": ["e2e"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../fixtures"
|
||||
"path": "../../tests/kit"
|
||||
},
|
||||
{
|
||||
"path": "../kit"
|
||||
"path": "../../tests/fixtures"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
"include": ["e2e"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../kit"
|
||||
"path": "../../tests/kit"
|
||||
},
|
||||
{
|
||||
"path": "../fixtures"
|
||||
"path": "../../tests/fixtures"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
@@ -0,0 +1,3 @@
|
||||
<script>
|
||||
window.global = window;
|
||||
</script>
|
||||
@@ -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;
|
||||
@@ -0,0 +1 @@
|
||||
# Storybook
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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>,
|
||||
],
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
@@ -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} />;
|
||||
};
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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'),
|
||||
};
|
||||
@@ -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)
|
||||
@@ -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 />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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',
|
||||
};
|
||||
@@ -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 />;
|
||||
};
|
||||
@@ -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'),
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -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];
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>;
|
||||
};
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user