From e0481d29ad24a5fa843a6841e44d0507e7fe7b77 Mon Sep 17 00:00:00 2001 From: Himself65 Date: Wed, 1 Mar 2023 01:40:01 -0600 Subject: [PATCH] refactor!: next generation AFFiNE code structure (#1176) --- .eslintignore | 1 + .eslintrc.js | 1 + .github/deployment/Dockerfile | 2 +- .gitignore | 3 +- apps/web/.env.local.template | 3 + apps/web/CHANGELOG.md | 7 - apps/web/README.md | 34 - apps/web/next.config.mjs | 68 +- apps/web/package.json | 57 +- apps/web/preset.config.mjs | 6 + apps/web/scripts/__tests__/printer.spec.ts | 23 - apps/web/scripts/printer.mjs | 18 - apps/web/src/atoms/index.ts | 36 + apps/web/src/atoms/public-workspace/index.ts | 48 + .../src/blocksuite/__tests__/index.spec.ts | 32 + apps/web/src/blocksuite/index.ts | 26 + apps/web/src/blocksuite/providers/index.ts | 71 ++ apps/web/src/components/404/index.tsx | 29 - apps/web/src/components/404/styles.ts | 19 - .../__tests__/ProviderComposer.spec.tsx | 30 + .../__tests__/WorkSpaceSliderBar.spec.tsx | 89 ++ .../ProviderComposer.spec.tsx.snap | 7 + apps/web/src/components/affine/README.md | 19 + .../affine/affine-error-eoundary.tsx | 129 +++ .../enable-affine-cloud-modal/index.tsx | 64 ++ .../enable-affine-cloud-modal}/style.ts | 0 .../index.tsx | 59 +- .../style.ts | 40 + .../affine/workspace-setting-detail/index.tsx | 159 ++++ .../panel/collaboration/index.tsx | 227 +++++ .../invite-member-modal/index.tsx} | 134 ++- .../panel/collaboration}/style.ts | 4 +- .../panel/export/index.tsx} | 7 +- .../panel/general/delete/index.tsx} | 48 +- .../panel}/general/delete/style.ts | 0 .../panel}/general/icons.tsx | 0 .../panel/general/index.tsx} | 130 ++- .../panel/general/leave/index.tsx} | 10 +- .../panel}/general/leave/style.ts | 0 .../panel}/general/style.ts | 2 + .../panel/publish/index.tsx | 147 +++ .../workspace-setting-detail}/style.ts | 18 + .../blocksuite/block-suite-editor/index.tsx | 104 +++ .../block-suite-page-list/index.tsx | 36 + .../page-list/DateCell.tsx | 3 +- .../page-list/Empty.tsx | 0 .../page-list/OperationCell.tsx | 171 ++++ .../block-suite-page-list/page-list/index.tsx | 216 +++++ .../page-list/styles.ts | 0 .../header}/editor-mode-switch/Icons.tsx | 0 .../header}/editor-mode-switch/index.tsx | 46 +- .../header}/editor-mode-switch/style.ts | 0 .../header}/editor-mode-switch/type.ts | 6 +- .../header-right-items/EditorOptionMenu.tsx | 69 +- .../header/header-right-items/SyncUser.tsx | 101 ++- .../header-right-items/TrashButtonGroup.tsx | 83 ++ .../theme-mode-switch/Icons.tsx | 0 .../theme-mode-switch/index.tsx | 3 +- .../theme-mode-switch/style.ts | 0 .../header/header.tsx} | 42 +- .../components/blocksuite/header/index.tsx | 90 ++ .../header/quick-search-button/index.tsx} | 8 +- .../{ => blocksuite}/header/styles.ts | 4 +- .../{ => blocksuite}/header/utils.tsx | 19 +- .../src/components/delete-workspace/index.tsx | 95 -- .../src/components/edgeless-toolbar/Icons.tsx | 151 ---- .../src/components/edgeless-toolbar/index.tsx | 164 ---- .../src/components/edgeless-toolbar/reply.svg | 3 - .../src/components/edgeless-toolbar/style.ts | 41 - apps/web/src/components/editor/index.tsx | 72 -- .../src/components/header/EditorHeader.tsx | 80 -- .../src/components/header/PageListHeader.tsx | 23 - .../header-right-items/TrashButtonGroup.tsx | 57 -- apps/web/src/components/header/index.tsx | 3 - apps/web/src/components/help-island/index.tsx | 97 -- apps/web/src/components/import/index.tsx | 140 --- apps/web/src/components/import/styles.ts | 25 - .../src/components/login-modal/GoogleIcon.tsx | 28 - apps/web/src/components/login-modal/index.tsx | 77 -- apps/web/src/components/logout-modal/icon.tsx | 48 - .../web/src/components/logout-modal/index.tsx | 150 ---- .../message-center-handler/index.tsx | 55 -- apps/web/src/components/mobile-modal/bg.png | Bin 150723 -> 0 bytes .../web/src/components/mobile-modal/index.tsx | 49 - .../web/src/components/mobile-modal/styles.ts | 38 - .../web/src/components/page-detail-editor.tsx | 65 ++ .../components/page-list/OperationCell.tsx | 130 --- apps/web/src/components/page-list/index.tsx | 172 ---- ...ider-composer.ts => provider-composer.tsx} | 2 - .../{ => pure}/contact-modal/Icons.tsx | 0 .../contact-modal/affine-text-logo.png | Bin .../{ => pure}/contact-modal/index.tsx | 0 .../{ => pure}/contact-modal/style.ts | 0 .../create-workspace-modal}/index.tsx | 52 +- .../{ => pure}/file-upload/index.tsx | 18 +- apps/web/src/components/pure/footer/index.tsx | 125 +++ .../footer}/styles.ts | 4 +- .../{ => pure}/help-island/Icons.tsx | 0 .../src/components/pure/help-island/index.tsx | 103 +++ .../{ => pure}/help-island/style.ts | 0 .../src/components/{ => pure}/icons/index.tsx | 0 .../components/{ => pure}/loading/Loading.tsx | 0 .../{ => pure}/loading/PageLoading.tsx | 0 .../components/{ => pure}/loading/index.tsx | 0 .../components/{ => pure}/loading/styled.ts | 0 .../pure/quick-search-modal/Footer.tsx | 55 ++ .../pure/quick-search-modal/Input.tsx | 86 ++ .../quick-search-modal}/NoResultSVG.tsx | 6 +- .../quick-search-modal/PublishedResults.tsx | 97 ++ .../pure/quick-search-modal/Results.tsx | 118 +++ .../pure/quick-search-modal/config.ts | 44 + .../quick-search-modal}/index.tsx | 152 ++-- .../quick-search-modal}/style.ts | 0 .../{ => pure}/shortcuts-modal/Icons.tsx | 0 .../{ => pure}/shortcuts-modal/config.ts | 0 .../{ => pure}/shortcuts-modal/index.tsx | 28 +- .../{ => pure}/shortcuts-modal/style.ts | 0 .../pure/workspace-avatar/index.tsx | 114 +++ .../components/pure/workspace-card/index.tsx | 101 +++ .../components/pure/workspace-card/styles.ts | 172 ++++ .../pure/workspace-list-modal/index.tsx | 118 +++ .../workspace-list-modal/language-menu.tsx} | 18 +- .../pure/workspace-list-modal/styles.ts | 172 ++++ .../WorkspaceSelector/WorkspaceSelector.tsx | 41 + .../WorkspaceSelector/index.ts | 0 .../WorkspaceSelector/styles.ts | 0 .../{ => pure}/workspace-slider-bar/icons.tsx | 0 .../workspace-slider-bar/icons/Icons.tsx | 0 .../pure/workspace-slider-bar/index.tsx | 273 ++++++ .../{ => pure}/workspace-slider-bar/style.ts | 34 +- .../components/pure/workspace-title/index.tsx | 24 + .../src/components/quick-search/Footer.tsx | 47 - .../quick-search/PublishedResults.tsx | 89 -- .../src/components/quick-search/Results.tsx | 103 --- .../components/quick-search/SearchInput.tsx | 33 - .../web/src/components/quick-search/config.ts | 44 - .../src/components/simple-counter/index.ts | 59 -- .../components/workspace-avatar/Avatar.tsx | 59 -- .../workspace-avatar/WorkspaceUnitAvatar.tsx | 44 - .../src/components/workspace-avatar/index.ts | 2 - .../src/components/workspace-layout/index.tsx | 44 - .../src/components/workspace-modal/Footer.tsx | 66 -- .../workspace-modal/WorkspaceCard.tsx | 85 -- .../src/components/workspace-modal/index.tsx | 149 --- .../workspace-setting/PublishPage.tsx | 99 -- .../components/workspace-setting/SyncPage.tsx | 111 --- .../workspace-setting/general/delete/index.ts | 1 - .../workspace-setting/general/index.ts | 1 - .../workspace-setting/general/leave/index.ts | 1 - .../src/components/workspace-setting/index.ts | 5 - .../workspace-setting/member/MembersPage.tsx | 181 ---- .../workspace-setting/member/index.ts | 1 - .../workspace-slider-bar/DragLine.tsx | 92 -- .../WorkspaceItem/ListItem.tsx | 38 - .../WorkspaceItem/LoginItem.tsx | 54 -- .../WorkspaceSelector/WorkspaceItem/index.ts | 2 - .../WorkspaceSelector/WorkspaceItem/styles.ts | 41 - .../WorkspaceSelector/WorkspaceSelector.tsx | 49 - .../components/workspace-slider-bar/index.tsx | 210 ----- .../__snapshots__/index.spec.tsx.snap | 17 + apps/web/src/hooks/__tests__/index.spec.tsx | 180 ++++ .../use-blocksuite-workspace-helper.spec.ts | 53 ++ .../hooks/__tests__/use-feature-flag.spec.ts | 25 + .../hooks/__tests__/use-router-helper.spec.ts | 101 +++ .../hooks/__tests__/use-system-online.spec.ts | 23 + .../hooks/affine/use-is-workspace-owner.ts | 8 + apps/web/src/hooks/affine/use-members.ts | 43 + .../affine/use-toggle-workspace-publish.ts | 21 + .../src/hooks/affine/use-users-by-email.ts | 24 + .../src/hooks/current/use-current-page-id.ts | 10 + .../web/src/hooks/current/use-current-user.ts | 11 + .../hooks/current/use-current-workspace.ts | 24 + .../hooks/use-blocksuite-workspace-helper.ts | 29 + .../use-blocksuite-workspace-page-title.ts | 26 + apps/web/src/hooks/use-change-page-meta.ts | 26 - apps/web/src/hooks/use-current-page-meta.ts | 42 - apps/web/src/hooks/use-ensure-workspace.ts | 73 -- apps/web/src/hooks/use-feature-flag.ts | 46 + apps/web/src/hooks/use-history-update.ts | 37 - .../src/hooks/use-last-opened-workspace.ts | 41 + .../src/hooks/use-load-public-workspace.ts | 35 - apps/web/src/hooks/use-load-workspace.ts | 13 + apps/web/src/hooks/use-local-storage.ts | 33 - apps/web/src/hooks/use-members.ts | 55 -- apps/web/src/hooks/use-page-helper.ts | 152 ---- apps/web/src/hooks/use-page-meta.ts | 49 + apps/web/src/hooks/use-props-updated.ts | 39 - apps/web/src/hooks/use-router-helper.ts | 42 + apps/web/src/hooks/use-router-title.ts | 26 + ...-router-with-current-workspace-and-page.ts | 166 ++++ .../use-sync-router-with-current-workspace.ts | 55 ++ apps/web/src/hooks/use-system-online.ts | 21 + apps/web/src/hooks/use-system-theme.ts | 35 + apps/web/src/hooks/use-workspace-blob.ts | 30 + apps/web/src/hooks/use-workspace-helper.ts | 60 -- apps/web/src/hooks/use-workspace.ts | 12 + apps/web/src/hooks/use-workspaces.ts | 195 ++++ apps/web/src/layouts/index.tsx | 134 +++ .../workspace-layout => layouts}/styles.ts | 0 apps/web/src/pages/404.tsx | 53 +- apps/web/src/pages/_app.tsx | 173 ++-- apps/web/src/pages/_document.tsx | 49 +- apps/web/src/pages/index.tsx | 76 +- apps/web/src/pages/invite/[invite_code].tsx | 104 --- apps/web/src/pages/preview/[previewId].tsx | 125 +++ .../pages/public-workspace/[workspaceId].tsx | 77 ++ .../[workspaceId]/[pageId].tsx | 220 +++-- .../public-workspace/[workspaceId]/index.tsx | 83 -- apps/web/src/pages/temporary.css | 15 - .../workspace/[workspaceId]/[pageId].tsx | 207 ++--- .../src/pages/workspace/[workspaceId]/all.tsx | 104 ++- .../workspace/[workspaceId]/favorite.tsx | 63 +- .../pages/workspace/[workspaceId]/index.tsx | 46 - .../pages/workspace/[workspaceId]/setting.tsx | 322 ++++--- .../pages/workspace/[workspaceId]/trash.tsx | 70 +- apps/web/src/pages/workspace/index.tsx | 24 - apps/web/src/plugins/affine/fetcher.ts | 80 ++ apps/web/src/plugins/affine/index.tsx | 198 ++++ apps/web/src/plugins/index.tsx | 90 ++ apps/web/src/plugins/local/index.tsx | 220 +++++ .../src/providers/AffineSWRConfigProvider.tsx | 15 + apps/web/src/providers/ConfirmProvider.tsx | 101 --- apps/web/src/providers/ModalProvider.tsx | 118 +++ apps/web/src/providers/ThemeProvider.tsx | 129 ++- .../providers/app-state-provider/Provider.tsx | 51 -- .../src/providers/app-state-provider/index.ts | 2 - .../providers/app-state-provider/interface.ts | 28 - .../src/providers/app-state-provider/utils.ts | 9 - apps/web/src/shared/apis.ts | 24 + apps/web/src/shared/env.ts | 44 + apps/web/src/shared/index.ts | 167 ++++ apps/web/src/store/app/blocksuite/index.ts | 61 -- apps/web/src/store/app/datacenter/index.tsx | 142 --- apps/web/src/store/app/index.tsx | 89 -- apps/web/src/store/app/user/index.ts | 65 -- apps/web/src/store/globalModal/index.tsx | 187 ---- apps/web/{public => src/styles}/globals.css | 22 + .../Welcome-to-AFFiNE-Abbey-Alpha-Wood.md | 2 + .../Welcome-to-AFFiNE-Alpha-Downhills.md | 2 + .../templates/Welcome-to-the-AFFiNE-Alpha.md | 2 + apps/web/src/types.ts | 10 - apps/web/src/types/index.ts | 14 + .../src/{globals.d.ts => types/types.d.ts} | 1 + .../src/utils/__tests__/get-is-mobile.spec.ts | 13 - apps/web/src/utils/colors.ts | 20 - apps/web/src/utils/env.ts | 1 - apps/web/src/utils/get-is-mobile.ts | 2 - apps/web/src/utils/index.ts | 41 +- apps/web/src/utils/print-build-info.ts | 60 -- apps/web/src/utils/tools.ts | 10 - apps/web/src/utils/useragent.ts | 4 +- apps/web/tsconfig.json | 8 +- package.json | 7 +- packages/component/src/styles/types.ts | 4 +- packages/component/src/ui/confirm/Confirm.tsx | 10 +- packages/data-center/src/index.ts | 9 + .../src/provider/affine/apis/workspace.ts | 2 +- packages/i18n/package.json | 6 +- packages/i18n/src/index.ts | 60 +- pnpm-lock.yaml | 848 ++++++++++++------ scripts/vitest/next-config-mock.ts | 10 + tests/change-page-mode.spec.ts | 2 +- tests/console.spec.ts | 24 - tests/libs/page-logic.ts | 9 +- tests/local-first-workspace-list.spec.ts | 4 +- tests/local-first-workspace.spec.ts | 14 +- tests/quick-search.spec.ts | 15 +- tests/theme.spec.ts | 3 +- tsconfig.json | 1 - vitest.config.ts | 16 +- 270 files changed, 8308 insertions(+), 6829 deletions(-) delete mode 100644 apps/web/CHANGELOG.md delete mode 100644 apps/web/README.md create mode 100644 apps/web/preset.config.mjs delete mode 100644 apps/web/scripts/__tests__/printer.spec.ts delete mode 100644 apps/web/scripts/printer.mjs create mode 100644 apps/web/src/atoms/index.ts create mode 100644 apps/web/src/atoms/public-workspace/index.ts create mode 100644 apps/web/src/blocksuite/__tests__/index.spec.ts create mode 100644 apps/web/src/blocksuite/index.ts create mode 100644 apps/web/src/blocksuite/providers/index.ts delete mode 100644 apps/web/src/components/404/index.tsx delete mode 100644 apps/web/src/components/404/styles.ts create mode 100644 apps/web/src/components/__tests__/ProviderComposer.spec.tsx create mode 100644 apps/web/src/components/__tests__/WorkSpaceSliderBar.spec.tsx create mode 100644 apps/web/src/components/__tests__/__snapshots__/ProviderComposer.spec.tsx.snap create mode 100644 apps/web/src/components/affine/README.md create mode 100644 apps/web/src/components/affine/affine-error-eoundary.tsx create mode 100644 apps/web/src/components/affine/enable-affine-cloud-modal/index.tsx rename apps/web/src/components/{enable-workspace-modal => affine/enable-affine-cloud-modal}/style.ts (100%) rename apps/web/src/components/{enable-workspace-modal => affine/transform-workspace-to-affine-modal}/index.tsx (51%) create mode 100644 apps/web/src/components/affine/transform-workspace-to-affine-modal/style.ts create mode 100644 apps/web/src/components/affine/workspace-setting-detail/index.tsx create mode 100644 apps/web/src/components/affine/workspace-setting-detail/panel/collaboration/index.tsx rename apps/web/src/components/{workspace-setting/member/InviteMemberModal.tsx => affine/workspace-setting-detail/panel/collaboration/invite-member-modal/index.tsx} (61%) rename apps/web/src/components/{workspace-setting/member => affine/workspace-setting-detail/panel/collaboration}/style.ts (93%) rename apps/web/src/components/{workspace-setting/ExportPage.tsx => affine/workspace-setting-detail/panel/export/index.tsx} (53%) rename apps/web/src/components/{workspace-setting/general/delete/Delete.tsx => affine/workspace-setting-detail/panel/general/delete/index.tsx} (65%) rename apps/web/src/components/{workspace-setting => affine/workspace-setting-detail/panel}/general/delete/style.ts (100%) rename apps/web/src/components/{workspace-setting => affine/workspace-setting-detail/panel}/general/icons.tsx (100%) rename apps/web/src/components/{workspace-setting/general/General.tsx => affine/workspace-setting-detail/panel/general/index.tsx} (62%) rename apps/web/src/components/{workspace-setting/general/leave/Leave.tsx => affine/workspace-setting-detail/panel/general/leave/index.tsx} (80%) rename apps/web/src/components/{workspace-setting => affine/workspace-setting-detail/panel}/general/leave/style.ts (100%) rename apps/web/src/components/{workspace-setting => affine/workspace-setting-detail/panel}/general/style.ts (99%) create mode 100644 apps/web/src/components/affine/workspace-setting-detail/panel/publish/index.tsx rename apps/web/src/components/{workspace-setting => affine/workspace-setting-detail}/style.ts (86%) create mode 100644 apps/web/src/components/blocksuite/block-suite-editor/index.tsx create mode 100644 apps/web/src/components/blocksuite/block-suite-page-list/index.tsx rename apps/web/src/components/{ => blocksuite/block-suite-page-list}/page-list/DateCell.tsx (91%) rename apps/web/src/components/{ => blocksuite/block-suite-page-list}/page-list/Empty.tsx (100%) create mode 100644 apps/web/src/components/blocksuite/block-suite-page-list/page-list/OperationCell.tsx create mode 100644 apps/web/src/components/blocksuite/block-suite-page-list/page-list/index.tsx rename apps/web/src/components/{ => blocksuite/block-suite-page-list}/page-list/styles.ts (100%) rename apps/web/src/components/{ => blocksuite/header}/editor-mode-switch/Icons.tsx (100%) rename apps/web/src/components/{ => blocksuite/header}/editor-mode-switch/index.tsx (75%) rename apps/web/src/components/{ => blocksuite/header}/editor-mode-switch/style.ts (100%) rename apps/web/src/components/{ => blocksuite/header}/editor-mode-switch/type.ts (68%) rename apps/web/src/components/{ => blocksuite}/header/header-right-items/EditorOptionMenu.tsx (59%) rename apps/web/src/components/{ => blocksuite}/header/header-right-items/SyncUser.tsx (58%) create mode 100644 apps/web/src/components/blocksuite/header/header-right-items/TrashButtonGroup.tsx rename apps/web/src/components/{ => blocksuite}/header/header-right-items/theme-mode-switch/Icons.tsx (100%) rename apps/web/src/components/{ => blocksuite}/header/header-right-items/theme-mode-switch/index.tsx (94%) rename apps/web/src/components/{ => blocksuite}/header/header-right-items/theme-mode-switch/style.ts (100%) rename apps/web/src/components/{header/Header.tsx => blocksuite/header/header.tsx} (58%) create mode 100644 apps/web/src/components/blocksuite/header/index.tsx rename apps/web/src/components/{header/QuickSearchButton.tsx => blocksuite/header/quick-search-button/index.tsx} (83%) rename apps/web/src/components/{ => blocksuite}/header/styles.ts (97%) rename apps/web/src/components/{ => blocksuite}/header/utils.tsx (65%) delete mode 100644 apps/web/src/components/delete-workspace/index.tsx delete mode 100644 apps/web/src/components/edgeless-toolbar/Icons.tsx delete mode 100644 apps/web/src/components/edgeless-toolbar/index.tsx delete mode 100644 apps/web/src/components/edgeless-toolbar/reply.svg delete mode 100644 apps/web/src/components/edgeless-toolbar/style.ts delete mode 100644 apps/web/src/components/editor/index.tsx delete mode 100644 apps/web/src/components/header/EditorHeader.tsx delete mode 100644 apps/web/src/components/header/PageListHeader.tsx delete mode 100644 apps/web/src/components/header/header-right-items/TrashButtonGroup.tsx delete mode 100644 apps/web/src/components/header/index.tsx delete mode 100644 apps/web/src/components/help-island/index.tsx delete mode 100644 apps/web/src/components/import/index.tsx delete mode 100644 apps/web/src/components/import/styles.ts delete mode 100644 apps/web/src/components/login-modal/GoogleIcon.tsx delete mode 100644 apps/web/src/components/login-modal/index.tsx delete mode 100644 apps/web/src/components/logout-modal/icon.tsx delete mode 100644 apps/web/src/components/logout-modal/index.tsx delete mode 100644 apps/web/src/components/message-center-handler/index.tsx delete mode 100644 apps/web/src/components/mobile-modal/bg.png delete mode 100644 apps/web/src/components/mobile-modal/index.tsx delete mode 100644 apps/web/src/components/mobile-modal/styles.ts create mode 100644 apps/web/src/components/page-detail-editor.tsx delete mode 100644 apps/web/src/components/page-list/OperationCell.tsx delete mode 100644 apps/web/src/components/page-list/index.tsx rename apps/web/src/components/{provider-composer.ts => provider-composer.tsx} (90%) rename apps/web/src/components/{ => pure}/contact-modal/Icons.tsx (100%) rename apps/web/src/components/{ => pure}/contact-modal/affine-text-logo.png (100%) rename apps/web/src/components/{ => pure}/contact-modal/index.tsx (100%) rename apps/web/src/components/{ => pure}/contact-modal/style.ts (100%) rename apps/web/src/components/{create-workspace => pure/create-workspace-modal}/index.tsx (70%) rename apps/web/src/components/{ => pure}/file-upload/index.tsx (80%) create mode 100644 apps/web/src/components/pure/footer/index.tsx rename apps/web/src/components/{workspace-modal => pure/footer}/styles.ts (97%) rename apps/web/src/components/{ => pure}/help-island/Icons.tsx (100%) create mode 100644 apps/web/src/components/pure/help-island/index.tsx rename apps/web/src/components/{ => pure}/help-island/style.ts (100%) rename apps/web/src/components/{ => pure}/icons/index.tsx (100%) rename apps/web/src/components/{ => pure}/loading/Loading.tsx (100%) rename apps/web/src/components/{ => pure}/loading/PageLoading.tsx (100%) rename apps/web/src/components/{ => pure}/loading/index.tsx (100%) rename apps/web/src/components/{ => pure}/loading/styled.ts (100%) create mode 100644 apps/web/src/components/pure/quick-search-modal/Footer.tsx create mode 100644 apps/web/src/components/pure/quick-search-modal/Input.tsx rename apps/web/src/components/{quick-search => pure/quick-search-modal}/NoResultSVG.tsx (99%) create mode 100644 apps/web/src/components/pure/quick-search-modal/PublishedResults.tsx create mode 100644 apps/web/src/components/pure/quick-search-modal/Results.tsx create mode 100644 apps/web/src/components/pure/quick-search-modal/config.ts rename apps/web/src/components/{quick-search => pure/quick-search-modal}/index.tsx (52%) rename apps/web/src/components/{quick-search => pure/quick-search-modal}/style.ts (100%) rename apps/web/src/components/{ => pure}/shortcuts-modal/Icons.tsx (100%) rename apps/web/src/components/{ => pure}/shortcuts-modal/config.ts (100%) rename apps/web/src/components/{ => pure}/shortcuts-modal/index.tsx (86%) rename apps/web/src/components/{ => pure}/shortcuts-modal/style.ts (100%) create mode 100644 apps/web/src/components/pure/workspace-avatar/index.tsx create mode 100644 apps/web/src/components/pure/workspace-card/index.tsx create mode 100644 apps/web/src/components/pure/workspace-card/styles.ts create mode 100644 apps/web/src/components/pure/workspace-list-modal/index.tsx rename apps/web/src/components/{workspace-modal/SelectLanguageMenu.tsx => pure/workspace-list-modal/language-menu.tsx} (79%) create mode 100644 apps/web/src/components/pure/workspace-list-modal/styles.ts create mode 100644 apps/web/src/components/pure/workspace-slider-bar/WorkspaceSelector/WorkspaceSelector.tsx rename apps/web/src/components/{ => pure}/workspace-slider-bar/WorkspaceSelector/index.ts (100%) rename apps/web/src/components/{ => pure}/workspace-slider-bar/WorkspaceSelector/styles.ts (100%) rename apps/web/src/components/{ => pure}/workspace-slider-bar/icons.tsx (100%) rename apps/web/src/components/{ => pure}/workspace-slider-bar/icons/Icons.tsx (100%) create mode 100644 apps/web/src/components/pure/workspace-slider-bar/index.tsx rename apps/web/src/components/{ => pure}/workspace-slider-bar/style.ts (84%) create mode 100644 apps/web/src/components/pure/workspace-title/index.tsx delete mode 100644 apps/web/src/components/quick-search/Footer.tsx delete mode 100644 apps/web/src/components/quick-search/PublishedResults.tsx delete mode 100644 apps/web/src/components/quick-search/Results.tsx delete mode 100644 apps/web/src/components/quick-search/SearchInput.tsx delete mode 100644 apps/web/src/components/quick-search/config.ts delete mode 100644 apps/web/src/components/simple-counter/index.ts delete mode 100644 apps/web/src/components/workspace-avatar/Avatar.tsx delete mode 100644 apps/web/src/components/workspace-avatar/WorkspaceUnitAvatar.tsx delete mode 100644 apps/web/src/components/workspace-avatar/index.ts delete mode 100644 apps/web/src/components/workspace-layout/index.tsx delete mode 100644 apps/web/src/components/workspace-modal/Footer.tsx delete mode 100644 apps/web/src/components/workspace-modal/WorkspaceCard.tsx delete mode 100644 apps/web/src/components/workspace-modal/index.tsx delete mode 100644 apps/web/src/components/workspace-setting/PublishPage.tsx delete mode 100644 apps/web/src/components/workspace-setting/SyncPage.tsx delete mode 100644 apps/web/src/components/workspace-setting/general/delete/index.ts delete mode 100644 apps/web/src/components/workspace-setting/general/index.ts delete mode 100644 apps/web/src/components/workspace-setting/general/leave/index.ts delete mode 100644 apps/web/src/components/workspace-setting/index.ts delete mode 100644 apps/web/src/components/workspace-setting/member/MembersPage.tsx delete mode 100644 apps/web/src/components/workspace-setting/member/index.ts delete mode 100644 apps/web/src/components/workspace-slider-bar/DragLine.tsx delete mode 100644 apps/web/src/components/workspace-slider-bar/WorkspaceSelector/WorkspaceItem/ListItem.tsx delete mode 100644 apps/web/src/components/workspace-slider-bar/WorkspaceSelector/WorkspaceItem/LoginItem.tsx delete mode 100644 apps/web/src/components/workspace-slider-bar/WorkspaceSelector/WorkspaceItem/index.ts delete mode 100644 apps/web/src/components/workspace-slider-bar/WorkspaceSelector/WorkspaceItem/styles.ts delete mode 100644 apps/web/src/components/workspace-slider-bar/WorkspaceSelector/WorkspaceSelector.tsx delete mode 100644 apps/web/src/components/workspace-slider-bar/index.tsx create mode 100644 apps/web/src/hooks/__tests__/__snapshots__/index.spec.tsx.snap create mode 100644 apps/web/src/hooks/__tests__/index.spec.tsx create mode 100644 apps/web/src/hooks/__tests__/use-blocksuite-workspace-helper.spec.ts create mode 100644 apps/web/src/hooks/__tests__/use-feature-flag.spec.ts create mode 100644 apps/web/src/hooks/__tests__/use-router-helper.spec.ts create mode 100644 apps/web/src/hooks/__tests__/use-system-online.spec.ts create mode 100644 apps/web/src/hooks/affine/use-is-workspace-owner.ts create mode 100644 apps/web/src/hooks/affine/use-members.ts create mode 100644 apps/web/src/hooks/affine/use-toggle-workspace-publish.ts create mode 100644 apps/web/src/hooks/affine/use-users-by-email.ts create mode 100644 apps/web/src/hooks/current/use-current-page-id.ts create mode 100644 apps/web/src/hooks/current/use-current-user.ts create mode 100644 apps/web/src/hooks/current/use-current-workspace.ts create mode 100644 apps/web/src/hooks/use-blocksuite-workspace-helper.ts create mode 100644 apps/web/src/hooks/use-blocksuite-workspace-page-title.ts delete mode 100644 apps/web/src/hooks/use-change-page-meta.ts delete mode 100644 apps/web/src/hooks/use-current-page-meta.ts delete mode 100644 apps/web/src/hooks/use-ensure-workspace.ts create mode 100644 apps/web/src/hooks/use-feature-flag.ts delete mode 100644 apps/web/src/hooks/use-history-update.ts create mode 100644 apps/web/src/hooks/use-last-opened-workspace.ts delete mode 100644 apps/web/src/hooks/use-load-public-workspace.ts create mode 100644 apps/web/src/hooks/use-load-workspace.ts delete mode 100644 apps/web/src/hooks/use-local-storage.ts delete mode 100644 apps/web/src/hooks/use-members.ts delete mode 100644 apps/web/src/hooks/use-page-helper.ts create mode 100644 apps/web/src/hooks/use-page-meta.ts delete mode 100644 apps/web/src/hooks/use-props-updated.ts create mode 100644 apps/web/src/hooks/use-router-helper.ts create mode 100644 apps/web/src/hooks/use-router-title.ts create mode 100644 apps/web/src/hooks/use-sync-router-with-current-workspace-and-page.ts create mode 100644 apps/web/src/hooks/use-sync-router-with-current-workspace.ts create mode 100644 apps/web/src/hooks/use-system-online.ts create mode 100644 apps/web/src/hooks/use-system-theme.ts create mode 100644 apps/web/src/hooks/use-workspace-blob.ts delete mode 100644 apps/web/src/hooks/use-workspace-helper.ts create mode 100644 apps/web/src/hooks/use-workspace.ts create mode 100644 apps/web/src/hooks/use-workspaces.ts create mode 100644 apps/web/src/layouts/index.tsx rename apps/web/src/{components/workspace-layout => layouts}/styles.ts (100%) delete mode 100644 apps/web/src/pages/invite/[invite_code].tsx create mode 100644 apps/web/src/pages/preview/[previewId].tsx create mode 100644 apps/web/src/pages/public-workspace/[workspaceId].tsx delete mode 100644 apps/web/src/pages/public-workspace/[workspaceId]/index.tsx delete mode 100644 apps/web/src/pages/temporary.css delete mode 100644 apps/web/src/pages/workspace/[workspaceId]/index.tsx delete mode 100644 apps/web/src/pages/workspace/index.tsx create mode 100644 apps/web/src/plugins/affine/fetcher.ts create mode 100644 apps/web/src/plugins/affine/index.tsx create mode 100644 apps/web/src/plugins/index.tsx create mode 100644 apps/web/src/plugins/local/index.tsx create mode 100644 apps/web/src/providers/AffineSWRConfigProvider.tsx delete mode 100644 apps/web/src/providers/ConfirmProvider.tsx create mode 100644 apps/web/src/providers/ModalProvider.tsx delete mode 100644 apps/web/src/providers/app-state-provider/Provider.tsx delete mode 100644 apps/web/src/providers/app-state-provider/index.ts delete mode 100644 apps/web/src/providers/app-state-provider/interface.ts delete mode 100644 apps/web/src/providers/app-state-provider/utils.ts create mode 100644 apps/web/src/shared/apis.ts create mode 100644 apps/web/src/shared/env.ts create mode 100644 apps/web/src/shared/index.ts delete mode 100644 apps/web/src/store/app/blocksuite/index.ts delete mode 100644 apps/web/src/store/app/datacenter/index.tsx delete mode 100644 apps/web/src/store/app/index.tsx delete mode 100644 apps/web/src/store/app/user/index.ts delete mode 100644 apps/web/src/store/globalModal/index.tsx rename apps/web/{public => src/styles}/globals.css (87%) delete mode 100644 apps/web/src/types.ts create mode 100644 apps/web/src/types/index.ts rename apps/web/src/{globals.d.ts => types/types.d.ts} (63%) delete mode 100644 apps/web/src/utils/__tests__/get-is-mobile.spec.ts delete mode 100644 apps/web/src/utils/colors.ts delete mode 100644 apps/web/src/utils/env.ts delete mode 100644 apps/web/src/utils/print-build-info.ts delete mode 100644 apps/web/src/utils/tools.ts create mode 100644 scripts/vitest/next-config-mock.ts delete mode 100644 tests/console.spec.ts diff --git a/.eslintignore b/.eslintignore index ea887a3d50..52d14c3637 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,3 +1,4 @@ node_modules dist .next +out diff --git a/.eslintrc.js b/.eslintrc.js index 630cee3d94..0b9f30dfeb 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -36,6 +36,7 @@ module.exports = { 'no-empty': 'off', 'no-func-assign': 'off', 'no-cond-assign': 'off', + 'react/prop-types': 'off', '@typescript-eslint/no-non-null-assertion': 'off', '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-empty-function': 'off', diff --git a/.github/deployment/Dockerfile b/.github/deployment/Dockerfile index 59639d6c49..7b7a868b90 100644 --- a/.github/deployment/Dockerfile +++ b/.github/deployment/Dockerfile @@ -10,4 +10,4 @@ COPY --from=relocate /app . EXPOSE 80 ENV API_SERVER=$API_SERVER -CMD ["caddy", "run"] \ No newline at end of file +CMD ["caddy", "run"] diff --git a/.gitignore b/.gitignore index 7b54b96e70..655db92dc6 100644 --- a/.gitignore +++ b/.gitignore @@ -58,7 +58,8 @@ module-resolve.cjs # Cache .eslintcache +next-env.d.ts # generated assets apps/desktop/public/affine-out -apps/desktop/public/preload \ No newline at end of file +apps/desktop/public/preload diff --git a/apps/web/.env.local.template b/apps/web/.env.local.template index fadb3c8bbd..0371eb4412 100644 --- a/apps/web/.env.local.template +++ b/apps/web/.env.local.template @@ -9,3 +9,6 @@ NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID= LOCAL_BLOCK_SUITE= # see next.config.js NODE_API_SERVER= +# save workspace to idb +ENABLE_IDB_PROVIDER=1 +PREFETCH_WORKSPACE=1 diff --git a/apps/web/CHANGELOG.md b/apps/web/CHANGELOG.md deleted file mode 100644 index 236fe7333f..0000000000 --- a/apps/web/CHANGELOG.md +++ /dev/null @@ -1,7 +0,0 @@ -# @affine/app - -## 1.0.0 - -### Major Changes - -- cc72448: add changeset config diff --git a/apps/web/README.md b/apps/web/README.md deleted file mode 100644 index c87e0421d2..0000000000 --- a/apps/web/README.md +++ /dev/null @@ -1,34 +0,0 @@ -This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). - -## Getting Started - -First, run the development server: - -```bash -npm run dev -# or -yarn dev -``` - -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. - -[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. - -The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. - -## Learn More - -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs index 643717e814..cd3411f244 100644 --- a/apps/web/next.config.mjs +++ b/apps/web/next.config.mjs @@ -1,17 +1,14 @@ -import { getGitVersion, getCommitHash } from './scripts/gitInfo.mjs'; -import fs from 'node:fs'; import path from 'node:path'; -import { printer } from './scripts/printer.mjs'; import debugLocal from 'next-debug-local'; +import preset from './preset.config.mjs'; +import { createRequire } from 'node:module'; +import { getCommitHash, getGitVersion } from './scripts/gitInfo.mjs'; -const dependencies = JSON.parse(fs.readFileSync('./package.json', 'utf8'))[ - 'dependencies' -]; +const require = createRequire(import.meta.url); + +console.info('Runtime Preset', preset); const enableDebugLocal = path.isAbsolute(process.env.LOCAL_BLOCK_SUITE ?? ''); -const EDITOR_VERSION = enableDebugLocal - ? 'local-version' - : dependencies['@blocksuite/editor']; const profileTarget = { ac: '100.85.73.88:12001', @@ -20,6 +17,7 @@ const profileTarget = { stage: '', pro: 'http://pathfinder.affine.pro', local: '127.0.0.1:3000', + rem: 'stage.affine.pro', }; const getRedirectConfig = profile => { @@ -41,23 +39,39 @@ const getRedirectConfig = profile => { /** @type {import('next').NextConfig} */ const nextConfig = { productionBrowserSourceMaps: true, - reactStrictMode: true, - swcMinify: false, - publicRuntimeConfig: { - NODE_ENV: process.env.NODE_ENV, - PROJECT_NAME: process.env.npm_package_name, - BUILD_DATE: new Date().toISOString(), - CI: process.env.CI || null, - VERSION: getGitVersion(), - COMMIT_HASH: getCommitHash(), - EDITOR_VERSION, + compiler: { + removeConsole: { + exclude: ['error', 'log', 'warn', 'info'], + }, + emotion: { + sourceMap: true, + }, }, + images: { + unoptimized: true, + }, + experimental: { + swcPlugins: [ + ['@swc-jotai/debug-label', {}], + // ['@swc-jotai/react-refresh', {}], + ], + }, + reactStrictMode: true, transpilePackages: [ '@affine/component', - '@affine/i18n', '@affine/datacenter', - '@toeverything/pathfinder-logger', + '@affine/i18n', ], + publicRuntimeConfig: { + PROJECT_NAME: process.env.npm_package_name, + BUILD_DATE: new Date().toISOString(), + gitVersion: getGitVersion(), + hash: getCommitHash(), + serverAPI: + profileTarget[process.env.NODE_API_SERVER || 'dev'] ?? profileTarget.dev, + editorVersion: require('./package.json').dependencies['@blocksuite/editor'], + ...preset, + }, webpack: config => { config.experiments = { ...config.experiments, topLevelAwait: true }; config.module.rules.push({ @@ -67,20 +81,14 @@ const nextConfig = { return config; }, - images: { - unoptimized: true, - }, rewrites: async () => { const [profile, target, desc] = getRedirectConfig( process.env.NODE_API_SERVER ); - printer.info(`API request proxy to [${desc} Server]: ` + target); + console.info(`API request proxy to [${desc} Server]: ` + target); return profile; }, basePath: process.env.NEXT_BASE_PATH, - experimental: { - forceSwcTransforms: true, - }, }; const baseDir = process.env.LOCAL_BLOCK_SUITE ?? '/'; @@ -111,9 +119,9 @@ const withDebugLocal = debugLocal( const detectFirebaseConfig = () => { if (!process.env.NEXT_PUBLIC_FIREBASE_API_KEY) { - printer.warn('NEXT_PUBLIC_FIREBASE_API_KEY not found, please check it'); + console.warn('NEXT_PUBLIC_FIREBASE_API_KEY not found, please check it'); } else { - printer.info('NEXT_PUBLIC_FIREBASE_API_KEY found'); + console.info('NEXT_PUBLIC_FIREBASE_API_KEY found'); } }; detectFirebaseConfig(); diff --git a/apps/web/package.json b/apps/web/package.json index ccce01408d..8d616de485 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@affine/app", - "version": "0.3.1", + "private": true, "scripts": { "dev": "next dev -p 8080", "build": "next build", @@ -11,47 +11,44 @@ "dependencies": { "@affine/component": "workspace:*", "@affine/datacenter": "workspace:*", - "@affine/debug": "workspace:*", "@affine/i18n": "workspace:*", "@blocksuite/blocks": "0.4.1-20230220214107-0a354de", "@blocksuite/editor": "0.4.1-20230220214107-0a354de", - "@blocksuite/global": "0.4.1-20230220214107-0a354de", - "@blocksuite/icons": "2.0.17", + "@blocksuite/icons": "^2.0.17", + "@blocksuite/react": "0.4.1-20230220214107-0a354de", "@blocksuite/store": "0.4.1-20230220214107-0a354de", + "@emotion/cache": "^11.10.5", "@emotion/css": "^11.10.6", "@emotion/react": "^11.10.6", - "@emotion/server": "^11.10.0", - "@emotion/styled": "^11.10.6", - "@fontsource/poppins": "^4.5.10", - "@fontsource/space-mono": "^4.5.12", - "@mui/base": "5.0.0-alpha.118", - "@mui/icons-material": "^5.11.9", - "@mui/material": "^5.11.9", - "@toeverything/pathfinder-logger": "workspace:@affine/logger@*", + "@mui/material": "^5.11.10", "cmdk": "^0.1.22", "css-spring": "^4.1.0", "dayjs": "^1.11.7", + "jotai": "^2.0.2", + "jotai-devtools": "^0.2.0", "lit": "^2.6.1", - "next": "13.1.0", - "next-debug-local": "^0.1.5", - "prettier": "^2.8.4", - "quill": "^1.3.7", - "quill-cursors": "^4.0.0", - "react": "18.2.0", - "react-dom": "18.2.0", - "yjs": "^13.5.46", - "zustand": "^4.3.3" + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-helmet-async": "^1.3.0", + "swr": "^2.0.4", + "y-indexeddb": "^9.0.9", + "yjs": "^13.5.47", + "zod": "^3.20.6" }, "devDependencies": { - "@types/node": "18.14.0", - "@types/react": "18.0.28", - "@types/react-dom": "18.0.11", - "@types/wicg-file-system-access": "^2020.9.5", - "chalk": "^5.2.0", - "eslint": "^8.34.0", - "eslint-config-next": "13.1.6", + "@redux-devtools/extension": "^3.2.5", + "@swc-jotai/debug-label": "^0.0.6", + "@swc-jotai/react-refresh": "^0.0.4", + "@types/react": "^18.0.28", + "@types/react-dom": "^18.0.11", + "@types/webpack-env": "^1.18.0", + "dotenv": "^16.0.3", + "eslint-config-next": "^13.2.2", + "next": "^13.2.2", + "next-debug-local": "^0.1.5", + "next-router-mock": "^0.9.2", "raw-loader": "^4.0.2", - "typescript": "^4.9.5", - "webpack": "^5.75.0" + "redux": "^4.2.1", + "typescript": "^4.9.5" } } diff --git a/apps/web/preset.config.mjs b/apps/web/preset.config.mjs new file mode 100644 index 0000000000..baac73bbc2 --- /dev/null +++ b/apps/web/preset.config.mjs @@ -0,0 +1,6 @@ +import 'dotenv/config'; + +export default { + enableIndexedDBProvider: Boolean(process.env.ENABLE_IDB_PROVIDER ?? '1'), + prefetchWorkspace: Boolean(process.env.PREFETCH_WORKSPACE ?? '1'), +}; diff --git a/apps/web/scripts/__tests__/printer.spec.ts b/apps/web/scripts/__tests__/printer.spec.ts deleted file mode 100644 index 4a7716d4c5..0000000000 --- a/apps/web/scripts/__tests__/printer.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { expect, test } from '@playwright/test'; - -import { printer } from './../printer'; -// eslint-disable-next-line @typescript-eslint/no-var-requires -const chalk = require('chalk'); -test.describe('printer', () => { - test('test debug', () => { - expect(printer.debug('test debug')).toBe( - chalk.green`debug` + chalk.white(' - test debug') - ); - }); - - test('test info', () => { - expect(printer.info('test info')).toBe( - chalk.rgb(19, 167, 205)`info` + chalk.white(' - test info') - ); - }); - test('test warn', () => { - expect(printer.warn('test warn')).toBe( - chalk.yellow`warn` + chalk.white(' - test warn') - ); - }); -}); diff --git a/apps/web/scripts/printer.mjs b/apps/web/scripts/printer.mjs deleted file mode 100644 index b180187ad3..0000000000 --- a/apps/web/scripts/printer.mjs +++ /dev/null @@ -1,18 +0,0 @@ -import chalk from 'chalk'; -export const printer = { - debug: msg => { - const result = chalk.green`debug` + chalk.white(' - ' + msg); - console.log(result); - return result; - }, - info: msg => { - const result = chalk.rgb(19, 167, 205)`info` + chalk.white(' - ' + msg); - console.log(result); - return result; - }, - warn: msg => { - const result = chalk.yellow`warn` + chalk.white(' - ' + msg); - console.log(result); - return result; - }, -}; diff --git a/apps/web/src/atoms/index.ts b/apps/web/src/atoms/index.ts new file mode 100644 index 0000000000..d62ec0baf0 --- /dev/null +++ b/apps/web/src/atoms/index.ts @@ -0,0 +1,36 @@ +import { atom } from 'jotai'; +import { createStore } from 'jotai/index'; +import { atomWithStorage } from 'jotai/utils'; +import { unstable_batchedUpdates } from 'react-dom'; + +// workspace necessary atoms +export const currentWorkspaceIdAtom = atomWithStorage( + 'affine-current-workspace-id', + null +); +export const currentPageIdAtom = atomWithStorage( + 'affine-current-page-id', + null +); +// If the workspace is locked, it means that the user maybe updating the workspace +// from local to remote or vice versa +export const workspaceLockAtom = atom(false); +export async function lockMutex(fn: () => Promise) { + if (jotaiStore.get(workspaceLockAtom)) { + throw new Error('Workspace is locked'); + } + unstable_batchedUpdates(() => { + jotaiStore.set(workspaceLockAtom, true); + }); + await fn(); + unstable_batchedUpdates(() => { + jotaiStore.set(workspaceLockAtom, false); + }); +} + +// modal atoms +export const openWorkspacesModalAtom = atom(false); +export const openCreateWorkspaceModalAtom = atom(false); +export const openQuickSearchModalAtom = atom(false); + +export const jotaiStore = createStore(); diff --git a/apps/web/src/atoms/public-workspace/index.ts b/apps/web/src/atoms/public-workspace/index.ts new file mode 100644 index 0000000000..bf13b09882 --- /dev/null +++ b/apps/web/src/atoms/public-workspace/index.ts @@ -0,0 +1,48 @@ +import { atom } from 'jotai/index'; + +import { + BlockSuiteWorkspace, + LocalWorkspace, + RemWorkspaceFlavour, +} from '../../shared'; +import { apis } from '../../shared/apis'; +import { createEmptyBlockSuiteWorkspace } from '../../utils'; + +export const publicWorkspaceIdAtom = atom(null); +export const publicBlockSuiteAtom = atom>( + async get => { + const workspaceId = get(publicWorkspaceIdAtom); + if (!workspaceId) { + throw new Error('No workspace id'); + } + const binary = await apis.downloadWorkspace(workspaceId, true); + const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(workspaceId); + BlockSuiteWorkspace.Y.applyUpdate( + blockSuiteWorkspace.doc, + new Uint8Array(binary) + ); + blockSuiteWorkspace.awarenessStore.setFlag('enable_block_hub', false); + blockSuiteWorkspace.awarenessStore.setFlag('enable_set_remote_flag', false); + blockSuiteWorkspace.awarenessStore.setFlag('enable_database', false); + blockSuiteWorkspace.awarenessStore.setFlag( + 'enable_edgeless_toolbar', + false + ); + blockSuiteWorkspace.awarenessStore.setFlag('enable_slash_menu', false); + blockSuiteWorkspace.awarenessStore.setFlag('enable_drag_handle', false); + return new Promise(resolve => { + setTimeout(() => { + const workspace: LocalWorkspace = { + id: workspaceId, + blockSuiteWorkspace, + flavour: RemWorkspaceFlavour.LOCAL, + syncBinary: () => Promise.resolve(workspace), + providers: [], + }; + dataCenter.workspaces.push(workspace); + dataCenter.callbacks.forEach(cb => cb()); + resolve(blockSuiteWorkspace); + }, 0); + }); + } +); diff --git a/apps/web/src/blocksuite/__tests__/index.spec.ts b/apps/web/src/blocksuite/__tests__/index.spec.ts new file mode 100644 index 0000000000..3c22366dbd --- /dev/null +++ b/apps/web/src/blocksuite/__tests__/index.spec.ts @@ -0,0 +1,32 @@ +/** + * @vitest-environment happy-dom + */ +import 'fake-indexeddb/auto'; + +import { beforeEach, describe, expect, test } from 'vitest'; + +import { BlockSuiteWorkspace } from '../../shared'; +import { createAffineProviders, createLocalProviders } from '..'; + +let blockSuiteWorkspace: BlockSuiteWorkspace; + +beforeEach(() => { + blockSuiteWorkspace = new BlockSuiteWorkspace({ + room: 'test', + }); +}); + +describe('blocksuite providers', () => { + test('should be valid provider', () => { + [createLocalProviders, createAffineProviders].forEach(createProviders => { + createProviders(blockSuiteWorkspace).forEach(provider => { + expect(provider).toBeTypeOf('object'); + expect(provider).toHaveProperty('flavour'); + expect(provider).toHaveProperty('connect'); + expect(provider.connect).toBeTypeOf('function'); + expect(provider).toHaveProperty('disconnect'); + expect(provider.disconnect).toBeTypeOf('function'); + }); + }); + }); +}); diff --git a/apps/web/src/blocksuite/index.ts b/apps/web/src/blocksuite/index.ts new file mode 100644 index 0000000000..5d3c60019b --- /dev/null +++ b/apps/web/src/blocksuite/index.ts @@ -0,0 +1,26 @@ +import { BlockSuiteWorkspace, Provider } from '../shared'; +import { config } from '../shared/env'; +import { createIndexedDBProvider, createWebSocketProvider } from './providers'; + +export const createAffineProviders = ( + blockSuiteWorkspace: BlockSuiteWorkspace +): Provider[] => { + return ( + [ + createWebSocketProvider(blockSuiteWorkspace), + config.enableIndexedDBProvider && + createIndexedDBProvider(blockSuiteWorkspace), + ] as any[] + ).filter(v => Boolean(v)); +}; + +export const createLocalProviders = ( + blockSuiteWorkspace: BlockSuiteWorkspace +): Provider[] => { + return ( + [ + config.enableIndexedDBProvider && + createIndexedDBProvider(blockSuiteWorkspace), + ] as any[] + ).filter(v => Boolean(v)); +}; diff --git a/apps/web/src/blocksuite/providers/index.ts b/apps/web/src/blocksuite/providers/index.ts new file mode 100644 index 0000000000..b8df35b25e --- /dev/null +++ b/apps/web/src/blocksuite/providers/index.ts @@ -0,0 +1,71 @@ +import { WebsocketProvider } from '@affine/datacenter'; +import { assertExists } from '@blocksuite/store'; +import { IndexeddbPersistence } from 'y-indexeddb'; + +import { + AffineWebSocketProvider, + BlockSuiteWorkspace, + LocalIndexedDBProvider, +} from '../../shared'; +import { apis } from '../../shared/apis'; + +export const createWebSocketProvider = ( + blockSuiteWorkspace: BlockSuiteWorkspace +): AffineWebSocketProvider => { + let webSocketProvider: WebsocketProvider | null = null; + return { + flavour: 'affine-websocket', + cleanup: () => { + assertExists(webSocketProvider); + webSocketProvider?.destroy(); + }, + connect: () => { + const wsUrl = `${ + window.location.protocol === 'https:' ? 'wss' : 'ws' + }://${window.location.host}/api/sync/`; + webSocketProvider = new WebsocketProvider( + wsUrl, + blockSuiteWorkspace.room as string, + blockSuiteWorkspace.doc, + { + params: { token: apis.auth.refresh }, + // @ts-expect-error ignore the type + awareness: blockSuiteWorkspace.awarenessStore.awareness, + } + ); + console.log('connect', webSocketProvider.roomname); + webSocketProvider.connect(); + }, + disconnect: () => { + assertExists(webSocketProvider); + console.log('disconnect', webSocketProvider.roomname); + webSocketProvider?.disconnect(); + }, + }; +}; + +export const createIndexedDBProvider = ( + blockSuiteWorkspace: BlockSuiteWorkspace +): LocalIndexedDBProvider => { + let indexdbProvider: IndexeddbPersistence | null = null; + return { + flavour: 'local-indexeddb', + cleanup: () => { + assertExists(indexdbProvider); + indexdbProvider.clearData(); + }, + connect: () => { + console.info('connect indexeddb provider', blockSuiteWorkspace.room); + indexdbProvider = new IndexeddbPersistence( + blockSuiteWorkspace.room as string, + blockSuiteWorkspace.doc + ); + }, + disconnect: () => { + assertExists(indexdbProvider); + console.info('disconnect indexeddb provider', blockSuiteWorkspace.room); + indexdbProvider.destroy(); + indexdbProvider = null; + }, + }; +}; diff --git a/apps/web/src/components/404/index.tsx b/apps/web/src/components/404/index.tsx deleted file mode 100644 index c4b0aa31e4..0000000000 --- a/apps/web/src/components/404/index.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { Button } from '@affine/component'; -import { useTranslation } from '@affine/i18n'; -import Image from 'next/image'; -import { useRouter } from 'next/router'; - -import ErrorImg from '../../../public/imgs/invite-error.svg'; -import { StyledContainer } from './styles'; - -export const NotfoundPage = () => { - const { t } = useTranslation(); - const router = useRouter(); - return ( - - 404 - -

{t('404 - Page Not Found')}

- -
- ); -}; - -export default NotfoundPage; diff --git a/apps/web/src/components/404/styles.ts b/apps/web/src/components/404/styles.ts deleted file mode 100644 index c69fe45e74..0000000000 --- a/apps/web/src/components/404/styles.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { displayFlex, styled } from '@affine/component'; - -export const StyledContainer = styled.div(() => { - return { - ...displayFlex('center', 'center'), - flexDirection: 'column', - height: '100vh', - - img: { - width: '360px', - height: '270px', - }, - p: { - fontSize: '22px', - fontWeight: 600, - margin: '24px 0', - }, - }; -}); diff --git a/apps/web/src/components/__tests__/ProviderComposer.spec.tsx b/apps/web/src/components/__tests__/ProviderComposer.spec.tsx new file mode 100644 index 0000000000..9fc3942185 --- /dev/null +++ b/apps/web/src/components/__tests__/ProviderComposer.spec.tsx @@ -0,0 +1,30 @@ +/** + * @vitest-environment happy-dom + */ +import { render } from '@testing-library/react'; +import React, { createContext, useContext } from 'react'; +import { expect, test } from 'vitest'; + +import { ProviderComposer } from '../provider-composer'; + +test('ProviderComposer', async () => { + const Context = createContext('null'); + const Provider: React.FC = ({ children }) => { + return {children}; + }; + const ConsumerComponent = () => { + const value = useContext(Context); + return <>{value}; + }; + const Component = () => { + return ( + ]}> + + + ); + }; + + const result = render(); + await result.findByText('test1'); + expect(result.asFragment()).toMatchSnapshot(); +}); diff --git a/apps/web/src/components/__tests__/WorkSpaceSliderBar.spec.tsx b/apps/web/src/components/__tests__/WorkSpaceSliderBar.spec.tsx new file mode 100644 index 0000000000..a6d3127a21 --- /dev/null +++ b/apps/web/src/components/__tests__/WorkSpaceSliderBar.spec.tsx @@ -0,0 +1,89 @@ +/** + * @vitest-environment happy-dom + */ +import 'fake-indexeddb/auto'; + +import { assertExists } from '@blocksuite/store'; +import { render, renderHook } from '@testing-library/react'; +import { useRouter } from 'next/router'; +import { useCallback, useState } from 'react'; +import { describe, expect, test, vi } from 'vitest'; +import createFetchMock from 'vitest-fetch-mock'; + +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'; +import { useWorkspacesHelper } from '../../hooks/use-workspaces'; +import { ThemeProvider } from '../../providers/ThemeProvider'; +import { pathGenerator } from '../../shared'; +import { WorkSpaceSliderBar } from '../pure/workspace-slider-bar'; + +const fetchMocker = createFetchMock(vi); + +// fetchMocker.enableMocks(); + +describe('WorkSpaceSliderBar', () => { + test('basic', async () => { + // fetchMocker.mock + + const onOpenWorkspaceListModalFn = vi.fn(); + const onOpenQuickSearchModalFn = vi.fn(); + const mutationHook = renderHook(() => useWorkspacesHelper()); + const id = mutationHook.result.current.createRemLocalWorkspace('test0'); + mutationHook.result.current.createWorkspacePage(id, 'test1'); + const currentWorkspaceHook = renderHook(() => useCurrentWorkspace()); + let i = 0; + const Component = () => { + const [show, setShow] = useState(false); + const [currentWorkspace] = useCurrentWorkspace(); + const [currentPageId] = useCurrentPageId(); + assertExists(currentWorkspace); + const helper = useBlockSuiteWorkspaceHelper( + currentWorkspace.blockSuiteWorkspace + ); + return ( + {}, [])} + createPage={() => { + i++; + return helper.createPage('page-test-' + i); + }} + show={show} + setShow={setShow} + currentPath={useRouter().asPath} + paths={pathGenerator} + isPublicWorkspace={false} + /> + ); + }; + const App = () => { + return ( + + + + ); + }; + currentWorkspaceHook.result.current[1](id); + const app = render(); + const card = await app.findByTestId('current-workspace'); + expect(onOpenWorkspaceListModalFn).toBeCalledTimes(0); + card.click(); + expect(onOpenWorkspaceListModalFn).toBeCalledTimes(1); + const newPageButton = await app.findByTestId('new-page-button'); + newPageButton.click(); + expect( + currentWorkspaceHook.result.current[0]?.blockSuiteWorkspace.meta + .pageMetas[1].id + ).toBe('page-test-1'); + expect(onOpenQuickSearchModalFn).toBeCalledTimes(0); + const quickSearchButton = await app.findByTestId( + 'slider-bar-quick-search-button' + ); + quickSearchButton.click(); + expect(onOpenQuickSearchModalFn).toBeCalledTimes(1); + }); +}); diff --git a/apps/web/src/components/__tests__/__snapshots__/ProviderComposer.spec.tsx.snap b/apps/web/src/components/__tests__/__snapshots__/ProviderComposer.spec.tsx.snap new file mode 100644 index 0000000000..52836a18e3 --- /dev/null +++ b/apps/web/src/components/__tests__/__snapshots__/ProviderComposer.spec.tsx.snap @@ -0,0 +1,7 @@ +// Vitest Snapshot v1 + +exports[`ProviderComposer 1`] = ` + + test1 + +`; diff --git a/apps/web/src/components/affine/README.md b/apps/web/src/components/affine/README.md new file mode 100644 index 0000000000..82c58f9852 --- /dev/null +++ b/apps/web/src/components/affine/README.md @@ -0,0 +1,19 @@ +# Affine Official Workspace Component + +This component need specific configuration to work properly. + +## Configuration + +### SWR + +Each component use SWR to fetch data from the API. You need to provide a configuration to SWR to make it work. + +```tsx +const Wrapper = () => { + return ( + + + + ); +}; +``` diff --git a/apps/web/src/components/affine/affine-error-eoundary.tsx b/apps/web/src/components/affine/affine-error-eoundary.tsx new file mode 100644 index 0000000000..bd5958aa40 --- /dev/null +++ b/apps/web/src/components/affine/affine-error-eoundary.tsx @@ -0,0 +1,129 @@ +import { RequestError } from '@affine/datacenter'; +import { NextRouter } from 'next/router'; +import React, { Component, ErrorInfo } from 'react'; + +import { BlockSuiteWorkspace } from '../../shared'; + +export type AffineErrorBoundaryProps = React.PropsWithChildren<{ + router: NextRouter; +}>; + +export class PageNotFoundError extends TypeError { + readonly workspace: BlockSuiteWorkspace; + readonly pageId: string; + + constructor(workspace: BlockSuiteWorkspace, pageId: string) { + super(); + this.workspace = workspace; + this.pageId = pageId; + } +} + +export class WorkspaceNotFoundError extends TypeError { + readonly workspaceId: string; + + constructor(workspaceId: string) { + super(); + this.workspaceId = workspaceId; + } +} + +export class QueryParamError extends TypeError { + readonly targetKey: string; + readonly query: unknown; + + constructor(targetKey: string, query: unknown) { + super(); + this.targetKey = targetKey; + this.query = query; + } +} + +export class Unreachable extends Error { + constructor(message?: string) { + super(message); + } +} + +type AffineError = + | QueryParamError + | Unreachable + | WorkspaceNotFoundError + | PageNotFoundError + | RequestError + | Error; + +interface AffineErrorBoundaryState { + error: AffineError | null; +} + +export class AffineErrorBoundary extends Component< + AffineErrorBoundaryProps, + AffineErrorBoundaryState +> { + public state: AffineErrorBoundaryState = { + error: null, + }; + + public static getDerivedStateFromError( + error: AffineError + ): AffineErrorBoundaryState { + return { error }; + } + + public componentDidCatch(error: AffineError, errorInfo: ErrorInfo) { + console.error('Uncaught error:', error, errorInfo); + } + + public render() { + if (this.state.error) { + const error = this.state.error; + if (error instanceof PageNotFoundError) { + return ( + <> +

