mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-25 10:22:55 +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 {
|
||||
|
||||
70
tests/storybook/.storybook/main.ts
Normal file
70
tests/storybook/.storybook/main.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { runCli } from '@magic-works/i18n-codegen';
|
||||
import type { StorybookConfig } from '@storybook/react-vite';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { mergeConfig } from 'vite';
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin';
|
||||
import { getRuntimeConfig } from '../../../packages/frontend/core/.webpack/runtime-config';
|
||||
|
||||
runCli(
|
||||
{
|
||||
config: fileURLToPath(
|
||||
new URL('../../../.i18n-codegen.json', import.meta.url)
|
||||
),
|
||||
watch: false,
|
||||
},
|
||||
error => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
);
|
||||
|
||||
export default {
|
||||
stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
|
||||
staticDirs: ['../../../packages/frontend/core/public'],
|
||||
addons: [
|
||||
'@storybook/addon-links',
|
||||
'@storybook/addon-essentials',
|
||||
'@storybook/addon-interactions',
|
||||
'@storybook/addon-storysource',
|
||||
'storybook-dark-mode',
|
||||
'storybook-addon-react-router-v6',
|
||||
],
|
||||
framework: {
|
||||
name: '@storybook/react-vite',
|
||||
},
|
||||
async viteFinal(config, _options) {
|
||||
return mergeConfig(config, {
|
||||
assetsInclude: ['**/*.md'],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@toeverything/infra': fileURLToPath(
|
||||
new URL('../../../packages/common/infra/src', import.meta.url)
|
||||
),
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
vanillaExtractPlugin(),
|
||||
tsconfigPaths({
|
||||
root: fileURLToPath(new URL('../../../', import.meta.url)),
|
||||
ignoreConfigErrors: true,
|
||||
}),
|
||||
],
|
||||
define: {
|
||||
'process.env': {},
|
||||
'process.env.COVERAGE': JSON.stringify(!!process.env.COVERAGE),
|
||||
'process.env.SHOULD_REPORT_TRACE': `${Boolean(
|
||||
process.env.SHOULD_REPORT_TRACE === 'true'
|
||||
)}`,
|
||||
'process.env.TRACE_REPORT_ENDPOINT': `"${process.env.TRACE_REPORT_ENDPOINT}"`,
|
||||
'process.env.CAPTCHA_SITE_KEY': `"${process.env.CAPTCHA_SITE_KEY}"`,
|
||||
runtimeConfig: getRuntimeConfig({
|
||||
distribution: 'browser',
|
||||
mode: 'development',
|
||||
channel: 'canary',
|
||||
coverage: false,
|
||||
}),
|
||||
},
|
||||
});
|
||||
},
|
||||
} as StorybookConfig;
|
||||
3
tests/storybook/.storybook/preview-head.html
Normal file
3
tests/storybook/.storybook/preview-head.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<script>
|
||||
window.global = window;
|
||||
</script>
|
||||
200
tests/storybook/.storybook/preview.tsx
Normal file
200
tests/storybook/.storybook/preview.tsx
Normal file
@@ -0,0 +1,200 @@
|
||||
import 'ses';
|
||||
import '@affine/component/theme/global.css';
|
||||
import '@affine/component/theme/theme.css';
|
||||
import '@toeverything/components/style.css';
|
||||
import { createI18n } from '@affine/i18n';
|
||||
import MockSessionContext, {
|
||||
mockAuthStates,
|
||||
// @ts-ignore
|
||||
} from '@tomfreudenberg/next-auth-mock';
|
||||
import { ThemeProvider, useTheme } from 'next-themes';
|
||||
import { useDarkMode } from 'storybook-dark-mode';
|
||||
import { AffineContext } from '@affine/component/context';
|
||||
import useSWR from 'swr';
|
||||
import type { Decorator } from '@storybook/react';
|
||||
import { createStore } from 'jotai/vanilla';
|
||||
import { _setCurrentStore } from '@toeverything/infra/atom';
|
||||
import { setupGlobal } from '@affine/env/global';
|
||||
|
||||
import type { Preview } from '@storybook/react';
|
||||
import { useLayoutEffect, useRef } from 'react';
|
||||
|
||||
setupGlobal();
|
||||
export const parameters = {
|
||||
backgrounds: { disable: true },
|
||||
actions: { argTypesRegex: '^on[A-Z].*' },
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const SB_PARAMETER_KEY = 'nextAuthMock';
|
||||
export const mockAuthPreviewToolbarItem = ({
|
||||
name = 'mockAuthState',
|
||||
description = 'Set authentication state',
|
||||
defaultValue = null,
|
||||
icon = 'user',
|
||||
items = mockAuthStates,
|
||||
} = {}) => {
|
||||
return {
|
||||
mockAuthState: {
|
||||
name,
|
||||
description,
|
||||
defaultValue,
|
||||
toolbar: {
|
||||
icon,
|
||||
items: Object.keys(items).map(e => ({
|
||||
value: e,
|
||||
title: items[e].title,
|
||||
})),
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const withMockAuth: Decorator = (Story, context) => {
|
||||
// Set a session value for mocking
|
||||
const session = (() => {
|
||||
// Allow overwrite of session value by parameter in story
|
||||
const paramValue = context?.parameters[SB_PARAMETER_KEY];
|
||||
if (typeof paramValue?.session === 'string') {
|
||||
return mockAuthStates[paramValue.session]?.session;
|
||||
} else {
|
||||
return paramValue?.session
|
||||
? paramValue.session
|
||||
: mockAuthStates[context.globals.mockAuthState]?.session;
|
||||
}
|
||||
})();
|
||||
|
||||
return (
|
||||
<MockSessionContext session={session}>
|
||||
<Story {...context} />
|
||||
</MockSessionContext>
|
||||
);
|
||||
};
|
||||
|
||||
const i18n = createI18n();
|
||||
const withI18n: Decorator = (Story, context) => {
|
||||
const locale = context.globals.locale;
|
||||
useSWR(
|
||||
locale,
|
||||
async () => {
|
||||
await i18n.changeLanguage(locale);
|
||||
},
|
||||
{
|
||||
suspense: true,
|
||||
}
|
||||
);
|
||||
return <Story {...context} />;
|
||||
};
|
||||
|
||||
const ThemeChange = () => {
|
||||
const isDark = useDarkMode();
|
||||
const theme = useTheme();
|
||||
if (theme.resolvedTheme === 'dark' && !isDark) {
|
||||
theme.setTheme('light');
|
||||
} else if (theme.resolvedTheme === 'light' && isDark) {
|
||||
theme.setTheme('dark');
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const storeMap = new Map<string, ReturnType<typeof createStore>>();
|
||||
|
||||
const bootstrapPluginSystemPromise = import(
|
||||
'@affine/core/bootstrap/register-plugins'
|
||||
).then(({ bootstrapPluginSystem }) => bootstrapPluginSystem);
|
||||
|
||||
const setupPromise = import('@affine/core/bootstrap/setup').then(
|
||||
({ setup }) => setup
|
||||
);
|
||||
|
||||
const withContextDecorator: Decorator = (Story, context) => {
|
||||
const { data: store } = useSWR(
|
||||
context.id,
|
||||
async () => {
|
||||
if (storeMap.has(context.id)) {
|
||||
return storeMap.get(context.id);
|
||||
}
|
||||
localStorage.clear();
|
||||
const store = createStore();
|
||||
_setCurrentStore(store);
|
||||
const setup = await setupPromise;
|
||||
await setup(store);
|
||||
const bootstrapPluginSystem = await bootstrapPluginSystemPromise;
|
||||
await bootstrapPluginSystem(store);
|
||||
storeMap.set(context.id, store);
|
||||
return store;
|
||||
},
|
||||
{
|
||||
suspense: true,
|
||||
}
|
||||
);
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<AffineContext store={store}>
|
||||
<ThemeChange />
|
||||
<Story {...context} />
|
||||
</AffineContext>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const platforms = ['web', 'desktop-macos', 'desktop-windows'] as const;
|
||||
|
||||
const withPlatformSelectionDecorator: Decorator = (Story, context) => {
|
||||
const setupCounterRef = useRef(0);
|
||||
useLayoutEffect(() => {
|
||||
if (setupCounterRef.current++ === 0) {
|
||||
return;
|
||||
}
|
||||
switch (context.globals.platform) {
|
||||
case 'desktop-macos':
|
||||
environment.isDesktop = true;
|
||||
environment.isMacOs = true;
|
||||
environment.isWindows = false;
|
||||
break;
|
||||
case 'desktop-windows':
|
||||
environment.isDesktop = true;
|
||||
environment.isMacOs = false;
|
||||
environment.isWindows = true;
|
||||
break;
|
||||
default:
|
||||
globalThis.$AFFINE_SETUP = false;
|
||||
setupGlobal();
|
||||
break;
|
||||
}
|
||||
}, [context.globals.platform]);
|
||||
|
||||
return <Story key={context.globals.platform} {...context} />;
|
||||
};
|
||||
|
||||
const decorators = [
|
||||
withContextDecorator,
|
||||
withI18n,
|
||||
withMockAuth,
|
||||
withPlatformSelectionDecorator,
|
||||
];
|
||||
|
||||
const preview: Preview = {
|
||||
decorators,
|
||||
globalTypes: {
|
||||
platform: {
|
||||
description: 'Rendering platform target',
|
||||
defaultValue: 'web',
|
||||
toolbar: {
|
||||
// The label to show for this toolbar item
|
||||
title: 'platform',
|
||||
// Array of plain string values or MenuItem shape (see below)
|
||||
items: platforms,
|
||||
// Change title based on selected value
|
||||
dynamicTitle: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default preview;
|
||||
1
tests/storybook/README.md
Normal file
1
tests/storybook/README.md
Normal file
@@ -0,0 +1 @@
|
||||
# Storybook
|
||||
61
tests/storybook/package.json
Normal file
61
tests/storybook/package.json
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"name": "@affine/storybook",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "storybook dev -p 6006",
|
||||
"build": "storybook build",
|
||||
"test": "test-storybook"
|
||||
},
|
||||
"dependencies": {
|
||||
"@affine/component": "workspace:*",
|
||||
"@affine/i18n": "workspace:*",
|
||||
"@mui/material": "^5.14.13",
|
||||
"@storybook/addon-actions": "^7.4.6",
|
||||
"@storybook/addon-essentials": "^7.4.6",
|
||||
"@storybook/addon-interactions": "^7.4.6",
|
||||
"@storybook/addon-links": "^7.4.6",
|
||||
"@storybook/addon-storysource": "^7.4.6",
|
||||
"@storybook/blocks": "^7.4.6",
|
||||
"@storybook/builder-vite": "^7.4.6",
|
||||
"@storybook/jest": "^0.2.3",
|
||||
"@storybook/react": "^7.4.6",
|
||||
"@storybook/react-vite": "^7.4.6",
|
||||
"@storybook/test-runner": "^0.13.0",
|
||||
"@storybook/testing-library": "^0.2.2",
|
||||
"@vitejs/plugin-react": "^4.1.0",
|
||||
"concurrently": "^8.2.1",
|
||||
"jest-mock": "^29.7.0",
|
||||
"serve": "^14.2.1",
|
||||
"ses": "^0.18.8",
|
||||
"storybook": "^7.4.6",
|
||||
"storybook-dark-mode": "^3.0.1",
|
||||
"wait-on": "^7.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@blocksuite/block-std": "0.0.0-20231018100009-361737d3-nightly",
|
||||
"@blocksuite/blocks": "0.0.0-20231018100009-361737d3-nightly",
|
||||
"@blocksuite/editor": "0.0.0-20231018100009-361737d3-nightly",
|
||||
"@blocksuite/global": "0.0.0-20231018100009-361737d3-nightly",
|
||||
"@blocksuite/icons": "2.1.34",
|
||||
"@blocksuite/lit": "0.0.0-20231018100009-361737d3-nightly",
|
||||
"@blocksuite/store": "0.0.0-20231018100009-361737d3-nightly",
|
||||
"@dnd-kit/sortable": "^7.0.2",
|
||||
"@tomfreudenberg/next-auth-mock": "^0.5.6",
|
||||
"chromatic": "^7.4.0",
|
||||
"foxact": "^0.2.20",
|
||||
"jotai": "^2.4.3",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-router-dom": "^6.16.0",
|
||||
"storybook-addon-react-router-v6": "^2.0.7"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@blocksuite/blocks": "*",
|
||||
"@blocksuite/editor": "*",
|
||||
"@blocksuite/global": "*",
|
||||
"@blocksuite/icons": "2.1.34",
|
||||
"@blocksuite/lit": "*",
|
||||
"@blocksuite/store": "*"
|
||||
},
|
||||
"version": "0.10.0-canary.1"
|
||||
}
|
||||
49
tests/storybook/project.json
Normal file
49
tests/storybook/project.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "@affine/storybook",
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "nx:run-script",
|
||||
"dependsOn": ["^build"],
|
||||
"inputs": [
|
||||
"default",
|
||||
"^production",
|
||||
"{projectRoot}/.storybook/**/*",
|
||||
"{workspaceRoot}/packages/frontend/core/src/**/*",
|
||||
"{workspaceRoot}/packages/common/infra/**/*",
|
||||
"{workspaceRoot}/packages/common/sdk/**/*",
|
||||
{
|
||||
"runtime": "node -v"
|
||||
},
|
||||
{
|
||||
"env": "BUILD_TYPE"
|
||||
},
|
||||
{
|
||||
"env": "PERFSEE_TOKEN"
|
||||
},
|
||||
{
|
||||
"env": "SENTRY_ORG"
|
||||
},
|
||||
{
|
||||
"env": "SENTRY_PROJECT"
|
||||
},
|
||||
{
|
||||
"env": "SENTRY_AUTH_TOKEN"
|
||||
},
|
||||
{
|
||||
"env": "NEXT_PUBLIC_SENTRY_DSN"
|
||||
},
|
||||
{
|
||||
"env": "DISTRIBUTION"
|
||||
},
|
||||
{
|
||||
"env": "COVERAGE"
|
||||
}
|
||||
],
|
||||
"options": {
|
||||
"script": "build"
|
||||
},
|
||||
"outputs": ["{projectRoot}/storybook-static"]
|
||||
}
|
||||
}
|
||||
}
|
||||
36
tests/storybook/src/stories/affine-banner.stories.tsx
Normal file
36
tests/storybook/src/stories/affine-banner.stories.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { BrowserWarning, DownloadTips } from '@affine/component/affine-banner';
|
||||
import type { StoryFn } from '@storybook/react';
|
||||
import { useState } from 'react';
|
||||
|
||||
export default {
|
||||
title: 'AFFiNE/Banner',
|
||||
component: BrowserWarning,
|
||||
};
|
||||
|
||||
export const Default: StoryFn = () => {
|
||||
const [closed, setIsClosed] = useState(true);
|
||||
return (
|
||||
<div>
|
||||
<BrowserWarning
|
||||
message={<span>test</span>}
|
||||
show={closed}
|
||||
onClose={() => {
|
||||
setIsClosed(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Download: StoryFn = () => {
|
||||
const [, setIsClosed] = useState(true);
|
||||
return (
|
||||
<div>
|
||||
<DownloadTips
|
||||
onClose={() => {
|
||||
setIsClosed(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
149
tests/storybook/src/stories/app-sidebar.stories.tsx
Normal file
149
tests/storybook/src/stories/app-sidebar.stories.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
import {
|
||||
AppSidebar,
|
||||
AppSidebarFallback,
|
||||
appSidebarOpenAtom,
|
||||
SidebarSwitch,
|
||||
} from '@affine/component/app-sidebar';
|
||||
import { AddPageButton } from '@affine/component/app-sidebar';
|
||||
import { CategoryDivider } from '@affine/component/app-sidebar';
|
||||
import { navHeaderStyle } from '@affine/component/app-sidebar';
|
||||
import { MenuLinkItem } from '@affine/component/app-sidebar';
|
||||
import { QuickSearchInput } from '@affine/component/app-sidebar';
|
||||
import {
|
||||
SidebarContainer,
|
||||
SidebarScrollableContainer,
|
||||
} from '@affine/component/app-sidebar';
|
||||
import { DeleteTemporarilyIcon, SettingsIcon } from '@blocksuite/icons';
|
||||
import type { Meta, StoryFn } from '@storybook/react';
|
||||
import { useAtom } from 'jotai';
|
||||
import { type PropsWithChildren, useState } from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
|
||||
export default {
|
||||
title: 'AFFiNE/AppSidebar',
|
||||
component: AppSidebar,
|
||||
} satisfies Meta;
|
||||
|
||||
const Container = ({ children }: PropsWithChildren) => (
|
||||
<MemoryRouter>
|
||||
<main
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: '100vw',
|
||||
height: 'calc(100vh - 40px)',
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</main>
|
||||
</MemoryRouter>
|
||||
);
|
||||
const Main = () => {
|
||||
const [open] = useAtom(appSidebarOpenAtom);
|
||||
return (
|
||||
<div>
|
||||
<div className={navHeaderStyle}>
|
||||
<SidebarSwitch show={!open} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Default: StoryFn = () => {
|
||||
return (
|
||||
<>
|
||||
<Container>
|
||||
<AppSidebar />
|
||||
<Main />
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const Fallback = () => {
|
||||
return (
|
||||
<Container>
|
||||
<AppSidebarFallback />
|
||||
<Main />
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export const WithItems: StoryFn = () => {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
return (
|
||||
<Container>
|
||||
<AppSidebar>
|
||||
<SidebarContainer>
|
||||
<QuickSearchInput />
|
||||
<div style={{ height: '20px' }} />
|
||||
<MenuLinkItem
|
||||
icon={<SettingsIcon />}
|
||||
to="/test"
|
||||
onClick={() => alert('opened')}
|
||||
>
|
||||
Settings
|
||||
</MenuLinkItem>
|
||||
<MenuLinkItem
|
||||
icon={<SettingsIcon />}
|
||||
to="/test"
|
||||
onClick={() => alert('opened')}
|
||||
>
|
||||
Settings
|
||||
</MenuLinkItem>
|
||||
<MenuLinkItem
|
||||
icon={<SettingsIcon />}
|
||||
to="/test"
|
||||
onClick={() => alert('opened')}
|
||||
>
|
||||
Settings
|
||||
</MenuLinkItem>
|
||||
</SidebarContainer>
|
||||
|
||||
<SidebarScrollableContainer>
|
||||
<CategoryDivider label="Favorites" />
|
||||
<MenuLinkItem
|
||||
collapsed={collapsed}
|
||||
onCollapsedChange={setCollapsed}
|
||||
icon={<SettingsIcon />}
|
||||
to="/test"
|
||||
onClick={() => alert('opened')}
|
||||
>
|
||||
Collapsible Item
|
||||
</MenuLinkItem>
|
||||
<MenuLinkItem
|
||||
collapsed={!collapsed}
|
||||
onCollapsedChange={setCollapsed}
|
||||
icon={<SettingsIcon />}
|
||||
to="/test"
|
||||
onClick={() => alert('opened')}
|
||||
>
|
||||
Collapsible Item
|
||||
</MenuLinkItem>
|
||||
<MenuLinkItem
|
||||
icon={<SettingsIcon />}
|
||||
to="/test"
|
||||
onClick={() => alert('opened')}
|
||||
>
|
||||
Settings
|
||||
</MenuLinkItem>
|
||||
|
||||
<CategoryDivider label="Others" />
|
||||
<MenuLinkItem
|
||||
icon={<DeleteTemporarilyIcon />}
|
||||
to="/test"
|
||||
onClick={() => alert('opened')}
|
||||
>
|
||||
Trash
|
||||
</MenuLinkItem>
|
||||
</SidebarScrollableContainer>
|
||||
<SidebarContainer>
|
||||
<AddPageButton />
|
||||
</SidebarContainer>
|
||||
</AppSidebar>
|
||||
<Main />
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
60
tests/storybook/src/stories/app-updater-button.stories.tsx
Normal file
60
tests/storybook/src/stories/app-updater-button.stories.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import {
|
||||
type AddPageButtonPureProps,
|
||||
AppUpdaterButtonPure,
|
||||
} from '@affine/component/app-sidebar';
|
||||
import type { Meta, StoryFn } from '@storybook/react';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
|
||||
export default {
|
||||
title: 'AFFiNE/AppUpdaterButton',
|
||||
component: AppUpdaterButtonPure,
|
||||
parameters: {
|
||||
chromatic: { disableSnapshot: true },
|
||||
},
|
||||
} satisfies Meta<typeof AppUpdaterButtonPure>;
|
||||
|
||||
const Container = ({ children }: PropsWithChildren) => (
|
||||
<main
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: '320px',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
backgroundColor: '#eee',
|
||||
padding: '16px',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</main>
|
||||
);
|
||||
|
||||
export const Default: StoryFn<AddPageButtonPureProps> = props => {
|
||||
return (
|
||||
<Container>
|
||||
<AppUpdaterButtonPure {...props} />
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
Default.args = {
|
||||
appQuitting: false,
|
||||
updateReady: true,
|
||||
updateAvailable: {
|
||||
version: '1.0.0-beta.1',
|
||||
allowAutoUpdate: true,
|
||||
},
|
||||
downloadProgress: 42,
|
||||
currentChangelogUnread: true,
|
||||
};
|
||||
|
||||
export const Updated: StoryFn<AddPageButtonPureProps> = props => {
|
||||
return (
|
||||
<Container>
|
||||
<AppUpdaterButtonPure {...props} updateAvailable={null} />
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
Updated.args = {
|
||||
currentChangelogUnread: true,
|
||||
};
|
||||
41
tests/storybook/src/stories/breadcrumbs.stories.tsx
Normal file
41
tests/storybook/src/stories/breadcrumbs.stories.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
/* deepscan-disable USELESS_ARROW_FUNC_BIND */
|
||||
import { Breadcrumbs } from '@affine/component';
|
||||
import { Link, Typography } from '@mui/material';
|
||||
import { expect } from '@storybook/jest';
|
||||
import type { Meta, StoryFn } from '@storybook/react';
|
||||
import { within } from '@storybook/testing-library';
|
||||
export default {
|
||||
title: 'AFFiNE/Breadcrumbs',
|
||||
component: Breadcrumbs,
|
||||
parameters: {
|
||||
chromatic: { disableSnapshot: true },
|
||||
},
|
||||
} as Meta<typeof Breadcrumbs>;
|
||||
|
||||
const Template: StoryFn = args => <Breadcrumbs {...args} />;
|
||||
|
||||
export const Primary = Template.bind(undefined);
|
||||
Primary.play = async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const text = canvas.getByText('AFFiNE');
|
||||
expect(text.getAttribute('data-testid')).toBe('affine');
|
||||
};
|
||||
Primary.args = {
|
||||
children: [
|
||||
<Link
|
||||
data-testid="affine"
|
||||
key="1"
|
||||
underline="hover"
|
||||
color="inherit"
|
||||
href="/"
|
||||
>
|
||||
AFFiNE
|
||||
</Link>,
|
||||
<Link key="2" underline="hover" color="inherit" href="/Docs/">
|
||||
Docs
|
||||
</Link>,
|
||||
<Typography key="3" color="text.primary">
|
||||
Introduction
|
||||
</Typography>,
|
||||
],
|
||||
};
|
||||
75
tests/storybook/src/stories/card.stories.tsx
Normal file
75
tests/storybook/src/stories/card.stories.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import { toast } from '@affine/component';
|
||||
import { BlockCard } from '@affine/component/card/block-card';
|
||||
import { WorkspaceCard } from '@affine/component/card/workspace-card';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { getOrCreateWorkspace } from '@affine/workspace/manager';
|
||||
import {
|
||||
EdgelessIcon,
|
||||
ExportToHtmlIcon,
|
||||
HelpIcon,
|
||||
PageIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import type { Meta } from '@storybook/react';
|
||||
import { Tooltip } from '@toeverything/components/tooltip';
|
||||
|
||||
export default {
|
||||
title: 'AFFiNE/Card',
|
||||
component: WorkspaceCard,
|
||||
parameters: {
|
||||
chromatic: { disableSnapshot: true },
|
||||
},
|
||||
} satisfies Meta;
|
||||
|
||||
const blockSuiteWorkspace = getOrCreateWorkspace(
|
||||
'blocksuite-local',
|
||||
WorkspaceFlavour.LOCAL
|
||||
);
|
||||
|
||||
blockSuiteWorkspace.meta.setName('Hello World');
|
||||
|
||||
export const AffineWorkspaceCard = () => {
|
||||
return (
|
||||
<WorkspaceCard
|
||||
meta={{
|
||||
id: 'blocksuite-local',
|
||||
flavour: WorkspaceFlavour.LOCAL,
|
||||
}}
|
||||
onClick={() => {}}
|
||||
onSettingClick={() => {}}
|
||||
currentWorkspaceId={null}
|
||||
isOwner={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const AffineBlockCard = () => {
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
|
||||
<BlockCard title={'New Page'} onClick={() => toast('clicked')} />
|
||||
<BlockCard
|
||||
title={'New Page'}
|
||||
desc={'Write with a blank page'}
|
||||
right={<PageIcon width={20} height={20} />}
|
||||
onClick={() => toast('clicked page')}
|
||||
/>
|
||||
<BlockCard
|
||||
title={'New Edgeless'}
|
||||
desc={'Draw with a blank whiteboard'}
|
||||
left={<PageIcon width={20} height={20} />}
|
||||
right={<EdgelessIcon width={20} height={20} />}
|
||||
onClick={() => toast('clicked edgeless')}
|
||||
/>
|
||||
<BlockCard
|
||||
left={<ExportToHtmlIcon width={20} height={20} />}
|
||||
title="HTML"
|
||||
disabled
|
||||
right={
|
||||
<Tooltip content={'Learn how to Import pages into AFFiNE.'}>
|
||||
<HelpIcon />
|
||||
</Tooltip>
|
||||
}
|
||||
onClick={() => toast('click HTML')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
202
tests/storybook/src/stories/core.stories.tsx
Normal file
202
tests/storybook/src/stories/core.stories.tsx
Normal file
@@ -0,0 +1,202 @@
|
||||
import { routes } from '@affine/core/router';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import type { StoryFn } from '@storybook/react';
|
||||
import { screen, userEvent, waitFor, within } from '@storybook/testing-library';
|
||||
import { Outlet, useLocation } from 'react-router-dom';
|
||||
import {
|
||||
reactRouterOutlets,
|
||||
reactRouterParameters,
|
||||
withRouter,
|
||||
} from 'storybook-addon-react-router-v6';
|
||||
|
||||
const FakeApp = () => {
|
||||
const location = useLocation();
|
||||
// fixme: `key` is a hack to force the storybook to re-render the outlet
|
||||
return <Outlet key={location.pathname} />;
|
||||
};
|
||||
|
||||
export default {
|
||||
title: 'Preview/Core',
|
||||
parameters: {
|
||||
chromatic: { disableSnapshot: false },
|
||||
},
|
||||
};
|
||||
|
||||
export const Index: StoryFn = () => {
|
||||
return <FakeApp />;
|
||||
};
|
||||
Index.decorators = [withRouter];
|
||||
Index.parameters = {
|
||||
reactRouter: reactRouterParameters({
|
||||
routing: reactRouterOutlets(routes),
|
||||
}),
|
||||
};
|
||||
|
||||
export const SettingPage: StoryFn = () => {
|
||||
return <FakeApp />;
|
||||
};
|
||||
SettingPage.play = async ({ canvasElement, step }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await waitFor(
|
||||
() => {
|
||||
assertExists(canvasElement.querySelector('v-line'));
|
||||
},
|
||||
{
|
||||
timeout: 10000,
|
||||
}
|
||||
);
|
||||
await step('click setting modal button', async () => {
|
||||
await userEvent.click(canvas.getByTestId('settings-modal-trigger'));
|
||||
});
|
||||
await waitFor(async () => {
|
||||
assertExists(
|
||||
document.body.querySelector('[data-testid="language-menu-button"]')
|
||||
);
|
||||
});
|
||||
|
||||
// Menu button may have "pointer-events: none" style, await 100ms to avoid this weird situation.
|
||||
await new Promise(resolve => window.setTimeout(resolve, 100));
|
||||
|
||||
await step('click language menu button', async () => {
|
||||
await userEvent.click(
|
||||
document.body.querySelector(
|
||||
'[data-testid="language-menu-button"]'
|
||||
) as HTMLElement
|
||||
);
|
||||
});
|
||||
};
|
||||
SettingPage.decorators = [withRouter];
|
||||
SettingPage.parameters = {
|
||||
reactRouter: reactRouterParameters({
|
||||
routing: reactRouterOutlets(routes),
|
||||
}),
|
||||
};
|
||||
|
||||
export const NotFoundPage: StoryFn = () => {
|
||||
return <FakeApp />;
|
||||
};
|
||||
NotFoundPage.decorators = [withRouter];
|
||||
NotFoundPage.parameters = {
|
||||
reactRouter: reactRouterParameters({
|
||||
routing: reactRouterOutlets(routes),
|
||||
location: {
|
||||
path: '/404',
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
export const WorkspaceList: StoryFn = () => {
|
||||
return <FakeApp />;
|
||||
};
|
||||
WorkspaceList.play = async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
// click current-workspace
|
||||
const currentWorkspace = await waitFor(
|
||||
() => {
|
||||
assertExists(canvas.getByTestId('current-workspace'));
|
||||
return canvas.getByTestId('current-workspace');
|
||||
},
|
||||
{
|
||||
timeout: 5000,
|
||||
}
|
||||
);
|
||||
|
||||
// todo: figure out why userEvent cannot click this element?
|
||||
// await userEvent.click(currentWorkspace);
|
||||
currentWorkspace.click();
|
||||
};
|
||||
WorkspaceList.decorators = [withRouter];
|
||||
WorkspaceList.parameters = {
|
||||
reactRouter: reactRouterParameters({
|
||||
routing: reactRouterOutlets(routes),
|
||||
location: {
|
||||
path: '/',
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
export const SearchPage: StoryFn = () => {
|
||||
return <FakeApp />;
|
||||
};
|
||||
SearchPage.play = async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await waitFor(
|
||||
() => {
|
||||
assertExists(canvasElement.querySelector('v-line'));
|
||||
},
|
||||
{
|
||||
timeout: 10000,
|
||||
}
|
||||
);
|
||||
await userEvent.click(canvas.getByTestId('slider-bar-quick-search-button'));
|
||||
await waitFor(
|
||||
() => {
|
||||
assertExists(screen.getByTestId('cmdk-quick-search'));
|
||||
},
|
||||
{
|
||||
timeout: 3000,
|
||||
}
|
||||
);
|
||||
};
|
||||
SearchPage.decorators = [withRouter];
|
||||
SearchPage.parameters = {
|
||||
reactRouter: reactRouterParameters({
|
||||
routing: reactRouterOutlets(routes),
|
||||
location: {
|
||||
path: '/',
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
export const ImportPage: StoryFn = () => {
|
||||
return <FakeApp />;
|
||||
};
|
||||
ImportPage.play = async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await waitFor(
|
||||
() => {
|
||||
assertExists(canvasElement.querySelector('v-line'));
|
||||
},
|
||||
{
|
||||
timeout: 10000,
|
||||
}
|
||||
);
|
||||
await waitFor(() => {
|
||||
assertExists(
|
||||
canvasElement.querySelector('[data-testid="header-dropDownButton"]')
|
||||
);
|
||||
});
|
||||
await userEvent.click(canvas.getByTestId('header-dropDownButton'));
|
||||
await waitFor(() => {
|
||||
assertExists(
|
||||
document.body.querySelector('[data-testid="editor-option-menu-import"]')
|
||||
);
|
||||
});
|
||||
await userEvent.click(screen.getByTestId('editor-option-menu-import'));
|
||||
};
|
||||
ImportPage.decorators = [withRouter];
|
||||
ImportPage.parameters = {
|
||||
reactRouter: reactRouterParameters({
|
||||
routing: reactRouterOutlets(routes),
|
||||
location: {
|
||||
path: '/',
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
export const OpenAppPage: StoryFn = () => {
|
||||
return <FakeApp />;
|
||||
};
|
||||
OpenAppPage.decorators = [withRouter];
|
||||
OpenAppPage.parameters = {
|
||||
reactRouter: reactRouterParameters({
|
||||
routing: reactRouterOutlets(routes),
|
||||
location: {
|
||||
path: '/open-app/url',
|
||||
searchParams: {
|
||||
url: 'affine-beta://foo-bar.com',
|
||||
open: 'false',
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
16
tests/storybook/src/stories/datepicker.stories.tsx
Normal file
16
tests/storybook/src/stories/datepicker.stories.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { AFFiNEDatePicker } from '@affine/component/date-picker';
|
||||
import type { Meta, StoryFn } from '@storybook/react';
|
||||
import { useState } from 'react';
|
||||
|
||||
export default {
|
||||
title: 'AFFiNE/AFFiNEDatePicker',
|
||||
component: AFFiNEDatePicker,
|
||||
parameters: {
|
||||
chromatic: { disableSnapshot: true },
|
||||
},
|
||||
} satisfies Meta;
|
||||
|
||||
export const Default: StoryFn = () => {
|
||||
const [value, setValue] = useState<string>(new Date().toString());
|
||||
return <AFFiNEDatePicker value={value} onChange={setValue} />;
|
||||
};
|
||||
77
tests/storybook/src/stories/image-preview-modal.stories.tsx
Normal file
77
tests/storybook/src/stories/image-preview-modal.stories.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import { BlockHubWrapper } from '@affine/component/block-hub';
|
||||
import { BlockSuiteEditor } from '@affine/component/block-suite-editor';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
import { ImagePreviewModal } from '@affine/image-preview-plugin/src/component';
|
||||
import { rootBlockHubAtom } from '@affine/workspace/atom';
|
||||
import { getOrCreateWorkspace } from '@affine/workspace/manager';
|
||||
import type { Meta } from '@storybook/react';
|
||||
import { initEmptyPage } from '@toeverything/infra/blocksuite';
|
||||
import { useCallback } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
export default {
|
||||
title: 'Component/ImagePreviewModal',
|
||||
component: ImagePreviewModal,
|
||||
} satisfies Meta;
|
||||
|
||||
const workspace = getOrCreateWorkspace('test', WorkspaceFlavour.LOCAL);
|
||||
const page = workspace.createPage('page0');
|
||||
initEmptyPage(page);
|
||||
fetch(new URL('@affine-test/fixtures/large-image.png', import.meta.url))
|
||||
.then(res => res.arrayBuffer())
|
||||
.then(async buffer => {
|
||||
const id = await workspace.blobs.set(
|
||||
new Blob([buffer], { type: 'image/png' })
|
||||
);
|
||||
const frameId = page.getBlockByFlavour('affine:note')[0].id;
|
||||
page.addBlock(
|
||||
'affine:paragraph',
|
||||
{
|
||||
text: new page.Text('Please double click the image to preview it.'),
|
||||
},
|
||||
frameId
|
||||
);
|
||||
page.addBlock(
|
||||
'affine:image',
|
||||
{
|
||||
sourceId: id,
|
||||
},
|
||||
frameId
|
||||
);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Failed to load large-image.png', err);
|
||||
});
|
||||
|
||||
export const Default = () => {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
height: '100vh',
|
||||
width: '100vw',
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
<BlockSuiteEditor
|
||||
mode="page"
|
||||
page={page}
|
||||
onInit={useCallback(async page => initEmptyPage(page), [])}
|
||||
/>
|
||||
{createPortal(
|
||||
<ImagePreviewModal pageId={page.id} workspace={page.workspace} />,
|
||||
document.body
|
||||
)}
|
||||
</div>
|
||||
<BlockHubWrapper
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: 12,
|
||||
bottom: 12,
|
||||
}}
|
||||
blockHubAtom={rootBlockHubAtom}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
23
tests/storybook/src/stories/import-page.stories.tsx
Normal file
23
tests/storybook/src/stories/import-page.stories.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
/* deepscan-disable USELESS_ARROW_FUNC_BIND */
|
||||
import { toast } from '@affine/component';
|
||||
import { ImportPage } from '@affine/component/import-page';
|
||||
import type { StoryFn } from '@storybook/react';
|
||||
import type { Meta } from '@storybook/react';
|
||||
|
||||
export default {
|
||||
title: 'AFFiNE/ImportPage',
|
||||
component: ImportPage,
|
||||
parameters: {
|
||||
chromatic: { disableSnapshot: true },
|
||||
},
|
||||
} satisfies Meta;
|
||||
|
||||
const Template: StoryFn<typeof ImportPage> = args => <ImportPage {...args} />;
|
||||
|
||||
export const Basic = Template.bind(undefined);
|
||||
Basic.args = {
|
||||
importHtml: () => toast('Click importHtml'),
|
||||
importMarkdown: () => toast('Click importMarkdown'),
|
||||
importNotion: () => toast('Click importNotion'),
|
||||
onClose: () => toast('Click onClose'),
|
||||
};
|
||||
18
tests/storybook/src/stories/introduction.stories.mdx
Normal file
18
tests/storybook/src/stories/introduction.stories.mdx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Meta } from '@storybook/blocks';
|
||||
|
||||
<Meta title="Introduction" />
|
||||
|
||||
# AFFiNE UI Storybook
|
||||
|
||||
This is a UI component dev environment for AFFiNE UI.
|
||||
It allows you to browse the AFFiNE UI components,
|
||||
view the different states of each component,
|
||||
and interactively develop and test components.
|
||||
|
||||
## Bug Reporting
|
||||
|
||||
If you find a bug, please file an issue on [GitHub](https://github.com/toeverything/AFFiNE/issues)
|
||||
|
||||
## Contributing
|
||||
|
||||
We welcome contributions from the community! [Get started here](https://github.com/toeverything/AFFiNE/blob/master/docs/BUILDING.md)
|
||||
249
tests/storybook/src/stories/notification-center.stories.tsx
Normal file
249
tests/storybook/src/stories/notification-center.stories.tsx
Normal file
@@ -0,0 +1,249 @@
|
||||
import {
|
||||
expandNotificationCenterAtom,
|
||||
NotificationCenter,
|
||||
pushNotificationAtom,
|
||||
} from '@affine/component/notification-center';
|
||||
import type { Meta } from '@storybook/react';
|
||||
import { useAtomValue, useSetAtom } from 'jotai';
|
||||
|
||||
export default {
|
||||
title: 'AFFiNE/NotificationCenter',
|
||||
component: NotificationCenter,
|
||||
parameters: {
|
||||
chromatic: { disableSnapshot: true },
|
||||
},
|
||||
} satisfies Meta<typeof NotificationCenter>;
|
||||
|
||||
let id = 0;
|
||||
const image = (
|
||||
<video autoPlay muted loop>
|
||||
<source
|
||||
src="http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"
|
||||
type="video/mp4"
|
||||
/>
|
||||
</video>
|
||||
);
|
||||
export const Basic = () => {
|
||||
const push = useSetAtom(pushNotificationAtom);
|
||||
const expand = useAtomValue(expandNotificationCenterAtom);
|
||||
return (
|
||||
<>
|
||||
<div>{expand ? 'expanded' : 'collapsed'}</div>
|
||||
<div>
|
||||
<button
|
||||
onClick={() => {
|
||||
const key = id++;
|
||||
push({
|
||||
key: `${key}`,
|
||||
title: `${key} title`,
|
||||
message: `${key} message`,
|
||||
timeout: 3000,
|
||||
progressingBar: true,
|
||||
undo: async () => {
|
||||
console.log('undo');
|
||||
},
|
||||
type: 'info',
|
||||
});
|
||||
}}
|
||||
>
|
||||
Push timeout notification
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
onClick={() => {
|
||||
const key = id++;
|
||||
push({
|
||||
key: `${key}`,
|
||||
title: `${key} title`,
|
||||
message: `${key} message`,
|
||||
type: 'info',
|
||||
});
|
||||
}}
|
||||
>
|
||||
Push notification with no timeout
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
onClick={() => {
|
||||
const key = id++;
|
||||
push({
|
||||
key: `${key}`,
|
||||
title: `${key} title`,
|
||||
message: ``,
|
||||
type: 'info',
|
||||
});
|
||||
}}
|
||||
>
|
||||
Push notification with no message
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
onClick={() => {
|
||||
const key = id++;
|
||||
push({
|
||||
key: `${key}`,
|
||||
title: `${key} title`,
|
||||
message: ``,
|
||||
type: 'success',
|
||||
theme: 'light',
|
||||
});
|
||||
}}
|
||||
>
|
||||
light success
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
onClick={() => {
|
||||
const key = id++;
|
||||
push({
|
||||
key: `${key}`,
|
||||
title: `${key} title`,
|
||||
message: ``,
|
||||
type: 'success',
|
||||
theme: 'dark',
|
||||
});
|
||||
}}
|
||||
>
|
||||
dark success
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
onClick={() => {
|
||||
const key = id++;
|
||||
push({
|
||||
key: `${key}`,
|
||||
title: `${key} title`,
|
||||
message: ``,
|
||||
type: 'info',
|
||||
theme: 'light',
|
||||
});
|
||||
}}
|
||||
>
|
||||
light info
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
onClick={() => {
|
||||
const key = id++;
|
||||
push({
|
||||
key: `${key}`,
|
||||
title: `${key} title`,
|
||||
message: ``,
|
||||
type: 'info',
|
||||
theme: 'dark',
|
||||
});
|
||||
}}
|
||||
>
|
||||
dark info
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
onClick={() => {
|
||||
const key = id++;
|
||||
push({
|
||||
key: `${key}`,
|
||||
title: `${key} title`,
|
||||
message: ``,
|
||||
type: 'warning',
|
||||
theme: 'light',
|
||||
});
|
||||
}}
|
||||
>
|
||||
light warning
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
onClick={() => {
|
||||
const key = id++;
|
||||
push({
|
||||
key: `${key}`,
|
||||
title: `${key} title`,
|
||||
message: ``,
|
||||
type: 'warning',
|
||||
theme: 'dark',
|
||||
});
|
||||
}}
|
||||
>
|
||||
dark warning
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
onClick={() => {
|
||||
const key = id++;
|
||||
push({
|
||||
key: `${key}`,
|
||||
title: `${key} title`,
|
||||
message: ``,
|
||||
type: 'error',
|
||||
theme: 'light',
|
||||
});
|
||||
}}
|
||||
>
|
||||
light error
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
onClick={() => {
|
||||
const key = id++;
|
||||
push({
|
||||
key: `${key}`,
|
||||
title: `${key} title`,
|
||||
message: ``,
|
||||
type: 'error',
|
||||
theme: 'dark',
|
||||
});
|
||||
}}
|
||||
>
|
||||
dark error
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
onClick={() => {
|
||||
const key = id++;
|
||||
push({
|
||||
key: `${key}`,
|
||||
title: `${key} title`,
|
||||
message: `gif test`,
|
||||
type: 'info',
|
||||
multimedia: image,
|
||||
timeout: 3000,
|
||||
undo: async () => {
|
||||
console.log('undo');
|
||||
},
|
||||
progressingBar: true,
|
||||
});
|
||||
}}
|
||||
>
|
||||
gif
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
onClick={() => {
|
||||
const key = id++;
|
||||
push({
|
||||
title: `${key} title`,
|
||||
type: 'info',
|
||||
theme: 'default',
|
||||
timeout: 3000,
|
||||
});
|
||||
}}
|
||||
>
|
||||
default message
|
||||
</button>
|
||||
</div>
|
||||
<NotificationCenter />
|
||||
</>
|
||||
);
|
||||
};
|
||||
18
tests/storybook/src/stories/onboarding-modal.stories.tsx
Normal file
18
tests/storybook/src/stories/onboarding-modal.stories.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
/* deepscan-disable USELESS_ARROW_FUNC_BIND */
|
||||
import { TourModal } from '@affine/component/tour-modal';
|
||||
import type { Meta, StoryFn } from '@storybook/react';
|
||||
|
||||
export default {
|
||||
title: 'AFFiNE/TourModal',
|
||||
component: TourModal,
|
||||
parameters: {
|
||||
chromatic: { disableSnapshot: true },
|
||||
},
|
||||
} satisfies Meta;
|
||||
|
||||
export const Basic: StoryFn = () => {
|
||||
return <TourModal open={true} />;
|
||||
};
|
||||
Basic.args = {
|
||||
logoSrc: '/imgs/affine-text-logo.png',
|
||||
};
|
||||
14
tests/storybook/src/stories/page-detail-skeleton.stories.tsx
Normal file
14
tests/storybook/src/stories/page-detail-skeleton.stories.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { PageDetailSkeleton } from '@affine/component/page-detail-skeleton';
|
||||
import type { Meta } from '@storybook/react';
|
||||
|
||||
export default {
|
||||
title: 'AFFiNE/PageDetailSkeleton',
|
||||
component: PageDetailSkeleton,
|
||||
parameters: {
|
||||
chromatic: { disableSnapshot: true },
|
||||
},
|
||||
} satisfies Meta<typeof PageDetailSkeleton>;
|
||||
|
||||
export const Basic = () => {
|
||||
return <PageDetailSkeleton />;
|
||||
};
|
||||
221
tests/storybook/src/stories/page-list.stories.tsx
Normal file
221
tests/storybook/src/stories/page-list.stories.tsx
Normal file
@@ -0,0 +1,221 @@
|
||||
import { Empty, toast } from '@affine/component';
|
||||
import type { OperationCellProps } from '@affine/component/page-list';
|
||||
import {
|
||||
NewPageButton,
|
||||
OperationCell,
|
||||
PageList,
|
||||
PageListTrashView,
|
||||
} from '@affine/component/page-list';
|
||||
import type { Collection } from '@affine/env/filter';
|
||||
import { PageIcon } from '@blocksuite/icons';
|
||||
import { expect } from '@storybook/jest';
|
||||
import type { Meta, StoryFn } from '@storybook/react';
|
||||
import { userEvent } from '@storybook/testing-library';
|
||||
import { atom } from 'jotai';
|
||||
|
||||
export default {
|
||||
title: 'AFFiNE/PageList',
|
||||
component: PageList,
|
||||
parameters: {
|
||||
chromatic: { disableSnapshot: true },
|
||||
},
|
||||
} satisfies Meta;
|
||||
|
||||
export const AffineOperationCell: StoryFn<OperationCellProps> = ({
|
||||
...props
|
||||
}) => <OperationCell {...props} />;
|
||||
|
||||
AffineOperationCell.args = {
|
||||
favorite: false,
|
||||
isPublic: true,
|
||||
onToggleFavoritePage: () => toast('Toggle favorite page'),
|
||||
onDisablePublicSharing: () => toast('Disable public sharing'),
|
||||
onOpenPageInNewTab: () => toast('Open page in new tab'),
|
||||
onRemoveToTrash: () => toast('Remove to trash'),
|
||||
};
|
||||
AffineOperationCell.play = async ({ canvasElement }) => {
|
||||
{
|
||||
const button = canvasElement.querySelector(
|
||||
'[data-testid="page-list-operation-button"]'
|
||||
) as HTMLButtonElement;
|
||||
expect(button).not.toBeNull();
|
||||
await userEvent.click(button);
|
||||
}
|
||||
};
|
||||
|
||||
export const AffineNewPageButton: StoryFn<typeof NewPageButton> = ({
|
||||
...props
|
||||
}) => <NewPageButton {...props} />;
|
||||
AffineNewPageButton.args = {
|
||||
createNewPage: () => toast('Create new page'),
|
||||
createNewEdgeless: () => toast('Create new edgeless'),
|
||||
};
|
||||
|
||||
AffineNewPageButton.play = async ({ canvasElement }) => {
|
||||
const button = canvasElement.querySelector('button') as HTMLButtonElement;
|
||||
expect(button).not.toBeNull();
|
||||
const dropdown = button.querySelector('svg') as SVGSVGElement;
|
||||
expect(dropdown).not.toBeNull();
|
||||
await userEvent.click(dropdown);
|
||||
};
|
||||
|
||||
export const AffineAllPageList: StoryFn<typeof PageList> = ({ ...props }) => (
|
||||
<PageList {...props} />
|
||||
);
|
||||
|
||||
const baseAtom = atom<Collection[]>([]);
|
||||
AffineAllPageList.args = {
|
||||
isPublicWorkspace: false,
|
||||
onCreateNewPage: () => toast('Create new page'),
|
||||
onCreateNewEdgeless: () => toast('Create new edgeless'),
|
||||
onImportFile: () => toast('Import file'),
|
||||
collectionsAtom: atom(
|
||||
get => get(baseAtom),
|
||||
async (_, set, update) => {
|
||||
set(baseAtom, update);
|
||||
}
|
||||
),
|
||||
list: [
|
||||
{
|
||||
pageId: '1',
|
||||
favorite: false,
|
||||
icon: <PageIcon />,
|
||||
isPublicPage: true,
|
||||
title: 'Last Page',
|
||||
tags: [],
|
||||
preview: 'this is page preview',
|
||||
createDate: new Date('2021-01-01'),
|
||||
updatedDate: new Date('2023-08-15'),
|
||||
bookmarkPage: () => toast('Bookmark page'),
|
||||
onClickPage: () => toast('Click page'),
|
||||
onDisablePublicSharing: () => toast('Disable public sharing'),
|
||||
onOpenPageInNewTab: () => toast('Open page in new tab'),
|
||||
removeToTrash: () => toast('Remove to trash'),
|
||||
},
|
||||
{
|
||||
pageId: '3',
|
||||
favorite: false,
|
||||
icon: <PageIcon />,
|
||||
isPublicPage: true,
|
||||
title:
|
||||
'1 Example Public Page with long title that will be truncated because it is too too long',
|
||||
tags: [],
|
||||
preview:
|
||||
'this is page preview and it is very long and will be truncated because it is too long and it is very long and will be truncated because it is too long',
|
||||
createDate: new Date('2021-01-01'),
|
||||
updatedDate: new Date('2021-01-02'),
|
||||
bookmarkPage: () => toast('Bookmark page'),
|
||||
onClickPage: () => toast('Click page'),
|
||||
onDisablePublicSharing: () => toast('Disable public sharing'),
|
||||
onOpenPageInNewTab: () => toast('Open page in new tab'),
|
||||
removeToTrash: () => toast('Remove to trash'),
|
||||
},
|
||||
{
|
||||
pageId: '2',
|
||||
favorite: true,
|
||||
isPublicPage: false,
|
||||
icon: <PageIcon />,
|
||||
title: '2 Favorited Page 2021',
|
||||
tags: [],
|
||||
createDate: new Date('2021-01-02'),
|
||||
updatedDate: new Date('2021-01-01'),
|
||||
bookmarkPage: () => toast('Bookmark page'),
|
||||
onClickPage: () => toast('Click page'),
|
||||
onDisablePublicSharing: () => toast('Disable public sharing'),
|
||||
onOpenPageInNewTab: () => toast('Open page in new tab'),
|
||||
removeToTrash: () => toast('Remove to trash'),
|
||||
},
|
||||
{
|
||||
pageId: '4',
|
||||
favorite: false,
|
||||
isPublicPage: false,
|
||||
icon: <PageIcon />,
|
||||
title: 'page created in 2023-04-01',
|
||||
tags: [],
|
||||
createDate: new Date('2023-04-01'),
|
||||
updatedDate: new Date('2023-04-01'),
|
||||
bookmarkPage: () => toast('Bookmark page'),
|
||||
onClickPage: () => toast('Click page'),
|
||||
onDisablePublicSharing: () => toast('Disable public sharing'),
|
||||
onOpenPageInNewTab: () => toast('Open page in new tab'),
|
||||
removeToTrash: () => toast('Remove to trash'),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const AffineEmptyAllPageList: StoryFn<typeof PageList> = ({
|
||||
...props
|
||||
}) => <PageList {...props} />;
|
||||
|
||||
AffineEmptyAllPageList.args = {
|
||||
isPublicWorkspace: false,
|
||||
onCreateNewPage: () => toast('Create new page'),
|
||||
onCreateNewEdgeless: () => toast('Create new edgeless'),
|
||||
onImportFile: () => toast('Import file'),
|
||||
list: [],
|
||||
fallback: (
|
||||
<Empty
|
||||
title="Empty"
|
||||
description={
|
||||
<div>
|
||||
empty description, click{' '}
|
||||
<button
|
||||
onClick={() => {
|
||||
toast('click');
|
||||
}}
|
||||
>
|
||||
button
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
export const AffinePublicPageList: StoryFn<typeof PageList> = ({
|
||||
...props
|
||||
}) => <PageList {...props} />;
|
||||
AffinePublicPageList.args = {
|
||||
...AffineAllPageList.args,
|
||||
isPublicWorkspace: true,
|
||||
};
|
||||
|
||||
export const AffineAllPageMobileList: StoryFn<typeof PageList> = ({
|
||||
...props
|
||||
}) => <PageList {...props} />;
|
||||
|
||||
AffineAllPageMobileList.args = AffineAllPageList.args;
|
||||
AffineAllPageMobileList.parameters = {
|
||||
viewport: {
|
||||
defaultViewport: 'mobile2',
|
||||
},
|
||||
};
|
||||
|
||||
export const AffineTrashPageList: StoryFn<typeof PageListTrashView> = ({
|
||||
...props
|
||||
}) => <PageListTrashView {...props} />;
|
||||
|
||||
AffineTrashPageList.args = {
|
||||
list: [
|
||||
{
|
||||
pageId: '1',
|
||||
icon: <PageIcon />,
|
||||
title: 'Example Page',
|
||||
preview: 'this is trash page preview',
|
||||
createDate: new Date('2021-01-01'),
|
||||
trashDate: new Date('2021-01-01'),
|
||||
onClickPage: () => toast('Click page'),
|
||||
onPermanentlyDeletePage: () => toast('Permanently delete page'),
|
||||
onRestorePage: () => toast('Restore page'),
|
||||
},
|
||||
{
|
||||
pageId: '2',
|
||||
icon: <PageIcon />,
|
||||
title: 'Example Page with long title that will be truncated',
|
||||
createDate: new Date('2021-01-01'),
|
||||
onClickPage: () => toast('Click page'),
|
||||
onPermanentlyDeletePage: () => toast('Permanently delete page'),
|
||||
onRestorePage: () => toast('Restore page'),
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
149
tests/storybook/src/stories/share-menu.stories.tsx
Normal file
149
tests/storybook/src/stories/share-menu.stories.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
import { toast } from '@affine/component';
|
||||
import {
|
||||
PublicLinkDisableModal,
|
||||
StyledDisableButton,
|
||||
} from '@affine/component/share-menu';
|
||||
import { ShareMenu } from '@affine/component/share-menu';
|
||||
import type {
|
||||
AffineCloudWorkspace,
|
||||
LocalWorkspace,
|
||||
} from '@affine/env/workspace';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { getOrCreateWorkspace } from '@affine/workspace/manager';
|
||||
import type { Page } from '@blocksuite/store';
|
||||
import { expect } from '@storybook/jest';
|
||||
import type { Meta, StoryFn } from '@storybook/react';
|
||||
import { use } from 'foxact/use';
|
||||
import { useState } from 'react';
|
||||
|
||||
export default {
|
||||
title: 'AFFiNE/ShareMenu',
|
||||
component: ShareMenu,
|
||||
parameters: {
|
||||
chromatic: { disableSnapshot: true },
|
||||
},
|
||||
} satisfies Meta;
|
||||
|
||||
const sharePageMap = new Map<string, boolean>([]);
|
||||
// todo: use a real hook
|
||||
const useIsSharedPage = (
|
||||
_workspaceId: string,
|
||||
pageId: string
|
||||
): [isSharePage: boolean, setIsSharePage: (enable: boolean) => void] => {
|
||||
const [isShared, setIsShared] = useState(sharePageMap.get(pageId) ?? false);
|
||||
const togglePagePublic = (enable: boolean) => {
|
||||
setIsShared(enable);
|
||||
sharePageMap.set(pageId, enable);
|
||||
};
|
||||
return [isShared, togglePagePublic];
|
||||
};
|
||||
|
||||
async function initPage(page: Page) {
|
||||
await page.waitForLoaded();
|
||||
// Add page block and surface block at root level
|
||||
const pageBlockId = page.addBlock('affine:page', {
|
||||
title: new page.Text('Hello, world!'),
|
||||
});
|
||||
page.addBlock('affine:surface', {}, pageBlockId);
|
||||
const frameId = page.addBlock('affine:note', {}, pageBlockId);
|
||||
page.addBlock(
|
||||
'affine:paragraph',
|
||||
{
|
||||
text: new page.Text('This is a paragraph.'),
|
||||
},
|
||||
frameId
|
||||
);
|
||||
page.resetHistory();
|
||||
}
|
||||
|
||||
const blockSuiteWorkspace = getOrCreateWorkspace(
|
||||
'test-workspace',
|
||||
WorkspaceFlavour.LOCAL
|
||||
);
|
||||
|
||||
const promise = Promise.all([
|
||||
initPage(blockSuiteWorkspace.createPage({ id: 'page0' })),
|
||||
initPage(blockSuiteWorkspace.createPage({ id: 'page1' })),
|
||||
initPage(blockSuiteWorkspace.createPage({ id: 'page2' })),
|
||||
]);
|
||||
|
||||
const localWorkspace: LocalWorkspace = {
|
||||
id: 'test-workspace',
|
||||
flavour: WorkspaceFlavour.LOCAL,
|
||||
blockSuiteWorkspace,
|
||||
};
|
||||
|
||||
const affineWorkspace: AffineCloudWorkspace = {
|
||||
id: 'test-workspace',
|
||||
flavour: WorkspaceFlavour.AFFINE_CLOUD,
|
||||
blockSuiteWorkspace,
|
||||
};
|
||||
|
||||
async function unimplemented() {
|
||||
toast('work in progress');
|
||||
}
|
||||
|
||||
export const Basic: StoryFn = () => {
|
||||
use(promise);
|
||||
return (
|
||||
<ShareMenu
|
||||
currentPage={blockSuiteWorkspace.getPage('page0') as Page}
|
||||
useIsSharedPage={useIsSharedPage}
|
||||
workspace={localWorkspace}
|
||||
onEnableAffineCloud={unimplemented}
|
||||
togglePagePublic={unimplemented}
|
||||
exportHandler={unimplemented}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
Basic.play = async ({ canvasElement }) => {
|
||||
{
|
||||
const button = canvasElement.querySelector(
|
||||
'[data-testid="share-menu-button"]'
|
||||
) as HTMLButtonElement;
|
||||
expect(button).not.toBeNull();
|
||||
button.click();
|
||||
}
|
||||
await new Promise(resolve => window.setTimeout(resolve, 100));
|
||||
{
|
||||
const button = canvasElement.querySelector(
|
||||
'[data-testid="share-menu-enable-affine-cloud-button"]'
|
||||
);
|
||||
expect(button).not.toBeNull();
|
||||
}
|
||||
};
|
||||
|
||||
export const AffineBasic: StoryFn = () => {
|
||||
use(promise);
|
||||
return (
|
||||
<ShareMenu
|
||||
currentPage={blockSuiteWorkspace.getPage('page0') as Page}
|
||||
useIsSharedPage={useIsSharedPage}
|
||||
workspace={affineWorkspace}
|
||||
onEnableAffineCloud={unimplemented}
|
||||
togglePagePublic={unimplemented}
|
||||
exportHandler={unimplemented}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const DisableModal: StoryFn = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
use(promise);
|
||||
return (
|
||||
<>
|
||||
<StyledDisableButton onClick={() => setOpen(!open)}>
|
||||
Disable Public Link
|
||||
</StyledDisableButton>
|
||||
<PublicLinkDisableModal
|
||||
open={open}
|
||||
onConfirm={() => {
|
||||
toast('Disabled');
|
||||
setOpen(false);
|
||||
}}
|
||||
onOpenChange={setOpen}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
15
tests/storybook/src/stories/switch.stories.tsx
Normal file
15
tests/storybook/src/stories/switch.stories.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
/* deepscan-disable USELESS_ARROW_FUNC_BIND */
|
||||
import { Switch } from '@affine/component';
|
||||
import type { Meta, StoryFn } from '@storybook/react';
|
||||
|
||||
export default {
|
||||
title: 'AFFiNE/Switch',
|
||||
component: Switch,
|
||||
parameters: {
|
||||
chromatic: { disableSnapshot: true },
|
||||
},
|
||||
} satisfies Meta;
|
||||
|
||||
export const Basic: StoryFn = () => {
|
||||
return <Switch>Switch</Switch>;
|
||||
};
|
||||
63
tests/storybook/src/stories/workspace-list.stories.tsx
Normal file
63
tests/storybook/src/stories/workspace-list.stories.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import type { WorkspaceListProps } from '@affine/component/workspace-list';
|
||||
import { WorkspaceList } from '@affine/component/workspace-list';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { getOrCreateWorkspace } from '@affine/workspace/manager';
|
||||
import { arrayMove } from '@dnd-kit/sortable';
|
||||
import type { Meta } from '@storybook/react';
|
||||
import { useState } from 'react';
|
||||
|
||||
export default {
|
||||
title: 'AFFiNE/WorkspaceList',
|
||||
component: WorkspaceList,
|
||||
parameters: {
|
||||
chromatic: { disableSnapshot: true },
|
||||
},
|
||||
} satisfies Meta<WorkspaceListProps>;
|
||||
|
||||
export const Default = () => {
|
||||
const [items, setItems] = useState(() => {
|
||||
const items = [
|
||||
{
|
||||
id: '1',
|
||||
flavour: WorkspaceFlavour.LOCAL,
|
||||
blockSuiteWorkspace: getOrCreateWorkspace('1', WorkspaceFlavour.LOCAL),
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
flavour: WorkspaceFlavour.LOCAL,
|
||||
blockSuiteWorkspace: getOrCreateWorkspace('2', WorkspaceFlavour.LOCAL),
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
flavour: WorkspaceFlavour.LOCAL,
|
||||
blockSuiteWorkspace: getOrCreateWorkspace('3', WorkspaceFlavour.LOCAL),
|
||||
},
|
||||
] satisfies WorkspaceListProps['items'];
|
||||
|
||||
items.forEach(item => {
|
||||
item.blockSuiteWorkspace.meta.setName(item.id);
|
||||
});
|
||||
|
||||
return items;
|
||||
});
|
||||
return (
|
||||
<WorkspaceList
|
||||
currentWorkspaceId={null}
|
||||
items={items}
|
||||
onClick={() => {}}
|
||||
onSettingClick={() => {}}
|
||||
onDragEnd={event => {
|
||||
const { active, over } = event;
|
||||
|
||||
if (active.id !== over?.id) {
|
||||
setItems(items => {
|
||||
const oldIndex = items.findIndex(item => item.id === active.id);
|
||||
const newIndex = items.findIndex(item => item.id === over?.id);
|
||||
|
||||
return arrayMove(items, oldIndex, newIndex);
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
31
tests/storybook/tsconfig.json
Normal file
31
tests/storybook/tsconfig.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": ["./src"],
|
||||
"compilerOptions": {
|
||||
// Workaround for storybook build
|
||||
"baseUrl": "../..",
|
||||
"composite": true,
|
||||
"noEmit": false,
|
||||
"outDir": "lib"
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"path": "../../packages/frontend/core"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/frontend/component"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/common/env"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/frontend/workspace"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/plugins/image-preview"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
25
tests/storybook/tsconfig.node.json
Normal file
25
tests/storybook/tsconfig.node.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"jsx": "react-jsx",
|
||||
"moduleResolution": "bundler",
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"noEmit": false,
|
||||
"outDir": "lib/.storybook"
|
||||
},
|
||||
"include": [".storybook"],
|
||||
"exclude": ["lib"],
|
||||
"references": [
|
||||
{ "path": "../../packages/frontend/core" },
|
||||
{ "path": "../../packages/frontend/i18n" },
|
||||
{
|
||||
"path": "../../packages/common/env"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/frontend/core/tsconfig.node.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user