diff --git a/apps/web/package.json b/apps/web/package.json index 58ceb588e3..69691697d5 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -44,6 +44,7 @@ "devDependencies": { "@perfsee/webpack": "^1.5.0", "@redux-devtools/extension": "^3.2.5", + "@rich-data/viewer": "^2.2.4", "@swc-jotai/debug-label": "^0.0.9", "@swc-jotai/react-refresh": "^0.0.7", "@types/react": "^18.0.28", diff --git a/apps/web/src/components/pure/message-center/index.tsx b/apps/web/src/components/pure/message-center/index.tsx index 422a88f2eb..ea731f8d5c 100644 --- a/apps/web/src/components/pure/message-center/index.tsx +++ b/apps/web/src/components/pure/message-center/index.tsx @@ -4,7 +4,7 @@ import { messages } from '@affine/datacenter'; import type React from 'react'; import { memo, useEffect, useState } from 'react'; -import { useOnGoogleLogout } from '../../../hooks/use-on-google-logout'; +import { useAffineLogOut } from '../../../hooks/affine/use-affine-log-out'; import { apis } from '../../../shared/apis'; declare global { @@ -17,7 +17,7 @@ declare global { export const MessageCenter: React.FC = memo(function MessageCenter() { const [popup, setPopup] = useState(false); - const onLogout = useOnGoogleLogout(); + const onLogout = useAffineLogOut(); useEffect(() => { const listener = ( event: CustomEvent<{ diff --git a/apps/web/src/hooks/affine/use-affine-log-in.ts b/apps/web/src/hooks/affine/use-affine-log-in.ts new file mode 100644 index 0000000000..3397c3e4ab --- /dev/null +++ b/apps/web/src/hooks/affine/use-affine-log-in.ts @@ -0,0 +1,26 @@ +import { toast } from '@affine/component'; +import { + createAffineAuth, + setLoginStorage, + SignMethod, +} from '@affine/workspace/affine/login'; +import { useRouter } from 'next/router'; +import { useCallback } from 'react'; + +import { apis } from '../../shared/apis'; + +export const affineAuth = createAffineAuth(); + +export function useAffineLogIn() { + const router = useRouter(); + return useCallback(async () => { + const response = await affineAuth.generateToken(SignMethod.Google); + if (response) { + setLoginStorage(response); + apis.auth.setLogin(response); + router.reload(); + } else { + toast('Login failed'); + } + }, [router]); +} diff --git a/apps/web/src/hooks/use-on-google-logout.ts b/apps/web/src/hooks/affine/use-affine-log-out.ts similarity index 66% rename from apps/web/src/hooks/use-on-google-logout.ts rename to apps/web/src/hooks/affine/use-affine-log-out.ts index 4bee098a84..6af933c022 100644 --- a/apps/web/src/hooks/use-on-google-logout.ts +++ b/apps/web/src/hooks/affine/use-affine-log-out.ts @@ -1,13 +1,14 @@ +import { clearLoginStorage } from '@affine/workspace/affine/login'; import { WorkspaceFlavour } from '@affine/workspace/type'; import { useSetAtom } from 'jotai'; import { useRouter } from 'next/router'; import { useCallback } from 'react'; -import { jotaiWorkspacesAtom } from '../atoms'; -import { WorkspacePlugins } from '../plugins'; -import { apis } from '../shared/apis'; +import { jotaiWorkspacesAtom } from '../../atoms'; +import { WorkspacePlugins } from '../../plugins'; +import { apis } from '../../shared/apis'; -export function useOnGoogleLogout() { +export function useAffineLogOut() { const set = useSetAtom(jotaiWorkspacesAtom); const router = useRouter(); return useCallback(() => { @@ -18,6 +19,7 @@ export function useOnGoogleLogout() { ) ); WorkspacePlugins[WorkspaceFlavour.AFFINE].cleanup?.(); + clearLoginStorage(); router.reload(); }, [router, set]); } diff --git a/apps/web/src/hooks/affine/use-affine-refresh-auth-token.ts b/apps/web/src/hooks/affine/use-affine-refresh-auth-token.ts new file mode 100644 index 0000000000..5796db4dfb --- /dev/null +++ b/apps/web/src/hooks/affine/use-affine-refresh-auth-token.ts @@ -0,0 +1,36 @@ +import { DebugLogger } from '@affine/debug'; +import { + getLoginStorage, + isExpired, + parseIdToken, + setLoginStorage, +} from '@affine/workspace/affine/login'; +import useSWR from 'swr'; + +import { apis } from '../../shared/apis'; +import { affineAuth } from './use-affine-log-in'; + +const logger = new DebugLogger('auth-token'); + +const revalidate = async () => { + const storage = getLoginStorage(); + if (storage) { + const tokenMessage = parseIdToken(storage.token); + logger.debug('revalidate affine user'); + if (isExpired(tokenMessage)) { + logger.debug('need to refresh token'); + const response = await affineAuth.refreshToken(storage); + if (response) { + setLoginStorage(response); + apis.auth.setLogin(response); + } + } + } + return true; +}; + +export function useAffineRefreshAuthToken() { + useSWR('autoRefreshToken', { + fetcher: revalidate, + }); +} diff --git a/apps/web/src/layouts/index.tsx b/apps/web/src/layouts/index.tsx index 5526c6bb7b..499b7f8fe7 100644 --- a/apps/web/src/layouts/index.tsx +++ b/apps/web/src/layouts/index.tsx @@ -19,6 +19,7 @@ import { import { HelpIsland } from '../components/pure/help-island'; import { PageLoading } from '../components/pure/loading'; import WorkSpaceSliderBar from '../components/pure/workspace-slider-bar'; +import { useAffineRefreshAuthToken } from '../hooks/affine/use-affine-refresh-auth-token'; import { useCurrentPageId } from '../hooks/current/use-current-page-id'; import { useCurrentWorkspace } from '../hooks/current/use-current-workspace'; import { useBlockSuiteWorkspaceHelper } from '../hooks/use-blocksuite-workspace-helper'; @@ -92,6 +93,11 @@ export const WorkspaceLayout: React.FC = ); }; +function AffineWorkspaceEffect() { + useAffineRefreshAuthToken(); + return null; +} + export const WorkspaceLayoutInner: React.FC = ({ children, }) => { @@ -196,6 +202,7 @@ export const WorkspaceLayoutInner: React.FC = ({ paths={isPublicWorkspace ? publicPathGenerator : pathGenerator} /> + {children} {/* fixme(himself65): remove this */} diff --git a/apps/web/src/pages/_app.tsx b/apps/web/src/pages/_app.tsx index 5a2e759299..9eb998ba3d 100644 --- a/apps/web/src/pages/_app.tsx +++ b/apps/web/src/pages/_app.tsx @@ -67,39 +67,6 @@ const App = function App({ > AFFiNE - - - - - - - - - - - - {getLayout()} diff --git a/apps/web/src/pages/_debug/login.dev.tsx b/apps/web/src/pages/_debug/login.dev.tsx index 467744a6a5..a756a2191c 100644 --- a/apps/web/src/pages/_debug/login.dev.tsx +++ b/apps/web/src/pages/_debug/login.dev.tsx @@ -11,8 +11,16 @@ import { } from '@affine/workspace/affine/login'; import { useAtom } from 'jotai'; import type { NextPage } from 'next'; +import dynamic from 'next/dynamic'; import { useMemo } from 'react'; +const Viewer = dynamic( + () => import('@rich-data/viewer').then(m => ({ default: m.JsonViewer })), + { ssr: false } +); + +import { useTheme } from 'next-themes'; + import { StyledPage, StyledWrapper } from '../../layouts/styles'; const LoginDevPage: NextPage = () => { @@ -69,7 +77,24 @@ const LoginDevPage: NextPage = () => { > Reset Storage - {user && JSON.stringify(user)} + + ); diff --git a/apps/web/src/pages/_document.tsx b/apps/web/src/pages/_document.tsx index f8f8d45191..d97b1c60e1 100644 --- a/apps/web/src/pages/_document.tsx +++ b/apps/web/src/pages/_document.tsx @@ -51,6 +51,30 @@ export default class AppDocument extends Document<{ /> + + + + + + + + + + + + {this.props.emotionStyleTags} diff --git a/apps/web/src/providers/ModalProvider.tsx b/apps/web/src/providers/ModalProvider.tsx index 7e61e4de69..bf90e7be8d 100644 --- a/apps/web/src/providers/ModalProvider.tsx +++ b/apps/web/src/providers/ModalProvider.tsx @@ -9,13 +9,13 @@ import { openCreateWorkspaceModalAtom, openWorkspacesModalAtom, } from '../atoms'; +import { useAffineLogIn } from '../hooks/affine/use-affine-log-in'; +import { useAffineLogOut } from '../hooks/affine/use-affine-log-out'; import { useCurrentUser } from '../hooks/current/use-current-user'; import { useCurrentWorkspace } from '../hooks/current/use-current-workspace'; -import { useOnGoogleLogout } from '../hooks/use-on-google-logout'; import { useRouterHelper } from '../hooks/use-router-helper'; import { useWorkspaces, useWorkspacesHelper } from '../hooks/use-workspaces'; import { WorkspaceSubPath } from '../shared'; -import { apis } from '../shared/apis'; const WorkspaceListModal = dynamic( async () => @@ -69,12 +69,8 @@ export function Modals() { }, [jumpToSubPath, setCurrentWorkspace, setOpenWorkspacesModal] )} - onClickLogin={useCallback(() => { - apis.signInWithGoogle().then(() => { - router.reload(); - }); - }, [router])} - onClickLogout={useOnGoogleLogout()} + onClickLogin={useAffineLogIn()} + onClickLogout={useAffineLogOut()} onCreateWorkspace={useCallback(() => { setOpenCreateWorkspaceModal(true); }, [setOpenCreateWorkspaceModal])} diff --git a/apps/web/src/shared/apis.ts b/apps/web/src/shared/apis.ts index a55f02b1ec..28b106739f 100644 --- a/apps/web/src/shared/apis.ts +++ b/apps/web/src/shared/apis.ts @@ -5,6 +5,10 @@ import { GoogleAuth, } from '@affine/datacenter'; import { config } from '@affine/env'; +import { + createUserApis, + createWorkspaceApis, +} from '@affine/workspace/affine/api'; import { isValidIPAddress } from '../utils/is-valid-ip-address'; @@ -27,6 +31,22 @@ if (typeof window === 'undefined') { params.get('prefixUrl') && (prefixUrl = params.get('prefixUrl') as string); } +declare global { + // eslint-disable-next-line no-var + var affineApis: + | undefined + | (ReturnType & + ReturnType); +} + +const affineApis = {} as ReturnType & + ReturnType; +Object.assign(affineApis, createUserApis(prefixUrl)); +Object.assign(affineApis, createWorkspaceApis(prefixUrl)); +if (!globalThis.affineApis) { + globalThis.affineApis = affineApis; +} + const bareAuth = createBareClient(prefixUrl); const googleAuth = new GoogleAuth(bareAuth); export const clientAuth = createAuthClient(bareAuth, googleAuth); diff --git a/package.json b/package.json index 1fc8d69644..aa0bf80867 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "license": "MPL-2.0", "workspaces": [ "apps/*", - "packages/*" + "packages/*", + "tests/fixtures" ], "scripts": { "dev": "dev-web", diff --git a/packages/data-center/package.json b/packages/data-center/package.json index bb3d6abaec..a464015786 100644 --- a/packages/data-center/package.json +++ b/packages/data-center/package.json @@ -15,6 +15,7 @@ "dependencies": { "@affine/debug": "workspace:*", "@blocksuite/blocks": "0.5.0-20230323085636-3110abb", + "@blocksuite/global": "0.5.0-20230323085636-3110abb", "@blocksuite/store": "0.5.0-20230323085636-3110abb", "@tauri-apps/api": "^1.2.0", "encoding": "^0.1.13", diff --git a/packages/workspace/package.json b/packages/workspace/package.json index f917d73417..78482d12b9 100644 --- a/packages/workspace/package.json +++ b/packages/workspace/package.json @@ -4,9 +4,11 @@ "exports": { "./utils": "./src/utils.ts", "./type": "./src/type.ts", - "./affine/*": "./src/affine/*.ts" + "./affine/*": "./src/affine/*.ts", + "./affine/api": "./src/affine/api/index.ts" }, "dependencies": { + "@affine-test/fixtures": "workspace:^", "@affine/component": "workspace:*", "@affine/debug": "workspace:*", "@affine/env": "workspace:*", @@ -17,6 +19,7 @@ "js-base64": "^3.7.5", "ky": "^0.33.3", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "zod": "^3.21.4" } } diff --git a/packages/workspace/src/affine/__tests__/api.spec.ts b/packages/workspace/src/affine/__tests__/api.spec.ts new file mode 100644 index 0000000000..5599d8d780 --- /dev/null +++ b/packages/workspace/src/affine/__tests__/api.spec.ts @@ -0,0 +1,86 @@ +/** + * @vitest-environment happy-dom + */ +import 'fake-indexeddb/auto'; + +import userA from '@affine-test/fixtures/userA.json'; +import { Workspace } from '@blocksuite/store'; +import { beforeAll, beforeEach, describe, expect, test } from 'vitest'; + +import { createWorkspaceApis, createWorkspaceResponseSchema } from '../api'; +import { loginResponseSchema, setLoginStorage } from '../login'; + +let workspaceApis: ReturnType; + +beforeAll(() => { + workspaceApis = createWorkspaceApis('http://localhost:3000/'); +}); + +beforeEach(async () => { + let data; + // first step: try to log in + const response = await fetch('http://localhost:3000/api/user/token', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + type: 'DebugLoginUser', + email: userA.email, + password: userA.password, + }), + }); + if (!response.ok) { + data = await fetch('http://localhost:3000/api/user/token', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + type: 'DebugCreateUser', + ...userA, + }), + }).then(r => r.json()); + setLoginStorage(data); + } else { + setLoginStorage((data = await response.json())); + } + loginResponseSchema.parse(data); +}); + +describe('api', () => { + test( + 'create workspace', + async () => { + const workspace = new Workspace({ + id: 'test', + }); + const binary = Workspace.Y.encodeStateAsUpdate(workspace.doc); + const data = await workspaceApis.createWorkspace(new Blob([binary])); + createWorkspaceResponseSchema.parse(data); + }, + { + timeout: 30000, + } + ); + + test( + 'delete workspace', + async () => { + const workspace = new Workspace({ + id: 'test', + }); + const binary = Workspace.Y.encodeStateAsUpdate(workspace.doc); + const data = await workspaceApis.createWorkspace(new Blob([binary])); + createWorkspaceResponseSchema.parse(data); + const id = data.id; + const response = await workspaceApis.deleteWorkspace({ + id, + }); + expect(response).toBe(true); + }, + { + timeout: 30000, + } + ); +}); diff --git a/packages/workspace/src/affine/api/index.ts b/packages/workspace/src/affine/api/index.ts new file mode 100644 index 0000000000..12d9bafac2 --- /dev/null +++ b/packages/workspace/src/affine/api/index.ts @@ -0,0 +1,293 @@ +import { assertExists } from '@blocksuite/global/utils'; +import { z } from 'zod'; + +import { getLoginStorage } from '../login'; + +export interface User { + id: string; + name: string; + email: string; + avatar_url: string; + create_at: string; +} + +export interface GetUserByEmailParams { + email: string; + workspace_id: string; +} + +export function createUserApis(prefixUrl = '/') { + return { + getUserByEmail: async ( + params: GetUserByEmailParams + ): Promise => { + const auth = getLoginStorage(); + assertExists(auth); + const target = new URL(prefixUrl + 'api/user', window.location.origin); + target.searchParams.append('email', params.email); + target.searchParams.append('workspace_id', params.workspace_id); + return fetch(target, { + method: 'GET', + headers: { + Authorization: auth.token, + }, + }).then(r => r.json()); + }, + } as const; +} + +export interface GetWorkspaceDetailParams { + id: string; +} + +export enum WorkspaceType { + Private = 0, + Normal = 1, +} + +export enum PermissionType { + Read = 0, + Write = 1, + Admin = 10, + Owner = 99, +} + +export interface Workspace { + id: string; + type: WorkspaceType; + public: boolean; + permission: PermissionType; +} + +export interface WorkspaceDetail extends Workspace { + owner: User; + member_count: number; +} + +export interface Permission { + id: string; + type: PermissionType; + workspace_id: string; + user_id: string; + user_email: string; + accepted: boolean; + create_at: number; +} + +export interface RegisteredUser extends User { + type: 'Registered'; +} + +export interface UnregisteredUser { + type: 'Unregistered'; + email: string; +} + +export interface Member extends Permission { + user: RegisteredUser | UnregisteredUser; +} + +export interface GetWorkspaceMembersParams { + id: string; +} + +export interface CreateWorkspaceParams { + name: string; +} + +export interface UpdateWorkspaceParams { + id: string; + public: boolean; +} + +export interface DeleteWorkspaceParams { + id: string; +} + +export interface InviteMemberParams { + id: string; + email: string; +} + +export interface RemoveMemberParams { + permissionId: number; +} + +export interface AcceptInvitingParams { + invitingCode: string; +} + +export interface LeaveWorkspaceParams { + id: number | string; +} + +export const createWorkspaceResponseSchema = z.object({ + id: z.string(), + public: z.boolean(), + type: z.nativeEnum(WorkspaceType), + created_at: z.number(), +}); + +export function createWorkspaceApis(prefixUrl = '/') { + return { + getWorkspaces: async (): Promise => { + const auth = getLoginStorage(); + assertExists(auth); + return fetch(prefixUrl + 'api/workspace', { + method: 'GET', + headers: { + Authorization: auth.token, + 'Cache-Control': 'no-cache', + }, + }).then(r => r.json()); + }, + getWorkspaceDetail: async ( + params: GetWorkspaceDetailParams + ): Promise => { + const auth = getLoginStorage(); + assertExists(auth); + return fetch(prefixUrl + `api/workspace/${params.id}`, { + method: 'GET', + headers: { + Authorization: auth.token, + }, + }).then(r => r.json()); + }, + getWorkspaceMembers: async ( + params: GetWorkspaceDetailParams + ): Promise => { + const auth = getLoginStorage(); + assertExists(auth); + return fetch(prefixUrl + `api/workspace/${params.id}/permission`, { + method: 'GET', + headers: { + Authorization: auth.token, + }, + }).then(r => r.json()); + }, + createWorkspace: async (encodedYDoc: Blob): Promise<{ id: string }> => { + const auth = getLoginStorage(); + assertExists(auth); + return fetch(prefixUrl + 'api/workspace', { + method: 'POST', + body: encodedYDoc, + headers: { + Authorization: auth.token, + }, + }).then(r => r.json()); + }, + updateWorkspace: async ( + params: UpdateWorkspaceParams + ): Promise<{ public: boolean | null }> => { + const auth = getLoginStorage(); + assertExists(auth); + return fetch(prefixUrl + `api/workspace/${params.id}`, { + method: 'POST', + body: JSON.stringify({ + public: params.public, + }), + headers: { + 'Content-Type': 'application/json', + Authorization: auth.token, + }, + }).then(r => r.json()); + }, + deleteWorkspace: async ( + params: DeleteWorkspaceParams + ): Promise => { + const auth = getLoginStorage(); + assertExists(auth); + return fetch(prefixUrl + `api/workspace/${params.id}`, { + method: 'DELETE', + headers: { + Authorization: auth.token, + }, + }).then(r => r.ok); + }, + + /** + * Notice: Only support normal(contrast to private) workspace. + */ + inviteMember: async (params: InviteMemberParams): Promise => { + const auth = getLoginStorage(); + assertExists(auth); + return fetch(prefixUrl + `api/workspace/${params.id}/permission`, { + method: 'POST', + body: JSON.stringify({ + email: params.email, + }), + headers: { + 'Content-Type': 'application/json', + Authorization: auth.token, + }, + }).then(r => r.json()); + }, + removeMember: async (params: RemoveMemberParams): Promise => { + const auth = getLoginStorage(); + assertExists(auth); + return fetch(prefixUrl + `api/permission/${params.permissionId}`, { + method: 'DELETE', + headers: { + Authorization: auth.token, + }, + }).then(r => r.json()); + }, + acceptInviting: async ( + params: AcceptInvitingParams + ): Promise => { + return fetch(prefixUrl + `api/invitation/${params.invitingCode}`, { + method: 'POST', + }).then(r => r.json()); + }, + uploadBlob: async (params: { blob: Blob }): Promise => { + const auth = getLoginStorage(); + assertExists(auth); + return fetch(prefixUrl + 'api/blob', { + method: 'PUT', + body: params.blob, + headers: { + Authorization: auth.token, + }, + }).then(r => r.text()); + }, + getBlob: async (params: { blobId: string }): Promise => { + const auth = getLoginStorage(); + assertExists(auth); + return fetch(prefixUrl + `api/blob/${params.blobId}`, { + method: 'GET', + headers: { + Authorization: auth.token, + }, + }).then(r => r.arrayBuffer()); + }, + leaveWorkspace: async ({ id }: LeaveWorkspaceParams) => { + const auth = getLoginStorage(); + assertExists(auth); + return fetch(prefixUrl + `api/workspace/${id}/permission`, { + method: 'DELETE', + headers: { + Authorization: auth.token, + }, + }).then(r => r.json()); + }, + downloadWorkspace: async ( + workspaceId: string, + published = false + ): Promise => { + if (published) { + return fetch(prefixUrl + `api/public/doc/${workspaceId}`, { + method: 'GET', + }).then(r => r.arrayBuffer()); + } else { + const auth = getLoginStorage(); + assertExists(auth); + return fetch(prefixUrl + `api/workspace/${workspaceId}/doc`, { + method: 'GET', + headers: { + Authorization: auth.token, + }, + }).then(r => r.arrayBuffer()); + } + }, + } as const; +} diff --git a/packages/workspace/src/affine/atom.ts b/packages/workspace/src/affine/atom.ts index 87dcb1bfce..7ccf20bdb6 100644 --- a/packages/workspace/src/affine/atom.ts +++ b/packages/workspace/src/affine/atom.ts @@ -1,4 +1,7 @@ import type { AccessTokenMessage } from '@affine/workspace/affine/login'; -import { atom } from 'jotai'; +import { atomWithStorage } from 'jotai/utils'; -export const currentAffineUserAtom = atom(null); +export const currentAffineUserAtom = atomWithStorage( + 'affine-user-atom', + null +); diff --git a/packages/workspace/src/affine/login.ts b/packages/workspace/src/affine/login.ts index 0a557b98d8..f2b14fa125 100644 --- a/packages/workspace/src/affine/login.ts +++ b/packages/workspace/src/affine/login.ts @@ -10,6 +10,7 @@ import { signInWithPopup, } from 'firebase/auth'; import { decode } from 'js-base64'; +import { z } from 'zod'; // Connect emulators based on env vars const envConnectEmulators = process.env.REACT_APP_FIREBASE_EMULATORS === 'true'; @@ -27,12 +28,12 @@ export type LoginParams = { token: string; }; -export type LoginResponse = { - // access token, expires in a very short time - token: string; - // Refresh token - refresh: string; -}; +export const loginResponseSchema = z.object({ + token: z.string(), + refresh: z.string(), +}); + +export type LoginResponse = z.infer; const logger = new DebugLogger('token'); diff --git a/tests/fixtures/package.json b/tests/fixtures/package.json new file mode 100644 index 0000000000..e7c9cfd29d --- /dev/null +++ b/tests/fixtures/package.json @@ -0,0 +1,6 @@ +{ + "name": "@affine-test/fixtures", + "exports": { + "./*": "./*" + } +} diff --git a/tests/fixtures/tsconfig.json b/tests/fixtures/tsconfig.json new file mode 100644 index 0000000000..7513033f03 --- /dev/null +++ b/tests/fixtures/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.json", + "include": ["./src"] +} diff --git a/tsconfig.json b/tsconfig.json index 7e21aab389..8017eb0649 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -28,7 +28,8 @@ "@affine/env": ["./packages/env"], "@affine/env/*": ["./packages/env/src/*"], "@affine/utils": ["./packages/utils"], - "@affine/workspace/*": ["./packages/workspace/src/*"] + "@affine/workspace/*": ["./packages/workspace/src/*"], + "@affine-test/fixtures/*": ["./tests/fixtures/*"] } }, "references": [ @@ -58,6 +59,9 @@ }, { "path": "./packages/workspace" + }, + { + "path": "./tests/fixtures" } ], "files": [], diff --git a/yarn.lock b/yarn.lock index bcf37828ad..fee22dbf60 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12,6 +12,12 @@ __metadata: languageName: node linkType: hard +"@affine-test/fixtures@workspace:^, @affine-test/fixtures@workspace:tests/fixtures": + version: 0.0.0-use.local + resolution: "@affine-test/fixtures@workspace:tests/fixtures" + languageName: unknown + linkType: soft + "@affine/app@workspace:apps/web": version: 0.0.0-use.local resolution: "@affine/app@workspace:apps/web" @@ -34,6 +40,7 @@ __metadata: "@mui/material": ^5.11.13 "@perfsee/webpack": ^1.5.0 "@redux-devtools/extension": ^3.2.5 + "@rich-data/viewer": ^2.2.4 "@swc-jotai/debug-label": ^0.0.9 "@swc-jotai/react-refresh": ^0.0.7 "@types/react": ^18.0.28 @@ -166,6 +173,7 @@ __metadata: dependencies: "@affine/debug": "workspace:*" "@blocksuite/blocks": 0.5.0-20230323085636-3110abb + "@blocksuite/global": 0.5.0-20230323085636-3110abb "@blocksuite/store": 0.5.0-20230323085636-3110abb "@tauri-apps/api": ^1.2.0 encoding: ^0.1.13 @@ -256,6 +264,7 @@ __metadata: version: 0.0.0-use.local resolution: "@affine/workspace@workspace:packages/workspace" dependencies: + "@affine-test/fixtures": "workspace:^" "@affine/component": "workspace:*" "@affine/debug": "workspace:*" "@affine/env": "workspace:*" @@ -267,6 +276,7 @@ __metadata: ky: ^0.33.3 react: ^18.2.0 react-dom: ^18.2.0 + zod: ^3.21.4 languageName: unknown linkType: soft @@ -4685,6 +4695,22 @@ __metadata: languageName: node linkType: hard +"@rich-data/viewer@npm:^2.2.4": + version: 2.14.1 + resolution: "@rich-data/viewer@npm:2.14.1" + dependencies: + "@mui/material": ^5.11.13 + copy-to-clipboard: ^3.3.3 + zustand: ^4.3.6 + peerDependencies: + "@emotion/react": ^11.10 + "@emotion/styled": ^11.10 + react: ^17 || ^18 + react-dom: ^17 || ^18 + checksum: a3fa1748eff116d5377ed2a3ecd24db3f6b0147d0663df66791ad9f065126ee77d2cd8b1158570be546b8735ae6db64f39081ad8f6dacda6c47455230aaefe54 + languageName: node + linkType: hard + "@rollup/pluginutils@npm:^4.2.0": version: 4.2.1 resolution: "@rollup/pluginutils@npm:4.2.1" @@ -9340,6 +9366,15 @@ __metadata: languageName: node linkType: hard +"copy-to-clipboard@npm:^3.3.3": + version: 3.3.3 + resolution: "copy-to-clipboard@npm:3.3.3" + dependencies: + toggle-selection: ^1.0.6 + checksum: e0a325e39b7615108e6c1c8ac110ae7b829cdc4ee3278b1df6a0e4228c490442cc86444cd643e2da344fbc424b3aab8909e2fec82f8bc75e7e5b190b7c24eecf + languageName: node + linkType: hard + "core-js-compat@npm:^3.25.1": version: 3.29.1 resolution: "core-js-compat@npm:3.29.1" @@ -19499,6 +19534,13 @@ __metadata: languageName: node linkType: hard +"toggle-selection@npm:^1.0.6": + version: 1.0.6 + resolution: "toggle-selection@npm:1.0.6" + checksum: a90dc80ed1e7b18db8f4e16e86a5574f87632dc729cfc07d9ea3ced50021ad42bb4e08f22c0913e0b98e3837b0b717e0a51613c65f30418e21eb99da6556a74c + languageName: node + linkType: hard + "toidentifier@npm:1.0.1": version: 1.0.1 resolution: "toidentifier@npm:1.0.1" @@ -20077,7 +20119,7 @@ __metadata: languageName: node linkType: hard -"use-sync-external-store@npm:^1.2.0": +"use-sync-external-store@npm:1.2.0, use-sync-external-store@npm:^1.2.0": version: 1.2.0 resolution: "use-sync-external-store@npm:1.2.0" peerDependencies: @@ -21016,6 +21058,23 @@ __metadata: languageName: node linkType: hard +"zustand@npm:^4.3.6": + version: 4.3.6 + resolution: "zustand@npm:4.3.6" + dependencies: + use-sync-external-store: 1.2.0 + peerDependencies: + immer: ">=9.0" + react: ">=16.8" + peerDependenciesMeta: + immer: + optional: true + react: + optional: true + checksum: 4d3cec03526f04ff3de6dc45b6f038c47f091836af9660fbf5f682cae1628221102882df20e4048dfe699a43f67424e5d6afc1116f3838a80eea5dd4f95ddaed + languageName: node + linkType: hard + "zx@npm:^7.2.1": version: 7.2.1 resolution: "zx@npm:7.2.1"