From ff76c9f3b9962d21e152131770d898c70832fdfc Mon Sep 17 00:00:00 2001 From: Alex Yang Date: Mon, 6 Nov 2023 23:43:48 -0600 Subject: [PATCH 01/32] feat: support multiple workspace --- apps/desktop/src/web/main.tsx | 57 ++++-- apps/desktop/tests/basic.spec.ts | 2 - apps/desktop/tsconfig.json | 8 + apps/desktop/tsconfig.src.json | 3 - apps/docs/components/preview.tsx | 2 +- apps/web/package.json | 3 +- apps/web/src/app/layout.tsx | 8 +- apps/web/src/app/page.tsx | 83 +++++---- apps/web/src/app/provider.tsx | 24 +-- apps/web/src/store.ts | 111 +++++++++++ packages/core/package.json | 10 +- packages/core/src/app.tsx | 79 -------- packages/core/src/components.tsx | 39 ++++ packages/core/src/components/editor/index.tsx | 14 +- .../core/src/components/page-list/index.tsx | 52 ++++++ .../src/components/setting-profile/index.tsx | 17 ++ packages/core/src/store.ts | 15 +- packages/core/src/store/api.ts | 9 +- packages/core/src/store/index.ts | 126 ++++++++++++- packages/core/src/store/preference.ts | 123 +++++++----- packages/core/test/preference.spec.ts | 61 +++--- packages/core/vite.config.ts | 5 +- pnpm-lock.yaml | 176 +++++++++++++++++- 23 files changed, 771 insertions(+), 256 deletions(-) create mode 100644 apps/web/src/store.ts delete mode 100644 packages/core/src/app.tsx create mode 100644 packages/core/src/components.tsx create mode 100644 packages/core/src/components/page-list/index.tsx create mode 100644 packages/core/src/components/setting-profile/index.tsx diff --git a/apps/desktop/src/web/main.tsx b/apps/desktop/src/web/main.tsx index 6df2048..c696e30 100644 --- a/apps/desktop/src/web/main.tsx +++ b/apps/desktop/src/web/main.tsx @@ -1,9 +1,9 @@ import { StrictMode, lazy } from 'react' import { createRoot } from 'react-dom/client' import '@blocksuite/editor/themes/affine.css' -import { themeAtom } from '@refine/core/store' -import { inject } from 'jotai-inject' import './index.css' +import { workspaceManager } from '@refine/core/store' +import { getDefaultStore } from 'jotai/vanilla' declare global { interface Window { @@ -14,24 +14,47 @@ declare global { } } -inject( - themeAtom, - () => window.apis.getTheme(), - (theme) => { - window.apis.changeTheme(theme).catch(console.error) - document.documentElement.setAttribute('data-theme', theme) - } -) +const workspaceAtom = workspaceManager.getWorkspaceAtom('workspace:0') +const store = getDefaultStore() + +const promise = workspaceManager.withLocalProvider(). + then(workspaceManager.inject). + then( + () => store.get(workspaceAtom).then(async workspace => { + const page = workspace.getPage('page0') + if (!page) { + const page = workspace.createPage({ + id: 'page0' + }) + await page.waitForLoaded() + const pageBlockId = page.addBlock('affine:page', { + children: [], + title: new page.Text('Untitled') + }) + page.addBlock('affine:surface', {}, pageBlockId) + const noteBlockId = page.addBlock('affine:note', {}, pageBlockId) + page.addBlock('affine:paragraph', {}, noteBlockId) + } else { + await page.waitForLoaded() + } + }) + ) -export const LazyApp = lazy( - () => import('@refine/core/app').then(({ App }) => ({ default: App }))) +export const Editor = lazy( + () => import('@refine/core/components').then( + ({ Editor }) => ({ default: Editor }))) const div = document.getElementById('root') if (!div) throw new Error('Root element not found') const root = createRoot(div) -root.render( - - - -) +promise.then(() => { + root.render( + + + + ) +}) diff --git a/apps/desktop/tests/basic.spec.ts b/apps/desktop/tests/basic.spec.ts index c2aa12d..ef10791 100644 --- a/apps/desktop/tests/basic.spec.ts +++ b/apps/desktop/tests/basic.spec.ts @@ -49,8 +49,6 @@ test.afterAll(async () => { test.describe('app basic functionality', () => { test('should create new page success success', async () => { - await expect(page.getByText('Page not found')).toBeVisible() - await page.getByTestId('create-page').click() await expect(page.getByText('Untitled')).toBeVisible() }) }) diff --git a/apps/desktop/tsconfig.json b/apps/desktop/tsconfig.json index b591f00..426ee0b 100644 --- a/apps/desktop/tsconfig.json +++ b/apps/desktop/tsconfig.json @@ -10,5 +10,13 @@ "include": [ "vite.config.ts", "rollup.config.ts" + ], + "references": [ + { + "path": "./tsconfig.src.json" + }, + { + "path": "./tsconfig.web.json" + } ] } diff --git a/apps/desktop/tsconfig.src.json b/apps/desktop/tsconfig.src.json index e35bfed..8d65d6f 100644 --- a/apps/desktop/tsconfig.src.json +++ b/apps/desktop/tsconfig.src.json @@ -17,9 +17,6 @@ "references": [ { "path": "./tsconfig.web.json" - }, - { - "path": "./tsconfig.node.json" } ] } diff --git a/apps/docs/components/preview.tsx b/apps/docs/components/preview.tsx index 1a196c6..ec7ab72 100644 --- a/apps/docs/components/preview.tsx +++ b/apps/docs/components/preview.tsx @@ -37,7 +37,7 @@ const NoSsr: FC = ({ } export const Preview = (): ReactElement => { - const { App } = use(importAppPromise) + const { Editor } = use(importAppPromise) return ( -if (typeof window !== 'undefined') { - importAppPromise = import('@refine/core/app') -} else { - importAppPromise = Promise.resolve({ - App: () => <> - }) -} +const Editor = dynamic(() => import('@refine/core/components').then( + ({ Editor }) => ({ default: Editor })), { + ssr: false +}) -const socket = io('http://localhost:3030') +const PageList = dynamic(() => import('@refine/core/components').then( + ({ PageList }) => ({ default: PageList })), { + ssr: false +}) + +declare global { + interface Window { + workspace: unknown + page: unknown + } +} const NoSsr: FC = ({ children @@ -35,38 +43,39 @@ const NoSsr: FC = ({ ) } -let injectPromise = Promise.resolve() -if (typeof window !== 'undefined' && !workspaceManager.injected) { - injectPromise = workspaceManager.withLocalProvider().then(async () => { - const { createSyncProvider } = await import('y-io/sync-provider') - await workspaceManager.with(undefined, ( - workspace - ) => createSyncProvider(socket, workspace.doc)) - }).then(workspaceManager.inject) -} - -declare global { - interface Window { - workspace: unknown - } +function HomeImpl () { + const workspaceId = useAtomValue(workspaceIdAtom) + const pageId = useAtomValue(pageIdAtom) + const pageAtom = workspaceManager.getWorkspacePageAtom(workspaceId, pageId) + const page = useAtomValue(pageAtom) + useEffect(() => { + window.page = page + }, [page]) + return ( +
+ + + +
+ ) } -export default function Home () { - const { App } = use(importAppPromise) - use(injectPromise) - const workspaceId = 'workspace:0' - const workspaceAtom = workspaceManager.getWorkspaceAtom(workspaceId) +export default function Home (): ReactElement { + const workspaceId = useAtomValue(workspaceIdAtom) + const workspace = useAtomValue(workspaceManager.getWorkspaceAtom(workspaceId)) const effectAtom = workspaceManager.getWorkspaceEffectAtom(workspaceId) - const workspace = useAtomValue(workspaceAtom) + useAtomValue(effectAtom) useEffect(() => { window.workspace = workspace }, [workspace]) - useAtomValue(effectAtom) return ( -
+ + - + -
+ ) } diff --git a/apps/web/src/app/provider.tsx b/apps/web/src/app/provider.tsx index 2068087..834ab83 100644 --- a/apps/web/src/app/provider.tsx +++ b/apps/web/src/app/provider.tsx @@ -1,23 +1,25 @@ 'use client' -import { ThemeProvider, useTheme } from 'next-themes' +import { ThemeProvider } from 'next-themes' +import { Provider as JotaiProvider } from 'jotai/react' import type { FC, PropsWithChildren } from 'react' -import { themeAtom } from '@refine/core/store' -import { useInject } from 'jotai-inject' +import { injectPromise } from '../store' +import { use } from 'react' -const ThemeProviderInner: FC = ({ +const ProviderInner: FC = ({ children }) => { - const { setTheme: setUpstreamTheme, theme: upstreamTheme } = useTheme() - useInject(themeAtom, upstreamTheme === 'dark' ? 'dark' : 'light', setUpstreamTheme) + use(injectPromise) return children } export const Provider: FC = function Providers ({ children }) { return ( - - - {children} - - + + + + {children} + + + ) } diff --git a/apps/web/src/store.ts b/apps/web/src/store.ts new file mode 100644 index 0000000..de0d56a --- /dev/null +++ b/apps/web/src/store.ts @@ -0,0 +1,111 @@ +import { userInfoAtom, workspaceManager } from '@refine/core/store' +import { inject } from 'jotai-inject' +import { noop } from 'foxact/noop' +import { atom, getDefaultStore } from 'jotai/vanilla' + +let injectPromise: Promise +if (typeof window !== 'undefined' && !workspaceManager.injected) { + injectPromise = workspaceManager.withLocalProvider(). + then(workspaceManager.inject).then(() => { + console.log('workspaceManager.injected') + }) +} else { + injectPromise = Promise.resolve() +} + +export { + injectPromise +} + +const userId = (((): string => { + if (typeof window !== 'undefined') { + let id = localStorage.getItem('anonymous-user-id') + if (!id) { + localStorage.setItem('anonymous-user-id', id = window.crypto.randomUUID()) + } + return id + } else { + return 'server-side-no-user-id' + } +})()) + +inject(userInfoAtom, () => ({ + username: 'anonymous user', + id: userId +}), noop) + +const primitiveWorkspaceIdAtom = atom(null) +const primitivePageIdAtom = atom(null) + +declare global { + interface Window { + initWorkspace: boolean + } +} + +let promise: Promise | null = null + +if (typeof window !== 'undefined') { + if (localStorage.getItem('workspace-id') === null) { + const randomId = crypto.randomUUID() + const randomPageId = crypto.randomUUID() + localStorage.setItem('workspace-id', randomId) + localStorage.setItem('page-id', randomPageId) + const workspaceAtom = workspaceManager.getWorkspaceAtom(randomId) + const store = getDefaultStore() + promise = store.get(workspaceAtom).then(async workspace => { + const page = workspace.createPage({ + id: randomPageId + }) + await page.waitForLoaded() + const pageBlockId = page.addBlock('affine:page', { + children: [], + title: new page.Text('Untitled') + }) + page.addBlock('affine:surface', {}, pageBlockId) + const noteBlockId = page.addBlock('affine:note', {}, pageBlockId) + page.addBlock('affine:paragraph', {}, noteBlockId) + }) + } +} + +export const workspaceIdAtom = atom(async (get) => { + if (promise) { + await promise + } + const workspaceId = get(primitiveWorkspaceIdAtom) + if (workspaceId !== null) { + return workspaceId + } + if (typeof window === 'undefined') { + return crypto.randomUUID() + } else { + const item = localStorage.getItem('workspace-id') ?? crypto.randomUUID() + localStorage.setItem('workspace-id', item) + return item + } +}, (_, set, workspaceId: string) => { + set(primitiveWorkspaceIdAtom, workspaceId) + if (typeof window !== 'undefined') { + localStorage.setItem('workspace-id', workspaceId) + } +}) + +export const pageIdAtom = atom((get) => { + const pageId = get(primitivePageIdAtom) + if (pageId !== null) { + return pageId + } + if (typeof window === 'undefined') { + return crypto.randomUUID() + } else { + const item = localStorage.getItem('page-id') ?? crypto.randomUUID() + localStorage.setItem('page-id', item) + return item + } +}, (_, set, pageId: string) => { + set(primitivePageIdAtom, pageId) + if (typeof window !== 'undefined') { + localStorage.setItem('page-id', pageId) + } +}) diff --git a/packages/core/package.json b/packages/core/package.json index 91cd69f..6f79024 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -2,10 +2,10 @@ "name": "@refine/core", "type": "module", "exports": { - "./app": { - "types": "./dist/src/app.d.ts", - "import": "./dist/app.js", - "require": "./dist/app.cjs" + "./components": { + "types": "./dist/src/components.d.ts", + "import": "./dist/components.js", + "require": "./dist/components.cjs" }, "./store": { "types": "./dist/src/store.d.ts", @@ -41,12 +41,14 @@ "nanoid": "^4.0.2", "react": "18.3.0-canary-8039e6d0b-20231026", "react-dom": "18.3.0-canary-8039e6d0b-20231026", + "uuid": "^9.0.1", "y-utility": "^0.1.3", "yjs": "^13.6.8" }, "devDependencies": { "@testing-library/react": "^14.0.0", "@testing-library/user-event": "^14.5.1", + "@types/uuid": "^9.0.6", "@vitejs/plugin-react": "^4.1.1", "@vitest/coverage-v8": "^0.34.6", "@vitest/ui": "^0.34.6", diff --git a/packages/core/src/app.tsx b/packages/core/src/app.tsx deleted file mode 100644 index c008a27..0000000 --- a/packages/core/src/app.tsx +++ /dev/null @@ -1,79 +0,0 @@ -'use client' -import { type ReactElement, useCallback, useEffect, useState } from 'react' -import { use } from 'react' -import { themeAtom } from './store/api.js' -import { workspaceManager } from './store/index' -import { BlockSuiteEditor } from './components/editor' -import { useAtom, useAtomValue } from 'jotai/react' - -export type AppProps = { - className?: string -} - -export const App = (props: AppProps): ReactElement => { - const [theme, setTheme] = useAtom(themeAtom) - const workspaceAtom = workspaceManager.getWorkspaceAtom('workspace:0') - const effectAtom = workspaceManager.getWorkspaceEffectAtom('workspace:0') - const workspace = useAtomValue(workspaceAtom) - useAtomValue(effectAtom) - const [page, setPage] = useState(workspace.getPage('page0')) - useEffect(() => { - const dispose = workspace.slots.pageAdded.on((pageId) => { - if (pageId === 'page0') { - setPage(workspace.getPage('page0')) - } - }) - return () => { - dispose.dispose() - } - }, [workspace]) - - const handleNewPage = async () => { - const page = workspace.createPage({ id: 'page0' }) - workspace.setPageMeta('page0', { - tags: [] - }) - await page.waitForLoaded() - const pageBlockId = page.addBlock('affine:page', { - children: [], - title: new page.Text('Untitled') - }) - page.addBlock('affine:surface', {}, pageBlockId) - const noteBlockId = page.addBlock('affine:note', {}, pageBlockId) - page.addBlock('affine:paragraph', {}, noteBlockId) - setPage(page) - } - - const handleChangeTheme = useCallback(() => { - setTheme(theme => theme === 'light' ? 'dark' : 'light') - }, [setTheme]) - - if (!page) { - return ( -
- Page not found - -
- ) - } - - if (!page.loaded) { - use(page.waitForLoaded()) - } - - return ( -
- - -
- ) -} diff --git a/packages/core/src/components.tsx b/packages/core/src/components.tsx new file mode 100644 index 0000000..8447ef2 --- /dev/null +++ b/packages/core/src/components.tsx @@ -0,0 +1,39 @@ +'use client' +import { type ReactElement } from 'react' +import { use } from 'react' +import { workspaceManager } from './store' +import { BlockSuiteEditor } from './components/editor' +import { useAtomValue } from 'jotai/react' +import { PageList } from './components/page-list' + +export type EditorProps = { + className?: string + workspaceId: string, + pageId: string +} + +export const Editor = (props: EditorProps): ReactElement => { + const { workspaceId, pageId } = props + const effectAtom = workspaceManager.getWorkspaceEffectAtom(workspaceId) + const pageAtom = workspaceManager.getWorkspacePageAtom(workspaceId, pageId) + useAtomValue(effectAtom) + const page = useAtomValue(pageAtom) + + if (!page.loaded) { + use(page.waitForLoaded()) + } + + return ( +
+ +
+ ) +} + +Editor.displayName = 'RefineEditor' + +export { + PageList +} diff --git a/packages/core/src/components/editor/index.tsx b/packages/core/src/components/editor/index.tsx index c8df260..d8f5e2f 100644 --- a/packages/core/src/components/editor/index.tsx +++ b/packages/core/src/components/editor/index.tsx @@ -3,7 +3,7 @@ import type { EditorContainer } from '@blocksuite/editor' import { assertExists } from '@blocksuite/global/utils' import type { Page } from '@blocksuite/store' import type { CSSProperties, ReactElement } from 'react' -import { memo, useEffect, useRef, use, Suspense } from 'react' +import { memo, useEffect, useRef, use } from 'react' const EditorContainerPromise = import('@blocksuite/editor').then( m => m.EditorContainer) @@ -70,21 +70,11 @@ const BlockSuiteEditorImpl = (props: EditorProps): ReactElement => { ) } -export const EditorFallback = () => { - return ( -
- Loading... -
- ) -} - export const BlockSuiteEditor = memo(function BlockSuiteEditor ( props: EditorProps ): ReactElement { return ( - }> - - + ) }) diff --git a/packages/core/src/components/page-list/index.tsx b/packages/core/src/components/page-list/index.tsx new file mode 100644 index 0000000..880c311 --- /dev/null +++ b/packages/core/src/components/page-list/index.tsx @@ -0,0 +1,52 @@ +'use client' +import type { ReactElement } from 'react' +import { Workspace } from '@blocksuite/store' +import { v4 } from 'uuid' +import { useAtomValue } from 'jotai/react' +import { getPageListAtom } from '../../store' + +export type PageListProps = { + workspace: Workspace +} + +export const PageList = ( + props: PageListProps +): ReactElement => { + const { + workspace + } = props + const pageList = useAtomValue(getPageListAtom(workspace)) + return ( + <> +
+ +
+ + + + + + + + {pageList.map((page) => ( + + + + + + ))} + +
TitleIDCreate Date
{page.title}{page.id}{page.createDate}
+ + ) +} + +PageList.displayName = 'RefinePageList' diff --git a/packages/core/src/components/setting-profile/index.tsx b/packages/core/src/components/setting-profile/index.tsx new file mode 100644 index 0000000..36916fb --- /dev/null +++ b/packages/core/src/components/setting-profile/index.tsx @@ -0,0 +1,17 @@ +import type { ReactElement } from 'react' +import type { ThemeAtom } from '../../store/api' +import { useAtomValue } from 'jotai/react' + +type SettingProfileProps = { + themeAtom: ThemeAtom +} + +export const SettingProfile = (props: SettingProfileProps): ReactElement => { + const { themeAtom } = props + const theme = useAtomValue(themeAtom) + return ( +
+ currentTheme: {theme} +
+ ) +} diff --git a/packages/core/src/store.ts b/packages/core/src/store.ts index 22732d8..4189a0f 100644 --- a/packages/core/src/store.ts +++ b/packages/core/src/store.ts @@ -1,7 +1,16 @@ -import { themeAtom } from './store/api' -import { workspaceManager } from './store/index' +import type { Theme, ThemeAtom } from './store/api' +import { userInfoAtom } from './store/api' +import { getPageListAtom, workspaceManager } from './store/index' +import { settingAtom } from './store/preference' export { + getPageListAtom, workspaceManager, - themeAtom + userInfoAtom, + settingAtom +} + +export type { + ThemeAtom, + Theme } diff --git a/packages/core/src/store/api.ts b/packages/core/src/store/api.ts index d0067b4..ce51f20 100644 --- a/packages/core/src/store/api.ts +++ b/packages/core/src/store/api.ts @@ -1,5 +1,12 @@ import { atom } from 'jotai/vanilla' +import { settingAtom } from './preference' export type Theme = 'light' | 'dark' +export type User = { + username: string + id: string +} -export const themeAtom = atom('light') +// need inject from upstream +export const userInfoAtom = atom(null as unknown as User) +export type ThemeAtom = ReturnType> diff --git a/packages/core/src/store/index.ts b/packages/core/src/store/index.ts index 2c8f168..0971a75 100644 --- a/packages/core/src/store/index.ts +++ b/packages/core/src/store/index.ts @@ -1,4 +1,4 @@ -import { Schema, Workspace } from '@blocksuite/store' +import { Page, Schema, Workspace } from '@blocksuite/store' import { atom, type Atom } from 'jotai/vanilla' import { atomEffect } from 'jotai-effect' import { AffineSchemas, __unstableSchemas } from '@blocksuite/blocks/models' @@ -17,7 +17,11 @@ export type ProviderCreator = (workspace: Workspace) => { export const globalWorkspaceMap = new Map() class WorkspaceManager { - #wokrspaceAtomWeakMap = new WeakMap>>() + #workspaceAtomWeakMap = new WeakMap>>() + #workspacePageAtomWeakMap = new WeakMap< + Workspace, + Map>> + >() #workspaceEffectAtomWeakMap = new WeakMap< Workspace, Atom @@ -45,7 +49,7 @@ class WorkspaceManager { globalWorkspaceMap.set(workspaceId, workspace) } { - const workspaceAtom = this.#wokrspaceAtomWeakMap.get(workspace) + const workspaceAtom = this.#workspaceAtomWeakMap.get(workspace) if (workspaceAtom) { return workspaceAtom } @@ -59,11 +63,85 @@ class WorkspaceManager { } return ensureWorkspace }) - this.#wokrspaceAtomWeakMap.set(workspace, workspaceAtom) + this.#workspaceAtomWeakMap.set(workspace, workspaceAtom) return workspaceAtom } } + getWorkspacePageAtom = ( + workspaceId: string, + pageId: string + ): Atom> => { + let workspace = globalWorkspaceMap.get(workspaceId) + let map: Map>> + if (workspace && this.#workspacePageAtomWeakMap.has(workspace)) { + map = this.#workspacePageAtomWeakMap.get(workspace) as Map< + string, + Atom> + > + } else if (!workspace) { + workspace = new Workspace({ + id: workspaceId, + schema: this.#schema + }) + globalWorkspaceMap.set(workspaceId, workspace) + map = new Map() + this.#workspacePageAtomWeakMap.set(workspace, map) + } else { + map = new Map() + this.#workspacePageAtomWeakMap.set(workspace, map) + } + + if (map.has(pageId)) { + return map.get(pageId) as Atom> + } + + const workspaceAtom = this.getWorkspaceAtom(workspaceId) + const primitivePageAtom = atom(null) + const pageAtom = atom(async (get) => { + const primitivePage = get(primitivePageAtom) + if (primitivePage !== null) { + return primitivePage + } + const workspace = await get(workspaceAtom) + const page = workspace.getPage(pageId) + if (page === null) { + console.trace('error') + throw new Error(`page ${pageId} not found`) + } + if (!page.loaded) { + await page.waitForLoaded() + } + return page + }) + primitivePageAtom.onMount = (setSelf) => { + const workspace = globalWorkspaceMap.get(workspaceId) + if (!workspace) { + console.error(`workspace ${workspaceId} not found`) + return + } + if (workspace.getPage(pageId) !== null) { + setSelf(workspace.getPage(pageId)) + } + const onPageRemoved = workspace.slots.pageRemoved.on((id) => { + if (id === pageId) { + setSelf(null) + } + }) + const onPageAdded = workspace.slots.pageAdded.on((id) => { + if (id === pageId) { + setSelf(workspace.getPage(pageId)) + } + }) + return () => { + onPageRemoved.dispose() + onPageAdded.dispose() + } + } + map.set(pageId, pageAtom) + return pageAtom + } + getWorkspaceEffectAtom = (workspaceId: string): Atom => { let workspace = globalWorkspaceMap.get(workspaceId) if (!workspace) { @@ -118,7 +196,7 @@ class WorkspaceManager { downloadBinary } = await import('@toeverything/y-indexeddb') this.#preloads.push(async (workspace) => { - const binary = await downloadBinary(workspace.doc.guid, 'refine-db') + const binary = await downloadBinary(workspace.doc.guid, 'refine-indexeddb') if (binary) { // only download root doc applyUpdate(workspace.doc, binary) @@ -138,7 +216,8 @@ class WorkspaceManager { }) } - public with = async (preload?: Preload, providerCreator?: ProviderCreator) => { + public with = async ( + preload?: Preload, providerCreator?: ProviderCreator) => { preload && this.#preloads.push(preload) providerCreator && this.#providers.push(providerCreator) } @@ -160,3 +239,38 @@ class WorkspaceManager { } export const workspaceManager = new WorkspaceManager() + + +type PageMeta = { + id: string; + title: string; + tags: string[]; + createDate: number; +} + +const pageListAtomWeakMap = new WeakMap>() + +export function getPageListAtom ( + workspace: Workspace +): Atom { + let pageListAtom = pageListAtomWeakMap.get(workspace) + if (!pageListAtom) { + const primitivePageListAtom = atom([]) + const effectAtom = atomEffect((_, set) => { + set(primitivePageListAtom, [...workspace.meta.pageMetas]) + const onPageMetaAdded = workspace.meta.pageMetasUpdated.on(() => { + set(primitivePageListAtom, [...workspace.meta.pageMetas]) + }) + return () => { + onPageMetaAdded.dispose() + } + }) + + pageListAtom = atom((get) => { + get(effectAtom) + return get(primitivePageListAtom) + }) + pageListAtomWeakMap.set(workspace, pageListAtom) + } + return pageListAtom +} diff --git a/packages/core/src/store/preference.ts b/packages/core/src/store/preference.ts index 9870b0c..13de1b4 100644 --- a/packages/core/src/store/preference.ts +++ b/packages/core/src/store/preference.ts @@ -1,84 +1,117 @@ import { Workspace } from '@blocksuite/store' +import { v5 } from 'uuid' import { Array as YArray, Doc } from 'yjs' import { YKeyValue } from 'y-utility/y-keyvalue' -import { atom } from 'jotai/vanilla' +import { type Atom, atom } from 'jotai/vanilla' import { RESET } from 'jotai/utils' +import { userInfoAtom } from './api' +import { atomEffect } from 'jotai-effect' -function getWorkspacePreferenceDoc ( - workspace: Workspace, - userId: string +const workspacePreferenceNamespace = '373a988e-670f-48ef-bacb-f345ae09ca24' + +function getWorkspacePreferenceDocAtom ( + workspaceAtom: Atom> ) { - const rootDoc = workspace.doc - const settingsMap = rootDoc.getMap('settings') - if (!settingsMap.has(userId)) { - settingsMap.set(userId, new Doc({ - guid: crypto.randomUUID() - })) - } - return settingsMap.get(userId) as Doc + return atom(async get => { + const workspace = await get(workspaceAtom) + const { id } = get(userInfoAtom) + const rootDoc = workspace.doc + const settingsMap = rootDoc.getMap('settings') + if (!settingsMap.has(id)) { + settingsMap.set(id, new Doc({ + guid: v5(id, workspacePreferenceNamespace) + })) + } + return settingsMap.get(id) as Doc + }) } -function getSettingKV ( - workspace: Workspace, - userId: string +function getSettingKVAtom ( + workspaceAtom: Atom> ) { - const doc = getWorkspacePreferenceDoc(workspace, userId) - const array = doc.getArray('setting') as YArray<{ - key: string, - val: unknown - }> - return new YKeyValue(array) + const targetDocAtom = getWorkspacePreferenceDocAtom(workspaceAtom) + return atom(async get => { + const doc = await get(targetDocAtom) + const array = doc.getArray('setting') as YArray<{ + key: string, + val: unknown + }> + return new YKeyValue(array) + }) } type Changes = Map export function settingAtom ( - workspace: Workspace, - userId: string, + workspaceAtom: Atom>, key: string, defaultValue: Value ) { - const kv = getSettingKV(workspace, userId) - const valueAtom = atom((kv.get(key) || defaultValue) as Value) + const kvAtom = getSettingKVAtom(workspaceAtom) + const primitiveAtom = atom(null as unknown as Value) - valueAtom.onMount = set => { - set((kv.get(key) as Value) || defaultValue) - const onChange = (changes: Changes) => { - if (changes.has(key)) { - const change = changes.get(key)! - if (change.action === 'delete') { - set(defaultValue) - } else { - set(change.newValue as Value) + const effectAtom = atomEffect((get, set) => { + const abortController = new AbortController() + get(kvAtom).then(kv => { + const value = (kv.get(key) as Value) || defaultValue + set(primitiveAtom, value) + if (abortController.signal.aborted) { + return + } + const onChange = (changes: Changes) => { + if (changes.has(key)) { + const change = changes.get(key)! + if (change.action === 'delete') { + set(primitiveAtom, defaultValue) + } else { + set(primitiveAtom, change.newValue as Value) + } } } - } - kv.on('change', onChange) + kv.on('change', onChange) + abortController.signal.addEventListener('abort', () => { + kv.off('change', onChange) + }) + }) + return () => { - kv.off('change', onChange) + abortController.abort() } - } + }) return atom< - Value, + Value | Promise, [Value | ((prev: Value) => Value) | typeof RESET], - void + Promise >( - (get) => get(valueAtom), - (get, set, newValue) => { - const oldValue = get(valueAtom) + async (get) => { + get(effectAtom) + if (get(primitiveAtom) !== null) { + return get(primitiveAtom) + } else { + const kv = await get(kvAtom) + if (kv.has(key)) { + return kv.get(key) as Value + } else { + return defaultValue + } + } + }, + async (get, set, newValue) => { + const oldValue = get(primitiveAtom) if (typeof newValue === 'function') { newValue = (newValue as (prev: Value) => Value)(oldValue) } if (newValue === oldValue) { return } + const kv = await get(kvAtom) if (newValue === RESET) { kv.delete(key) - set(valueAtom, defaultValue) + set(primitiveAtom, defaultValue) } else { kv.set(key, newValue) - set(valueAtom, newValue) + set(primitiveAtom, newValue) } } ) diff --git a/packages/core/test/preference.spec.ts b/packages/core/test/preference.spec.ts index 56241a4..5e02d35 100644 --- a/packages/core/test/preference.spec.ts +++ b/packages/core/test/preference.spec.ts @@ -1,11 +1,13 @@ import { test, beforeAll, expect, vi } from 'vitest' import { settingAtom } from '../src/store/preference' -import { Schema, Workspace } from '@blocksuite/store' +import { Schema } from '@blocksuite/store' import { AffineSchemas } from '@blocksuite/blocks/models' import { getDefaultStore } from 'jotai/vanilla' import type { Doc, Array as YArray } from 'yjs' import { YKeyValue } from 'y-utility/y-keyvalue' import { RESET } from 'jotai/utils' +import { workspaceManager } from '../src/store' +import { userInfoAtom } from '../src/store/api' const schema = new Schema() beforeAll(() => { @@ -13,21 +15,22 @@ beforeAll(() => { }) test('settingAtom', async () => { - const workspace = new Workspace({ - id: 'test-workspace', - schema: new Schema() - }) + const workspaceAtom = workspaceManager.getWorkspaceAtom('test-workspace') const testValueAtom = settingAtom( - workspace, - 'test-user', + workspaceAtom, 'test-key', 'test-value' ) const store = getDefaultStore() - expect(store.get(testValueAtom)).toBe('test-value') - store.set(testValueAtom, 'test-value-2') - expect(store.get(testValueAtom)).toBe('test-value-2') - store.set(testValueAtom, () => 'test-value-2') + store.set(userInfoAtom, { + username: 'testUser', + id: 'test-user' + }) + expect(await store.get(testValueAtom)).toBe('test-value') + await store.set(testValueAtom, 'test-value-2') + expect(await store.get(testValueAtom)).toBe('test-value-2') + await store.set(testValueAtom, () => 'test-value-2') + const workspace = await store.get(workspaceAtom) const settingDoc = workspace.doc.getMap('settings').get('test-user') as Doc const kv = new YKeyValue(settingDoc.getArray('setting') as YArray<{ key: string, @@ -37,35 +40,41 @@ test('settingAtom', async () => { const unsub = store.sub(testValueAtom, vi.fn()) kv.set('test-key', 'test-value-3') kv.delete('test-key') - expect(store.get(testValueAtom)).toBe('test-value') + await vi.waitFor(async () => { + expect(await store.get(testValueAtom)).toBe('test-value') + }) unsub() - store.set(testValueAtom, 'test-value-2') - expect(store.get(testValueAtom)).toBe('test-value-2') - store.set(testValueAtom, RESET) - expect(store.get(testValueAtom)).toBe('test-value') + await store.set(testValueAtom, 'test-value-2') + expect(await store.get(testValueAtom)).toBe('test-value-2') + await store.set(testValueAtom, RESET) + expect(await store.get(testValueAtom)).toBe('test-value') }) -test('settingAtom with defaultValue', () => { - const workspace = new Workspace({ - id: 'test-workspace', - schema: new Schema() +test('settingAtom with defaultValue', async () => { + const store = getDefaultStore() + const workspaceAtom = workspaceManager.getWorkspaceAtom('test-workspace') + store.set(userInfoAtom, { + username: 'testUser', + id: 'test-user' }) const testValueAtom = settingAtom( - workspace, - 'test-user', + workspaceAtom, 'test-key', 'test-value' ) + await store.get(testValueAtom) + const workspace = await store.get(workspaceAtom) const settingDoc = workspace.doc.getMap('settings').get('test-user') as Doc const kv = new YKeyValue(settingDoc.getArray('setting') as YArray<{ key: string, val: unknown }>) - const store = getDefaultStore() expect(kv.get('test-key')).toBe(undefined) - expect(store.get(testValueAtom)).toBe('test-value') + expect(await store.get(testValueAtom)).toBe('test-value') const unsub = store.sub(testValueAtom, vi.fn()) - expect(kv.get('test-key')).toBe(undefined) - expect(store.get(testValueAtom)).toBe('test-value') + await store.set(testValueAtom, 'test-value-2') + expect(kv.get('test-key')).toBe('test-value-2') + kv.delete('test-key') + expect(await store.get(testValueAtom)).toBe('test-value') unsub() }) diff --git a/packages/core/vite.config.ts b/packages/core/vite.config.ts index 3c6bfc7..811a93f 100644 --- a/packages/core/vite.config.ts +++ b/packages/core/vite.config.ts @@ -6,7 +6,7 @@ export default defineConfig({ build: { lib: { entry: { - app: './src/app.tsx', + components: './src/components.tsx', store: './src/store.ts', 'store/preference': './src/store/preference.ts', }, @@ -22,7 +22,8 @@ export default defineConfig({ /^jotai/, /^yjs/, /^jotai-effect/, - /^y-utility/ + /^y-utility/, + 'uuid' ] } }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2b0e98f..719a7ec 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -170,6 +170,9 @@ importers: '@refine/core': specifier: workspace:* version: link:../../packages/core + foxact: + specifier: ^0.2.22 + version: 0.2.22(react@18.3.0-canary-8039e6d0b-20231026) jotai: specifier: ^2.5.1 version: 2.5.1(@types/react@18.2.36)(react@18.3.0-canary-8039e6d0b-20231026) @@ -180,11 +183,11 @@ importers: specifier: workspace:* version: link:../../packages/jotai-inject next: - specifier: 14.0.0 - version: 14.0.0(react-dom@18.3.0-canary-8039e6d0b-20231026)(react@18.3.0-canary-8039e6d0b-20231026) + specifier: 14.0.1 + version: 14.0.1(react-dom@18.3.0-canary-8039e6d0b-20231026)(react@18.3.0-canary-8039e6d0b-20231026) next-themes: specifier: ^0.2.1 - version: 0.2.1(next@14.0.0)(react-dom@18.3.0-canary-8039e6d0b-20231026)(react@18.3.0-canary-8039e6d0b-20231026) + version: 0.2.1(next@14.0.1)(react-dom@18.3.0-canary-8039e6d0b-20231026)(react@18.3.0-canary-8039e6d0b-20231026) react: specifier: 18.3.0-canary-8039e6d0b-20231026 version: 18.3.0-canary-8039e6d0b-20231026 @@ -258,6 +261,9 @@ importers: react-dom: specifier: 18.3.0-canary-8039e6d0b-20231026 version: 18.3.0-canary-8039e6d0b-20231026(react@18.3.0-canary-8039e6d0b-20231026) + uuid: + specifier: ^9.0.1 + version: 9.0.1 y-utility: specifier: ^0.1.3 version: 0.1.3(yjs@13.6.8) @@ -271,6 +277,9 @@ importers: '@testing-library/user-event': specifier: ^14.5.1 version: 14.5.1(@testing-library/dom@9.3.3) + '@types/uuid': + specifier: ^9.0.6 + version: 9.0.6 '@vitejs/plugin-react': specifier: ^4.1.1 version: 4.1.1(vite@4.5.0) @@ -1409,6 +1418,10 @@ packages: resolution: {integrity: sha512-cIKhxkfVELB6hFjYsbtEeTus2mwrTC+JissfZYM0n+8Fv+g8ucUfOlm3VEDtwtwydZ0Nuauv3bl0qF82nnCAqA==} dev: false + /@next/env@14.0.1: + resolution: {integrity: sha512-Ms8ZswqY65/YfcjrlcIwMPD7Rg/dVjdLapMcSHG26W6O67EJDF435ShW4H4LXi1xKO1oRc97tLXUpx8jpLe86A==} + dev: false + /@next/eslint-plugin-next@14.0.1: resolution: {integrity: sha512-bLjJMwXdzvhnQOnxvHoTTUh/+PYk6FF/DCgHi4BXwXCINer+o1ZYfL9aVeezj/oI7wqGJOqwGIXrlBvPbAId3w==} dependencies: @@ -1424,6 +1437,15 @@ packages: dev: false optional: true + /@next/swc-darwin-arm64@14.0.1: + resolution: {integrity: sha512-JyxnGCS4qT67hdOKQ0CkgFTp+PXub5W1wsGvIq98TNbF3YEIN7iDekYhYsZzc8Ov0pWEsghQt+tANdidITCLaw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + /@next/swc-darwin-x64@14.0.0: resolution: {integrity: sha512-4YyQLMSaCgX/kgC1jjF3s3xSoBnwHuDhnF6WA1DWNEYRsbOOPWjcYhv8TKhRe2ApdOam+VfQSffC4ZD+X4u1Cg==} engines: {node: '>= 10'} @@ -1433,6 +1455,15 @@ packages: dev: false optional: true + /@next/swc-darwin-x64@14.0.1: + resolution: {integrity: sha512-625Z7bb5AyIzswF9hvfZWa+HTwFZw+Jn3lOBNZB87lUS0iuCYDHqk3ujuHCkiyPtSC0xFBtYDLcrZ11mF/ap3w==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: false + optional: true + /@next/swc-linux-arm64-gnu@14.0.0: resolution: {integrity: sha512-io7fMkJ28Glj7SH8yvnlD6naIhRDnDxeE55CmpQkj3+uaA2Hko6WGY2pT5SzpQLTnGGnviK85cy8EJ2qsETj/g==} engines: {node: '>= 10'} @@ -1442,6 +1473,15 @@ packages: dev: false optional: true + /@next/swc-linux-arm64-gnu@14.0.1: + resolution: {integrity: sha512-iVpn3KG3DprFXzVHM09kvb//4CNNXBQ9NB/pTm8LO+vnnnaObnzFdS5KM+w1okwa32xH0g8EvZIhoB3fI3mS1g==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + /@next/swc-linux-arm64-musl@14.0.0: resolution: {integrity: sha512-nC2h0l1Jt8LEzyQeSs/BKpXAMe0mnHIMykYALWaeddTqCv5UEN8nGO3BG8JAqW/Y8iutqJsaMe2A9itS0d/r8w==} engines: {node: '>= 10'} @@ -1451,6 +1491,15 @@ packages: dev: false optional: true + /@next/swc-linux-arm64-musl@14.0.1: + resolution: {integrity: sha512-mVsGyMxTLWZXyD5sen6kGOTYVOO67lZjLApIj/JsTEEohDDt1im2nkspzfV5MvhfS7diDw6Rp/xvAQaWZTv1Ww==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: false + optional: true + /@next/swc-linux-x64-gnu@14.0.0: resolution: {integrity: sha512-Wf+WjXibJQ7hHXOdNOmSMW5bxeJHVf46Pwb3eLSD2L76NrytQlif9NH7JpHuFlYKCQGfKfgSYYre5rIfmnSwQw==} engines: {node: '>= 10'} @@ -1460,6 +1509,15 @@ packages: dev: false optional: true + /@next/swc-linux-x64-gnu@14.0.1: + resolution: {integrity: sha512-wMqf90uDWN001NqCM/auRl3+qVVeKfjJdT9XW+RMIOf+rhUzadmYJu++tp2y+hUbb6GTRhT+VjQzcgg/QTD9NQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + /@next/swc-linux-x64-musl@14.0.0: resolution: {integrity: sha512-WTZb2G7B+CTsdigcJVkRxfcAIQj7Lf0ipPNRJ3vlSadU8f0CFGv/ST+sJwF5eSwIe6dxKoX0DG6OljDBaad+rg==} engines: {node: '>= 10'} @@ -1469,6 +1527,15 @@ packages: dev: false optional: true + /@next/swc-linux-x64-musl@14.0.1: + resolution: {integrity: sha512-ol1X1e24w4j4QwdeNjfX0f+Nza25n+ymY0T2frTyalVczUmzkVD7QGgPTZMHfR1aLrO69hBs0G3QBYaj22J5GQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: false + optional: true + /@next/swc-win32-arm64-msvc@14.0.0: resolution: {integrity: sha512-7R8/x6oQODmNpnWVW00rlWX90sIlwluJwcvMT6GXNIBOvEf01t3fBg0AGURNKdTJg2xNuP7TyLchCL7Lh2DTiw==} engines: {node: '>= 10'} @@ -1478,6 +1545,15 @@ packages: dev: false optional: true + /@next/swc-win32-arm64-msvc@14.0.1: + resolution: {integrity: sha512-WEmTEeWs6yRUEnUlahTgvZteh5RJc4sEjCQIodJlZZ5/VJwVP8p2L7l6VhzQhT4h7KvLx/Ed4UViBdne6zpIsw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: false + optional: true + /@next/swc-win32-ia32-msvc@14.0.0: resolution: {integrity: sha512-RLK1nELvhCnxaWPF07jGU4x3tjbyx2319q43loZELqF0+iJtKutZ+Lk8SVmf/KiJkYBc7Cragadz7hb3uQvz4g==} engines: {node: '>= 10'} @@ -1487,6 +1563,15 @@ packages: dev: false optional: true + /@next/swc-win32-ia32-msvc@14.0.1: + resolution: {integrity: sha512-oFpHphN4ygAgZUKjzga7SoH2VGbEJXZa/KL8bHCAwCjDWle6R1SpiGOdUdA8EJ9YsG1TYWpzY6FTbUA+iAJeww==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: false + optional: true + /@next/swc-win32-x64-msvc@14.0.0: resolution: {integrity: sha512-g6hLf1SUko+hnnaywQQZzzb3BRecQsoKkF3o/C+F+dOA4w/noVAJngUVkfwF0+2/8FzNznM7ofM6TGZO9svn7w==} engines: {node: '>= 10'} @@ -1496,6 +1581,15 @@ packages: dev: false optional: true + /@next/swc-win32-x64-msvc@14.0.1: + resolution: {integrity: sha512-FFp3nOJ/5qSpeWT0BZQ+YE1pSMk4IMpkME/1DwKBwhg4mJLB9L+6EXuJi4JEwaJdl5iN+UUlmUD3IsR1kx5fAg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: false + optional: true + /@nodelib/fs.scandir@2.1.5: resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -2051,6 +2145,10 @@ packages: /@types/unist@3.0.1: resolution: {integrity: sha512-ue/hDUpPjC85m+PM9OQDMZr3LywT+CT6mPsQq8OJtCLiERkGRcQUFvu9XASF5XWqyZFXbf15lvb3JFJ4dRLWPg==} + /@types/uuid@9.0.6: + resolution: {integrity: sha512-BT2Krtx4xaO6iwzwMFUYvWBWkV2pr37zD68Vmp1CDV196MzczBRxuEpD6Pr395HAgebC/co7hOphs53r8V7jew==} + dev: true + /@types/verror@1.10.8: resolution: {integrity: sha512-YhUhnxRYs/NiVUbIs3F/EzviDP/NZCEAE2Mx5DUqLdldUmphOhFCVh7Kc+7zlYEExM0P8dzfbJi0yRlNb2Bw5g==} requiresBuild: true @@ -4997,6 +5095,19 @@ packages: engines: {node: '>= 0.6'} dev: false + /foxact@0.2.22(react@18.3.0-canary-8039e6d0b-20231026): + resolution: {integrity: sha512-RQd5+XSqWbRNYc4Un5eS6+P6oFrZxqOIb7xVw71SXWNnRlHoFqSuGDOt3bShrvQPmBXSw60We+tX5Gf6naxxGA==} + peerDependencies: + react: '*' + peerDependenciesMeta: + react: + optional: true + dependencies: + client-only: 0.0.1 + react: 18.3.0-canary-8039e6d0b-20231026 + server-only: 0.0.1 + dev: false + /fraction.js@4.3.7: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} dev: true @@ -5256,7 +5367,7 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} dependencies: dir-glob: 3.0.1 - fast-glob: 3.3.1 + fast-glob: 3.3.2 ignore: 5.2.4 merge2: 1.4.1 slash: 4.0.0 @@ -7538,6 +7649,18 @@ packages: react-dom: 18.3.0-canary-8039e6d0b-20231026(react@18.3.0-canary-8039e6d0b-20231026) dev: false + /next-themes@0.2.1(next@14.0.1)(react-dom@18.3.0-canary-8039e6d0b-20231026)(react@18.3.0-canary-8039e6d0b-20231026): + resolution: {integrity: sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==} + peerDependencies: + next: '*' + react: '*' + react-dom: '*' + dependencies: + next: 14.0.1(react-dom@18.3.0-canary-8039e6d0b-20231026)(react@18.3.0-canary-8039e6d0b-20231026) + react: 18.3.0-canary-8039e6d0b-20231026 + react-dom: 18.3.0-canary-8039e6d0b-20231026(react@18.3.0-canary-8039e6d0b-20231026) + dev: false + /next@14.0.0(react-dom@18.3.0-canary-8039e6d0b-20231026)(react@18.3.0-canary-8039e6d0b-20231026): resolution: {integrity: sha512-J0jHKBJpB9zd4+c153sair0sz44mbaCHxggs8ryVXSFBuBqJ8XdE9/ozoV85xGh2VnSjahwntBZZgsihL9QznA==} engines: {node: '>=18.17.0'} @@ -7577,6 +7700,45 @@ packages: - babel-plugin-macros dev: false + /next@14.0.1(react-dom@18.3.0-canary-8039e6d0b-20231026)(react@18.3.0-canary-8039e6d0b-20231026): + resolution: {integrity: sha512-s4YaLpE4b0gmb3ggtmpmV+wt+lPRuGtANzojMQ2+gmBpgX9w5fTbjsy6dXByBuENsdCX5pukZH/GxdFgO62+pA==} + engines: {node: '>=18.17.0'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + react: ^18.2.0 + react-dom: ^18.2.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + sass: + optional: true + dependencies: + '@next/env': 14.0.1 + '@swc/helpers': 0.5.2 + busboy: 1.6.0 + caniuse-lite: 1.0.30001547 + postcss: 8.4.31 + react: 18.3.0-canary-8039e6d0b-20231026 + react-dom: 18.3.0-canary-8039e6d0b-20231026(react@18.3.0-canary-8039e6d0b-20231026) + styled-jsx: 5.1.1(react@18.3.0-canary-8039e6d0b-20231026) + watchpack: 2.4.0 + optionalDependencies: + '@next/swc-darwin-arm64': 14.0.1 + '@next/swc-darwin-x64': 14.0.1 + '@next/swc-linux-arm64-gnu': 14.0.1 + '@next/swc-linux-arm64-musl': 14.0.1 + '@next/swc-linux-x64-gnu': 14.0.1 + '@next/swc-linux-x64-musl': 14.0.1 + '@next/swc-win32-arm64-msvc': 14.0.1 + '@next/swc-win32-ia32-msvc': 14.0.1 + '@next/swc-win32-x64-msvc': 14.0.1 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + dev: false + /nextra-theme-blog@2.13.2(next@14.0.0)(nextra@2.13.2)(react-dom@18.3.0-canary-8039e6d0b-20231026)(react@18.3.0-canary-8039e6d0b-20231026): resolution: {integrity: sha512-/g2Y/e0PkyXhDo0wuybb9+9hKQRLATWzFvKBzwcLq7XwOYT5njn/bHY/kx5zzkVyNkLnH5maz/EXOTlfG1b27Q==} peerDependencies: @@ -8702,6 +8864,10 @@ packages: - supports-color dev: false + /server-only@0.0.1: + resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==} + dev: false + /set-function-length@1.1.1: resolution: {integrity: sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==} engines: {node: '>= 0.4'} @@ -9182,7 +9348,7 @@ packages: chokidar: 3.5.3 didyoumean: 1.2.2 dlv: 1.1.3 - fast-glob: 3.3.1 + fast-glob: 3.3.2 glob-parent: 6.0.2 is-glob: 4.0.3 jiti: 1.20.0 From 68ecfce84eb236a9c9399016486c6bc6ad45f380 Mon Sep 17 00:00:00 2001 From: Alex Yang Date: Mon, 6 Nov 2023 23:57:06 -0600 Subject: [PATCH 02/32] test: add test case --- .../core/src/components/page-list/index.tsx | 3 +- packages/core/test/page-list.spec.tsx | 40 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 packages/core/test/page-list.spec.tsx diff --git a/packages/core/src/components/page-list/index.tsx b/packages/core/src/components/page-list/index.tsx index 880c311..d510609 100644 --- a/packages/core/src/components/page-list/index.tsx +++ b/packages/core/src/components/page-list/index.tsx @@ -20,6 +20,7 @@ export const PageList = ( <>