From 1e5a4a68498c2f05d2f65c2b11feccdbde652477 Mon Sep 17 00:00:00 2001
From: Alex Yang <himself65@outlook.com>
Date: Wed, 16 Aug 2023 15:07:55 -0500
Subject: [PATCH] feat(storybook): improve code (#3786)

---
 .github/workflows/publish-storybook.yml       |  2 +-
 apps/storybook/.storybook/main.ts             |  6 +-
 apps/storybook/.storybook/preview.tsx         | 64 +++++++------
 apps/storybook/package.json                   |  3 +-
 .../stories/block-suite-editor.stories.tsx    | 92 -------------------
 apps/storybook/src/stories/core.stories.tsx   |  3 +
 .../src/stories/datepicker.stories.tsx        |  7 +-
 .../src/stories/import-page.stories.tsx       |  3 +-
 .../src/stories/page-list.stories.tsx         |  3 +
 yarn.lock                                     |  8 ++
 10 files changed, 63 insertions(+), 128 deletions(-)
 delete mode 100644 apps/storybook/src/stories/block-suite-editor.stories.tsx

diff --git a/.github/workflows/publish-storybook.yml b/.github/workflows/publish-storybook.yml
index 4a65bff2da..dbd7db584f 100644
--- a/.github/workflows/publish-storybook.yml
+++ b/.github/workflows/publish-storybook.yml
@@ -34,5 +34,5 @@ jobs:
         with:
           workingDir: apps/storybook
           buildScriptName: build
-          onlyStoryNames: 'Preview/**'
+          onlyChanged: true
           projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
diff --git a/apps/storybook/.storybook/main.ts b/apps/storybook/.storybook/main.ts
index aa5d7ef2c6..42d67843a4 100644
--- a/apps/storybook/.storybook/main.ts
+++ b/apps/storybook/.storybook/main.ts
@@ -5,6 +5,7 @@ import { mergeConfig } from 'vite';
 import tsconfigPaths from 'vite-tsconfig-paths';
 import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin';
 import { getRuntimeConfig } from '../../core/.webpack/runtime-config';
+import turbosnap from 'vite-plugin-turbosnap';
 
 runCli(
   {
@@ -33,7 +34,7 @@ export default {
   framework: {
     name: '@storybook/react-vite',
   },
-  async viteFinal(config, _) {
+  async viteFinal(config, { configType }) {
     return mergeConfig(config, {
       assetsInclude: ['**/*.md'],
       plugins: [
@@ -41,6 +42,9 @@ export default {
         tsconfigPaths({
           root: fileURLToPath(new URL('../../../', import.meta.url)),
         }),
+        configType === 'PRODUCTION'
+          ? turbosnap({ rootDir: config.root ?? process.cwd() })
+          : null,
       ],
       define: {
         'process.env': {},
diff --git a/apps/storybook/.storybook/preview.tsx b/apps/storybook/.storybook/preview.tsx
index 783454fc06..f8b7551cc4 100644
--- a/apps/storybook/.storybook/preview.tsx
+++ b/apps/storybook/.storybook/preview.tsx
@@ -4,12 +4,12 @@ import '@affine/component/theme/theme.css';
 import '@toeverything/components/style.css';
 import { createI18n } from '@affine/i18n';
 import { ThemeProvider, useTheme } from 'next-themes';
-import type { ComponentType } from 'react';
-import { useEffect } from 'react';
 import { useDarkMode } from 'storybook-dark-mode';
 import { setup } from '@affine/core/bootstrap/setup';
 import { AffineContext } from '@affine/component/context';
 import { use } from 'foxact/use';
+import useSWR from 'swr';
+import type { Decorator } from '@storybook/react';
 
 const setupPromise = setup();
 
@@ -24,38 +24,42 @@ export const parameters = {
   },
 };
 
-const createI18nDecorator = () => {
-  const i18n = createI18n();
-  const withI18n = (Story: any, context: any) => {
-    const locale = context.globals.locale;
-    useEffect(() => {
-      i18n.changeLanguage(locale);
-    }, [locale]);
-    return <Story {...context} />;
-  };
-  return withI18n;
+const 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 Component = () => {
+const ThemeChange = () => {
   const isDark = useDarkMode();
   const theme = useTheme();
-  useEffect(() => {
-    theme.setTheme(isDark ? 'dark' : 'light');
-  }, [isDark]);
+  if (theme.resolvedTheme === 'dark' && !isDark) {
+    theme.setTheme('light');
+  } else if (theme.resolvedTheme === 'light' && isDark) {
+    theme.setTheme('dark');
+  }
   return null;
 };
 
-export const decorators = [
-  (Story: ComponentType) => {
-    use(setupPromise);
-    return (
-      <ThemeProvider>
-        <AffineContext>
-          <Component />
-          <Story />
-        </AffineContext>
-      </ThemeProvider>
-    );
-  },
-  createI18nDecorator(),
-];
+const withContextDecorator: Decorator = (Story, context) => {
+  use(setupPromise);
+  return (
+    <ThemeProvider>
+      <AffineContext>
+        <ThemeChange />
+        <Story {...context} />
+      </AffineContext>
+    </ThemeProvider>
+  );
+};
+
+export const decorators = [withContextDecorator, withI18n];
diff --git a/apps/storybook/package.json b/apps/storybook/package.json
index b16d98302a..99cb37ed50 100644
--- a/apps/storybook/package.json
+++ b/apps/storybook/package.json
@@ -41,7 +41,8 @@
     "chromatic": "^6.22.0",
     "react": "18.2.0",
     "react-dom": "18.2.0",
-    "storybook-addon-react-router-v6": "^2.0.4"
+    "storybook-addon-react-router-v6": "^2.0.4",
+    "vite-plugin-turbosnap": "^1.0.2"
   },
   "peerDependencies": {
     "@blocksuite/blocks": "*",
diff --git a/apps/storybook/src/stories/block-suite-editor.stories.tsx b/apps/storybook/src/stories/block-suite-editor.stories.tsx
deleted file mode 100644
index c640ef8918..0000000000
--- a/apps/storybook/src/stories/block-suite-editor.stories.tsx
+++ /dev/null
@@ -1,92 +0,0 @@
-/* deepscan-disable USELESS_ARROW_FUNC_BIND */
-import { BlockHubWrapper } from '@affine/component/block-hub';
-import type { EditorProps } from '@affine/component/block-suite-editor';
-import { BlockSuiteEditor } from '@affine/component/block-suite-editor';
-import { rootBlockHubAtom } from '@affine/workspace/atom';
-import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models';
-import type { EditorContainer } from '@blocksuite/editor';
-import type { Page } from '@blocksuite/store';
-import { createMemoryStorage, Schema, Workspace } from '@blocksuite/store';
-import { expect } from '@storybook/jest';
-import type { Meta, StoryFn } from '@storybook/react';
-import { use } from 'foxact/use';
-
-const schema = new Schema();
-
-schema.register(AffineSchemas).register(__unstableSchemas);
-
-const blockSuiteWorkspace = new Workspace({
-  id: 'test',
-  blobStorages: [createMemoryStorage],
-  schema,
-});
-
-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 page = blockSuiteWorkspace.createPage('page0');
-
-type BlockSuiteMeta = Meta<typeof BlockSuiteEditor>;
-export default {
-  title: 'BlockSuite/Editor',
-  component: BlockSuiteEditor,
-} satisfies BlockSuiteMeta;
-
-const Template: StoryFn<EditorProps> = (props: Partial<EditorProps>) => {
-  if (!page.loaded) {
-    use(initPage(page));
-  }
-  return (
-    <div
-      style={{
-        height: '100vh',
-        width: '100vw',
-        overflow: 'auto',
-      }}
-    >
-      <BlockSuiteEditor onInit={initPage} page={page} mode="page" {...props} />
-      <BlockHubWrapper
-        style={{
-          position: 'absolute',
-          right: 12,
-          bottom: 12,
-        }}
-        blockHubAtom={rootBlockHubAtom}
-      />
-    </div>
-  );
-};
-
-export const Empty = Template.bind({});
-Empty.play = async ({ canvasElement }) => {
-  await new Promise<void>(resolve => {
-    setTimeout(() => resolve(), 500);
-  });
-  const editorContainer = canvasElement.querySelector(
-    '[data-testid="editor-page0"]'
-  ) as HTMLDivElement;
-  expect(editorContainer).not.toBeNull();
-  const editor = editorContainer.querySelector(
-    'editor-container'
-  ) as EditorContainer;
-  expect(editor).not.toBeNull();
-};
-
-Empty.args = {
-  mode: 'page',
-};
diff --git a/apps/storybook/src/stories/core.stories.tsx b/apps/storybook/src/stories/core.stories.tsx
index 76bb513537..d6c1278c87 100644
--- a/apps/storybook/src/stories/core.stories.tsx
+++ b/apps/storybook/src/stories/core.stories.tsx
@@ -24,6 +24,9 @@ const FakeApp = () => {
 
 export default {
   title: 'Preview/Core',
+  parameters: {
+    chromatic: { disableSnapshot: false },
+  },
 };
 
 export const Index: StoryFn = () => {
diff --git a/apps/storybook/src/stories/datepicker.stories.tsx b/apps/storybook/src/stories/datepicker.stories.tsx
index 52490f2ff4..f69cc96359 100644
--- a/apps/storybook/src/stories/datepicker.stories.tsx
+++ b/apps/storybook/src/stories/datepicker.stories.tsx
@@ -1,11 +1,14 @@
 import { AFFiNEDatePicker } from '@affine/component/date-picker';
-import type { StoryFn } from '@storybook/react';
+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());
diff --git a/apps/storybook/src/stories/import-page.stories.tsx b/apps/storybook/src/stories/import-page.stories.tsx
index f7b4df5e16..37867a60c1 100644
--- a/apps/storybook/src/stories/import-page.stories.tsx
+++ b/apps/storybook/src/stories/import-page.stories.tsx
@@ -2,11 +2,12 @@
 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,
-};
+} satisfies Meta;
 
 const Template: StoryFn<typeof ImportPage> = args => <ImportPage {...args} />;
 
diff --git a/apps/storybook/src/stories/page-list.stories.tsx b/apps/storybook/src/stories/page-list.stories.tsx
index 7a9086dee3..b3ef056fc0 100644
--- a/apps/storybook/src/stories/page-list.stories.tsx
+++ b/apps/storybook/src/stories/page-list.stories.tsx
@@ -13,6 +13,9 @@ import { userEvent } from '@storybook/testing-library';
 export default {
   title: 'AFFiNE/PageList',
   component: PageList,
+  parameters: {
+    chromatic: { disableSnapshot: true },
+  },
 };
 
 export const AffineOperationCell: StoryFn<OperationCellProps> = ({
diff --git a/yarn.lock b/yarn.lock
index f84fce762d..b12d1715e3 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -727,6 +727,7 @@ __metadata:
     storybook: ^7.3.1
     storybook-addon-react-router-v6: ^2.0.4
     storybook-dark-mode: ^3.0.1
+    vite-plugin-turbosnap: ^1.0.2
     wait-on: ^7.0.1
   peerDependencies:
     "@blocksuite/blocks": "*"
@@ -32465,6 +32466,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"vite-plugin-turbosnap@npm:^1.0.2":
+  version: 1.0.2
+  resolution: "vite-plugin-turbosnap@npm:1.0.2"
+  checksum: c5da204cd9fa0dbf8a5f3c151681057da0f8cec3acbec94bdc04e525d410d85bd87c46b73498ba20c487fe61a68b1486b0f88bc51fec48d7053d1f3597411231
+  languageName: node
+  linkType: hard
+
 "vite-tsconfig-paths@npm:^4.2.0":
   version: 4.2.0
   resolution: "vite-tsconfig-paths@npm:4.2.0"