Sorry.. there was an error

+ <> + Page error + + Cannot find page {error.pageId} in workspace{' '} + {error.workspace.meta.name} + + + + + ); + } else if (error instanceof RequestError) { + return ( + <> +

Sorry.. there was an error

+ {error.message} + + ); + } + return ( + <> +

Sorry.. there was an error

+ + ); + } + + return this.props.children; + } +} diff --git a/apps/web/src/components/affine/enable-affine-cloud-modal/index.tsx b/apps/web/src/components/affine/enable-affine-cloud-modal/index.tsx new file mode 100644 index 0000000000..6e2dd0330e --- /dev/null +++ b/apps/web/src/components/affine/enable-affine-cloud-modal/index.tsx @@ -0,0 +1,64 @@ +import { IconButton, Modal, ModalWrapper } from '@affine/component'; +import { useTranslation } from '@affine/i18n'; +import { CloseIcon } from '@blocksuite/icons'; +import React from 'react'; + +import { useCurrentUser } from '../../../hooks/current/use-current-user'; +import { AffineRemoteWorkspace } from '../../../shared'; +import { Content, ContentTitle, Header, StyleButton, StyleTips } from './style'; + +interface EnableAffineCloudModalProps { + workspace: AffineRemoteWorkspace; + open: boolean; + onConfirm: () => void; + onClose: () => void; +} + +export const EnableAffineCloudModal: React.FC = ({ + onConfirm, + open, + onClose, +}) => { + const { t } = useTranslation(); + const user = useCurrentUser(); + + return ( + + +
+ { + onClose(); + }} + > + + +
+ + {t('Enable AFFiNE Cloud')}? + {t('Enable AFFiNE Cloud Description')} + {/* {t('Retain cached cloud data')} */} +
+ { + onConfirm(); + }} + > + {user ? t('Enable') : t('Sign in and Enable')} + + { + onClose(); + }} + > + {t('Not now')} + +
+
+
+
+ ); +}; diff --git a/apps/web/src/components/enable-workspace-modal/style.ts b/apps/web/src/components/affine/enable-affine-cloud-modal/style.ts similarity index 100% rename from apps/web/src/components/enable-workspace-modal/style.ts rename to apps/web/src/components/affine/enable-affine-cloud-modal/style.ts diff --git a/apps/web/src/components/enable-workspace-modal/index.tsx b/apps/web/src/components/affine/transform-workspace-to-affine-modal/index.tsx similarity index 51% rename from apps/web/src/components/enable-workspace-modal/index.tsx rename to apps/web/src/components/affine/transform-workspace-to-affine-modal/index.tsx index 7d9c696ae1..c7c38c31e9 100644 --- a/apps/web/src/components/enable-workspace-modal/index.tsx +++ b/apps/web/src/components/affine/transform-workspace-to-affine-modal/index.tsx @@ -1,31 +1,22 @@ -import { IconButton, Modal, ModalWrapper, toast } from '@affine/component'; +import { IconButton, Modal, ModalWrapper } from '@affine/component'; import { useTranslation } from '@affine/i18n'; import { CloseIcon } from '@blocksuite/icons'; -import { useRouter } from 'next/router'; -import { useCallback, useState } from 'react'; - -import { useGlobalState } from '@/store/app'; +import React from 'react'; +import { useCurrentUser } from '../../../hooks/current/use-current-user'; import { Content, ContentTitle, Header, StyleButton, StyleTips } from './style'; -interface EnableWorkspaceModalProps { +export type TransformWorkspaceToAffineModalProps = { open: boolean; onClose: () => void; -} + onConform: () => void; +}; -export const EnableWorkspaceModal = ({ - open, - onClose, -}: EnableWorkspaceModalProps) => { +export const TransformWorkspaceToAffineModal: React.FC< + TransformWorkspaceToAffineModalProps +> = ({ open, onClose, onConform }) => { const { t } = useTranslation(); - const login = useGlobalState(store => store.login); - const user = useGlobalState(store => store.user); - const dataCenter = useGlobalState(store => store.dataCenter); - const currentWorkspace = useGlobalState( - useCallback(store => store.currentDataCenterWorkspace, []) - ); - const [loading, setLoading] = useState(false); - const router = useRouter(); + const user = useCurrentUser(); return ( @@ -47,22 +38,22 @@ export const EnableWorkspaceModal = ({ { - setLoading(true); - if (user || (await login())) { - if (currentWorkspace) { - const workspace = await dataCenter.enableWorkspaceCloud( - currentWorkspace - ); - toast(t('Enabled success')); - - if (workspace) { - router.push(`/workspace/${workspace.id}/setting`); - } - } - } - setLoading(false); + onConform(); + // setLoading(true); + // if (user || (await login())) { + // if (currentWorkspace) { + // const workspace = await dataCenter.enableWorkspaceCloud( + // currentWorkspace + // ); + // toast(t('Enabled success')); + // + // if (workspace) { + // router.push(`/workspace/${workspace.id}/setting`); + // } + // } + // } + // setLoading(false); }} > {user ? t('Enable') : t('Sign in and Enable')} diff --git a/apps/web/src/components/affine/transform-workspace-to-affine-modal/style.ts b/apps/web/src/components/affine/transform-workspace-to-affine-modal/style.ts new file mode 100644 index 0000000000..d49dd1a77d --- /dev/null +++ b/apps/web/src/components/affine/transform-workspace-to-affine-modal/style.ts @@ -0,0 +1,40 @@ +import { Button, styled } from '@affine/component'; + +export const Header = styled('div')({ + height: '44px', + display: 'flex', + flexDirection: 'row-reverse', + paddingRight: '10px', + paddingTop: '10px', + flexShrink: 0, +}); + +export const Content = styled('div')({ + textAlign: 'center', +}); + +export const ContentTitle = styled('h1')({ + fontSize: '20px', + lineHeight: '28px', + fontWeight: 600, + textAlign: 'center', +}); + +export const StyleTips = styled('div')(() => { + return { + userSelect: 'none', + width: '400px', + margin: 'auto', + marginBottom: '32px', + marginTop: '12px', + }; +}); + +export const StyleButton = styled(Button)(() => { + return { + width: '284px', + display: 'block', + margin: 'auto', + marginTop: '16px', + }; +}); diff --git a/apps/web/src/components/affine/workspace-setting-detail/index.tsx b/apps/web/src/components/affine/workspace-setting-detail/index.tsx new file mode 100644 index 0000000000..1010183e79 --- /dev/null +++ b/apps/web/src/components/affine/workspace-setting-detail/index.tsx @@ -0,0 +1,159 @@ +import { useTranslation } from '@affine/i18n'; +import React, { + MouseEvent, + useCallback, + useEffect, + useMemo, + useRef, +} from 'react'; +import { preload } from 'swr'; + +import { useIsWorkspaceOwner } from '../../../hooks/affine/use-is-workspace-owner'; +import { fetcher, QueryKey } from '../../../plugins/affine/fetcher'; +import { + AffineOfficialWorkspace, + SettingPanel, + settingPanel, +} from '../../../shared'; +import { CollaborationPanel } from './panel/collaboration'; +import { ExportPanel } from './panel/export'; +import { GeneralPanel } from './panel/general'; +import { PublishPanel } from './panel/publish'; +import { + StyledIndicator, + StyledSettingContainer, + StyledSettingContent, + StyledTabButtonWrapper, + WorkspaceSettingTagItem, +} from './style'; + +export type WorkspaceSettingDetailProps = { + workspace: AffineOfficialWorkspace; + currentTab: SettingPanel; + onChangeTab: (tab: SettingPanel) => void; + onDeleteWorkspace: () => void; + onTransferWorkspace: (targetWorkspaceId: string) => void; +}; + +export type PanelProps = WorkspaceSettingDetailProps; + +const panelMap = { + [settingPanel.General]: { + name: 'General', + ui: GeneralPanel, + }, + [settingPanel.Collaboration]: { + name: 'Collaboration', + ui: CollaborationPanel, + }, + [settingPanel.Publish]: { + name: 'Publish', + ui: PublishPanel, + }, + [settingPanel.Export]: { + name: 'Export', + ui: ExportPanel, + }, +} satisfies { + [Key in SettingPanel]: { + name: string; + ui: React.FC; + }; +}; + +function assertInstanceOf( + obj: T, + type: new (...args: any[]) => U +): asserts obj is U { + if (!(obj instanceof type)) { + throw new Error('Object is not instance of type'); + } +} + +export const WorkspaceSettingDetail: React.FC< + WorkspaceSettingDetailProps +> = props => { + const { + workspace, + currentTab, + onChangeTab, + // onDeleteWorkspace, + // onTransferWorkspace, + } = props; + const isAffine = workspace.flavour === 'affine'; + const isOwner = useIsWorkspaceOwner(workspace); + if (!(workspace.flavour === 'affine' || workspace.flavour === 'local')) { + throw new Error('Unsupported workspace flavour'); + } + if (!(currentTab in panelMap)) { + throw new Error('Invalid activeTab: ' + currentTab); + } + const { t } = useTranslation(); + const workspaceId = workspace.id; + useEffect(() => { + if (isAffine && isOwner) { + preload([QueryKey.getMembers, workspaceId], fetcher); + } + }, [isAffine, isOwner, workspaceId]); + const containerRef = useRef(null); + const indicatorRef = useRef(null); + const startTransaction = useCallback(() => { + if (indicatorRef.current && containerRef.current) { + const indicator = indicatorRef.current; + const activeTabElement = containerRef.current.querySelector( + `[data-tab-key="${currentTab}"]` + ); + assertInstanceOf(activeTabElement, HTMLElement); + requestAnimationFrame(() => { + indicator.style.left = `${activeTabElement.offsetLeft}px`; + indicator.style.width = `${activeTabElement.offsetWidth}px`; + }); + } + }, [currentTab]); + const handleTabClick = useCallback( + (event: MouseEvent) => { + assertInstanceOf(event.target, HTMLElement); + const key = event.target.getAttribute('data-tab-key'); + if (!key || !(key in panelMap)) { + throw new Error('data-tab-key is invalid: ' + key); + } + onChangeTab(key as SettingPanel); + startTransaction(); + }, + [onChangeTab, startTransaction] + ); + const Component = useMemo(() => panelMap[currentTab].ui, [currentTab]); + return ( + + + {Object.entries(panelMap).map(([key, value]) => { + if (!isAffine && key === 'Sync') { + return null; + } + return ( + + {t(value.name)} + + ); + })} + { + indicatorRef.current = ref; + startTransaction(); + }} + /> + + + + + + ); +}; diff --git a/apps/web/src/components/affine/workspace-setting-detail/panel/collaboration/index.tsx b/apps/web/src/components/affine/workspace-setting-detail/panel/collaboration/index.tsx new file mode 100644 index 0000000000..ec0035a8cc --- /dev/null +++ b/apps/web/src/components/affine/workspace-setting-detail/panel/collaboration/index.tsx @@ -0,0 +1,227 @@ +import { + Button, + IconButton, + Menu, + MenuItem, + toast, + Wrapper, +} from '@affine/component'; +import { PermissionType } from '@affine/datacenter'; +import { useTranslation } from '@affine/i18n'; +import { + DeleteTemporarilyIcon, + EmailIcon, + MoreVerticalIcon, +} from '@blocksuite/icons'; +import React, { useCallback, useState } from 'react'; + +import { lockMutex } from '../../../../../atoms'; +import { useMembers } from '../../../../../hooks/affine/use-members'; +import { transformWorkspace } from '../../../../../plugins'; +import { + AffineRemoteWorkspace, + LocalWorkspace, + RemWorkspaceFlavour, +} from '../../../../../shared'; +import { Unreachable } from '../../../affine-error-eoundary'; +import { TransformWorkspaceToAffineModal } from '../../../transform-workspace-to-affine-modal'; +import { PanelProps } from '../../index'; +import { InviteMemberModal } from './invite-member-modal'; +import { + StyledMemberAvatar, + StyledMemberButtonContainer, + StyledMemberContainer, + StyledMemberEmail, + StyledMemberInfo, + StyledMemberListContainer, + StyledMemberListItem, + StyledMemberName, + StyledMemberNameContainer, + StyledMemberRoleContainer, + StyledMemberTitleContainer, + StyledMoreVerticalButton, + StyledMoreVerticalDiv, +} from './style'; + +const AffineRemoteCollaborationPanel: React.FC< + Omit & { + workspace: AffineRemoteWorkspace; + } +> = ({ workspace }) => { + const [isInviteModalShow, setIsInviteModalShow] = useState(false); + const { t } = useTranslation(); + const { members, removeMember } = useMembers(workspace.id); + return ( + <> + +
    + + + {t('Users')} ({members.length}) + + + {t('Access level')} + +
    +
    +
+ + + {members.length > 0 && ( + <> + {members + .sort((b, a) => a.type - b.type) + .map((member, index) => { + const user = { + avatar_url: '', + id: '', + name: '', + ...member.user, + }; + return ( + + + + + + + + {user.name} + + {member.user.email} + + + + + {member.accepted + ? member.type !== PermissionType.Owner + ? t('Member') + : t('Owner') + : t('Pending')} + + {member.type === PermissionType.Owner ? ( + + ) : ( + + + { + // FIXME: remove ignore + + // @ts-ignore + await removeMember(member.id); + toast( + t('Member has been removed', { + name: user.name, + }) + ); + }} + icon={} + > + {t('Remove from workspace')} + + + } + placement="bottom-end" + disablePortal={true} + trigger="click" + > + + + + + + )} + + ); + })} + + )} + + + + +
+ { + setIsInviteModalShow(false); + }, [])} + onInviteSuccess={useCallback(() => { + setIsInviteModalShow(false); + }, [])} + workspaceId={workspace.id} + open={isInviteModalShow} + /> + + ); +}; + +const LocalCollaborationPanel: React.FC< + Omit & { + workspace: LocalWorkspace; + } +> = ({ workspace, onTransferWorkspace }) => { + const { t } = useTranslation(); + const [open, setOpen] = useState(false); + return ( + <> + {t('Collaboration Description')} + + { + setOpen(false); + }} + onConform={() => { + // todo(himself65): move this function out of affine component + lockMutex(async () => { + const id = await transformWorkspace( + RemWorkspaceFlavour.LOCAL, + RemWorkspaceFlavour.AFFINE, + workspace + ); + onTransferWorkspace(id); + setOpen(false); + }); + }} + /> + + ); +}; + +export const CollaborationPanel: React.FC = props => { + switch (props.workspace.flavour) { + case RemWorkspaceFlavour.AFFINE: { + const workspace = props.workspace as AffineRemoteWorkspace; + return ( + + ); + } + case RemWorkspaceFlavour.LOCAL: { + const workspace = props.workspace as LocalWorkspace; + return ; + } + } + throw new Unreachable(); +}; diff --git a/apps/web/src/components/workspace-setting/member/InviteMemberModal.tsx b/apps/web/src/components/affine/workspace-setting-detail/panel/collaboration/invite-member-modal/index.tsx similarity index 61% rename from apps/web/src/components/workspace-setting/member/InviteMemberModal.tsx rename to apps/web/src/components/affine/workspace-setting-detail/panel/collaboration/invite-member-modal/index.tsx index 7d9e1aaab5..fddb323852 100644 --- a/apps/web/src/components/workspace-setting/member/InviteMemberModal.tsx +++ b/apps/web/src/components/affine/workspace-setting-detail/panel/collaboration/invite-member-modal/index.tsx @@ -3,12 +3,13 @@ import { Modal, ModalCloseButton, ModalWrapper } from '@affine/component'; import { Button } from '@affine/component'; import { Input } from '@affine/component'; import { MuiAvatar } from '@affine/component'; -import { User } from '@affine/datacenter'; import { useTranslation } from '@affine/i18n'; import { EmailIcon } from '@blocksuite/icons'; -import { useState } from 'react'; +import React, { Suspense, useCallback, useState } from 'react'; + +import { useMembers } from '../../../../../../hooks/affine/use-members'; +import { useUsersByEmail } from '../../../../../../hooks/affine/use-users-by-email'; -import useMembers from '@/hooks/use-members'; interface LoginModalProps { open: boolean; onClose: () => void; @@ -16,60 +17,47 @@ interface LoginModalProps { onInviteSuccess: () => void; } -export const debounce = any>( - fn: T, - time?: number, - immediate?: boolean -): ((...args: any) => any) => { - let timeoutId: null | number; - let defaultImmediate = immediate || false; - const delay = time || 300; +const gmailReg = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@gmail\.com$/; - return (...args: any) => { - if (defaultImmediate) { - fn.apply(this, args); - defaultImmediate = false; - return; - } - if (timeoutId) { - clearTimeout(timeoutId); - } - - // @ts-ignore - timeoutId = setTimeout(() => { - fn.apply(this, args); - timeoutId = null; - }, delay); - }; +const Result: React.FC<{ + workspaceId: string; + queryEmail: string; +}> = ({ workspaceId, queryEmail }) => { + const users = useUsersByEmail(workspaceId, queryEmail); + const firstUser = users?.at(0) ?? null; + if (!firstUser || !firstUser.email) { + return null; + } + return ( + + + {firstUser.avatar_url ? ( + + ) : ( + + + + )} + {firstUser.email} + {/*
invited
*/} +
+
+ ); }; -const gmailReg = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@gmail\.com$/; export const InviteMemberModal = ({ open, onClose, onInviteSuccess, + workspaceId, }: LoginModalProps) => { + const { inviteMember } = useMembers(workspaceId); const [email, setEmail] = useState(''); - const [showMember, setShowMember] = useState(false); - const [showTip, setShowTip] = useState(false); - const [userData, setUserData] = useState(null); - const { inviteMember, getUserByEmail } = useMembers(); + const [showMemberPreview, setShowMemberPreview] = useState(false); const { t } = useTranslation(); - const inputChange = (value: string) => { - setShowMember(true); - if (gmailReg.test(value)) { - setEmail(value); - setShowTip(false); - getUserByEmail(value).then(data => { - if (data?.name) { - setUserData(data); - setShowTip(false); - } - }); - } else { - setShowTip(true); - } - }; + const inputChange = useCallback((value: string) => { + setEmail(value); + }, []); return (
@@ -89,36 +77,24 @@ export const InviteMemberModal = ({ width={360} value={email} onChange={inputChange} - onBlur={() => { - setShowMember(false); - }} + onFocus={useCallback(() => { + setShowMemberPreview(true); + }, [])} + onBlur={useCallback(() => { + setShowMemberPreview(false); + }, [])} placeholder={t('Invite placeholder')} > - {showMember ? ( - - {showTip ? ( - {t('Non-Gmail')} - ) : ( - - {userData?.avatar ? ( - - ) : ( - - - - )} - {email} - {/*
invited
*/} -
- )} -
- ) : ( - <> + {showMemberPreview && gmailReg.test(email) && ( + + + )}
- } - onClick={async () => { - onLogin(); - }} - > - {t('Sign in')} - - )} - - ); -}; diff --git a/apps/web/src/components/workspace-modal/WorkspaceCard.tsx b/apps/web/src/components/workspace-modal/WorkspaceCard.tsx deleted file mode 100644 index 6de4352e30..0000000000 --- a/apps/web/src/components/workspace-modal/WorkspaceCard.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import { WorkspaceUnit } from '@affine/datacenter'; -import { useTranslation } from '@affine/i18n'; -import { useCallback } from 'react'; - -import { - CloudWorkspaceIcon, - JoinedWorkspaceIcon, - LocalDataIcon, - LocalWorkspaceIcon, - PublishIcon, -} from '@/components/icons'; -import { WorkspaceUnitAvatar } from '@/components/workspace-avatar'; -import { useGlobalState } from '@/store/app'; - -import { StyledCard, StyleWorkspaceInfo, StyleWorkspaceTitle } from './styles'; - -const WorkspaceType = ({ workspaceData }: { workspaceData: WorkspaceUnit }) => { - const user = useGlobalState(store => store.user); - const { t } = useTranslation(); - const isOwner = user?.id === workspaceData.owner?.id; - - if (workspaceData.provider === 'local') { - return ( -

- - {t('Local Workspace')} -

- ); - } - - return isOwner ? ( -

- - {t('Cloud Workspace')} -

- ) : ( -

- - {t('Joined Workspace')} -

- ); -}; - -export const WorkspaceCard = ({ - workspaceData, - onClick, -}: { - workspaceData: WorkspaceUnit; - onClick: (data: WorkspaceUnit) => void; -}) => { - const currentWorkspace = useGlobalState( - useCallback(store => store.currentDataCenterWorkspace, []) - ); - const { t } = useTranslation(); - return ( - { - onClick(workspaceData); - }} - active={workspaceData.id === currentWorkspace?.id} - > - - - - - {workspaceData.name || 'AFFiNE'} - - - {workspaceData.provider === 'local' && ( -

- - {t('Available Offline')} -

- )} - {workspaceData.published && ( -

- - {t('Published to Web')} -

- )} -
-
- ); -}; diff --git a/apps/web/src/components/workspace-modal/index.tsx b/apps/web/src/components/workspace-modal/index.tsx deleted file mode 100644 index d857810ab9..0000000000 --- a/apps/web/src/components/workspace-modal/index.tsx +++ /dev/null @@ -1,149 +0,0 @@ -import { Modal, ModalCloseButton, ModalWrapper } from '@affine/component'; -import { Tooltip } from '@affine/component'; -import { useTranslation } from '@affine/i18n'; -import { HelpIcon, PlusIcon } from '@blocksuite/icons'; -import { useRouter } from 'next/router'; -import { useState } from 'react'; - -import { useGlobalState } from '@/store/app'; - -import { CreateWorkspaceModal } from '../create-workspace'; -import { LoginModal } from '../login-modal'; -import { LogoutModal } from '../logout-modal'; -import { Footer } from './Footer'; -import { LanguageMenu } from './SelectLanguageMenu'; -import { - StyledCard, - StyledHelperContainer, - StyledModalContent, - StyledModalHeader, - StyledModalHeaderLeft, - StyledModalTitle, - StyledOperationWrapper, - StyledSplitLine, - StyleWorkspaceAdd, - StyleWorkspaceInfo, - StyleWorkspaceTitle, -} from './styles'; -import { WorkspaceCard } from './WorkspaceCard'; -interface WorkspaceModalProps { - open: boolean; - onClose: () => void; -} - -export const WorkspaceModal = ({ open, onClose }: WorkspaceModalProps) => { - const [createWorkspaceOpen, setCreateWorkspaceOpen] = useState(false); - const logout = useGlobalState(store => store.logout); - const dataCenter = useGlobalState(store => store.dataCenter); - const router = useRouter(); - const { t } = useTranslation(); - const [loginOpen, setLoginOpen] = useState(false); - const [logoutOpen, setLogoutOpen] = useState(false); - - return ( - <> - - - - - {t('My Workspaces')} - - - - - - - - - - - { - onClose(); - }} - absolute={false} - /> - - - - - {dataCenter.workspaces.map((item, index) => { - return ( - { - router.push(`/workspace/${workspaceData.id}/all`); - onClose(); - }} - key={index} - > - ); - })} - { - setCreateWorkspaceOpen(true); - }} - > - - - - - - {t('New Workspace')} -

{t('Create Or Import')}

-
-
-
- -