Compare commits

...

32 Commits

Author SHA1 Message Date
himself65
ec50d721ea chore: release 0.5.3 2023-04-16 16:04:21 -05:00
Himself65
7bbe67af43 refactor: workspace loading logic (#1966) 2023-04-16 16:02:41 -05:00
Himself65
caa292e097 test: mark public single page as fail (#1967) 2023-04-16 09:45:50 -05:00
HeJiachen-PM
73b8b805c6 Rewrite section 2.3 2023-04-16 15:19:22 +08:00
HeJiachen-PM
084d4e043a Add summery to subsections in section 2 2023-04-16 15:09:08 +08:00
HeJiachen-PM
69a9c34f11 Rewrite the third section 2023-04-16 04:37:35 +08:00
Himself65
d742cab1d5 fix: hydration error (#1961) 2023-04-15 13:10:24 -05:00
Horus
8b3c1fb363 fix: force to use powershell on windows to fix zx script crash (#1962) 2023-04-15 12:24:57 -05:00
Horus
ec445207d6 fix: fix windows build client error and release cannot open (#1959) 2023-04-16 00:00:47 +08:00
HeJiachen-PM
49281e68a6 Rewrite the second section 2023-04-15 15:31:56 +08:00
HeJiachen-PM
a918d6e14c Proofreading introduction 2023-04-15 15:27:09 +08:00
Himself65
7cf7187893 docs: add behind-the-code.md (#1957) 2023-04-15 00:19:13 -05:00
Himself65
2383165470 refactor: remove NoSsr on top level (#1951) 2023-04-14 17:07:41 -05:00
Himself65
43a96fe8e3 fix: move suspense to the correct place (#1954) 2023-04-14 15:44:23 -05:00
Himself65
b771a2504b test: fix flaky (#1953) 2023-04-14 15:03:16 -05:00
himself65
8d2fefb5f8 ci: fix labeler.yml 2023-04-14 14:14:58 -05:00
himself65
c71e5f1c96 fix(cli): run dev server at 8080 2023-04-14 11:06:22 -05:00
Skye Sun
5b96fb0db3 docs: update CLA.md (#1950) 2023-04-14 08:02:21 -05:00
Peng Xiao
46cd0c5c9a fix: share url (#1948) 2023-04-14 08:01:31 -05:00
Qi
261a41f8da feat: add history back & forward for desktop app (#1926) 2023-04-14 09:19:52 +00:00
Himself65
bd387f6551 fix: theme color (#1944) 2023-04-14 02:13:14 -05:00
JimmFly
5335118e93 chore: add translation (#1946) 2023-04-14 15:02:43 +08:00
Himself65
70313eb5ee chore: bump version (#1943) 2023-04-14 01:57:54 -05:00
himself65
ccd2b79d20 docs: update logo in README.md 2023-04-14 00:38:35 -05:00
Himself65
5ca94db5d2 fix: effect deps (#1940) 2023-04-14 00:24:44 -05:00
Himself65
d58f9db289 docs: update BUG-REPORT.yml (#1941) 2023-04-13 22:27:01 -05:00
Chi Zhang
93e78c315c Update jobs.md 2023-04-14 10:27:45 +08:00
himself65
3954f309aa chore: fix packages version 2023-04-13 18:33:21 -05:00
himself65
f902d0c324 ci: fix cache in build-master.yml 2023-04-13 18:22:20 -05:00
Himself65
e79fb1ae3a build: add log when coverage (#1933) 2023-04-13 18:20:41 -05:00
Himself65
08d67b316c docs: update README.md (#1931) 2023-04-13 17:54:20 -05:00
himself65
d12c00d5cb ci: fix coverage report 2023-04-13 17:53:34 -05:00
159 changed files with 4190 additions and 2733 deletions

View File

@@ -4,3 +4,4 @@ dist
out
storybook-static
affine-out
_next

View File

@@ -10,6 +10,7 @@ module.exports = {
},
extends: [
'eslint:recommended',
'plugin:react-hooks/recommended',
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'plugin:@typescript-eslint/recommended',

1
.github/CLA.md vendored
View File

@@ -53,3 +53,4 @@ Example:
- Aditya Sharma, @adityash1, 2023/03/21
- Fangdun Tsai, @fundon, 2023/03/21
- Zhilin Liu, @lzlme, 2023/04/09
- Skye Sun, @skyesun, 2023/04/14

View File

@@ -23,6 +23,11 @@ body:
options:
- app.affine.pro
- stage.affine.pro
- dev.affine.live
- affine-preview.vercel.app
- macOS x64
- macOS ARM 64
- Windows x64
validations:
required: true
- type: dropdown

2
.github/labeler.yml vendored
View File

@@ -3,7 +3,7 @@ docs:
- '**/README.md'
- 'packages/templates/**/*'
tests:
test:
- 'tests/**/*'
- '**/tests/**/*'
- '**/__tests__/**/*'

View File

@@ -137,9 +137,9 @@ jobs:
with:
path: |
${{ github.workspace }}/apps/web/.next/cache
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }}
key: ${{ runner.os }}-nextjs-dev-${{ hashFiles('**/yarn.lock') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }}
restore-keys: |
${{ runner.os }}-nextjs-${{ hashFiles('**/yarn.lock') }}-
${{ runner.os }}-nextjs-dev-${{ hashFiles('**/yarn.lock') }}-
- name: Build
run: yarn build
@@ -153,6 +153,7 @@ jobs:
NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID }}
API_SERVER_PROFILE: local
ENABLE_DEBUG_PAGE: true
COVERAGE: true
- name: Upload artifact
uses: actions/upload-artifact@v3
@@ -230,6 +231,10 @@ jobs:
name: storybook
path: ./packages/component/storybook-static
- name: Wait for Octobase Ready
run: |
node ./scripts/wait-3000-healthz.mjs
- name: Run playwright tests
run: yarn test --forbid-only --shard=${{ matrix.shard }}/${{ strategy.job-total }}
env:

View File

@@ -75,6 +75,7 @@ jobs:
NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID }}
API_SERVER_PROFILE: local
ENABLE_DEBUG_PAGE: true
COVERAGE: true
- name: Upload artifact
uses: actions/upload-artifact@v3
@@ -149,6 +150,10 @@ jobs:
name: storybook
path: ./packages/component/storybook-static
- name: Wait for Octobase Ready
run: |
node ./scripts/wait-3000-healthz.mjs
- name: Run playwright tests
run: yarn test --forbid-only --shard=${{ matrix.shard }}/${{ strategy.job-total }}
env:

2
.gitignore vendored
View File

@@ -5,7 +5,7 @@
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
.yarn/versions
# compiled output
*dist

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -11,5 +11,9 @@ npmPublishRegistry: 'https://registry.npmjs.org'
plugins:
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
spec: '@yarnpkg/plugin-interactive-tools'
- path: .yarn/plugins/@yarnpkg/plugin-version.cjs
spec: '@yarnpkg/plugin-version'
- path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
spec: '@yarnpkg/plugin-workspace-tools'
yarnPath: .yarn/releases/yarn-3.5.0.cjs

View File

@@ -24,7 +24,11 @@ See https://github.com/all-?/all-contributors/issues/361#issuecomment-637166066
<!-- ALL-CONTRIBUTORS-BADGE:END -->
[![affine-app-logo]](https://app.affine.pro)
[![AFFiNE Web](<https://img.shields.io/badge/-Try%20It%20Online%20%E2%86%92-rgb(84,56,255)?style=flat-square&logoColor=white&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAADAAAAAwAEwd99eAAABjElEQVRYhe1W0U3DMBB9RfyTDeoNyAYNG2QDOgJsECYgGxA26AZ4hIxgJqCZ4PjIGV+tUxK7raqiPsmKdXe5e3fOs7IiIlwSdxetfiNw7QRKAD0Ax/ssrI5QgQOw5v03AJOTJHcCL1x84LVmWzJyJlBg7P4BwCvb3pmIAbBPykZEqaulEU7YHNva1HypxUsKqIS9EvbynASs0n3ss+ciUIsuO8VvhL9emjdFBa3YO8XvALwpsZNYSqBB0PwUWgRZNksSL5GhlN0ngGd+dkpsD6AG8IGlslxwTh2fa09EBc3Dir32rRysuQlUAL54/wTAcpePPAXHPsOTGXhSEv69rAlYpZOt6DSO29J4D/TRRLJk6AvtaZSY9PkCFYVLqI9i/NF5YkkECgrXa6P4fVEn4iolrhNxRQqBZu7FqMNdZiMqAUPj2KdGZyicu1dHzlGqBHxn2sdTR53bmeJ+ebJd7LtXhGH4uQEwd0ttAPzMxGi5/6BdxTuMej41Bs59gGP+CU+Cq/4tvxH4HwR+Ab3Uqr/VGbqEAAAAAElFTkSuQmCC>)](https://app.affine.pro)
[![AFFiNE macOS M1/M2 Chip](https://img.shields.io/badge/-macOS_M_Chip%20%E2%86%92-black?style=flat-square&logo=apple&logoColor=white)](https://github.com/toeverything/AFFiNE/releases/latest)
[![AFFiNE macOS x64](https://img.shields.io/badge/-macOS_x86%20%E2%86%92-black?style=flat-square&logo=apple&logoColor=white)](https://github.com/toeverything/AFFiNE/releases/latest)
[![AFFiNE Window x64](https://img.shields.io/badge/-Windows%20%E2%86%92-blue?style=flat-square&logo=windows&logoColor=white)](https://github.com/toeverything/AFFiNE/releases/latest)
[![stars-icon]](https://github.com/toeverything/AFFiNE)
[![All Contributors][all-contributors-badge]](#contributors)
[![codecov]](https://codecov.io/gh/toeverything/AFFiNE)
@@ -36,6 +40,8 @@ See https://github.com/all-?/all-contributors/issues/361#issuecomment-637166066
</div>
---
<div align="center">
<a href="http://affine.pro"><img src="https://img.shields.io/badge/-AFFiNE-06449d?style=social&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEsAAABLCAMAAAAPkIrYAAAAP1BMVEU8b9w8b9w+b947cNw7b9w6b908b909b9w8b9w7b9w8b9w7cN08b9w7b908b9w7b9w8b907cNw8b9w8b91HcEx3NJCJAAAAFXRSTlP/3QWSgA+lHPlu6Di4XtIrxk/xRADGudUoAAAB9UlEQVR42tWYwbKjIBREG0GJKkRj/v9bZ1ZvRC99rzib11tTB9qqnKoW3/+X38vy7ifzQ1b/wk/8Q1bCv3y6Z6wFh2x2llIRGB6xRhzz6p+wVhRJD1gRZZYHrADYSyqsjFPGZtYbuFESesUysZXlcMnYyJpxTW5keQh5N7G6CUJCE2uHFNfEGiBmbmB1H4jxDawNcqbuPmtAJTtj6RZ0lpIwiR5jNmgfNtHHwLXPWfFYcS2NMdxkjac/dNaNCJPo3yf9pFuseHbDrBsRFguGs8te8Q4rXzTjVSPCIHp3FePKWbzi30xE+4zlBMmoJaGLfpLUmAmLiN4Xyibahy76WZRQMLJ2WX27on2oFvQVac8yi4p+J2forA0V8W1c++AVS1f1H6p9KKLHxk9RWKmsyB+VLC76gV65DLjokdg5KmsEMXsiDwXWSmTc9ezSoKJHoi9zUVihbMHfQOSsXB7Mrz1S1huKPde69sEsiKgNt8hYTjiWlAyENeu7IFe1D15RSEBN+yCiXw17K1RZm/w7UtJVWYN8f1ZyLlkVb2bT4vIVVrINH1dqX2YttkHmIWsfVWs646wcRFYis6fIVGpfYq1kjpGSW8kSRD+xYSmXRM0Ang9eSZioVdy/5pWaLqzIRyIpuVxYozvGf1m67I7pf/s3UXv+AP61NI2Y+BbSAAAAAElFTkSuQmCC" height=25></a>
&nbsp;
@@ -55,7 +61,6 @@ See https://github.com/all-?/all-contributors/issues/361#issuecomment-637166066
<em>See docs, canvas and tables are hyper merged with AFFiNE - just like the word affine (əˈɪn | a-fine).</em>
</div>
<br />
</div>
![img_v2_37a7cc04-ab3f-4405-ae9a-f84ceb4c948g](https://user-images.githubusercontent.com/79301703/230892907-5fd5c0c5-1665-4d75-8a35-744e0afc36a5.gif)
@@ -260,7 +265,6 @@ See [LICENSE] for details.
[jobs available]: ./docs/jobs.md
[latest packages]: https://github.com/toeverything/AFFiNE/pkgs/container/affine-self-hosted
[contributor license agreement]: https://github.com/toeverything/affine/edit/master/.github/CLA.md
[affine-app-logo]: https://img.shields.io/static/v1?label=Try%20Online&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAhpJREFUWEdjZEACtnl3MxgY/0YzMjAaMzAwcCLLUYH9/T/D/7MM/5mXHp6kPANmHiOI4Zx9Xfg3C+tKBob/zlSwiAgjGPey/vkdvneq5luwA+zy7+yhn+Vwv+89NFHFhREU7IyM/6YT4WyqK/n/nymT0Tb/1mFGBkYbqptOhIH/Gf4fYbTLv/2NBgmOCOvBSr6DHPCfWNW0UEe2A2x1uRlakiXBbtpx6jND+7KXZLmPbAdURokzeJjxwi31rrzH8OX7P5IdQbYDtnUoMXBzMMEt7Fj2imH7qU/0cQBy8MNsPHL5K0P13Of0cQB68MNsJScaSI4CHk4mhq3tSnCf3n36k0FZmh3Mn7L+DcPqgx9ICgWSHeBpxsdQESUGtgRk+eqDH+H8O09/MiR3P6atA1qTJRlsdLnhPgYlPOQQCW96wPDi3R+iHUFSCKAHP8wydEeREg0kOQA9+JOgwR1qL8CQEygC9jWp0UCSA+aVysIT3JqDHxgmr38DtlRCiIVhZZ0CPNhB6QDkEGIA0Q4gZAkuxxFyBNEOQA7ml+/+MIQ1PUAxG1kelAhB6YMYQLQDCPmQUAjhcgxRDiDWcEKOxOYIohyQGyjCEGIvANaPLfhhBiNHA6hmBBXNhABRDgCV/aBQAAFQpYMrn4PUgNTCACiXEMoNRDmAkC8okR8UDhjYRumAN8sHvGMCSkAD2jUDOWDAO6ewbDQQ3XMAy/oxKownQR0AAAAASUVORK5CYII=&color=orange&message=%E2%86%92
[rust-version-icon]: https://img.shields.io/badge/Rust-1.70.0-dea584
[stars-icon]: https://img.shields.io/github/stars/toeverything/AFFiNE.svg?style=flat&logo=github&colorB=red&label=stars
[codecov]: https://codecov.io/gh/toeverything/affine/branch/master/graphs/badge.svg?branch=master

View File

@@ -1,5 +1,5 @@
cacheFolder: '../../.yarn/cache'
# deferredVersionFolder: '../../.yarn/versions'
deferredVersionFolder: '../../.yarn/versions'
globalFolder: '../../.yarn/global'
installStatePath: '../../.yarn/install-state.gz'
patchFolder: '../../.yarn/patches'

View File

@@ -1,21 +1,17 @@
# AFFiNE Electron App
# ⚠️ NOTE ⚠️
Due to PNPM related issues, this project is currently using **yarn 3**.
See https://github.com/electron/forge/issues/2633
## Development
```
# in project root, start web app at :8080
To run AFFiNE Desktop Client Application locally, run the following commands:
```sh
# in repo root
yarn install
yarn dev
# build octobase-node
yarn workspace @affine/octobase-node build
# in /apps/electron, start electron app
yarn dev
# in apps/electron
yarn generate-assets
yarn dev # or yarn prod for production build
```
## Credits

View File

@@ -7,6 +7,7 @@ import { BrowserWindow, ipcMain, nativeTheme } from 'electron';
import fs from 'fs-extra';
import { parse } from 'url';
import { isMacOS } from '../../../utils';
import { getExchangeTokenParams, oauthEndpoint } from './google-auth';
const AFFINE_ROOT = path.join(os.homedir(), '.affine');
@@ -32,12 +33,15 @@ export const registerHandlers = () => {
ipcMain.handle('ui:sidebar-visibility-change', async (_, visible) => {
// todo
const windows = BrowserWindow.getAllWindows();
windows.forEach(w => {
// hide window buttons when sidebar is not visible
w.setWindowButtonVisibility(visible);
});
logger.info('sidebar visibility change', visible);
// detect if os is macos
if (isMacOS()) {
const windows = BrowserWindow.getAllWindows();
windows.forEach(w => {
// hide window buttons when sidebar is not visible
w.setWindowButtonVisibility(visible);
});
logger.info('sidebar visibility change', visible);
}
});
ipcMain.handle('ui:get-google-oauth-code', async () => {

View File

@@ -14,12 +14,12 @@ async function createWindow() {
const browserWindow = new BrowserWindow({
titleBarStyle: isMacOS() ? 'hiddenInset' : 'default',
trafficLightPosition: { x: 20, y: 18 },
trafficLightPosition: { x: 24, y: 18 },
x: mainWindowState.x,
y: mainWindowState.y,
width: mainWindowState.width,
minWidth: 640,
transparent: true,
transparent: isMacOS(),
visualEffectState: 'active',
vibrancy: 'under-window',
height: mainWindowState.height,

View File

@@ -2,7 +2,7 @@
"name": "@affine/electron",
"productName": "AFFiNE",
"private": true,
"version": "0.0.0",
"version": "0.5.3",
"author": "affine",
"description": "AFFiNE App",
"homepage": "https://github.com/toeverything/AFFiNE",

View File

@@ -29,6 +29,10 @@ console.log('build with following dir', {
await cleanup();
echo('Clean up done');
if (process.platform === 'win32') {
$.shell = 'powershell.exe';
$.prefix = '';
}
// step 1: build web (nextjs) dist
cd(repoRootDir);
await $`yarn add`;

View File

@@ -38,11 +38,11 @@ __metadata:
"@affine/i18n": "workspace:*"
"@affine/jotai": "workspace:*"
"@affine/workspace": "workspace:^"
"@blocksuite/blocks": 0.0.0-20230413112150-e058f87e-nightly
"@blocksuite/editor": 0.0.0-20230413112150-e058f87e-nightly
"@blocksuite/global": 0.0.0-20230413112150-e058f87e-nightly
"@blocksuite/blocks": 0.0.0-20230413190748-4d32b79a-nightly
"@blocksuite/editor": 0.0.0-20230413190748-4d32b79a-nightly
"@blocksuite/global": 0.0.0-20230413190748-4d32b79a-nightly
"@blocksuite/icons": ^2.1.10
"@blocksuite/store": 0.0.0-20230413112150-e058f87e-nightly
"@blocksuite/store": 0.0.0-20230413190748-4d32b79a-nightly
"@dnd-kit/core": ^6.0.8
"@dnd-kit/sortable": ^7.0.2
"@emotion/cache": ^11.10.7
@@ -67,7 +67,7 @@ __metadata:
"@storybook/test-runner": ^0.10.0
"@storybook/testing-library": ^0.1.0
"@toeverything/hooks": "workspace:*"
"@types/react": =18.0.31
"@types/react": ^18.0.35
"@types/react-dnd": ^3.0.2
"@types/react-dom": 18.0.11
"@vanilla-extract/css": ^1.11.0
@@ -75,6 +75,7 @@ __metadata:
clsx: ^1.2.1
concurrently: ^8.0.1
jest-mock: ^29.5.0
jotai: ^2.0.4
kebab-case: ^1.0.2
lit: ^2.7.2
lottie-web: ^5.11.0
@@ -92,11 +93,11 @@ __metadata:
wait-on: ^7.0.1
yjs: ^13.5.52
peerDependencies:
"@blocksuite/blocks": 0.0.0-20230409084303-221991d4-nightly
"@blocksuite/editor": 0.0.0-20230409084303-221991d4-nightly
"@blocksuite/global": 0.0.0-20230409084303-221991d4-nightly
"@blocksuite/icons": 2.1.10
"@blocksuite/store": 0.0.0-20230409084303-221991d4-nightly
"@blocksuite/blocks": "*"
"@blocksuite/editor": "*"
"@blocksuite/global": "*"
"@blocksuite/icons": "*"
"@blocksuite/store": "*"
languageName: unknown
linkType: soft
@@ -141,7 +142,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "@affine/env@workspace:../../packages/env"
dependencies:
"@blocksuite/global": 0.0.0-20230413112150-e058f87e-nightly
"@blocksuite/global": 0.0.0-20230413190748-4d32b79a-nightly
lit: ^2.7.2
next: =13.2.3
react: ^18.2.0
@@ -175,9 +176,17 @@ __metadata:
resolution: "@affine/jotai@workspace:../../packages/jotai"
dependencies:
"@affine/env": "workspace:*"
"@blocksuite/blocks": 0.0.0-20230413190748-4d32b79a-nightly
"@blocksuite/editor": 0.0.0-20230413190748-4d32b79a-nightly
"@blocksuite/global": 0.0.0-20230413190748-4d32b79a-nightly
"@blocksuite/store": 0.0.0-20230413190748-4d32b79a-nightly
jotai: ^2.0.4
lottie-web: ^5.11.0
peerDependencies:
"@blocksuite/blocks": "*"
"@blocksuite/editor": "*"
"@blocksuite/global": "*"
"@blocksuite/store": "*"
lottie-web: "*"
languageName: unknown
linkType: soft
@@ -219,8 +228,8 @@ __metadata:
yjs: ^13.5.52
zod: ^3.21.4
peerDependencies:
"@blocksuite/blocks": 0.0.0-20230409084303-221991d4-nightly
"@blocksuite/store": 0.0.0-20230409084303-221991d4-nightly
"@blocksuite/blocks": "*"
"@blocksuite/store": "*"
languageName: unknown
linkType: soft
@@ -1718,14 +1727,14 @@ __metadata:
languageName: node
linkType: hard
"@blocksuite/blocks@npm:0.0.0-20230413112150-e058f87e-nightly":
version: 0.0.0-20230413112150-e058f87e-nightly
resolution: "@blocksuite/blocks@npm:0.0.0-20230413112150-e058f87e-nightly"
"@blocksuite/blocks@npm:0.0.0-20230413190748-4d32b79a-nightly":
version: 0.0.0-20230413190748-4d32b79a-nightly
resolution: "@blocksuite/blocks@npm:0.0.0-20230413190748-4d32b79a-nightly"
dependencies:
"@blocksuite/connector": 0.0.0-20230413112150-e058f87e-nightly
"@blocksuite/global": 0.0.0-20230413112150-e058f87e-nightly
"@blocksuite/phasor": 0.0.0-20230413112150-e058f87e-nightly
"@blocksuite/virgo": 0.0.0-20230413112150-e058f87e-nightly
"@blocksuite/connector": 0.0.0-20230413190748-4d32b79a-nightly
"@blocksuite/global": 0.0.0-20230413190748-4d32b79a-nightly
"@blocksuite/phasor": 0.0.0-20230413190748-4d32b79a-nightly
"@blocksuite/virgo": 0.0.0-20230413190748-4d32b79a-nightly
"@popperjs/core": ^2.11.6
hotkeys-js: ^3.10.1
lit: ^2.6.1
@@ -1734,36 +1743,36 @@ __metadata:
turndown: ^7.1.1
zod: ^3.21.4
peerDependencies:
"@blocksuite/store": 0.0.0-20230413112150-e058f87e-nightly
checksum: 973c7215673a647ca2e2a009dc10c6e4a3aa960ef2b21dacc4c5331c907fc181d898557e2ccd9a575ce7e5e80b8dcf92af9496a02281e2dcc62aca8dd1130a7c
"@blocksuite/store": 0.0.0-20230413190748-4d32b79a-nightly
checksum: b7796ef629af37dd248372de17c446425bb1e7c6b6392a42486c94c95260462fbfd1e163273a2ec5133deafcfdf187e01a5327c4ca7cf44d75e1fa9a2b06e480
languageName: node
linkType: hard
"@blocksuite/connector@npm:0.0.0-20230413112150-e058f87e-nightly":
version: 0.0.0-20230413112150-e058f87e-nightly
resolution: "@blocksuite/connector@npm:0.0.0-20230413112150-e058f87e-nightly"
checksum: 4e399e9f5948ec8b3e1cb2fe920a97c6b0cb1fced936b15d24e3649674a57215c919ac65fd24693b491adfa8ffd5f28460404eff054f89cb5f60bcd2b15fdc12
"@blocksuite/connector@npm:0.0.0-20230413190748-4d32b79a-nightly":
version: 0.0.0-20230413190748-4d32b79a-nightly
resolution: "@blocksuite/connector@npm:0.0.0-20230413190748-4d32b79a-nightly"
checksum: 8245e6f8423fadf75bc9637ac856223efbb9e771e95e76d051f64d5f940b0c36206478f3aeccbf6f674157771de84e99b693220ba21713587b49c396d1e45d17
languageName: node
linkType: hard
"@blocksuite/editor@npm:0.0.0-20230413112150-e058f87e-nightly":
version: 0.0.0-20230413112150-e058f87e-nightly
resolution: "@blocksuite/editor@npm:0.0.0-20230413112150-e058f87e-nightly"
"@blocksuite/editor@npm:0.0.0-20230413190748-4d32b79a-nightly":
version: 0.0.0-20230413190748-4d32b79a-nightly
resolution: "@blocksuite/editor@npm:0.0.0-20230413190748-4d32b79a-nightly"
dependencies:
"@blocksuite/global": 0.0.0-20230413112150-e058f87e-nightly
"@blocksuite/global": 0.0.0-20230413190748-4d32b79a-nightly
lit: ^2.6.1
marked: ^4.2.12
turndown: ^7.1.1
peerDependencies:
"@blocksuite/blocks": 0.0.0-20230413112150-e058f87e-nightly
"@blocksuite/store": 0.0.0-20230413112150-e058f87e-nightly
checksum: 7f40a54150b81a16c6c8017587f61326c5d879ef1506fb6b56194da958b0720ed30f795a62541a1f6acaf7204b17168ed8f7343f91eb8ea93b01d0e7e0466364
"@blocksuite/blocks": 0.0.0-20230413190748-4d32b79a-nightly
"@blocksuite/store": 0.0.0-20230413190748-4d32b79a-nightly
checksum: 85587f088132e18707aaf03b111d61a942e368114e10bcef8a1fba1f295c603c473f7bf6697486992f6638aa9f51d4d86456a62de0787a2f60a0b05fffe87596
languageName: node
linkType: hard
"@blocksuite/global@npm:0.0.0-20230413112150-e058f87e-nightly":
version: 0.0.0-20230413112150-e058f87e-nightly
resolution: "@blocksuite/global@npm:0.0.0-20230413112150-e058f87e-nightly"
"@blocksuite/global@npm:0.0.0-20230413190748-4d32b79a-nightly":
version: 0.0.0-20230413190748-4d32b79a-nightly
resolution: "@blocksuite/global@npm:0.0.0-20230413190748-4d32b79a-nightly"
dependencies:
ansi-colors: ^4.1.3
zod: ^3.21.4
@@ -1772,7 +1781,7 @@ __metadata:
peerDependenciesMeta:
lit:
optional: true
checksum: 067cede6856db7725eaf903123f07f3a107d8b70b04d06ce1f7ebc781fe3b0f3c765462c4b92a39f73ee2abeac9030124d3addd5d58e33a8863a83d3d03d14db
checksum: c19950a3a9218331c68d17bd1b6b4c701bf4227e087813fb031812234d1941c09882da5f2f6c2b99759e5a37ec8bc660e9165c62f82674ab8f61325faaaa26b3
languageName: node
linkType: hard
@@ -1786,26 +1795,26 @@ __metadata:
languageName: node
linkType: hard
"@blocksuite/phasor@npm:0.0.0-20230413112150-e058f87e-nightly":
version: 0.0.0-20230413112150-e058f87e-nightly
resolution: "@blocksuite/phasor@npm:0.0.0-20230413112150-e058f87e-nightly"
"@blocksuite/phasor@npm:0.0.0-20230413190748-4d32b79a-nightly":
version: 0.0.0-20230413190748-4d32b79a-nightly
resolution: "@blocksuite/phasor@npm:0.0.0-20230413190748-4d32b79a-nightly"
dependencies:
"@blocksuite/global": 0.0.0-20230413112150-e058f87e-nightly
"@blocksuite/global": 0.0.0-20230413190748-4d32b79a-nightly
fractional-indexing: ^3.2.0
perfect-freehand: ^1.2.0
peerDependencies:
nanoid: ^4
yjs: ^13
checksum: 6cc62664823ce1026ad7d908e9071cba05a9bc9542e371ede0f9e930d868f89b1d5219da226a7621ac8da5091ddafbf48e6d742e378cc5d1a6e124d9fd968e75
checksum: 8126b22df8d18015d9b9b2c5a2187d59faa7961871e82a1af78a798d7322c979eb992a22b49c328198db0723efea4d41c05d4627ade8f69c5c8411712fa65d31
languageName: node
linkType: hard
"@blocksuite/store@npm:0.0.0-20230413112150-e058f87e-nightly":
version: 0.0.0-20230413112150-e058f87e-nightly
resolution: "@blocksuite/store@npm:0.0.0-20230413112150-e058f87e-nightly"
"@blocksuite/store@npm:0.0.0-20230413190748-4d32b79a-nightly":
version: 0.0.0-20230413190748-4d32b79a-nightly
resolution: "@blocksuite/store@npm:0.0.0-20230413190748-4d32b79a-nightly"
dependencies:
"@blocksuite/global": 0.0.0-20230413112150-e058f87e-nightly
"@blocksuite/virgo": 0.0.0-20230413112150-e058f87e-nightly
"@blocksuite/global": 0.0.0-20230413190748-4d32b79a-nightly
"@blocksuite/virgo": 0.0.0-20230413190748-4d32b79a-nightly
"@types/flexsearch": ^0.7.3
buffer: ^6.0.3
flexsearch: 0.7.21
@@ -1819,20 +1828,20 @@ __metadata:
zod: ^3.21.4
peerDependencies:
yjs: ^13
checksum: a5bbde8dacac844d7b22b8d56cc5b4a0e7762945395cd02b0c29ef31dd3134b90d2bdfa1b77745a853d8095af3e0b429aee8db8a7c59c903182b34a2b429ff7d
checksum: 435026941ec650cb3fc6bf5740496f9c659786f2e11c774584fc9002870a22d53a8840d1fcbb6ad2057d51fd4b67d72bc09e6d6bde1c909e5dd0dc2fb512a095
languageName: node
linkType: hard
"@blocksuite/virgo@npm:0.0.0-20230413112150-e058f87e-nightly":
version: 0.0.0-20230413112150-e058f87e-nightly
resolution: "@blocksuite/virgo@npm:0.0.0-20230413112150-e058f87e-nightly"
"@blocksuite/virgo@npm:0.0.0-20230413190748-4d32b79a-nightly":
version: 0.0.0-20230413190748-4d32b79a-nightly
resolution: "@blocksuite/virgo@npm:0.0.0-20230413190748-4d32b79a-nightly"
dependencies:
"@blocksuite/global": 0.0.0-20230413112150-e058f87e-nightly
"@blocksuite/global": 0.0.0-20230413190748-4d32b79a-nightly
zod: ^3.21.4
peerDependencies:
lit: ^2
yjs: ^13
checksum: 53b7c5e87db7714cd6de49e8e9d2027b53cd75c44ad40aa5aec9ea862996da603a0e41894c94fdb6a6ef62ec9810820fe3dffa3ef8b247684e8157d44bb5fa69
checksum: a09c20c102f67974905a835df40741a4e6a34c267eeec8bc7779ce5026329a736f4d21ee35b8af4f076bb033669b23ec9ea736a6f281c07998de125e67fbca8d
languageName: node
linkType: hard
@@ -5486,8 +5495,8 @@ __metadata:
version: 0.0.0-use.local
resolution: "@toeverything/y-indexeddb@workspace:../../packages/y-indexeddb"
dependencies:
"@blocksuite/blocks": 0.0.0-20230413112150-e058f87e-nightly
"@blocksuite/store": 0.0.0-20230413112150-e058f87e-nightly
"@blocksuite/blocks": 0.0.0-20230413190748-4d32b79a-nightly
"@blocksuite/store": 0.0.0-20230413190748-4d32b79a-nightly
idb: ^7.1.1
vite: ^4.2.1
vite-plugin-dts: ^2.2.0
@@ -6033,7 +6042,7 @@ __metadata:
languageName: node
linkType: hard
"@types/react@npm:*, @types/react@npm:>=16":
"@types/react@npm:*, @types/react@npm:>=16, @types/react@npm:^18.0.35":
version: 18.0.35
resolution: "@types/react@npm:18.0.35"
dependencies:
@@ -6044,17 +6053,6 @@ __metadata:
languageName: node
linkType: hard
"@types/react@npm:=18.0.31":
version: 18.0.31
resolution: "@types/react@npm:18.0.31"
dependencies:
"@types/prop-types": "*"
"@types/scheduler": "*"
csstype: ^3.0.2
checksum: 6befbd5587e266905b50fd6bbd7c1cacd557bddf99e6a9862ca2f1d06df3dca71b9d485a37d010479730f021aab93b852d417c714de5efc2f41be0ff4c09b4db
languageName: node
linkType: hard
"@types/responselike@npm:^1.0.0":
version: 1.0.0
resolution: "@types/responselike@npm:1.0.0"

View File

@@ -22,6 +22,10 @@ if (enableDebugLocal) {
console.info('Debugging local blocksuite');
}
if (process.env.COVERAGE === 'true') {
console.info('Enable coverage report');
}
const profileTarget = {
ac: '100.85.73.88:12001',
dev: '100.84.105.99:11001',
@@ -74,6 +78,7 @@ const nextConfig = {
},
reactStrictMode: true,
transpilePackages: [
'jotai-devtools',
'@affine/component',
'@affine/i18n',
'@affine/debug',

View File

@@ -17,11 +17,11 @@
"@affine/jotai": "workspace:*",
"@affine/templates": "workspace:*",
"@affine/workspace": "workspace:*",
"@blocksuite/blocks": "0.0.0-20230413112150-e058f87e-nightly",
"@blocksuite/editor": "0.0.0-20230413112150-e058f87e-nightly",
"@blocksuite/global": "0.0.0-20230413112150-e058f87e-nightly",
"@blocksuite/blocks": "0.0.0-20230413190748-4d32b79a-nightly",
"@blocksuite/editor": "0.0.0-20230413190748-4d32b79a-nightly",
"@blocksuite/global": "0.0.0-20230413190748-4d32b79a-nightly",
"@blocksuite/icons": "^2.1.10",
"@blocksuite/store": "0.0.0-20230413112150-e058f87e-nightly",
"@blocksuite/store": "0.0.0-20230413190748-4d32b79a-nightly",
"@dnd-kit/core": "^6.0.8",
"@dnd-kit/sortable": "^7.0.2",
"@emotion/cache": "^11.10.7",
@@ -29,12 +29,14 @@
"@emotion/server": "^11.10.0",
"@emotion/styled": "^11.10.6",
"@mui/material": "^5.12.0",
"@react-hookz/web": "^23.0.0",
"@sentry/nextjs": "^7.47.0",
"@toeverything/hooks": "workspace:*",
"cmdk": "^0.2.0",
"css-spring": "^4.1.0",
"dayjs": "^1.11.7",
"jotai": "^2.0.4",
"jotai-devtools": "^0.4.0",
"lit": "^2.7.2",
"lottie-web": "^5.11.0",
"next-themes": "^0.2.1",
@@ -53,7 +55,7 @@
"@sentry/webpack-plugin": "^1.20.0",
"@swc-jotai/debug-label": "^0.0.9",
"@swc-jotai/react-refresh": "^0.0.7",
"@types/react": "=18.0.31",
"@types/react": "^18.0.35",
"@types/react-dom": "^18.0.11",
"@types/webpack-env": "^1.18.0",
"@vanilla-extract/css": "^1.11.0",

View File

@@ -1,40 +1,68 @@
import { DebugLogger } from '@affine/debug';
import { atomWithSyncStorage } from '@affine/jotai';
import { jotaiWorkspacesAtom } from '@affine/workspace/atom';
import type { EditorContainer } from '@blocksuite/editor';
import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
import {
rootCurrentEditorAtom,
rootCurrentPageIdAtom,
rootCurrentWorkspaceIdAtom,
rootWorkspacesMetadataAtom,
} from '@affine/workspace/atom';
import type { Page } from '@blocksuite/store';
import { assertExists } from '@blocksuite/store';
import { atom } from 'jotai';
import { WorkspacePlugins } from '../plugins';
import type { AllWorkspace } from '../shared';
const logger = new DebugLogger('web:atoms');
// workspace necessary atoms
export const currentWorkspaceIdAtom = atom<string | null>(null);
export const currentPageIdAtom = atom<string | null>(null);
export const currentEditorAtom = atom<Readonly<EditorContainer> | null>(null);
/**
* @deprecated Use `rootCurrentWorkspaceIdAtom` directly instead.
*/
export const currentWorkspaceIdAtom = rootCurrentWorkspaceIdAtom;
// todo(himself65): move this to the workspace package
rootWorkspacesMetadataAtom.onMount = setAtom => {
function createFirst(): RootWorkspaceMetadata[] {
const Plugins = Object.values(WorkspacePlugins).sort(
(a, b) => a.loadPriority - b.loadPriority
);
return Plugins.flatMap(Plugin => {
return Plugin.Events['app:init']?.().map(
id =>
({
id,
flavour: Plugin.flavour,
} satisfies RootWorkspaceMetadata)
);
}).filter((ids): ids is RootWorkspaceMetadata => !!ids);
}
setAtom(metadata => {
if (metadata.length === 0) {
const newMetadata = createFirst();
logger.info('create first workspace', newMetadata);
return newMetadata;
}
return metadata;
});
};
/**
* @deprecated Use `rootCurrentPageIdAtom` directly instead.
*/
export const currentPageIdAtom = rootCurrentPageIdAtom;
/**
* @deprecated Use `rootCurrentEditorAtom` directly instead.
*/
export const currentEditorAtom = rootCurrentEditorAtom;
// modal atoms
export const openWorkspacesModalAtom = atom(false);
export const openCreateWorkspaceModalAtom = atom(false);
export const openQuickSearchModalAtom = atom(false);
export const workspacesAtom = atom<Promise<AllWorkspace[]>>(async get => {
const flavours: string[] = Object.values(WorkspacePlugins).map(
plugin => plugin.flavour
);
const jotaiWorkspaces = get(jotaiWorkspacesAtom).filter(workspace =>
flavours.includes(workspace.flavour)
);
const workspaces = await Promise.all(
jotaiWorkspaces.map(workspace => {
const plugin =
WorkspacePlugins[workspace.flavour as keyof typeof WorkspacePlugins];
assertExists(plugin);
const { CRUD } = plugin;
return CRUD.get(workspace.id);
})
);
return workspaces.filter(workspace => workspace !== null) as AllWorkspace[];
});
export { workspacesAtom } from './root';
type View = { id: string; mode: 'page' | 'edgeless' };

View File

@@ -9,13 +9,17 @@ import { affineApis } from '../../shared/apis';
function createPublicWorkspace(
workspaceId: string,
binary: ArrayBuffer
binary: ArrayBuffer,
singlePage = false
): AffinePublicWorkspace {
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
workspaceId,
(k: string) =>
// fixme: token could be expired
({ api: `api/workspace`, token: getLoginStorage()?.token }[k])
({ api: `api/workspace`, token: getLoginStorage()?.token }[k]),
{
cachePrefix: WorkspaceFlavour.PUBLIC + (singlePage ? '-single-page' : ''),
}
);
BlockSuiteWorkspace.Y.applyUpdate(
blockSuiteWorkspace.doc,
@@ -49,7 +53,7 @@ export const publicPageBlockSuiteAtom = atom<Promise<AffinePublicWorkspace>>(
workspaceId,
pageId
);
return createPublicWorkspace(workspaceId, binary);
return createPublicWorkspace(workspaceId, binary, true);
}
);
export const publicWorkspaceAtom = atom<Promise<AffinePublicWorkspace>>(
@@ -59,6 +63,6 @@ export const publicWorkspaceAtom = atom<Promise<AffinePublicWorkspace>>(
throw new Error('No workspace id');
}
const binary = await affineApis.downloadWorkspace(workspaceId, true);
return createPublicWorkspace(workspaceId, binary);
return createPublicWorkspace(workspaceId, binary, false);
}
);

View File

@@ -0,0 +1,78 @@
//#region async atoms that to load the real workspace data
import { DebugLogger } from '@affine/debug';
import {
rootCurrentWorkspaceIdAtom,
rootWorkspacesMetadataAtom,
} from '@affine/workspace/atom';
import { assertExists } from '@blocksuite/store';
import { atom } from 'jotai';
import { WorkspacePlugins } from '../plugins';
import type { AllWorkspace } from '../shared';
const logger = new DebugLogger('web:atoms:root');
/**
* Fetch all workspaces from the Plugin CRUD
*/
export const workspacesAtom = atom<Promise<AllWorkspace[]>>(async get => {
const flavours: string[] = Object.values(WorkspacePlugins).map(
plugin => plugin.flavour
);
const jotaiWorkspaces = get(rootWorkspacesMetadataAtom).filter(workspace =>
flavours.includes(workspace.flavour)
);
const workspaces = await Promise.all(
jotaiWorkspaces.map(workspace => {
const plugin =
WorkspacePlugins[workspace.flavour as keyof typeof WorkspacePlugins];
assertExists(plugin);
const { CRUD } = plugin;
return CRUD.get(workspace.id);
})
);
logger.info('workspaces', workspaces);
workspaces.forEach(workspace => {
if (workspace === null) {
console.warn(
'workspace is null. this should not happen. If you see this error, please report it to the developer.'
);
}
});
return workspaces.filter(workspace => workspace !== null) as AllWorkspace[];
});
/**
* This will throw an error if the workspace is not found,
* should not be used on the root component,
* use `rootCurrentWorkspaceIdAtom` instead
*/
export const rootCurrentWorkspaceAtom = atom<Promise<AllWorkspace>>(
async get => {
const metadata = get(rootWorkspacesMetadataAtom);
const targetId = get(rootCurrentWorkspaceIdAtom);
if (targetId === null) {
throw new Error(
'current workspace id is null. this should not happen. If you see this error, please report it to the developer.'
);
}
const targetWorkspace = metadata.find(meta => meta.id === targetId);
if (!targetWorkspace) {
throw new Error(`cannot find the workspace with id ${targetId}.`);
}
const workspace = await WorkspacePlugins[targetWorkspace.flavour].CRUD.get(
targetWorkspace.id
);
if (!workspace) {
throw new Error(
`cannot find the workspace with id ${targetId} in the plugin ${targetWorkspace.flavour}.`
);
}
return workspace;
}
);
// Do not add `rootCurrentWorkspacePageAtom`, this is not needed.
// It can be derived from `rootCurrentWorkspaceAtom` and `rootCurrentPageIdAtom`
//#endregion

View File

@@ -1,4 +1,3 @@
'use client';
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
import type { EditorContainer } from '@blocksuite/editor';
import type { Page } from '@blocksuite/store';
@@ -11,7 +10,9 @@ import { BlockSuiteEditor } from '../../blocksuite/block-suite-editor';
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
'test',
_ => undefined,
Generator.AutoIncrement
{
idGenerator: Generator.AutoIncrement,
}
);
const page = blockSuiteWorkspace.createPage('page0');

View File

@@ -3,6 +3,7 @@
*/
import 'fake-indexeddb/auto';
import { rootCurrentWorkspaceIdAtom } from '@affine/workspace/atom';
import type { PageMeta } from '@blocksuite/store';
import matchers from '@testing-library/jest-dom/matchers';
import type { RenderResult } from '@testing-library/react';
@@ -12,11 +13,9 @@ import type { FC, PropsWithChildren } from 'react';
import { beforeEach, describe, expect, test } from 'vitest';
import { workspacesAtom } from '../../atoms';
import {
currentWorkspaceAtom,
useCurrentWorkspace,
} from '../../hooks/current/use-current-workspace';
import { useWorkspacesHelper } from '../../hooks/use-workspaces';
import { rootCurrentWorkspaceAtom } from '../../atoms/root';
import { useCurrentWorkspace } from '../../hooks/current/use-current-workspace';
import { useAppHelper } from '../../hooks/use-workspaces';
import { ThemeProvider } from '../../providers/ThemeProvider';
import type { BlockSuiteWorkspace } from '../../shared';
import type { PinboardProps } from '../pure/workspace-slider-bar/Pinboard';
@@ -42,24 +41,26 @@ const initPinBoard = async () => {
// - pinboard2
// - noPinboardPage
const mutationHook = renderHook(() => useWorkspacesHelper(), {
const mutationHook = renderHook(() => useAppHelper(), {
wrapper: ProviderWrapper,
});
const rootPageIds = ['hasPinboardPage', 'noPinboardPage'];
const pinboardPageIds = ['pinboard1', 'pinboard2'];
const id = await mutationHook.result.current.createLocalWorkspace('test0');
await store.get(workspacesAtom);
mutationHook.rerender();
await store.get(currentWorkspaceAtom);
store.set(rootCurrentWorkspaceIdAtom, id);
await store.get(workspacesAtom);
await store.get(rootCurrentWorkspaceAtom);
const currentWorkspaceHook = renderHook(() => useCurrentWorkspace(), {
wrapper: ProviderWrapper,
});
currentWorkspaceHook.result.current[1](id);
const currentWorkspace = await store.get(currentWorkspaceAtom);
const currentWorkspace = await store.get(rootCurrentWorkspaceAtom);
const blockSuiteWorkspace =
currentWorkspace?.blockSuiteWorkspace as BlockSuiteWorkspace;
mutationHook.rerender();
// create root pinboard
mutationHook.result.current.createWorkspacePage(id, 'rootPinboard');
blockSuiteWorkspace.meta.setPageMeta('rootPinboard', {
@@ -73,7 +74,7 @@ const initPinBoard = async () => {
subpageIds: rootPageId === rootPageIds[0] ? pinboardPageIds : [],
});
});
// create children to firs parent
// create children to first parent
pinboardPageIds.forEach(pinboardId => {
mutationHook.result.current.createWorkspacePage(id, pinboardId);
blockSuiteWorkspace.meta.setPageMeta(pinboardId, {

View File

@@ -3,23 +3,27 @@
*/
import 'fake-indexeddb/auto';
import {
rootCurrentPageIdAtom,
rootCurrentWorkspaceIdAtom,
} from '@affine/workspace/atom';
import { WorkspaceFlavour } from '@affine/workspace/type';
import { assertExists } from '@blocksuite/store';
import { render, renderHook } from '@testing-library/react';
import { createStore, getDefaultStore, Provider } from 'jotai';
import { createStore, getDefaultStore, Provider, useAtomValue } from 'jotai';
import { useRouter } from 'next/router';
import type React from 'react';
import { useCallback } from 'react';
import { beforeEach, describe, expect, test, vi } from 'vitest';
import { workspacesAtom } from '../../atoms';
import { useCurrentPageId } from '../../hooks/current/use-current-page-id';
import { rootCurrentWorkspaceAtom } from '../../atoms/root';
import {
currentWorkspaceAtom,
useCurrentWorkspace,
} from '../../hooks/current/use-current-workspace';
import { useBlockSuiteWorkspaceHelper } from '../../hooks/use-blocksuite-workspace-helper';
import { useWorkspacesHelper } from '../../hooks/use-workspaces';
import { useAppHelper } from '../../hooks/use-workspaces';
import { ThemeProvider } from '../../providers/ThemeProvider';
import { pathGenerator } from '../../shared';
import { WorkSpaceSliderBar } from '../pure/workspace-slider-bar';
@@ -45,21 +49,22 @@ describe('WorkSpaceSliderBar', () => {
const onOpenWorkspaceListModalFn = vi.fn();
const onOpenQuickSearchModalFn = vi.fn();
const mutationHook = renderHook(() => useWorkspacesHelper(), {
const mutationHook = renderHook(() => useAppHelper(), {
wrapper: ProviderWrapper,
});
const id = await mutationHook.result.current.createLocalWorkspace('test0');
await store.get(workspacesAtom);
mutationHook.rerender();
mutationHook.result.current.createWorkspacePage(id, 'test1');
await store.get(currentWorkspaceAtom);
store.set(rootCurrentWorkspaceIdAtom, id);
await store.get(rootCurrentWorkspaceAtom);
const currentWorkspaceHook = renderHook(() => useCurrentWorkspace(), {
wrapper: ProviderWrapper,
});
let i = 0;
const Component = () => {
const [currentWorkspace] = useCurrentWorkspace();
const [currentPageId] = useCurrentPageId();
const currentPageId = useAtomValue(rootCurrentPageIdAtom);
assertExists(currentWorkspace);
const helper = useBlockSuiteWorkspaceHelper(
currentWorkspace.blockSuiteWorkspace

View File

@@ -12,7 +12,8 @@ export const StyledCollapsedButton = styled('button')<{
}>(({ collapse, show = true, theme }) => {
return {
width: '16px',
height: '16px',
height: '100%',
...displayFlex('center', 'center'),
fontSize: '16px',
position: 'absolute',
left: '0',
@@ -21,9 +22,13 @@ export const StyledCollapsedButton = styled('button')<{
margin: 'auto',
color: theme.colors.iconColor,
opacity: '.6',
transition: 'opacity .15s ease-in-out',
display: show ? 'flex' : 'none',
svg: {
transform: `rotate(${collapse ? '0' : '-90'}deg)`,
transform: `rotate(${collapse ? '-90' : '0'}deg)`,
},
':hover': {
opacity: '1',
},
};
});

View File

@@ -13,12 +13,14 @@ import { StyledSidebarSwitch } from './style';
type SidebarSwitchProps = {
visible?: boolean;
tooltipContent?: string;
testid?: string;
};
// fixme: the following code is not correct, SSR will fail because hydrate will not match the client side render
// in `StyledSidebarSwitch` component
export const SidebarSwitch = ({
visible = true,
tooltipContent,
testid = '',
...props
}: SidebarSwitchProps) => {
useUpdateTipsOnVersionChange();
const [open, setOpen] = useSidebarStatus();
@@ -38,9 +40,9 @@ export const SidebarSwitch = ({
visible={tooltipVisible}
>
<StyledSidebarSwitch
{...props}
visible={visible}
disabled={!visible}
data-testid={testid}
onClick={useCallback(() => {
setOpen(!open);
setTooltipVisible(false);

View File

@@ -89,8 +89,8 @@ const PublishPanelAffine: React.FC<PublishPanelAffineProps> = ({
<Wrapper marginBottom="42px">{t('Publishing Description')}</Wrapper>
<Button
data-testid="publish-to-web-button"
onClick={() => {
publishWorkspace(true);
onClick={async () => {
await publishWorkspace(true);
}}
type="light"
shape="circle"

View File

@@ -3,14 +3,20 @@ import { WorkspaceFlavour } from '@affine/workspace/type';
import { CloseIcon } from '@blocksuite/icons';
import type { Page } from '@blocksuite/store';
import type { FC, HTMLAttributes, PropsWithChildren } from 'react';
import { forwardRef, useEffect, useMemo, useState } from 'react';
import {
forwardRef,
lazy,
Suspense,
useEffect,
useMemo,
useState,
} from 'react';
import {
useSidebarFloating,
useSidebarStatus,
} from '../../../hooks/use-sidebar-status';
import type { AffineOfficialWorkspace } from '../../../shared';
import { SidebarSwitch } from '../../affine/sidebar-switch';
import { EditorOptionMenu } from './header-right-items/EditorOptionMenu';
import EditPage from './header-right-items/EditPage';
import { HeaderShareMenu } from './header-right-items/ShareMenu';
@@ -27,6 +33,12 @@ import {
} from './styles';
import { OSWarningMessage, shouldShowWarning } from './utils';
const SidebarSwitch = lazy(() =>
import('../../affine/sidebar-switch').then(module => ({
default: module.SidebarSwitch,
}))
);
const BrowserWarning = ({
show,
onClose,
@@ -152,11 +164,13 @@ export const Header = forwardRef<
data-testid="editor-header-items"
data-tauri-drag-region
>
<SidebarSwitch
visible={!open}
tooltipContent={t('Expand sidebar')}
testid="sliderBar-arrowButton-expand"
/>
<Suspense>
<SidebarSwitch
visible={!open}
tooltipContent={t('Expand sidebar')}
data-testid="sliderBar-arrowButton-expand"
/>
</Suspense>
{props.children}
<StyledHeaderRightSide>

View File

@@ -5,12 +5,13 @@ import { useBlockSuiteWorkspacePageTitle } from '@toeverything/hooks/use-blocksu
import { useAtomValue, useSetAtom } from 'jotai';
import Head from 'next/head';
import type React from 'react';
import { lazy, startTransition, Suspense, useCallback } from 'react';
import { startTransition, useCallback } from 'react';
import { currentEditorAtom, workspacePreferredModeAtom } from '../atoms';
import { usePageMeta } from '../hooks/use-page-meta';
import type { AffineOfficialWorkspace } from '../shared';
import { PageNotFoundError } from './affine/affine-error-eoundary';
import { BlockSuiteEditor as Editor } from './blocksuite/block-suite-editor';
import { WorkspaceHeader } from './blocksuite/workspace-header';
export type PageDetailEditorProps = {
@@ -23,12 +24,6 @@ export type PageDetailEditorProps = {
header?: React.ReactNode;
};
const Editor = lazy(() =>
import('./blocksuite/block-suite-editor').then(module => ({
default: module.BlockSuiteEditor,
}))
);
export const PageDetailEditor: React.FC<PageDetailEditorProps> = ({
workspace,
pageId,
@@ -64,34 +59,32 @@ export const PageDetailEditor: React.FC<PageDetailEditorProps> = ({
>
{header}
</WorkspaceHeader>
<Suspense>
<Editor
style={{
height: 'calc(100% - 52px)',
}}
key={pageId}
mode={isPublic ? 'page' : currentMode}
page={page}
onInit={useCallback(
(page: Page, editor: Readonly<EditorContainer>) => {
startTransition(() => {
setEditor(editor);
});
onInit(page, editor);
},
[onInit, setEditor]
)}
onLoad={useCallback(
(page: Page, editor: EditorContainer) => {
startTransition(() => {
setEditor(editor);
});
onLoad?.(page, editor);
},
[onLoad, setEditor]
)}
/>
</Suspense>
<Editor
style={{
height: 'calc(100% - 52px)',
}}
key={`${workspace.flavour}-${workspace.id}-${[pageId]}`}
mode={isPublic ? 'page' : currentMode}
page={page}
onInit={useCallback(
(page: Page, editor: Readonly<EditorContainer>) => {
startTransition(() => {
setEditor(editor);
});
onInit(page, editor);
},
[onInit, setEditor]
)}
onLoad={useCallback(
(page: Page, editor: EditorContainer) => {
startTransition(() => {
setEditor(editor);
});
onLoad?.(page, editor);
},
[onLoad, setEditor]
)}
/>
</>
);
};

View File

@@ -1,3 +1,4 @@
import { initPage } from '@affine/env/blocksuite';
import { useTranslation } from '@affine/i18n';
import type { PageBlockModel } from '@blocksuite/blocks';
import { PlusIcon } from '@blocksuite/icons';
@@ -5,6 +6,7 @@ import { assertEquals, nanoid } from '@blocksuite/store';
import { Command } from 'cmdk';
import type { NextRouter } from 'next/router';
import type React from 'react';
import { useCallback } from 'react';
import { useBlockSuiteWorkspaceHelper } from '../../../hooks/use-blocksuite-workspace-helper';
import { useRouterHelper } from '../../../hooks/use-router-helper';
@@ -35,25 +37,25 @@ export const Footer: React.FC<FooterProps> = ({
return (
<Command.Item
data-testid="quick-search-add-new-page"
onSelect={async () => {
onClose();
onSelect={useCallback(() => {
const id = nanoid();
const page = await createPage(id);
const page = createPage(id);
assertEquals(page.id, id);
await jumpToPage(blockSuiteWorkspace.id, page.id);
if (!query) {
return;
initPage(page);
const block = page.getBlockByFlavour(
'affine:page'
)[0] as PageBlockModel;
if (block) {
block.title.insert(query, 0);
} else {
console.warn('No page block found');
}
const newPage = blockSuiteWorkspace.getPage(page.id);
if (newPage) {
const block = newPage.getBlockByFlavour(
'affine:page'
)[0] as PageBlockModel;
if (block) {
block.title.insert(query, 0);
}
}
}}
blockSuiteWorkspace.setPageMeta(page.id, {
title: query,
});
onClose();
void jumpToPage(blockSuiteWorkspace.id, page.id);
}, [blockSuiteWorkspace, createPage, jumpToPage, onClose, query])}
>
<StyledModalFooterContent>
<PlusIcon />

View File

@@ -103,7 +103,7 @@ export const NavigationPath = ({
>
<IconButton
data-testid="navigation-path-expand-btn"
size="middle"
size="small"
className="collapse-btn"
onClick={() => {
setOpenExtend(!openExtend);
@@ -158,9 +158,7 @@ const NavigationPathExtendPanel = ({
show={open}
data-testid="navigation-path-expand-panel"
>
<div className="tree-container">
<TreeView data={data} indent={10} disableCollapse={true} />
</div>
<TreeView data={data} indent={10} disableCollapse={true} />
</StyledNavPathExtendContainer>
);
};

View File

@@ -55,12 +55,7 @@ export const StyledNavPathExtendContainer = styled('div')<{ show: boolean }>(
transition: 'top .15s',
fontSize: theme.font.sm,
color: theme.colors.secondaryTextColor,
paddingTop: '46px',
paddingRight: '12px',
'.tree-container': {
padding: '0 12px 0 15px',
},
padding: '46px 12px 0 15px',
};
}
);

View File

@@ -3,7 +3,7 @@ import type { PageMeta } from '@blocksuite/store';
export function findPath(metas: PageMeta[], meta: PageMeta): PageMeta[] {
function helper(group: PageMeta[]): PageMeta[] {
const last = group[group.length - 1];
const parent = metas.find(m => m.subpageIds.includes(last.id));
const parent = metas.find(m => (m.subpageIds ?? []).includes(last.id));
if (parent) {
return helper([...group, parent]);
}

View File

@@ -2,10 +2,10 @@ import { Button, Menu, MenuItem, styled } from '@affine/component';
import { LOCALES } from '@affine/i18n';
import { useTranslation } from '@affine/i18n';
import { ArrowDownSmallIcon } from '@blocksuite/icons';
import type React from 'react';
import type { FC, ReactElement } from 'react';
import { useCallback } from 'react';
const LanguageMenuContent: React.FC = () => {
const LanguageMenuContent: FC = () => {
const { i18n } = useTranslation();
const changeLanguage = useCallback(
(event: string) => {
@@ -38,7 +38,7 @@ export const LanguageMenu: React.FC = () => {
return (
<Menu
content={<LanguageMenuContent />}
content={(<LanguageMenuContent />) as ReactElement}
placement="bottom"
trigger="click"
disablePortal={true}

View File

@@ -0,0 +1,31 @@
import { IconButton } from '@affine/component';
import { ArrowLeftSmallIcon, ArrowRightSmallIcon } from '@blocksuite/icons';
import { StyledRouteNavigationWrapper } from './shared-styles';
export const RouteNavigation = () => {
if (!environment.isDesktop) {
return <></>;
}
return (
<StyledRouteNavigationWrapper>
<IconButton
size="middle"
onClick={() => {
window.history.back();
}}
>
<ArrowLeftSmallIcon />
</IconButton>
<IconButton
size="middle"
onClick={() => {
window.history.forward();
}}
style={{ marginLeft: '32px' }}
>
<ArrowRightSmallIcon />
</IconButton>
</StyledRouteNavigationWrapper>
);
};

View File

@@ -12,7 +12,7 @@ import {
import type { Page, PageMeta } from '@blocksuite/store';
import type React from 'react';
import type { UIEvent } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { lazy, Suspense, useCallback, useEffect, useState } from 'react';
import { usePageMeta } from '../../../hooks/use-page-meta';
import {
@@ -22,16 +22,16 @@ import {
useSidebarWidth,
} from '../../../hooks/use-sidebar-status';
import type { AllWorkspace } from '../../../shared';
import { SidebarSwitch } from '../../affine/sidebar-switch';
import { ChangeLog } from './changeLog';
import Favorite from './favorite';
import { Pinboard } from './Pinboard';
import { RouteNavigation } from './RouteNavigation';
import { StyledListItem } from './shared-styles';
import {
StyledLink,
StyledNewPageButton,
StyledScrollWrapper,
StyledSidebarSwitchWrapper,
StyledSidebarHeader,
StyledSliderBar,
StyledSliderBarInnerWrapper,
StyledSliderBarWrapper,
@@ -39,6 +39,12 @@ import {
} from './style';
import { WorkspaceSelector } from './WorkspaceSelector';
const SidebarSwitch = lazy(() =>
import('../../affine/sidebar-switch').then(module => ({
default: module.SidebarSwitch,
}))
);
export type FavoriteListProps = {
currentPageId: string | null;
openPage: (pageId: string) => void;
@@ -115,13 +121,16 @@ export const WorkSpaceSliderBar: React.FC<WorkSpaceSliderBarProps> = ({
data-testid="sliderBar-root"
>
<StyledSliderBar>
<StyledSidebarSwitchWrapper>
<SidebarSwitch
visible={sidebarOpen}
tooltipContent={t('Collapse sidebar')}
testid="sliderBar-arrowButton-collapse"
/>
</StyledSidebarSwitchWrapper>
<StyledSidebarHeader>
<RouteNavigation />
<Suspense>
<SidebarSwitch
visible={sidebarOpen}
tooltipContent={t('Collapse sidebar')}
data-testid="sliderBar-arrowButton-collapse"
/>
</Suspense>
</StyledSidebarHeader>
<StyledSliderBarInnerWrapper data-testid="sliderBar-inner">
<WorkspaceSelector
@@ -138,7 +147,6 @@ export const WorkSpaceSliderBar: React.FC<WorkSpaceSliderBarProps> = ({
<SearchIcon />
{t('Quick search')}
</StyledListItem>
<StyledListItem
active={
currentPath ===
@@ -159,7 +167,6 @@ export const WorkSpaceSliderBar: React.FC<WorkSpaceSliderBarProps> = ({
{t('Workspace Settings')}
</StyledLink>
</StyledListItem>
<StyledListItem
active={
currentPath ===
@@ -175,7 +182,6 @@ export const WorkSpaceSliderBar: React.FC<WorkSpaceSliderBarProps> = ({
<span data-testid="all-pages">{t('All pages')}</span>
</StyledLink>
</StyledListItem>
<StyledScrollWrapper
showTopBorder={!isScrollAtTop}
onScroll={(e: UIEvent<HTMLDivElement>) => {
@@ -199,7 +205,7 @@ export const WorkSpaceSliderBar: React.FC<WorkSpaceSliderBarProps> = ({
/>
)}
</StyledScrollWrapper>
<div style={{ height: 16 }}></div>
{currentWorkspace?.flavour === WorkspaceFlavour.AFFINE &&
currentWorkspace.public ? (
<StyledListItem>
@@ -236,9 +242,6 @@ export const WorkSpaceSliderBar: React.FC<WorkSpaceSliderBarProps> = ({
currentPath ===
(currentWorkspaceId && paths.trash(currentWorkspaceId))
}
style={{
marginTop: '16px',
}}
>
<StyledLink
href={{

View File

@@ -18,6 +18,7 @@ export const StyledListItem = styled('div')<{
cursor: 'pointer',
marginBottom: '4px',
position: 'relative',
flexShrink: 0,
userSelect: 'none',
...displayFlex('flex-start', 'center'),
...(disabled
@@ -44,7 +45,8 @@ export const StyledCollapseButton = styled('button')<{
}>(({ collapse, show = true, theme }) => {
return {
width: '16px',
height: '16px',
height: '100%',
...displayFlex('center', 'center'),
fontSize: '16px',
position: 'absolute',
left: '0',
@@ -53,10 +55,14 @@ export const StyledCollapseButton = styled('button')<{
margin: 'auto',
color: theme.colors.iconColor,
opacity: '.6',
transition: 'opacity .15s ease-in-out',
display: show ? 'flex' : 'none',
svg: {
transform: `rotate(${collapse ? '0' : '-90'}deg)`,
},
':hover': {
opacity: '1',
},
};
});
@@ -205,3 +211,10 @@ export const StyledChangeLogWrapper = styled('div')<{
overflow: 'hidden',
};
});
export const StyledRouteNavigationWrapper = styled('div')({
height: '32px',
width: '80px',
marginRight: '16px',
...displayFlex('space-between', 'center'),
});

View File

@@ -25,7 +25,7 @@ export const StyledSliderBarWrapper = styled('div')<{
};
});
export const StyledSliderBar = styled('div')(({ theme }) => {
export const StyledSliderBar = styled('div')(() => {
return {
whiteSpace: 'nowrap',
width: '100%',
@@ -34,19 +34,18 @@ export const StyledSliderBar = styled('div')(({ theme }) => {
flexShrink: 0,
display: 'flex',
flexDirection: 'column',
// overflow: 'hidden',
};
});
export const StyledSidebarSwitchWrapper = styled('div')(() => {
export const StyledSidebarHeader = styled('div')(() => {
return {
height: '52px',
flexShrink: 0,
padding: '0 16px',
padding: '0 16px 0 10px',
WebkitAppRegion: 'drag',
button: {
WebkitAppRegion: 'no-drag',
},
...displayFlex(macosElectron ? 'flex-end' : 'flex-start', 'center'),
...displayFlex(macosElectron ? 'flex-end' : 'space-between', 'center'),
};
});
export const StyledSliderBarInnerWrapper = styled('div')(() => {
@@ -55,6 +54,9 @@ export const StyledSliderBarInnerWrapper = styled('div')(() => {
// overflowX: 'hidden',
// overflowY: 'auto',
position: 'relative',
height: 'calc(100% - 52px * 2)',
display: 'flex',
flexDirection: 'column',
};
});
@@ -75,7 +77,7 @@ export const StyledNewPageButton = styled('button')(({ theme }) => {
...displayFlex('flex-start', 'center'),
borderTop: '1px solid',
borderColor: theme.colors.borderColor,
padding: '0 8px',
padding: '0 8px 0 16px',
svg: {
fontSize: '20px',
color: theme.colors.iconColor,
@@ -105,44 +107,11 @@ export const StyledSliderModalBackground = styled('div')<{ active: boolean }>(
};
}
);
export const StyledSliderResizer = styled('div')<{ isResizing: boolean }>(
() => {
return {
position: 'absolute',
top: 0,
right: 0,
bottom: 0,
width: '12px',
transform: 'translateX(50%)',
cursor: 'col-resize',
zIndex: 1,
userSelect: 'none',
':hover > *': {
background: 'rgba(0, 0, 0, 0.1)',
},
};
}
);
export const StyledSliderResizerInner = styled('div')<{ isResizing: boolean }>(
({ isResizing }) => {
return {
transition: 'background .15s .1s',
position: 'absolute',
top: 0,
right: '50%',
bottom: 0,
transform: 'translateX(0.5px)',
width: '2px',
background: isResizing ? 'rgba(0, 0, 0, 0.1)' : 'transparent',
};
}
);
export const StyledScrollWrapper = styled('div')<{
showTopBorder: boolean;
}>(({ showTopBorder, theme }) => {
return {
maxHeight: '360px',
overflowY: 'auto',
borderTop: '1px solid',
borderColor: showTopBorder ? theme.colors.borderColor : 'transparent',

View File

@@ -5,7 +5,10 @@ import 'fake-indexeddb/auto';
import assert from 'node:assert';
import { jotaiWorkspacesAtom } from '@affine/workspace/atom';
import {
rootCurrentWorkspaceIdAtom,
rootWorkspacesMetadataAtom,
} from '@affine/workspace/atom';
import type { LocalWorkspace } from '@affine/workspace/type';
import { WorkspaceFlavour } from '@affine/workspace/type';
import type { PageBlockModel } from '@blocksuite/blocks';
@@ -38,11 +41,7 @@ import {
useRecentlyViewed,
useSyncRecentViewsWithRouter,
} from '../use-recent-views';
import {
REDIRECT_TIMEOUT,
useSyncRouterWithCurrentWorkspaceAndPage,
} from '../use-sync-router-with-current-workspace-and-page';
import { useWorkspaces, useWorkspacesHelper } from '../use-workspaces';
import { useAppHelper, useWorkspaces } from '../use-workspaces';
vi.mock(
'../../components/blocksuite/header/editor-mode-switch/CustomLottie',
@@ -167,23 +166,24 @@ describe('usePageMetas', async () => {
describe('useWorkspacesHelper', () => {
test('basic', async () => {
const { ProviderWrapper, store } = await getJotaiContext();
const workspaceHelperHook = renderHook(() => useWorkspacesHelper(), {
const workspaceHelperHook = renderHook(() => useAppHelper(), {
wrapper: ProviderWrapper,
});
const id = await workspaceHelperHook.result.current.createLocalWorkspace(
'test'
);
const workspaces = await store.get(workspacesAtom);
expect(workspaces.length).toBe(1);
expect(workspaces[0].id).toBe(id);
expect(workspaces.length).toBe(2);
expect(workspaces[1].id).toBe(id);
const workspacesHook = renderHook(() => useWorkspaces(), {
wrapper: ProviderWrapper,
});
store.set(rootCurrentWorkspaceIdAtom, workspacesHook.result.current[1].id);
await store.get(currentWorkspaceAtom);
const currentWorkspaceHook = renderHook(() => useCurrentWorkspace(), {
wrapper: ProviderWrapper,
});
currentWorkspaceHook.result.current[1](workspacesHook.result.current[0].id);
currentWorkspaceHook.result.current[1](workspacesHook.result.current[1].id);
});
});
@@ -198,115 +198,35 @@ describe('useWorkspaces', () => {
test('mutation', async () => {
const { ProviderWrapper, store } = await getJotaiContext();
const { result } = renderHook(() => useWorkspacesHelper(), {
const { result } = renderHook(() => useAppHelper(), {
wrapper: ProviderWrapper,
});
{
const workspaces = await store.get(workspacesAtom);
expect(workspaces.length).toEqual(1);
}
await result.current.createLocalWorkspace('test');
const workspaces = await store.get(workspacesAtom);
console.log(workspaces);
expect(workspaces.length).toEqual(1);
{
const workspaces = await store.get(workspacesAtom);
expect(workspaces.length).toEqual(2);
}
const { result: result2 } = renderHook(() => useWorkspaces(), {
wrapper: ProviderWrapper,
});
expect(result2.current.length).toEqual(1);
const firstWorkspace = result2.current[0];
expect(result2.current.length).toEqual(2);
const firstWorkspace = result2.current[1];
expect(firstWorkspace.flavour).toBe('local');
assert(firstWorkspace.flavour === WorkspaceFlavour.LOCAL);
expect(firstWorkspace.blockSuiteWorkspace.meta.name).toBe('test');
});
});
describe('useSyncRouterWithCurrentWorkspaceAndPage', () => {
test('from "/"', async () => {
const { ProviderWrapper, store } = await getJotaiContext();
const mutationHook = renderHook(() => useWorkspacesHelper(), {
wrapper: ProviderWrapper,
});
const id = await mutationHook.result.current.createLocalWorkspace('test0');
await store.get(currentWorkspaceAtom);
mutationHook.rerender();
mutationHook.result.current.createWorkspacePage(id, 'page0');
const routerHook = renderHook(() => useRouter());
await routerHook.result.current.push('/');
routerHook.rerender();
expect(routerHook.result.current.asPath).toBe('/');
renderHook(
({ router }) => useSyncRouterWithCurrentWorkspaceAndPage(router),
{
wrapper: ProviderWrapper,
initialProps: {
router: routerHook.result.current,
},
}
);
expect(routerHook.result.current.asPath).toBe(`/workspace/${id}/page0`);
});
test('from empty workspace', async () => {
const { ProviderWrapper, store } = await getJotaiContext();
const mutationHook = renderHook(() => useWorkspacesHelper(), {
wrapper: ProviderWrapper,
});
const id = await mutationHook.result.current.createLocalWorkspace('test0');
const workspaces = await store.get(workspacesAtom);
expect(workspaces.length).toEqual(1);
mutationHook.rerender();
const routerHook = renderHook(() => useRouter());
await routerHook.result.current.push(`/workspace/${id}/not_exist`);
routerHook.rerender();
expect(routerHook.result.current.asPath).toBe(`/workspace/${id}/not_exist`);
renderHook(
({ router }) => useSyncRouterWithCurrentWorkspaceAndPage(router),
{
wrapper: ProviderWrapper,
initialProps: {
router: routerHook.result.current,
},
}
);
await new Promise(resolve => setTimeout(resolve, REDIRECT_TIMEOUT + 50));
expect(routerHook.result.current.asPath).toBe(`/workspace/${id}/all`);
});
test('from incorrect "/workspace/[workspaceId]/[pageId]"', async () => {
const { ProviderWrapper, store } = await getJotaiContext();
const mutationHook = renderHook(() => useWorkspacesHelper(), {
wrapper: ProviderWrapper,
});
const id = await mutationHook.result.current.createLocalWorkspace('test0');
const workspaces = await store.get(workspacesAtom);
expect(workspaces.length).toEqual(1);
mutationHook.rerender();
mutationHook.result.current.createWorkspacePage(id, 'page0');
const routerHook = renderHook(() => useRouter());
await routerHook.result.current.push(`/workspace/${id}/not_exist`);
routerHook.rerender();
expect(routerHook.result.current.asPath).toBe(`/workspace/${id}/not_exist`);
renderHook(
({ router }) => useSyncRouterWithCurrentWorkspaceAndPage(router),
{
wrapper: ProviderWrapper,
initialProps: {
router: routerHook.result.current,
},
}
);
await new Promise(resolve => setTimeout(resolve, REDIRECT_TIMEOUT + 50));
expect(routerHook.result.current.asPath).toBe(`/workspace/${id}/page0`);
});
});
describe('useRecentlyViewed', () => {
test('basic', async () => {
const { ProviderWrapper, store } = await getJotaiContext();
const workspaceId = blockSuiteWorkspace.id;
const pageId = 'page0';
store.set(jotaiWorkspacesAtom, [
store.set(rootWorkspacesMetadataAtom, [
{
id: workspaceId,
flavour: WorkspaceFlavour.LOCAL,

View File

@@ -3,7 +3,7 @@ import { useEffect } from 'react';
import { currentEditorAtom } from '../../atoms';
export function useReferenceLink(props?: {
export function useReferenceLinkEffect(props?: {
pageLinkClicked?: (params: { pageId: string }) => void;
subpageLinked?: (params: { pageId: string }) => void;
subpageUnlinked?: (params: { pageId: string }) => void;

View File

@@ -1,4 +1,4 @@
import { jotaiStore, jotaiWorkspacesAtom } from '@affine/workspace/atom';
import { rootStore, rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import type { AffineWorkspace } from '@affine/workspace/type';
import { useCallback } from 'react';
import useSWR from 'swr';
@@ -15,10 +15,8 @@ export function useToggleWorkspacePublish(workspace: AffineWorkspace) {
public: isPublish,
});
await mutate(QueryKey.getWorkspaces);
// force update
jotaiStore.set(jotaiWorkspacesAtom, [
...jotaiStore.get(jotaiWorkspacesAtom),
]);
// fixme: remove force update
rootStore.set(rootWorkspacesMetadataAtom, ws => [...ws]);
},
[mutate, workspace.id]
);

View File

@@ -1,43 +1,12 @@
import type { Page } from '@blocksuite/store';
import { atom, useAtom, useAtomValue } from 'jotai';
import { currentPageIdAtom } from '../../atoms';
import { currentWorkspaceAtom } from './use-current-workspace';
export const currentPageAtom = atom<Promise<Page | null>>(async get => {
const id = get(currentPageIdAtom);
const workspace = await get(currentWorkspaceAtom);
if (!workspace || !id) {
return Promise.resolve(null);
}
const page = workspace.blockSuiteWorkspace.getPage(id);
if (page) {
return page;
} else {
return new Promise(resolve => {
const dispose = workspace.blockSuiteWorkspace.slots.pageAdded.on(
pageId => {
if (pageId === id) {
resolve(page);
dispose.dispose();
}
}
);
});
}
});
export function useCurrentPage(): Page | null {
return useAtomValue(currentPageAtom);
}
import { rootCurrentPageIdAtom } from '@affine/workspace/atom';
import { useAtom } from 'jotai';
/**
* @deprecated
* @deprecated Use `rootCurrentPageIdAtom` directly instead.
*/
export function useCurrentPageId(): [
string | null,
(newId: string | null) => void
] {
return useAtom(currentPageIdAtom);
return useAtom(rootCurrentPageIdAtom);
}

View File

@@ -1,21 +1,15 @@
import { atomWithSyncStorage } from '@affine/jotai';
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai';
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import { useCallback } from 'react';
import {
currentPageIdAtom,
currentWorkspaceIdAtom,
workspacesAtom,
} from '../../atoms';
import { currentPageIdAtom, currentWorkspaceIdAtom } from '../../atoms';
import { rootCurrentWorkspaceAtom } from '../../atoms/root';
import type { AllWorkspace } from '../../shared';
export const currentWorkspaceAtom = atom<Promise<AllWorkspace | null>>(
async get => {
const id = get(currentWorkspaceIdAtom);
const workspaces = await get(workspacesAtom);
return workspaces.find(workspace => workspace.id === id) ?? null;
}
);
/**
* @deprecated use `rootCurrentWorkspaceAtom` instead
*/
export const currentWorkspaceAtom = rootCurrentWorkspaceAtom;
export const lastWorkspaceIdAtom = atomWithSyncStorage<string | null>(
'last_workspace_id',
@@ -26,7 +20,7 @@ export function useCurrentWorkspace(): [
AllWorkspace | null,
(id: string | null) => void
] {
const currentWorkspace = useAtomValue(currentWorkspaceAtom);
const currentWorkspace = useAtomValue(rootCurrentWorkspaceAtom);
const [, setId] = useAtom(currentWorkspaceIdAtom);
const [, setPageId] = useAtom(currentPageIdAtom);
const setLast = useSetAtom(lastWorkspaceIdAtom);

View File

@@ -9,7 +9,6 @@ import {
import type { WorkspaceRegistry } from '@affine/workspace/type';
import { WorkspaceFlavour } from '@affine/workspace/type';
import { useSetAtom } from 'jotai';
import { useRouter } from 'next/router';
import { useCallback } from 'react';
import { affineAuth } from '../../plugins/affine';
@@ -18,7 +17,6 @@ import { useTransformWorkspace } from '../use-transform-workspace';
export function useOnTransformWorkspace() {
const transformWorkspace = useTransformWorkspace();
const setUser = useSetAtom(currentAffineUserAtom);
const router = useRouter();
return useCallback(
async <From extends WorkspaceFlavour, To extends WorkspaceFlavour>(
from: From,
@@ -35,13 +33,6 @@ export function useOnTransformWorkspace() {
}
}
const workspaceId = await transformWorkspace(from, to, workspace);
await router.replace({
pathname: `/workspace/[workspaceId]/setting`,
query: {
...router.query,
workspaceId,
},
});
window.dispatchEvent(
new CustomEvent('affine-workspace:transform', {
detail: {
@@ -53,6 +44,6 @@ export function useOnTransformWorkspace() {
})
);
},
[router, setUser, transformWorkspace]
[setUser, transformWorkspace]
);
}

View File

@@ -1,30 +0,0 @@
import { jotaiStore, jotaiWorkspacesAtom } from '@affine/workspace/atom';
import { useEffect } from 'react';
import { WorkspacePlugins } from '../plugins';
export function useCreateFirstWorkspace() {
// may not need use effect at all, right?
useEffect(() => {
return jotaiStore.sub(jotaiWorkspacesAtom, () => {
const workspaces = jotaiStore.get(jotaiWorkspacesAtom);
if (workspaces.length === 0) {
createFirst();
}
/**
* Create a first workspace, only just once for a browser
*/
async function createFirst() {
const Plugins = Object.values(WorkspacePlugins).sort(
(a, b) => a.loadPriority - b.loadPriority
);
for (const Plugin of Plugins) {
await Plugin.Events['app:first-init']?.();
}
}
});
}, []);
}

View File

@@ -0,0 +1,84 @@
import { rootCurrentPageIdAtom } from '@affine/workspace/atom';
import { useAtom, useAtomValue } from 'jotai';
import type { NextRouter } from 'next/router';
import { useEffect, useRef } from 'react';
import { rootCurrentWorkspaceAtom } from '../atoms/root';
export const HALT_PROBLEM_TIMEOUT = 1000;
export function useRouterAndWorkspaceWithPageIdDefense(router: NextRouter) {
const currentWorkspace = useAtomValue(rootCurrentWorkspaceAtom);
const [currentPageId, setCurrentPageId] = useAtom(rootCurrentPageIdAtom);
const fallbackModeRef = useRef(false);
useEffect(() => {
if (!router.isReady) {
return;
}
const { workspaceId, pageId } = router.query;
if (typeof pageId !== 'string') {
console.warn('pageId is not a string', pageId);
return;
}
if (typeof workspaceId !== 'string') {
console.warn('workspaceId is not a string', workspaceId);
return;
}
if (currentWorkspace?.id !== workspaceId) {
console.warn('workspaceId is not currentWorkspace', workspaceId);
return;
}
if (currentPageId !== pageId && !fallbackModeRef.current) {
console.log('set current page id', pageId);
setCurrentPageId(pageId);
void router.push({
pathname: '/workspace/[workspaceId]/[pageId]',
query: {
...router.query,
workspaceId,
pageId,
},
});
}
}, [currentPageId, currentWorkspace.id, router, setCurrentPageId]);
useEffect(() => {
if (fallbackModeRef.current) {
return;
}
const id = setTimeout(() => {
if (currentPageId) {
const page =
currentWorkspace.blockSuiteWorkspace.getPage(currentPageId);
if (!page) {
const firstOne =
currentWorkspace.blockSuiteWorkspace.meta.pageMetas.at(0);
if (firstOne) {
console.warn(
'cannot find page',
currentPageId,
'so redirect to',
firstOne.id
);
setCurrentPageId(firstOne.id);
void router.push({
pathname: '/workspace/[workspaceId]/[pageId]',
query: {
...router.query,
workspaceId: currentWorkspace.id,
pageId: firstOne.id,
},
});
fallbackModeRef.current = true;
}
}
}
}, HALT_PROBLEM_TIMEOUT);
return () => {
clearTimeout(id);
};
}, [
currentPageId,
currentWorkspace.blockSuiteWorkspace,
currentWorkspace.id,
router,
setCurrentPageId,
]);
}

View File

@@ -25,6 +25,26 @@ export function useRouterHelper(router: NextRouter) {
},
[router]
);
const jumpToWorkspace = useCallback(
(workspaceId: string, logic: RouteLogic = RouteLogic.PUSH) => {
if (router.pathname === '/workspace/[workspaceId]/[pageId]') {
return router[logic]({
pathname: `/workspace/[workspaceId]`,
query: {
workspaceId: workspaceId,
},
});
} else {
return router[logic]({
pathname: router.pathname,
query: {
workspaceId: workspaceId,
},
});
}
},
[router]
);
const jumpToPublicWorkspacePage = useCallback(
(
workspaceId: string,
@@ -71,6 +91,7 @@ export function useRouterHelper(router: NextRouter) {
return {
jumpToPage,
jumpToWorkspace,
jumpToPublicWorkspacePage,
jumpToSubPath,
openPage,

View File

@@ -0,0 +1,48 @@
import {
rootCurrentPageIdAtom,
rootCurrentWorkspaceIdAtom,
rootWorkspacesMetadataAtom,
} from '@affine/workspace/atom';
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import type { NextRouter } from 'next/router';
import { useEffect } from 'react';
export function useRouterWithWorkspaceIdDefense(router: NextRouter) {
const metadata = useAtomValue(rootWorkspacesMetadataAtom);
const [currentWorkspaceId, setCurrentWorkspaceId] = useAtom(
rootCurrentWorkspaceIdAtom
);
const setCurrentPageId = useSetAtom(rootCurrentPageIdAtom);
useEffect(() => {
if (!router.isReady) {
return;
}
if (!currentWorkspaceId) {
return;
}
const exist = metadata.find(m => m.id === currentWorkspaceId);
if (!exist) {
// clean up
setCurrentWorkspaceId(null);
setCurrentPageId(null);
const firstOne = metadata.at(0);
if (!firstOne) {
throw new Error('no workspace');
}
void router.push({
pathname: '/workspace/[workspaceId]/all',
query: {
...router.query,
workspaceId: firstOne.id,
},
});
}
}, [
currentWorkspaceId,
metadata,
router,
router.isReady,
setCurrentPageId,
setCurrentWorkspaceId,
]);
}

View File

@@ -1,9 +1,10 @@
import { atomWithSyncStorage } from '@affine/jotai';
import { useMediaQuery, useTheme } from '@mui/material';
import { useTheme } from '@mui/material';
import { useMediaQuery } from '@react-hookz/web';
import { atom, useAtom } from 'jotai';
import { atomWithStorage } from 'jotai/utils';
const sideBarOpenAtom = atomWithSyncStorage('sidebarOpen', true);
const sideBarWidthAtom = atomWithSyncStorage('sidebarWidth', 256);
const sideBarOpenAtom = atomWithStorage('sidebarOpen', true);
const sideBarWidthAtom = atomWithStorage('sidebarWidth', 256);
const sidebarResizingAtom = atom(false);
export function useSidebarStatus() {
@@ -16,7 +17,10 @@ export function useSidebarWidth() {
export function useSidebarFloating() {
const theme = useTheme();
return useMediaQuery(theme.breakpoints.down('md'));
return (
useMediaQuery(theme.breakpoints.down('md').replace(/^@media( ?)/m, '')) ??
false
);
}
export function useSidebarResizing() {

View File

@@ -0,0 +1,18 @@
import { rootCurrentPageIdAtom } from '@affine/workspace/atom';
import { useSetAtom } from 'jotai';
import type { NextRouter } from 'next/router';
import { useEffect } from 'react';
export function useSyncRouterWithCurrentPageId(router: NextRouter) {
const setCurrentPageId = useSetAtom(rootCurrentPageIdAtom);
useEffect(() => {
if (!router.isReady) {
return;
}
const pageId = router.query.pageId;
if (typeof pageId === 'string') {
console.log('set page id', pageId);
setCurrentPageId(pageId);
}
}, [router.isReady, router.query.pageId, setCurrentPageId]);
}

View File

@@ -1,215 +0,0 @@
import { jotaiStore } from '@affine/workspace/atom';
import { WorkspaceFlavour } from '@affine/workspace/type';
import type { NextRouter } from 'next/router';
import { useEffect } from 'react';
import { currentPageIdAtom } from '../atoms';
import type { AllWorkspace } from '../shared';
import { WorkspaceSubPath } from '../shared';
import { useCurrentPageId } from './current/use-current-page-id';
import { useCurrentWorkspace } from './current/use-current-workspace';
import { RouteLogic, useRouterHelper } from './use-router-helper';
import { useWorkspaces } from './use-workspaces';
export function findSuitablePageId(
workspace: AllWorkspace,
targetId: string
): string | null {
switch (workspace.flavour) {
case WorkspaceFlavour.AFFINE: {
return (
workspace.blockSuiteWorkspace.meta.pageMetas.find(
page => page.id === targetId
)?.id ??
workspace.blockSuiteWorkspace.meta.pageMetas.at(0)?.id ??
null
);
}
case WorkspaceFlavour.LOCAL: {
return (
workspace.blockSuiteWorkspace.meta.pageMetas.find(
page => page.id === targetId
)?.id ??
workspace.blockSuiteWorkspace.meta.pageMetas.at(0)?.id ??
null
);
}
case WorkspaceFlavour.PUBLIC: {
return null;
}
}
}
export const REDIRECT_TIMEOUT = 1000;
export function useSyncRouterWithCurrentWorkspaceAndPage(router: NextRouter) {
const [currentWorkspace, setCurrentWorkspaceId] = useCurrentWorkspace();
const [currentPageId, setCurrentPageId] = useCurrentPageId();
const workspaces = useWorkspaces();
const { jumpToSubPath } = useRouterHelper(router);
useEffect(() => {
const listener: Parameters<typeof router.events.on>[1] = (url: string) => {
if (url.startsWith('/')) {
const path = url.split('/');
if (path.length === 4 && path[1] === 'workspace') {
if (
path[3] === 'all' ||
path[3] === 'setting' ||
path[3] === 'trash' ||
path[3] === 'favorite' ||
path[3] === 'shared'
) {
return;
}
setCurrentWorkspaceId(path[2]);
if (currentWorkspace && 'blockSuiteWorkspace' in currentWorkspace) {
if (currentWorkspace.blockSuiteWorkspace.getPage(path[3])) {
setCurrentPageId(path[3]);
}
}
}
}
};
router.events.on('routeChangeStart', listener);
return () => {
router.events.off('routeChangeStart', listener);
};
}, [currentWorkspace, router, setCurrentPageId, setCurrentWorkspaceId]);
useEffect(() => {
if (!router.isReady) {
return;
}
if (
router.pathname === '/workspace/[workspaceId]/[pageId]' ||
router.pathname === '/'
) {
const targetPageId = router.query.pageId;
const targetWorkspaceId = router.query.workspaceId;
if (currentWorkspace && currentPageId) {
if (
currentWorkspace.id === targetWorkspaceId &&
currentPageId === targetPageId
) {
return;
}
}
if (
typeof targetPageId !== 'string' ||
typeof targetWorkspaceId !== 'string'
) {
if (router.asPath === '/') {
const first = workspaces.at(0);
if (first && 'blockSuiteWorkspace' in first) {
const targetWorkspaceId = first.id;
const targetPageId =
first.blockSuiteWorkspace.meta.pageMetas.at(0)?.id;
if (targetPageId) {
setCurrentWorkspaceId(targetWorkspaceId);
setCurrentPageId(targetPageId);
router.push(`/workspace/${targetWorkspaceId}/${targetPageId}`);
}
}
}
return;
}
if (!currentWorkspace) {
const targetWorkspace = workspaces.find(
workspace => workspace.id === targetPageId
);
if (targetWorkspace) {
setCurrentWorkspaceId(targetWorkspace.id);
router.push({
query: {
...router.query,
workspaceId: targetWorkspace.id,
},
});
return;
} else {
const first = workspaces.at(0);
if (first) {
setCurrentWorkspaceId(first.id);
router.push({
query: {
...router.query,
workspaceId: first.id,
},
});
return;
}
}
} else {
if (!currentPageId && currentWorkspace) {
const targetId = findSuitablePageId(currentWorkspace, targetPageId);
if (targetId) {
setCurrentPageId(targetId);
router.push({
query: {
...router.query,
workspaceId: currentWorkspace.id,
pageId: targetId,
},
});
return;
} else {
const dispose =
currentWorkspace.blockSuiteWorkspace.slots.pageAdded.on(
pageId => {
if (pageId === targetPageId) {
dispose.dispose();
setCurrentPageId(pageId);
router.push({
query: {
...router.query,
workspaceId: currentWorkspace.id,
pageId: targetPageId,
},
});
}
}
);
const clearId = setTimeout(() => {
const pageId = jotaiStore.get(currentPageIdAtom);
if (pageId === null) {
const id =
currentWorkspace.blockSuiteWorkspace.meta.pageMetas.at(0)?.id;
if (id) {
router.push({
query: {
...router.query,
workspaceId: currentWorkspace.id,
pageId: id,
},
});
setCurrentPageId(id);
dispose.dispose();
return;
}
}
jumpToSubPath(
currentWorkspace.blockSuiteWorkspace.id,
WorkspaceSubPath.ALL,
RouteLogic.REPLACE
);
dispose.dispose();
}, REDIRECT_TIMEOUT);
return () => {
clearTimeout(clearId);
dispose.dispose();
};
}
}
}
}
}, [
currentPageId,
currentWorkspace,
router.query.workspaceId,
router.query.pageId,
setCurrentPageId,
setCurrentWorkspaceId,
workspaces,
router,
jumpToSubPath,
]);
}

View File

@@ -0,0 +1,51 @@
import {
rootCurrentWorkspaceIdAtom,
rootWorkspacesMetadataAtom,
} from '@affine/workspace/atom';
import { useAtom, useAtomValue } from 'jotai';
import type { NextRouter } from 'next/router';
import { useEffect } from 'react';
export function useSyncRouterWithCurrentWorkspaceId(router: NextRouter) {
const [currentWorkspaceId, setCurrentWorkspaceId] = useAtom(
rootCurrentWorkspaceIdAtom
);
const metadata = useAtomValue(rootWorkspacesMetadataAtom);
useEffect(() => {
if (!router.isReady) {
return;
}
const workspaceId = router.query.workspaceId;
if (typeof workspaceId !== 'string') {
return;
}
if (currentWorkspaceId) {
return;
}
const targetWorkspace = metadata.find(
workspace => workspace.id === workspaceId
);
if (targetWorkspace) {
console.log('set workspace id', workspaceId);
setCurrentWorkspaceId(targetWorkspace.id);
void router.push({
pathname: '/workspace/[workspaceId]/all',
query: {
workspaceId: targetWorkspace.id,
},
});
} else {
const targetWorkspace = metadata.at(0);
if (targetWorkspace) {
console.log('set workspace id', workspaceId);
setCurrentWorkspaceId(targetWorkspace.id);
void router.push({
pathname: '/workspace/[workspaceId]/all',
query: {
workspaceId: targetWorkspace.id,
},
});
}
}
}, [currentWorkspaceId, metadata, router, setCurrentWorkspaceId]);
}

View File

@@ -1,53 +0,0 @@
import type { NextRouter } from 'next/router';
import { useEffect } from 'react';
import { useCurrentWorkspace } from './current/use-current-workspace';
import { useWorkspaces } from './use-workspaces';
export function useSyncRouterWithCurrentWorkspace(router: NextRouter) {
const [currentWorkspace, setCurrentWorkspaceId] = useCurrentWorkspace();
const workspaces = useWorkspaces();
useEffect(() => {
const listener: Parameters<typeof router.events.on>[1] = (url: string) => {
if (url.startsWith('/')) {
const path = url.split('/');
if (path.length === 4 && path[1] === 'workspace') {
setCurrentWorkspaceId(path[2]);
}
}
};
router.events.on('routeChangeStart', listener);
return () => {
router.events.off('routeChangeStart', listener);
};
}, [currentWorkspace, router, setCurrentWorkspaceId]);
useEffect(() => {
if (!router.isReady) {
return;
}
const workspaceId = router.query.workspaceId;
if (typeof workspaceId !== 'string') {
return;
}
if (!currentWorkspace) {
const targetWorkspace = workspaces.find(
workspace => workspace.id === workspaceId
);
if (targetWorkspace) {
setCurrentWorkspaceId(targetWorkspace.id);
} else {
const targetWorkspace = workspaces.at(0);
if (targetWorkspace) {
setCurrentWorkspaceId(targetWorkspace.id);
router.push({
pathname: '/workspace/[workspaceId]/all',
query: {
workspaceId: targetWorkspace.id,
},
});
}
}
}
}, [currentWorkspace, router, setCurrentWorkspaceId, workspaces]);
}

View File

@@ -1,4 +1,7 @@
import { jotaiWorkspacesAtom } from '@affine/workspace/atom';
import {
rootCurrentWorkspaceIdAtom,
rootWorkspacesMetadataAtom,
} from '@affine/workspace/atom';
import type { WorkspaceFlavour } from '@affine/workspace/type';
import type { WorkspaceRegistry } from '@affine/workspace/type';
import { useSetAtom } from 'jotai';
@@ -12,7 +15,8 @@ import { WorkspacePlugins } from '../plugins';
* The logic here is to delete the old workspace and create a new one.
*/
export function useTransformWorkspace() {
const set = useSetAtom(jotaiWorkspacesAtom);
const setCurrentWorkspaceId = useSetAtom(rootCurrentWorkspaceIdAtom);
const set = useSetAtom(rootWorkspacesMetadataAtom);
return useCallback(
async <From extends WorkspaceFlavour, To extends WorkspaceFlavour>(
from: From,
@@ -31,8 +35,9 @@ export function useTransformWorkspace() {
});
return [...workspaces];
});
setCurrentWorkspaceId(newId);
return newId;
},
[set]
[set, setCurrentWorkspaceId]
);
}

View File

@@ -1,5 +1,5 @@
import { DebugLogger } from '@affine/debug';
import { jotaiWorkspacesAtom } from '@affine/workspace/atom';
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import type { LocalWorkspace } from '@affine/workspace/type';
import { WorkspaceFlavour } from '@affine/workspace/type';
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
@@ -18,10 +18,13 @@ export function useWorkspaces(): AllWorkspace[] {
const logger = new DebugLogger('use-workspaces');
export function useWorkspacesHelper() {
/**
* This hook has the permission to all workspaces. Be careful when using it.
*/
export function useAppHelper() {
const workspaces = useWorkspaces();
const jotaiWorkspaces = useAtomValue(jotaiWorkspacesAtom);
const set = useSetAtom(jotaiWorkspacesAtom);
const jotaiWorkspaces = useAtomValue(rootWorkspacesMetadataAtom);
const set = useSetAtom(rootWorkspacesMetadataAtom);
return {
createWorkspacePage: useCallback(
(workspaceId: string, pageId: string) => {

View File

@@ -1,4 +1,3 @@
import { ListSkeleton } from '@affine/component';
import type { AffinePublicWorkspace } from '@affine/workspace/type';
import { useAtom } from 'jotai';
import Head from 'next/head';
@@ -7,7 +6,6 @@ import type React from 'react';
import { lazy, Suspense } from 'react';
import { openQuickSearchModalAtom } from '../atoms';
import { StyledTableContainer } from '../components/blocksuite/block-suite-page-list/page-list/styles';
import { useRouterTitle } from '../hooks/use-router-title';
import { MainContainer, StyledPage } from './styles';
@@ -61,14 +59,6 @@ export const PublicWorkspaceLayout: React.FC<
React.PropsWithChildren
> = props => {
return (
<Suspense
fallback={
<StyledTableContainer>
<ListSkeleton />
</StyledTableContainer>
}
>
<PublicWorkspaceLayoutInner>{props.children}</PublicWorkspaceLayoutInner>
</Suspense>
<PublicWorkspaceLayoutInner>{props.children}</PublicWorkspaceLayoutInner>
);
};

View File

@@ -1,22 +1,24 @@
import { DebugLogger } from '@affine/debug';
import { config } from '@affine/env';
import { config, DEFAULT_HELLO_WORLD_PAGE_ID } from '@affine/env';
import { ensureRootPinboard, initPage } from '@affine/env/blocksuite';
import { setUpLanguage, useTranslation } from '@affine/i18n';
import { createAffineGlobalChannel } from '@affine/workspace/affine/sync';
import { jotaiStore, jotaiWorkspacesAtom } from '@affine/workspace/atom';
import {
rootCurrentPageIdAtom,
rootCurrentWorkspaceIdAtom,
rootStore,
rootWorkspacesMetadataAtom,
} from '@affine/workspace/atom';
import type { LocalIndexedDBProvider } from '@affine/workspace/type';
import { WorkspaceFlavour } from '@affine/workspace/type';
import { assertExists, nanoid } from '@blocksuite/store';
import { NoSsr } from '@mui/material';
import { assertEquals, assertExists, nanoid } from '@blocksuite/store';
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import Head from 'next/head';
import { useRouter } from 'next/router';
import type { FC, PropsWithChildren } from 'react';
import { lazy, Suspense, useCallback, useEffect } from 'react';
import type { FC, PropsWithChildren, ReactElement } from 'react';
import { lazy, Suspense, useCallback, useEffect, useState } from 'react';
import {
currentWorkspaceIdAtom,
openQuickSearchModalAtom,
openWorkspacesModalAtom,
} from '../atoms';
import { openQuickSearchModalAtom, openWorkspacesModalAtom } from '../atoms';
import {
publicWorkspaceAtom,
publicWorkspaceIdAtom,
@@ -24,18 +26,19 @@ import {
import { HelpIsland } from '../components/pure/help-island';
import { PageLoading } from '../components/pure/loading';
import WorkSpaceSliderBar from '../components/pure/workspace-slider-bar';
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 { useCreateFirstWorkspace } from '../hooks/use-create-first-workspace';
import { useRouterHelper } from '../hooks/use-router-helper';
import { useRouterTitle } from '../hooks/use-router-title';
import { useRouterWithWorkspaceIdDefense } from '../hooks/use-router-with-workspace-id-defense';
import {
useSidebarFloating,
useSidebarResizing,
useSidebarStatus,
useSidebarWidth,
} from '../hooks/use-sidebar-status';
import { useSyncRouterWithCurrentPageId } from '../hooks/use-sync-router-with-current-page-id';
import { useSyncRouterWithCurrentWorkspaceId } from '../hooks/use-sync-router-with-current-workspace-id';
import { useWorkspaces } from '../hooks/use-workspaces';
import { WorkspacePlugins } from '../plugins';
import { ModalProvider } from '../providers/ModalProvider';
@@ -115,6 +118,53 @@ const logger = new DebugLogger('workspace-layout');
const affineGlobalChannel = createAffineGlobalChannel(
WorkspacePlugins[WorkspaceFlavour.AFFINE].CRUD
);
export const AllWorkspaceContext = ({
children,
}: PropsWithChildren): ReactElement => {
const currentWorkspaceId = useAtomValue(rootCurrentWorkspaceIdAtom);
const workspaces = useWorkspaces();
useEffect(() => {
const providers = workspaces
// ignore current workspace
.filter(workspace => workspace.id !== currentWorkspaceId)
.flatMap(workspace =>
workspace.providers.filter(provider => provider.background)
);
providers.forEach(provider => {
provider.connect();
});
return () => {
providers.forEach(provider => {
provider.disconnect();
});
};
}, [currentWorkspaceId, workspaces]);
return <>{children}</>;
};
export const CurrentWorkspaceContext = ({
children,
}: PropsWithChildren): ReactElement => {
const router = useRouter();
const workspaceId = useAtomValue(rootCurrentWorkspaceIdAtom);
useSyncRouterWithCurrentWorkspaceId(router);
useSyncRouterWithCurrentPageId(router);
useRouterWithWorkspaceIdDefense(router);
const metadata = useAtomValue(rootWorkspacesMetadataAtom);
const exist = metadata.find(m => m.id === workspaceId);
if (!router.isReady) {
return <PageLoading text="Router is loading" />;
}
if (!workspaceId) {
return <PageLoading text="Finding workspace id" />;
}
if (!exist) {
return <PageLoading text="Workspace not found" />;
}
return <>{children}</>;
};
export const WorkspaceLayout: FC<PropsWithChildren> =
function WorkspacesSuspense({ children }) {
const { i18n } = useTranslation();
@@ -123,10 +173,9 @@ export const WorkspaceLayout: FC<PropsWithChildren> =
// todo(himself65): this is a hack, we should use a better way to set the language
setUpLanguage(i18n);
}, [i18n]);
useCreateFirstWorkspace();
const currentWorkspaceId = useAtomValue(currentWorkspaceIdAtom);
const jotaiWorkspaces = useAtomValue(jotaiWorkspacesAtom);
const set = useSetAtom(jotaiWorkspacesAtom);
const currentWorkspaceId = useAtomValue(rootCurrentWorkspaceIdAtom);
const jotaiWorkspaces = useAtomValue(rootWorkspacesMetadataAtom);
const set = useSetAtom(rootWorkspacesMetadataAtom);
useEffect(() => {
logger.info('mount');
const controller = new AbortController();
@@ -135,7 +184,7 @@ export const WorkspaceLayout: FC<PropsWithChildren> =
.map(({ CRUD }) => CRUD.list);
async function fetch() {
const jotaiWorkspaces = jotaiStore.get(jotaiWorkspacesAtom);
const jotaiWorkspaces = rootStore.get(rootWorkspacesMetadataAtom);
const items = [];
for (const list of lists) {
try {
@@ -179,24 +228,33 @@ export const WorkspaceLayout: FC<PropsWithChildren> =
}
}, [currentWorkspaceId, jotaiWorkspaces]);
return (
<NoSsr>
<>
{/* fixme(himself65): don't re-render whole modals */}
<ModalProvider key={currentWorkspaceId} />
<Suspense fallback={<PageLoading />}>
<WorkspaceLayoutInner>{children}</WorkspaceLayoutInner>
</Suspense>
</NoSsr>
<AllWorkspaceContext>
<CurrentWorkspaceContext>
<ModalProvider key={currentWorkspaceId} />
</CurrentWorkspaceContext>
</AllWorkspaceContext>
<CurrentWorkspaceContext>
<Suspense fallback={<PageLoading text="Finding current workspace" />}>
<WorkspaceLayoutInner>{children}</WorkspaceLayoutInner>
</Suspense>
</CurrentWorkspaceContext>
</>
);
};
export const WorkspaceLayoutInner: FC<PropsWithChildren> = ({ children }) => {
const [currentWorkspace] = useCurrentWorkspace();
const [currentPageId] = useCurrentPageId();
const workspaces = useWorkspaces();
const setCurrentPageId = useSetAtom(rootCurrentPageIdAtom);
const currentPageId = useAtomValue(rootCurrentPageIdAtom);
const router = useRouter();
const { jumpToPage } = useRouterHelper(router);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
logger.info('workspaces: ', workspaces);
}, [workspaces]);
logger.info('currentWorkspace: ', currentWorkspace);
}, [currentWorkspace]);
useEffect(() => {
if (currentWorkspace) {
@@ -204,38 +262,82 @@ export const WorkspaceLayoutInner: FC<PropsWithChildren> = ({ children }) => {
}
}, [currentWorkspace]);
useEffect(() => {
const providers = workspaces.flatMap(workspace =>
workspace.providers.filter(provider => provider.background)
);
providers.forEach(provider => {
provider.connect();
});
return () => {
providers.forEach(provider => {
provider.disconnect();
});
};
}, [workspaces]);
useEffect(() => {
if (currentWorkspace) {
currentWorkspace.providers.forEach(provider => {
if (provider.background) {
return;
}
provider.connect();
});
return () => {
currentWorkspace.providers.forEach(provider => {
if (provider.background) {
return;
}
provider.disconnect();
});
};
}
}, [currentWorkspace]);
const router = useRouter();
useEffect(() => {
if (!router.isReady) {
return;
}
if (!currentWorkspace) {
return;
}
const localProvider = currentWorkspace.providers.find(
provider => provider.flavour === 'local-indexeddb'
);
if (localProvider && localProvider.flavour === 'local-indexeddb') {
const provider = localProvider as LocalIndexedDBProvider;
const callback = () => {
setIsLoading(false);
if (currentWorkspace.blockSuiteWorkspace.isEmpty) {
// this is a new workspace, so we should redirect to the new page
const pageId = nanoid();
const page = currentWorkspace.blockSuiteWorkspace.createPage(pageId);
assertEquals(page.id, pageId);
currentWorkspace.blockSuiteWorkspace.setPageMeta(page.id, {
init: true,
});
initPage(page);
if (!router.query.pageId) {
setCurrentPageId(pageId);
void jumpToPage(currentWorkspace.id, pageId);
}
}
// no matter the workspace is empty, ensure the root pinboard exists
ensureRootPinboard(currentWorkspace.blockSuiteWorkspace);
};
provider.callbacks.add(callback);
return () => {
provider.callbacks.delete(callback);
};
}
}, [currentWorkspace, jumpToPage, router, setCurrentPageId]);
useEffect(() => {
if (!currentWorkspace) {
return;
}
const page = currentWorkspace.blockSuiteWorkspace.getPage(
DEFAULT_HELLO_WORLD_PAGE_ID
);
if (page && page.meta.jumpOnce) {
currentWorkspace.blockSuiteWorkspace.meta.setPageMeta(
DEFAULT_HELLO_WORLD_PAGE_ID,
{
jumpOnce: false,
}
);
setCurrentPageId(currentPageId);
void jumpToPage(currentWorkspace.id, page.id);
}
}, [
currentPageId,
currentWorkspace,
jumpToPage,
router.query.pageId,
setCurrentPageId,
]);
const { openPage } = useRouterHelper(router);
const [, setOpenWorkspacesModal] = useAtom(openWorkspacesModalAtom);
const helper = useBlockSuiteWorkspaceHelper(
@@ -340,7 +442,9 @@ export const WorkspaceLayoutInner: FC<PropsWithChildren> = ({ children }) => {
</StyledSpacer>
<MainContainerWrapper resizing={resizing} style={{ width: mainWidth }}>
<MainContainer className="main-container">
{children}
<Suspense fallback={<PageLoading text="Page is Loading" />}>
{isLoading ? <PageLoading text="Page is Loading" /> : children}
</Suspense>
<StyledToolWrapper>
{/* fixme(himself65): remove this */}
<div id="toolWrapper" style={{ marginBottom: '12px' }}>

View File

@@ -2,14 +2,15 @@ import '@affine/component/theme/global.css';
import { config, setupGlobal } from '@affine/env';
import { createI18n, I18nextProvider } from '@affine/i18n';
import { jotaiStore } from '@affine/workspace/atom';
import { rootStore } from '@affine/workspace/atom';
import type { EmotionCache } from '@emotion/cache';
import { CacheProvider } from '@emotion/react';
import { Provider } from 'jotai';
import { DevTools } from 'jotai-devtools';
import type { AppProps } from 'next/app';
import Head from 'next/head';
import { useRouter } from 'next/router';
import type { ReactElement } from 'react';
import type { PropsWithChildren, ReactElement } from 'react';
import React, { Suspense, useEffect, useMemo } from 'react';
import { AffineErrorBoundary } from '../components/affine/affine-error-eoundary';
@@ -30,6 +31,15 @@ const EmptyLayout = (page: ReactElement) => page;
const clientSideEmotionCache = createEmotionCache();
const DebugProvider = ({ children }: PropsWithChildren): ReactElement => {
return (
<>
{process.env.DEBUG_JOTAI === 'true' && <DevTools />}
{children}
</>
);
};
const App = function App({
Component,
pageProps,
@@ -55,10 +65,12 @@ const App = function App({
<Suspense fallback={<PageLoading key="RootPageLoading" />}>
<ProviderComposer
contexts={useMemo(
() => [
<Provider key="JotaiProvider" store={jotaiStore} />,
<ThemeProvider key="ThemeProvider" />,
],
() =>
[
<Provider key="JotaiProvider" store={rootStore} />,
<DebugProvider key="DebugProvider" />,
<ThemeProvider key="ThemeProvider" />,
].filter(Boolean),
[]
)}
>

View File

@@ -1,10 +1,9 @@
import { NoSsr } from '@mui/material';
import { initPage } from '@affine/env/blocksuite';
import { useRouter } from 'next/router';
import { lazy, Suspense } from 'react';
import { StyledPage, StyledWrapper } from '../../layouts/styles';
import type { NextPageWithLayout } from '../../shared';
import { initPage } from '../../utils/blocksuite';
const Editor = lazy(() =>
import('../../components/__debug__/client/Editor').then(module => ({
@@ -36,7 +35,3 @@ const InitPagePage: NextPageWithLayout = () => {
};
export default InitPagePage;
InitPagePage.getLayout = page => {
return <NoSsr>{page}</NoSsr>;
};

View File

@@ -5,18 +5,18 @@ import React, { Suspense, useEffect } from 'react';
import { PageLoading } from '../components/pure/loading';
import { useLastWorkspaceId } from '../hooks/affine/use-last-leave-workspace-id';
import { useCreateFirstWorkspace } from '../hooks/use-create-first-workspace';
import { RouteLogic, useRouterHelper } from '../hooks/use-router-helper';
import { useWorkspaces } from '../hooks/use-workspaces';
import { useAppHelper, useWorkspaces } from '../hooks/use-workspaces';
import { WorkspaceSubPath } from '../shared';
const logger = new DebugLogger('IndexPage');
const logger = new DebugLogger('index-page');
const IndexPageInner = () => {
const router = useRouter();
const { jumpToPage, jumpToSubPath } = useRouterHelper(router);
const workspaces = useWorkspaces();
const lastWorkspaceId = useLastWorkspaceId();
const helper = useAppHelper();
useEffect(() => {
if (!router.isReady) {
@@ -32,13 +32,12 @@ const IndexPageInner = () => {
targetWorkspace.blockSuiteWorkspace.meta.pageMetas.at(0)?.id;
if (pageId) {
logger.debug('Found target workspace. Jump to page', pageId);
jumpToPage(targetWorkspace.id, pageId, RouteLogic.REPLACE);
return;
void jumpToPage(targetWorkspace.id, pageId, RouteLogic.REPLACE);
} else {
const clearId = setTimeout(() => {
dispose.dispose();
logger.debug('Found target workspace. Jump to all pages');
jumpToSubPath(
void jumpToSubPath(
targetWorkspace.id,
WorkspaceSubPath.ALL,
RouteLogic.REPLACE
@@ -47,7 +46,7 @@ const IndexPageInner = () => {
const dispose =
targetWorkspace.blockSuiteWorkspace.slots.pageAdded.once(pageId => {
clearTimeout(clearId);
jumpToPage(targetWorkspace.id, pageId, RouteLogic.REPLACE);
void jumpToPage(targetWorkspace.id, pageId, RouteLogic.REPLACE);
});
return () => {
clearTimeout(clearId);
@@ -55,19 +54,16 @@ const IndexPageInner = () => {
};
}
} else {
logger.debug('No target workspace. jump to all pages');
// fixme: should create new workspace
jumpToSubPath('ERROR', WorkspaceSubPath.ALL, RouteLogic.REPLACE);
console.warn('No target workspace. This should not happen in production');
}
}, [jumpToPage, jumpToSubPath, lastWorkspaceId, router, workspaces]);
}, [helper, jumpToPage, jumpToSubPath, lastWorkspaceId, router, workspaces]);
return <PageLoading key="IndexPageInfinitePageLoading" />;
};
const IndexPage: NextPage = () => {
useCreateFirstWorkspace();
return (
<Suspense fallback={<PageLoading />}>
<Suspense fallback={<PageLoading text="Loading all workspaces" />}>
<IndexPageInner />
</Suspense>
);

View File

@@ -75,7 +75,13 @@ const ListPageInner: React.FC<{
<SearchIcon />
</IconButton>
</NavContainer>
<Suspense>
<Suspense
fallback={
<StyledTableContainer>
<ListSkeleton />
</StyledTableContainer>
}
>
<BlockSuitePublicPageList
onOpenPage={handleClickPage}
blockSuiteWorkspace={blockSuiteWorkspace}

View File

@@ -1,13 +1,14 @@
import { Breadcrumbs, displayFlex, styled } from '@affine/component';
import { initPage } from '@affine/env/blocksuite';
import { useTranslation } from '@affine/i18n';
import { PageIcon } from '@blocksuite/icons';
import { assertExists } from '@blocksuite/store';
import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-blocksuite-workspace-avatar-url';
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-blocksuite-workspace-name';
import { useAtomValue, useSetAtom } from 'jotai';
import { useAtom, useAtomValue } from 'jotai';
import Link from 'next/link';
import { useRouter } from 'next/router';
import type React from 'react';
import type { ReactElement } from 'react';
import { Suspense, useCallback, useEffect } from 'react';
import {
@@ -15,18 +16,16 @@ import {
publicWorkspaceIdAtom,
publicWorkspacePageIdAtom,
} from '../../../atoms/public-workspace';
import { QueryParamError } from '../../../components/affine/affine-error-eoundary';
import { PageDetailEditor } from '../../../components/page-detail-editor';
import { WorkspaceAvatar } from '../../../components/pure/footer';
import { PageLoading } from '../../../components/pure/loading';
import { useReferenceLink } from '../../../hooks/affine/use-reference-link';
import { useReferenceLinkEffect } from '../../../hooks/affine/use-reference-link-effect';
import { useRouterHelper } from '../../../hooks/use-router-helper';
import {
PublicQuickSearch,
PublicWorkspaceLayout,
} from '../../../layouts/public-workspace-layout';
import type { NextPageWithLayout } from '../../../shared';
import { initPage } from '../../../utils';
export const NavContainer = styled('div')(({ theme }) => {
return {
@@ -55,9 +54,9 @@ export const StyledBreadcrumbs = styled(Link)(({ theme }) => {
};
});
const PublicWorkspaceDetailPageInner: React.FC<{
pageId: string;
}> = ({ pageId }) => {
const PublicWorkspaceDetailPageInner = (): ReactElement => {
const pageId = useAtomValue(publicWorkspacePageIdAtom);
assertExists(pageId, 'pageId is null');
const publicWorkspace = useAtomValue(publicPageBlockSuiteAtom);
const blockSuiteWorkspace = publicWorkspace.blockSuiteWorkspace;
if (!blockSuiteWorkspace) {
@@ -65,10 +64,9 @@ const PublicWorkspaceDetailPageInner: React.FC<{
}
const router = useRouter();
const { openPage } = useRouterHelper(router);
useReferenceLink({
useReferenceLinkEffect({
pageLinkClicked: useCallback(
({ pageId }: { pageId: string }) => {
assertExists(currentWorkspace);
return openPage(blockSuiteWorkspace.id, pageId);
},
[blockSuiteWorkspace.id, openPage]
@@ -115,31 +113,31 @@ const PublicWorkspaceDetailPageInner: React.FC<{
export const PublicWorkspaceDetailPage: NextPageWithLayout = () => {
const router = useRouter();
const workspaceId = router.query.workspaceId;
const pageId = router.query.pageId;
const setWorkspaceId = useSetAtom(publicWorkspaceIdAtom);
const setPageId = useSetAtom(publicWorkspacePageIdAtom);
const [workspaceId, setWorkspaceId] = useAtom(publicWorkspaceIdAtom);
const [pageId, setPageId] = useAtom(publicWorkspacePageIdAtom);
useEffect(() => {
if (!router.isReady) {
return;
}
if (typeof workspaceId === 'string') {
setWorkspaceId(workspaceId);
if (typeof router.query.workspaceId === 'string') {
setWorkspaceId(router.query.workspaceId);
}
if (typeof pageId === 'string') {
setPageId(pageId);
if (typeof router.query.pageId === 'string') {
setPageId(router.query.pageId);
}
}, [pageId, router.isReady, setPageId, setWorkspaceId, workspaceId]);
const value = useAtomValue(publicWorkspaceIdAtom);
if (!router.isReady || !value) {
}, [
router.isReady,
router.query.pageId,
router.query.workspaceId,
setPageId,
setWorkspaceId,
]);
if (!router.isReady || !workspaceId || !pageId) {
return <PageLoading />;
}
if (typeof workspaceId !== 'string' || typeof pageId !== 'string') {
throw new QueryParamError('workspaceId, pageId', workspaceId);
}
return (
<Suspense fallback={<PageLoading />}>
<PublicWorkspaceDetailPageInner pageId={pageId} />
<PublicWorkspaceDetailPageInner />
</Suspense>
);
};

View File

@@ -1,20 +1,23 @@
import { rootCurrentPageIdAtom } from '@affine/workspace/atom';
import { WorkspaceFlavour } from '@affine/workspace/type';
import { assertExists } from '@blocksuite/store';
import { useBlockSuiteWorkspacePage } from '@toeverything/hooks/use-blocksuite-workspace-page';
import { useAtomValue } from 'jotai';
import { useRouter } from 'next/router';
import type React from 'react';
import { useCallback, useEffect } from 'react';
import { rootCurrentWorkspaceAtom } from '../../../atoms/root';
import { Unreachable } from '../../../components/affine/affine-error-eoundary';
import { PageLoading } from '../../../components/pure/loading';
import { useReferenceLink } from '../../../hooks/affine/use-reference-link';
import { useCurrentPageId } from '../../../hooks/current/use-current-page-id';
import { useReferenceLinkEffect } from '../../../hooks/affine/use-reference-link-effect';
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
import { usePageMeta, usePageMetaHelper } from '../../../hooks/use-page-meta';
import { usePinboardHandler } from '../../../hooks/use-pinboard-handler';
import { useSyncRecentViewsWithRouter } from '../../../hooks/use-recent-views';
import { useRouterAndWorkspaceWithPageIdDefense } from '../../../hooks/use-router-and-workspace-with-page-id-defense';
import { useRouterHelper } from '../../../hooks/use-router-helper';
import { useSyncRouterWithCurrentWorkspaceAndPage } from '../../../hooks/use-sync-router-with-current-workspace-and-page';
import { WorkspaceLayout } from '../../../layouts';
import { WorkspaceLayout } from '../../../layouts/workspace-layout';
import { WorkspacePlugins } from '../../../plugins';
import type { BlockSuiteWorkspace, NextPageWithLayout } from '../../../shared';
@@ -32,7 +35,7 @@ function enableFullFlags(blockSuiteWorkspace: BlockSuiteWorkspace) {
const WorkspaceDetail: React.FC = () => {
const router = useRouter();
const { openPage } = useRouterHelper(router);
const [currentPageId] = useCurrentPageId();
const currentPageId = useAtomValue(rootCurrentPageIdAtom);
const [currentWorkspace] = useCurrentWorkspace();
const blockSuiteWorkspace = currentWorkspace?.blockSuiteWorkspace ?? null;
const { setPageMeta, getPageMeta } = usePageMetaHelper(blockSuiteWorkspace);
@@ -43,7 +46,7 @@ const WorkspaceDetail: React.FC = () => {
useSyncRecentViewsWithRouter(router);
useReferenceLink({
useReferenceLinkEffect({
pageLinkClicked: useCallback(
({ pageId }: { pageId: string }) => {
assertExists(currentWorkspace);
@@ -80,7 +83,7 @@ const WorkspaceDetail: React.FC = () => {
return <PageLoading />;
}
if (!currentPageId) {
return <PageLoading />;
return <PageLoading text="Loading page." />;
}
if (currentWorkspace.flavour === WorkspaceFlavour.AFFINE) {
const PageDetail = WorkspacePlugins[currentWorkspace.flavour].UI.PageDetail;
@@ -104,14 +107,17 @@ const WorkspaceDetail: React.FC = () => {
const WorkspaceDetailPage: NextPageWithLayout = () => {
const router = useRouter();
useSyncRouterWithCurrentWorkspaceAndPage(router);
const currentWorkspace = useAtomValue(rootCurrentWorkspaceAtom);
const currentPageId = useAtomValue(rootCurrentPageIdAtom);
useRouterAndWorkspaceWithPageIdDefense(router);
const page = useBlockSuiteWorkspacePage(
currentWorkspace.blockSuiteWorkspace,
currentPageId
);
if (!router.isReady) {
return <PageLoading />;
} else if (
typeof router.query.pageId !== 'string' ||
typeof router.query.workspaceId !== 'string'
) {
throw new Error('Invalid router query');
return <PageLoading text="Router is loading" />;
} else if (!currentPageId || !page) {
return <PageLoading text="Page is loading" />;
}
return <WorkspaceDetail />;
};

View File

@@ -1,11 +1,10 @@
import { useTranslation } from '@affine/i18n';
import type { LocalIndexedDBProvider } from '@affine/workspace/type';
import { WorkspaceFlavour } from '@affine/workspace/type';
import { FolderIcon } from '@blocksuite/icons';
import { assertEquals, assertExists, nanoid } from '@blocksuite/store';
import { assertExists } from '@blocksuite/store';
import Head from 'next/head';
import { useRouter } from 'next/router';
import React, { useCallback, useEffect } from 'react';
import React, { useCallback } from 'react';
import {
QueryParamError,
@@ -15,56 +14,17 @@ import { PageLoading } from '../../../components/pure/loading';
import { WorkspaceTitle } from '../../../components/pure/workspace-title';
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
import { useRouterHelper } from '../../../hooks/use-router-helper';
import { useSyncRouterWithCurrentWorkspace } from '../../../hooks/use-sync-router-with-current-workspace';
import { WorkspaceLayout } from '../../../layouts';
import { useSyncRouterWithCurrentWorkspaceId } from '../../../hooks/use-sync-router-with-current-workspace-id';
import { WorkspaceLayout } from '../../../layouts/workspace-layout';
import { WorkspacePlugins } from '../../../plugins';
import type { NextPageWithLayout } from '../../../shared';
import { ensureRootPinboard } from '../../../utils';
const AllPage: NextPageWithLayout = () => {
const router = useRouter();
const { jumpToPage } = useRouterHelper(router);
const [currentWorkspace] = useCurrentWorkspace();
const { t } = useTranslation();
useSyncRouterWithCurrentWorkspace(router);
useEffect(() => {
if (!router.isReady) {
return;
}
if (!currentWorkspace) {
return;
}
if (currentWorkspace.flavour !== WorkspaceFlavour.LOCAL) {
// only create a new page for local workspace
// just ensure the root pinboard exists
ensureRootPinboard(currentWorkspace.blockSuiteWorkspace);
return;
}
const localProvider = currentWorkspace.providers.find(
provider => provider.flavour === 'local-indexeddb'
);
if (localProvider && localProvider.flavour === 'local-indexeddb') {
const provider = localProvider as LocalIndexedDBProvider;
const callback = () => {
if (currentWorkspace.blockSuiteWorkspace.isEmpty) {
// this is a new workspace, so we should redirect to the new page
const pageId = nanoid();
const page = currentWorkspace.blockSuiteWorkspace.createPage(pageId);
assertEquals(page.id, pageId);
currentWorkspace.blockSuiteWorkspace.setPageMeta(page.id, {
init: true,
});
jumpToPage(currentWorkspace.id, pageId);
}
// no matter workspace is empty, ensure the root pinboard exists
ensureRootPinboard(currentWorkspace.blockSuiteWorkspace);
};
provider.callbacks.add(callback);
return () => {
provider.callbacks.delete(callback);
};
}
}, [currentWorkspace, jumpToPage, router]);
useSyncRouterWithCurrentWorkspaceId(router);
const onClickPage = useCallback(
(pageId: string, newTab?: boolean) => {
assertExists(currentWorkspace);

View File

@@ -10,8 +10,8 @@ import { PageLoading } from '../../../components/pure/loading';
import { WorkspaceTitle } from '../../../components/pure/workspace-title';
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
import { useRouterHelper } from '../../../hooks/use-router-helper';
import { useSyncRouterWithCurrentWorkspace } from '../../../hooks/use-sync-router-with-current-workspace';
import { WorkspaceLayout } from '../../../layouts';
import { useSyncRouterWithCurrentWorkspaceId } from '../../../hooks/use-sync-router-with-current-workspace-id';
import { WorkspaceLayout } from '../../../layouts/workspace-layout';
import type { NextPageWithLayout } from '../../../shared';
const FavouritePage: NextPageWithLayout = () => {
@@ -19,7 +19,7 @@ const FavouritePage: NextPageWithLayout = () => {
const { jumpToPage } = useRouterHelper(router);
const [currentWorkspace] = useCurrentWorkspace();
const { t } = useTranslation();
useSyncRouterWithCurrentWorkspace(router);
useSyncRouterWithCurrentWorkspaceId(router);
const onClickPage = useCallback(
(pageId: string, newTab?: boolean) => {
assertExists(currentWorkspace);

View File

@@ -18,9 +18,9 @@ import { PageLoading } from '../../../components/pure/loading';
import { WorkspaceTitle } from '../../../components/pure/workspace-title';
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
import { useOnTransformWorkspace } from '../../../hooks/root/use-on-transform-workspace';
import { useSyncRouterWithCurrentWorkspace } from '../../../hooks/use-sync-router-with-current-workspace';
import { useWorkspacesHelper } from '../../../hooks/use-workspaces';
import { WorkspaceLayout } from '../../../layouts';
import { useSyncRouterWithCurrentWorkspaceId } from '../../../hooks/use-sync-router-with-current-workspace-id';
import { useAppHelper } from '../../../hooks/use-workspaces';
import { WorkspaceLayout } from '../../../layouts/workspace-layout';
import { WorkspacePlugins } from '../../../plugins';
import type { NextPageWithLayout } from '../../../shared';
@@ -33,7 +33,7 @@ const SettingPage: NextPageWithLayout = () => {
const router = useRouter();
const [currentWorkspace] = useCurrentWorkspace();
const { t } = useTranslation();
useSyncRouterWithCurrentWorkspace(router);
useSyncRouterWithCurrentWorkspaceId(router);
const [currentTab, setCurrentTab] = useAtom(settingPanelAtom);
useEffect(() => {});
const onChangeTab = useCallback(
@@ -92,7 +92,7 @@ const SettingPage: NextPageWithLayout = () => {
}
}, [currentTab, router, setCurrentTab]);
const helper = useWorkspacesHelper();
const helper = useAppHelper();
const onDeleteWorkspace = useCallback(() => {
assertExists(currentWorkspace);

View File

@@ -10,8 +10,8 @@ import { PageLoading } from '../../../components/pure/loading';
import { WorkspaceTitle } from '../../../components/pure/workspace-title';
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
import { useRouterHelper } from '../../../hooks/use-router-helper';
import { useSyncRouterWithCurrentWorkspace } from '../../../hooks/use-sync-router-with-current-workspace';
import { WorkspaceLayout } from '../../../layouts';
import { useSyncRouterWithCurrentWorkspaceId } from '../../../hooks/use-sync-router-with-current-workspace-id';
import { WorkspaceLayout } from '../../../layouts/workspace-layout';
import type { NextPageWithLayout } from '../../../shared';
const SharedPages: NextPageWithLayout = () => {
@@ -19,7 +19,7 @@ const SharedPages: NextPageWithLayout = () => {
const { jumpToPage } = useRouterHelper(router);
const [currentWorkspace] = useCurrentWorkspace();
const { t } = useTranslation();
useSyncRouterWithCurrentWorkspace(router);
useSyncRouterWithCurrentWorkspaceId(router);
const onClickPage = useCallback(
(pageId: string, newTab?: boolean) => {
assertExists(currentWorkspace);

View File

@@ -10,8 +10,8 @@ import { PageLoading } from '../../../components/pure/loading';
import { WorkspaceTitle } from '../../../components/pure/workspace-title';
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
import { useRouterHelper } from '../../../hooks/use-router-helper';
import { useSyncRouterWithCurrentWorkspace } from '../../../hooks/use-sync-router-with-current-workspace';
import { WorkspaceLayout } from '../../../layouts';
import { useSyncRouterWithCurrentWorkspaceId } from '../../../hooks/use-sync-router-with-current-workspace-id';
import { WorkspaceLayout } from '../../../layouts/workspace-layout';
import type { NextPageWithLayout } from '../../../shared';
const TrashPage: NextPageWithLayout = () => {
@@ -19,7 +19,7 @@ const TrashPage: NextPageWithLayout = () => {
const { jumpToPage } = useRouterHelper(router);
const [currentWorkspace] = useCurrentWorkspace();
const { t } = useTranslation();
useSyncRouterWithCurrentWorkspace(router);
useSyncRouterWithCurrentWorkspaceId(router);
const onClickPage = useCallback(
(pageId: string, newTab?: boolean) => {
assertExists(currentWorkspace);

View File

@@ -1,5 +1,5 @@
import { getLoginStorage } from '@affine/workspace/affine/login';
import { jotaiStore } from '@affine/workspace/atom';
import { rootStore } from '@affine/workspace/atom';
import type { AffineWorkspace } from '@affine/workspace/type';
import { WorkspaceFlavour } from '@affine/workspace/type';
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
@@ -43,7 +43,7 @@ export const fetcher = async (
if (typeof key !== 'string') {
throw new TypeError('key must be a string');
}
const workspaces = await jotaiStore.get(workspacesAtom);
const workspaces = await rootStore.get(workspacesAtom);
const workspace = workspaces.find(({ id }) => id === workspaceId);
assertExists(workspace);
const storage = await workspace.blockSuiteWorkspace.blobs;

View File

@@ -1,4 +1,5 @@
import { prefixUrl } from '@affine/env';
import { initPage } from '@affine/env/blocksuite';
import { currentAffineUserAtom } from '@affine/workspace/affine/atom';
import {
clearLoginStorage,
@@ -8,7 +9,7 @@ import {
setLoginStorage,
SignMethod,
} from '@affine/workspace/affine/login';
import { jotaiStore, jotaiWorkspacesAtom } from '@affine/workspace/atom';
import { rootStore, rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import type { AffineWorkspace } from '@affine/workspace/type';
import { LoadPriority, WorkspaceFlavour } from '@affine/workspace/type';
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
@@ -26,7 +27,7 @@ import { useAffineRefreshAuthToken } from '../../hooks/affine/use-affine-refresh
import { AffineSWRConfigProvider } from '../../providers/AffineSWRConfigProvider';
import { BlockSuiteWorkspace } from '../../shared';
import { affineApis } from '../../shared/apis';
import { initPage, toast } from '../../utils';
import { toast } from '../../utils';
import type { WorkspacePlugin } from '..';
import { QueryKey } from './fetcher';
@@ -81,20 +82,20 @@ export const AffinePlugin: WorkspacePlugin<WorkspaceFlavour.AFFINE> = {
if (response) {
setLoginStorage(response);
const user = parseIdToken(response.token);
jotaiStore.set(currentAffineUserAtom, user);
rootStore.set(currentAffineUserAtom, user);
} else {
toast('Login failed');
}
},
'workspace:revoke': async () => {
jotaiStore.set(jotaiWorkspacesAtom, workspaces =>
rootStore.set(rootWorkspacesMetadataAtom, workspaces =>
workspaces.filter(
workspace => workspace.flavour !== WorkspaceFlavour.AFFINE
)
);
storage.removeItem(kAffineLocal);
clearLoginStorage();
jotaiStore.set(currentAffineUserAtom, null);
rootStore.set(currentAffineUserAtom, null);
},
},
CRUD: {

View File

@@ -27,9 +27,7 @@ export const WorkspacePlugins = {
[WorkspaceFlavour.PUBLIC]: {
flavour: WorkspaceFlavour.PUBLIC,
loadPriority: LoadPriority.LOW,
Events: {
'app:first-init': async () => {},
},
Events: {} as Partial<AppEvents>,
// todo: implement this
CRUD: {
get: unimplemented,

View File

@@ -1,17 +1,23 @@
import { DebugLogger } from '@affine/debug';
import { DEFAULT_WORKSPACE_NAME } from '@affine/env';
import { jotaiStore, jotaiWorkspacesAtom } from '@affine/workspace/atom';
import { CRUD } from '@affine/workspace/local/crud';
import {
DEFAULT_HELLO_WORLD_PAGE_ID,
DEFAULT_WORKSPACE_NAME,
} from '@affine/env';
import { ensureRootPinboard, initPage } from '@affine/env/blocksuite';
import {
CRUD,
saveWorkspaceToLocalStorage,
} from '@affine/workspace/local/crud';
import { createIndexedDBProvider } from '@affine/workspace/providers';
import { LoadPriority, WorkspaceFlavour } from '@affine/workspace/type';
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
import { assertEquals, assertExists, nanoid } from '@blocksuite/store';
import { nanoid } from '@blocksuite/store';
import React from 'react';
import { PageNotFoundError } from '../../components/affine/affine-error-eoundary';
import { WorkspaceSettingDetail } from '../../components/affine/workspace-setting-detail';
import { BlockSuitePageList } from '../../components/blocksuite/block-suite-page-list';
import { PageDetailEditor } from '../../components/page-detail-editor';
import { initPage } from '../../utils';
import type { WorkspacePlugin } from '..';
const logger = new DebugLogger('use-create-first-workspace');
@@ -20,25 +26,29 @@ export const LocalPlugin: WorkspacePlugin<WorkspaceFlavour.LOCAL> = {
flavour: WorkspaceFlavour.LOCAL,
loadPriority: LoadPriority.LOW,
Events: {
'app:first-init': async () => {
'app:init': () => {
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
nanoid(),
(_: string) => undefined
);
blockSuiteWorkspace.meta.setName(DEFAULT_WORKSPACE_NAME);
const id = await LocalPlugin.CRUD.create(blockSuiteWorkspace);
const workspace = await LocalPlugin.CRUD.get(id);
assertExists(workspace);
assertEquals(workspace.id, id);
// todo: use a better way to set initial workspace
jotaiStore.set(jotaiWorkspacesAtom, ws => [
...ws,
{
id: workspace.id,
flavour: WorkspaceFlavour.LOCAL,
},
]);
logger.debug('create first workspace', workspace);
const page = blockSuiteWorkspace.createPage(DEFAULT_HELLO_WORLD_PAGE_ID);
blockSuiteWorkspace.setPageMeta(page.id, {
init: true,
});
initPage(page);
blockSuiteWorkspace.setPageMeta(page.id, {
jumpOnce: true,
});
const provider = createIndexedDBProvider(blockSuiteWorkspace);
provider.connect();
provider.callbacks.add(() => {
provider.disconnect();
});
ensureRootPinboard(blockSuiteWorkspace);
saveWorkspaceToLocalStorage(blockSuiteWorkspace.id);
logger.debug('create first workspace');
return [blockSuiteWorkspace.id];
},
},
CRUD,

View File

@@ -1,8 +1,8 @@
import { jotaiWorkspacesAtom } from '@affine/workspace/atom';
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import { arrayMove } from '@dnd-kit/sortable';
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import { useRouter } from 'next/router';
import type React from 'react';
import type { ReactElement } from 'react';
import { lazy, Suspense, useCallback, useTransition } from 'react';
import {
@@ -15,7 +15,7 @@ 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 { useRouterHelper } from '../hooks/use-router-helper';
import { useWorkspaces, useWorkspacesHelper } from '../hooks/use-workspaces';
import { useAppHelper, useWorkspaces } from '../hooks/use-workspaces';
import { WorkspaceSubPath } from '../shared';
const WorkspaceListModal = lazy(() =>
@@ -41,10 +41,10 @@ export function Modals() {
const { jumpToSubPath } = useRouterHelper(router);
const user = useCurrentUser();
const workspaces = useWorkspaces();
const setWorkspaces = useSetAtom(jotaiWorkspacesAtom);
const setWorkspaces = useSetAtom(rootWorkspacesMetadataAtom);
const currentWorkspaceId = useAtomValue(currentWorkspaceIdAtom);
const [, setCurrentWorkspace] = useCurrentWorkspace();
const { createLocalWorkspace } = useWorkspacesHelper();
const { createLocalWorkspace } = useAppHelper();
const [transitioning, transition] = useTransition();
return (
@@ -122,13 +122,10 @@ export function Modals() {
);
}
export const ModalProvider: React.FC<React.PropsWithChildren> = ({
children,
}) => {
export const ModalProvider = (): ReactElement => {
return (
<>
<Modals />
{children}
</>
);
};

View File

@@ -7,7 +7,7 @@ import {
import { currentAffineUserAtom } from '@affine/workspace/affine/atom';
import type { LoginResponse } from '@affine/workspace/affine/login';
import { parseIdToken, setLoginStorage } from '@affine/workspace/affine/login';
import { jotaiStore } from '@affine/workspace/atom';
import { rootStore } from '@affine/workspace/atom';
const affineApis = {} as ReturnType<typeof createUserApis> &
ReturnType<typeof createWorkspaceApis>;
@@ -19,7 +19,7 @@ const debugLogger = new DebugLogger('affine-debug-apis');
if (!globalThis.AFFINE_APIS) {
globalThis.AFFINE_APIS = affineApis;
globalThis.setLogin = (response: LoginResponse) => {
jotaiStore.set(currentAffineUserAtom, parseIdToken(response.token));
rootStore.set(currentAffineUserAtom, parseIdToken(response.token));
setLoginStorage(response);
};
const loginMockUser1 = async () => {

View File

@@ -1,5 +1,6 @@
import type { AffineWorkspace, LocalWorkspace } from '@affine/workspace/type';
import type { AffinePublicWorkspace } from '@affine/workspace/type';
import type { WorkspaceRegistry } from '@affine/workspace/type';
import { Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
import type { NextPage } from 'next';
import type { ReactElement, ReactNode } from 'react';
@@ -11,7 +12,7 @@ export type AffineOfficialWorkspace =
| LocalWorkspace
| AffinePublicWorkspace;
export type AllWorkspace = AffineOfficialWorkspace;
export type AllWorkspace = WorkspaceRegistry[keyof WorkspaceRegistry];
export type NextPageWithLayout<P = Record<string, unknown>, IP = P> = NextPage<
P,

View File

@@ -1,4 +1,3 @@
export * from './blocksuite';
export * from './create-emotion-cache';
export * from './string2color';
export * from './toast';

View File

@@ -0,0 +1,256 @@
# Behind the code - Code Design and Architecture of the AFFiNE platform
## Introduction
This document delves into the design and architecture of the AFFiNE platform, providing insights for developers interested in contributing to AFFiNE or gaining a better understanding of our design principles.
## Addressing the Challenge
AFFiNE is a platform designed to be the next-generation collaborative knowledge base for professionals. It is local-first, yet collaborative; It is robust as a foundational platform, yet friendly to extend. We believe that a knowledge base that truly meets the needs of professionals in different scenarios should be open-source and open to the community. By using AFFiNE, people can take full control of their data and workflow, thus achieving data sovereignty.
To do so, we should have a stable plugin system that is easy to use by the community and a well-modularized editor for customizability. Let's list the challenges from the perspective of data modeling, UI and feature plugins, and cross-platform support.
### Data might come from anywhere and go anywhere, in spite of the cloud
AFFiNE provides users with flexibility and control over their data storage. Our platform is designed to prioritize user ownership of data, which means data in AFFiNE is always accessible from local devices like a laptop's local file or the browser's indexedDB. In the mean while, data can also be stored in centralised cloud-native way.
Thanks to our use of CRDTs (Conflict-free Replicated Data Types), data in AFFiNE is always conflict-free, similar to a auto-resolve-conflict Git. This means that data synchronization, sharing, and real-time collaboration are seamless and can occur across any network layer so long as the data as passed. As a result, developers do not need to worry about whether the data was generated locally or remotely, as CRDTs treat both equally.
While a server-centric backend is supported with AFFiNE, it is not suggested. By having a local-first architecture, AFFiNE users can have real-time responsive UI, optimal performance and effortlessly synchronize data across multiple devices and locations. This includes peer-to-peer file replication, storing file in local or cloud storage, saving it to a server-side database, or using AFFiNE Cloud for real-time collaboration and synchronization.
### Customizable UI and features
AFFiNE is a platform that allows users to customize the UI and features of each part.
We need to consider the following cases:
- Pluggable features: Some features can be disabled or enabled. For example, individuals who use AFFiNE for personal purposes may not need authentication or collaboration features. On the other hand, enterprise users may require authentication and strong security.
- SDK for the developers, the developers can modify or build their own feature or UI plugins, such as AI writing support, self-hosted databases, or domain-specific editable blocks.
### Diverse platforms
AFFiNE supports various platforms, including desktop, mobile, and web while being local-first. However, it's important to note that certain features may differ on different platforms, and it's also possible for data and editor versions to become mismatched.
## The solution
### Loading Mechanism
The AFFiNE is built on the web platform, meaning that most code runs on the JavaScript runtime(v8, QuickJS).
Some interfaces, like in the Desktop, will be implemented in the native code like Rust.
But eventually, the main logic of AFFiNE is running on the JavaScript runtime. Since it is a single-threaded runtime, we need to ensure that the code is running in a non-blocking way.
Some logic has to be running in the blocking way.
We have to set up the environment before starting the core.
And for the Workspace, like local workspace or cloud workspace, we have to load the data from the storage before rendering the UI.
During this period, there will be transition animation and skeleton UI.
```mermaid
graph LR
subgraph Interactive unavailable
A[Loading] --> B[Setup Environment]
B --> C[Loading Initial Data]
C --> D[Skeleton UI]
end
D --> E[Render UI]
E --> F[Async fetching Data] --> E
```
In this way, we need to boost the performance of the loading process.
The initial data is the most costly part of the process.
We must ensure that the initial data is loaded as quickly as possible.
Here is an obvious conclusion that only one Workspace is active simultaneously in one browser.
So we need to load the data of the active Workspace as the initial data.
And other workspaces can be loaded in the background asynchronously.
For example, the local Workspace is saved in the browser's indexedDB.
One way to boost the performance is to use the Web Worker to load the data in the background.
Here is one pseudocode:
```tsx
// worker.ts
import { openDB } from 'idb';
const db = await openDB('local-db' /* ... */);
const data = await db.getAll('data');
self.postMessage(data);
// main.ts
const worker = new Worker('./worker.ts', { type: 'module' });
await new Promise<Data>(resolve => {
worker.addEventListener('message', e => resolve(e.data));
});
// ready to render the UI
renderUI(data);
```
We use React Suspense to deal with the initial data loading in the real code.
```tsx
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai';
const currentWorkspaceIdAtom = atom(null);
const currentWorkspaceAtom = atom<Workspace>(async get => {
const workspaceId = await get(currentWorkspaceIdAtom);
// async load the workspace data
return Workspace;
});
const Workspace = () => {
const currentWorkspace = useAtomValue(currentWorkspaceAtom);
return <WorkspaceUI workspace={currentWorkspace} />;
};
const App = () => {
const router = useRouter();
const workspaceId = router.query.workspaceId;
const [currentWorkspaceId, set] = useAtom(currentWorkspaceIdAtom);
if (!currentWorkspaceId) {
set(workspaceId);
return <Loading />;
}
return (
<Suspense fallback={<Skeleton />}>
<Workspace />
</Suspense>
);
};
```
### Data Storage and UI Rendering
We assume that the data is stored in different places and loaded differently.
In the current version, we have two places to store the data: local and Cloud storage.
The local storage is the browser's indexedDB, the default storage for the local Workspace.
The cloud storage is the AFFiNE Cloud, which is the default storage for the cloud workspace.
But since the Time to Interactive(TTI) is the most important metric for performance and user experience,
all initial data is loaded in the indexedDB.
And other data will be loaded and updated in the background.
With this design concept, we have the following data structure:
```ts
import { Workspace as Store } from '@blocksuite/store';
interface Prpvider {
type: 'local-indexeddb' | 'affine-cloud' | 'desktop-sqlite';
background: boolean; // if the provider is background, we will load the data in the background
necessary: boolean; // if the provider is necessary, we will block the UI rendering until this provider is ready
}
interface Workspace {
id: string;
store: Store;
providers: Provider[];
}
```
The `provider` is a connector that bridges the current data in memory and the data in another place.
You can combine different providers to build different data storage and loading strategy.
For example, if there is only have `affine-cloud`,
the data will be only loaded from the Cloud and not saved in the local storage,
which might be useful for the enterprise user.
Also, we want to distinguish the different types of Workspace.
Even though the providers are enough for the Workspace, when we display the Workspace in the UI, we need to know the type of Workspace.
AFFiNE Cloud Workspace needs user authentication; the local Workspace does not need it.
And there should have a way to create, read, update, and delete the Workspace.
Hence, we combine all details of the Workspace as we mentioned above into the `WorkspacePlugin` type.
```ts
import React from 'react';
interface UI<WorkspaceType> {
DetailPage: React.FC<UIProps<WorkspaceType>>;
SettingPage: React.FC<UIProps<WorkspaceType>>;
SettingPage: React.FC<UIProps<WorkspaceType>>;
}
interface CRUD<WorkspaceType> {
create: () => Promise<WorkspaceType>;
read: (id: string) => Promise<WorkspaceType>;
list: () => Promise<WorkspaceType[]>;
delete: (Workspace: WorkspaceType) => Promise<WorkspaceType>;
}
interface WorkspacePlugin<WorkspaceType> {
type: WorkspaceType;
ui: UI<WorkspaceType>;
crud: CRUD<WorkspaceType>;
}
```
```mermaid
graph TB
WorkspaceCRUD --> Cloud
WorkspaceCRUD --> SelfHostCloud
subgraph Remote
Cloud[AFFiNE Cloud]
SelfHostCloud[Self Host AFFiNE Server]
end
subgraph Computer
WorkspaceCRUD --> DesktopSqlite[Desktop Sqlite]
subgraph JavaScript Runtime
IndexedDB[IndexedDB]
WorkspaceCRUD --> IndexedDB
subgraph Next.js
Entry((entry point))
Entry --> NextApp[Next.js App]
NextApp --> App[App]
end
subgraph Workspace Runtime
App[App] --> WorkspaceUI
WorkspacePlugin[Workspace Plugin]
WorkspacePlugin[Workspace Plugin] --> WorkspaceUI
WorkspacePlugin[Workspace Plugin] --> WorkspaceCRUD[Workspace CRUD]
WorkspaceUI[Workspace UI] --> WorkspaceCRUD
WorkspaceUI -->|async init| Provider
Provider -->|update ui| WorkspaceUI
Provider -->|update data| WorkspaceCRUD
end
end
end
```
Notice that we do not assume the Workspace UI has to be written in React.js(for now, it has to be),
In the future, we can support other UI frameworks instead, like Vue and Svelte.
### Workspace Loading Details
```mermaid
flowchart TD
subgraph JavaScript Runtime
subgraph Next.js
Start((entry point)) -->|setup environment| OnMount{On mount}
OnMount -->|empty data| Init[Init Workspaces]
Init --> LoadData
OnMount -->|already have data| LoadData>Load data]
LoadData --> CurrentWorkspace[Current workspace]
LoadData --> Workspaces[Workspaces]
Workspaces --> Providers[Providers]
subgraph React
Router([Router]) -->|sync `query.workspaceId`| CurrentWorkspace
CurrentWorkspace -->|sync `currentWorkspaceId`| Router
CurrentWorkspace -->|render| WorkspaceUI[Workspace UI]
end
end
Providers -->|push new update| Persistence[(Persistence)]
Persistence -->|patch workspace| Providers
end
```

View File

@@ -75,3 +75,7 @@ See [building desktop client app](../building-desktop-client-app.md).
```shell
yarn workspace @affine/component storybook
```
## What's next?
- [Behind the code](./behind-the-code.md)

View File

@@ -54,6 +54,46 @@
</p>
</details>
- <b>AFFiNE client app</b> @[affine.pro]
<details><summary>Nodejs · TypeScript · Remote</summary>
<p>
## What we do
We **AFFiNE** hold a vision of shaping a world semantically connected through block components in collaboration applications.
We're open for Fullstack Engineer internship positions across the **Client Application Development** sub-team on creating **AFFiNE client app** for desktop and mobile devices.
## Fullstack Engineer Intern
### This position is for
- Developing AFFiNE **the open source way**, including coding and community engagement.
- Build the **client app** for desktop and mobile devices using web technologies.
### What we are looking for
- Software engineering experience with cross-platform client app development and professional real-world use cases.
- Experience and proficiency in **TypeScript** and a **second programming language** preferably **Rust**.
- Strong communication and writing skills in English.
- Ability to work in a diverse and cross-functional team with skill and ease.
- A lover for open source, sharing our visions and working under those values.
### It would be great if you are
- Heavy user of knowledge/project management tools.
- Experience in Napi.rs, Electron, Tauri, Flutter, React Native, etc.
- Enthusiastic about AFFiNE products as a user or contributor.
## Contact us
Interested? Send us your CV to [contact@toeverything.info].
Feel free to include any extra information (GitHub link, previous projects, personal blog etc.).
</p>
</details>
- <b>Fullstack Engineer - Mainly work with Rust</b> @[affine.pro]
@@ -107,7 +147,8 @@
</p>
</details>
- <b>Senior UI/UX Designer </b> @[affine.pro]
<details><summary>UI / UX · Creative Designer · Singapore / China / Remote</summary>

View File

@@ -1,6 +1,6 @@
{
"name": "AFFiNE",
"version": "0.5.0",
"version": "0.5.3",
"private": true,
"author": "toeverything",
"license": "MPL-2.0",
@@ -42,8 +42,8 @@
"@commitlint/config-conventional": "^17.6.0",
"@faker-js/faker": "^7.6.0",
"@istanbuljs/schema": "^0.1.3",
"@perfsee/sdk": "^1.5.2",
"@playwright/test": "^1.32.2",
"@perfsee/sdk": "^1.5.4",
"@playwright/test": "^1.32.3",
"@testing-library/react": "^14.0.0",
"@types/eslint": "^8.37.0",
"@types/node": "^18.15.11",
@@ -58,6 +58,7 @@
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-simple-import-sort": "^10.0.0",
"eslint-plugin-unused-imports": "^2.0.0",
"fake-indexeddb": "4.0.1",

View File

@@ -53,6 +53,7 @@ const env = {
API_SERVER_PROFILE: dev.server,
PATH: process.env.PATH,
NODE_ENV: 'development',
PORT: 8080,
};
if (dev.debugBlockSuite) {

View File

@@ -1,7 +1,6 @@
{
"name": "@affine/component",
"private": true,
"version": "0.3.1",
"main": "./src/index.ts",
"scripts": {
"storybook": "storybook dev -p 6006",
@@ -14,11 +13,11 @@
"./*": "./src/components/*/index.tsx"
},
"peerDependencies": {
"@blocksuite/blocks": "0.0.0-20230409084303-221991d4-nightly",
"@blocksuite/editor": "0.0.0-20230409084303-221991d4-nightly",
"@blocksuite/global": "0.0.0-20230409084303-221991d4-nightly",
"@blocksuite/icons": "2.1.10",
"@blocksuite/store": "0.0.0-20230409084303-221991d4-nightly"
"@blocksuite/blocks": "*",
"@blocksuite/editor": "*",
"@blocksuite/global": "*",
"@blocksuite/icons": "*",
"@blocksuite/store": "*"
},
"dependencies": {
"@affine/debug": "workspace:*",
@@ -37,6 +36,7 @@
"@radix-ui/react-avatar": "^1.0.2",
"@toeverything/hooks": "workspace:*",
"clsx": "^1.2.1",
"jotai": "^2.0.4",
"kebab-case": "^1.0.2",
"lit": "^2.7.2",
"lottie-web": "^5.11.0",
@@ -48,11 +48,11 @@
"react-is": "^18.2.0"
},
"devDependencies": {
"@blocksuite/blocks": "0.0.0-20230413112150-e058f87e-nightly",
"@blocksuite/editor": "0.0.0-20230413112150-e058f87e-nightly",
"@blocksuite/global": "0.0.0-20230413112150-e058f87e-nightly",
"@blocksuite/blocks": "0.0.0-20230413190748-4d32b79a-nightly",
"@blocksuite/editor": "0.0.0-20230413190748-4d32b79a-nightly",
"@blocksuite/global": "0.0.0-20230413190748-4d32b79a-nightly",
"@blocksuite/icons": "^2.1.10",
"@blocksuite/store": "0.0.0-20230413112150-e058f87e-nightly",
"@blocksuite/store": "0.0.0-20230413190748-4d32b79a-nightly",
"@storybook/addon-actions": "^7.0.4",
"@storybook/addon-coverage": "^0.0.8",
"@storybook/addon-essentials": "^7.0.4",
@@ -66,7 +66,7 @@
"@storybook/react-vite": "^7.0.4",
"@storybook/test-runner": "^0.10.0",
"@storybook/testing-library": "^0.1.0",
"@types/react": "=18.0.31",
"@types/react": "^18.0.35",
"@types/react-dnd": "^3.0.2",
"@types/react-dom": "18.0.11",
"@vanilla-extract/css": "^1.11.0",

View File

@@ -59,13 +59,13 @@ const Template: StoryFn<EditorProps> = (props: EditorProps) => {
export const Empty = Template.bind({});
Empty.play = async ({ canvasElement }) => {
await new Promise<void>(resolve => {
setTimeout(() => resolve(), 500);
});
const editorContainer = canvasElement.querySelector(
'[data-testid="editor-page0"]'
) as HTMLDivElement;
expect(editorContainer).not.toBeNull();
await new Promise<void>(resolve => {
setTimeout(() => resolve(), 50);
});
const editor = editorContainer.querySelector(
'editor-container'
) as EditorContainer;

View File

@@ -1,9 +1,11 @@
import { editorContainerModuleAtom } from '@affine/jotai';
import type { BlockHub } from '@blocksuite/blocks';
import { EditorContainer } from '@blocksuite/editor';
import type { EditorContainer } from '@blocksuite/editor';
import { assertExists } from '@blocksuite/global/utils';
import type { Page } from '@blocksuite/store';
import { useAtomValue } from 'jotai';
import type { CSSProperties, ReactElement } from 'react';
import { memo, useCallback, useEffect, useRef } from 'react';
import { memo, Suspense, useCallback, useEffect, useRef } from 'react';
import type { FallbackProps } from 'react-error-boundary';
import { ErrorBoundary } from 'react-error-boundary';
@@ -27,12 +29,15 @@ declare global {
}
const BlockSuiteEditorImpl = (props: EditorProps): ReactElement => {
const JotaiEditorContainer = useAtomValue(
editorContainerModuleAtom
) as typeof EditorContainer;
const page = props.page;
assertExists(page, 'page should not be null');
const editorRef = useRef<EditorContainer | null>(null);
const blockHubRef = useRef<BlockHub | null>(null);
if (editorRef.current === null) {
editorRef.current = new EditorContainer();
editorRef.current = new JotaiEditorContainer();
editorRef.current.autofocus = true;
globalThis.currentEditor = editorRef.current;
}
@@ -50,7 +55,7 @@ const BlockSuiteEditorImpl = (props: EditorProps): ReactElement => {
}
props.onLoad?.(page, editor);
}
}, [props.page, props.onInit, props.onLoad]);
}, [props.page, props.onInit, props.onLoad, editor, props, page]);
const ref = useRef<HTMLDivElement>(null);
@@ -83,7 +88,7 @@ const BlockSuiteEditorImpl = (props: EditorProps): ReactElement => {
blockHubRef.current?.remove();
container.removeChild(editor);
};
}, [page]);
}, [editor, page]);
return (
<div
data-testid={`editor-${props.page.id}`}
@@ -126,7 +131,9 @@ export const BlockSuiteEditor = memo(function BlockSuiteEditor(
[props.onReset]
)}
>
<BlockSuiteEditorImpl {...props} />
<Suspense fallback={null}>
<BlockSuiteEditorImpl {...props} />
</Suspense>
</ErrorBoundary>
);
});

View File

@@ -1,4 +1,5 @@
import { getEnvironment } from '@affine/env';
import { prefixUrl } from '@affine/env';
import { Trans, useTranslation } from '@affine/i18n';
import type { LocalWorkspace } from '@affine/workspace/type';
import { WorkspaceFlavour } from '@affine/workspace/type';
import { useBlockSuiteWorkspacePageIsPublic } from '@toeverything/hooks/use-blocksuite-workspace-page-is-public';
@@ -6,6 +7,7 @@ import type { FC } from 'react';
import { useState } from 'react';
import { useCallback, useMemo } from 'react';
import { toast } from '../..';
import { PublicLinkDisableModal } from './disable-public-link';
import {
descriptionStyle,
@@ -21,18 +23,17 @@ import {
} from './styles';
export const LocalSharePage: FC<ShareMenuProps> = props => {
const { t } = useTranslation();
return (
<div className={menuItemStyle}>
<div className={descriptionStyle}>
Sharing page publicly requires AFFiNE Cloud service.
</div>
<div className={descriptionStyle}>{t('Shared Pages Description')}</div>
<StyledButton
data-testid="share-menu-enable-affine-cloud-button"
onClick={() => {
props.onEnableAffineCloud(props.workspace as LocalWorkspace);
}}
>
Enable AFFiNE Cloud
{t('Enable AFFiNE Cloud')}
</StyledButton>
</div>
);
@@ -43,25 +44,22 @@ export const AffineSharePage: FC<ShareMenuProps> = props => {
props.currentPage
);
const [showDisable, setShowDisable] = useState(false);
const { t } = useTranslation();
const sharingUrl = useMemo(() => {
const env = getEnvironment();
if (env.isBrowser) {
return `${env.origin}/public-workspace/${props.workspace.id}/${props.currentPage.id}`;
} else {
return '';
}
return `${prefixUrl}public-workspace/${props.workspace.id}/${props.currentPage.id}`;
}, [props.workspace.id, props.currentPage.id]);
const onClickCreateLink = useCallback(() => {
setIsPublic(true);
}, [isPublic]);
}, [setIsPublic]);
const onClickCopyLink = useCallback(() => {
navigator.clipboard.writeText(sharingUrl);
}, []);
toast(t('Copied link to clipboard'));
}, [sharingUrl, t]);
return (
<div className={menuItemStyle}>
<div className={descriptionStyle}>
Create a link you can easily share with anyone.
{t('Create Shared Link Description')}
</div>
<div className={inputButtonRowStyle}>
<StyledInput
@@ -74,7 +72,7 @@ export const AffineSharePage: FC<ShareMenuProps> = props => {
data-testid="affine-share-create-link"
onClick={onClickCreateLink}
>
Create
{t('Create')}
</StyledButton>
)}
{isPublic && (
@@ -82,24 +80,27 @@ export const AffineSharePage: FC<ShareMenuProps> = props => {
data-testid="affine-share-copy-link"
onClick={onClickCopyLink}
>
Copy Link
{t('Copy Link')}
</StyledButton>
)}
</div>
<div className={descriptionStyle}>
The entire Workspace is published on the web and can be edited via
<StyledLinkSpan
onClick={() => {
props.onOpenWorkspaceSettings(props.workspace);
}}
>
Workspace Settings.
</StyledLinkSpan>
<Trans i18nKey="Shared Pages In Public Workspace Description">
The entire Workspace is published on the web and can be edited via
<StyledLinkSpan
onClick={() => {
props.onOpenWorkspaceSettings(props.workspace);
}}
>
Workspace Settings
</StyledLinkSpan>
.
</Trans>
</div>
{isPublic && (
<>
<StyledDisableButton onClick={() => setShowDisable(true)}>
Disable Public Link
{t('Disable Public Link')}
</StyledDisableButton>
<PublicLinkDisableModal
page={props.currentPage}

View File

@@ -1,3 +1,4 @@
import { useTranslation } from '@affine/i18n';
import type { AffineWorkspace, LocalWorkspace } from '@affine/workspace/type';
import { WorkspaceFlavour } from '@affine/workspace/type';
import type { FC } from 'react';
@@ -7,10 +8,11 @@ import type { ShareMenuProps } from './ShareMenu';
import { StyledButton } from './styles';
const ShareLocalWorkspace: FC<ShareMenuProps<LocalWorkspace>> = props => {
const { t } = useTranslation();
return (
<div className={menuItemStyle}>
<div className={descriptionStyle}>
Invite others to join the Workspace or publish it to web.
{t('Share Menu Public Workspace Description1')}
</div>
<StyledButton
data-testid="share-menu-enable-affine-cloud-button"
@@ -18,7 +20,7 @@ const ShareLocalWorkspace: FC<ShareMenuProps<LocalWorkspace>> = props => {
props.onOpenWorkspaceSettings(props.workspace);
}}
>
Open Workspace Settings
{t('Open Workspace Settings')}
</StyledButton>
</div>
);
@@ -26,12 +28,13 @@ const ShareLocalWorkspace: FC<ShareMenuProps<LocalWorkspace>> = props => {
const ShareAffineWorkspace: FC<ShareMenuProps<AffineWorkspace>> = props => {
const isPublicWorkspace = props.workspace.public;
const { t } = useTranslation();
return (
<div className={menuItemStyle}>
<div className={descriptionStyle}>
{isPublicWorkspace
? `Current workspace has been published to the web as a public workspace.`
: `Invite others to join the Workspace or publish it to web`}
? t('Share Menu Public Workspace Description2')
: t('Share Menu Public Workspace Description1')}
</div>
<StyledButton
data-testid="share-menu-publish-to-web-button"
@@ -39,7 +42,7 @@ const ShareAffineWorkspace: FC<ShareMenuProps<AffineWorkspace>> = props => {
props.onOpenWorkspaceSettings(props.workspace);
}}
>
Open Workspace Settings
{t('Open Workspace Settings')}
</StyledButton>
</div>
);

View File

@@ -32,7 +32,7 @@ export const PublicLinkDisableModal = ({
portal: document.body,
});
onClose();
}, []);
}, [onClose, setIsPublic]);
return (
<Modal open={open} onClose={onClose}>
<StyledModalWrapper>

View File

@@ -2,6 +2,7 @@ import '@emotion/react';
import type { EditorContainer } from '@blocksuite/editor';
import { nextDarkColorScheme, nextLightColorScheme } from './color-scheme';
import type { AffineTheme, AffineThemeCSSVariables } from './types';
const basicFontFamily =
@@ -16,11 +17,9 @@ export const getLightTheme = (
},
editorMode,
colors: {
primaryColor: '#5438FF',
pageBackground: '#fff',
hoverBackground: 'rgba(0,0,0,.04)',
innerHoverBackground: '#E9E9EC',
backgroundTertiaryColor: '#E9E9EC',
popoverBackground: '#fff',
tooltipBackground: '#261499',
codeBackground: '#f2f5f9',
@@ -33,23 +32,17 @@ export const getLightTheme = (
textColor: '#424149',
secondaryTextColor: '#8E8D91',
textEmphasisColor: '#5438FF',
edgelessTextColor: '#3A4C5C',
iconColor: '#77757D',
handleColor: '#c7c3d9',
linkColor: '#5438FF',
linkColor2: '#5438FF',
linkVisitedColor: '#5438FF',
tooltipColor: '#fff',
codeColor: '#77757D',
quoteColor: '#645F82',
placeHolderColor: '#C0BFC1',
selectedColor: 'rgba(84, 56, 255, 0.04)',
borderColor: '#E3E2E4',
disableColor: '#A9A9AD',
warningColor: '#906616',
errorColor: '#EB4335',
lineNumberColor: '#77757D',
...nextLightColorScheme,
},
font: {
title: '36px',
@@ -107,11 +100,9 @@ export const getDarkTheme = (
mode: 'dark',
},
colors: {
primaryColor: '#5438FF',
pageBackground: '#2c2c2c',
hoverBackground: 'rgba(0,0,0,.04)',
innerHoverBackground: '#5A5A5A',
backgroundTertiaryColor: '#1E1E1E',
popoverBackground: '#1F2021',
tooltipBackground: '#0C0A15',
codeBackground:
@@ -127,24 +118,18 @@ export const getDarkTheme = (
textColor: '#fff',
secondaryTextColor: '#8E8D91',
textEmphasisColor: '#D0CDDC',
edgelessTextColor: '#3A4C5C',
iconColor: '#77757D',
handleColor: '#c7c3d9',
linkColor: '#7D91FF',
linkColor2: '#0C0A15',
linkVisitedColor: '#505FAB',
tooltipColor: '#fff',
codeColor:
editorMode === 'edgeless' ? lightTheme.colors.codeColor : '#BDDBFD',
quoteColor: '#C6CBD9',
placeHolderColor: '#C7C7C7',
selectedColor: 'rgba(104, 128, 255, 0.1)',
borderColor: '#3C3A40',
disableColor: '#4b4b4b',
warningColor: '#906616',
errorColor: '#EB4335',
lineNumberColor: '#888A9E',
...nextDarkColorScheme,
},
shadow: {
popover:

View File

@@ -1,6 +1,8 @@
import type { EditorContainer } from '@blocksuite/editor';
import type { CSSProperties } from 'react';
import type { AffineNextLightColorScheme } from './color-scheme';
export type Theme = 'light' | 'dark';
export type ThemeMode = Theme | 'auto';
@@ -59,7 +61,7 @@ export interface AffineTheme {
warningColor: string;
errorColor: string;
lineNumberColor: string;
};
} & AffineNextLightColorScheme;
font: {
title: string;
h1: string;

View File

@@ -12,7 +12,7 @@ const SIZE_CONFIG = {
areaSize: 20,
},
[SIZE_MIDDLE]: {
iconSize: 16,
iconSize: 20,
areaSize: 24,
},
[SIZE_NORMAL]: {

View File

@@ -7,7 +7,7 @@ import { StyledMenuWrapper } from './styles';
export type MenuProps = {
width?: CSSProperties['width'];
} & PopperProps &
Omit<TooltipProps, 'title'>;
Omit<TooltipProps, 'title' | 'content'>;
export const Menu = (props: MenuProps) => {
const { width, content, placement = 'bottom-start', children } = props;
return content ? (

View File

@@ -60,4 +60,4 @@ export type PopperProps = {
popperHandlerRef?: Ref<PopperHandler>;
onClickAway?: () => void;
} & Omit<PopperUnstyledProps, 'open'>;
} & Omit<PopperUnstyledProps, 'open' | 'content'>;

View File

@@ -57,7 +57,7 @@ const StyledCircleContainer = styled('div')(({ theme }) => {
});
export const QuickSearchTips = (
props: PopperProps & Omit<TooltipProps, 'title'>
props: PopperProps & Omit<TooltipProps, 'title' | 'content'>
) => {
const { content, placement = 'top', children } = props;
return (

View File

@@ -78,7 +78,7 @@ const TreeNodeItemWithDnd = <RenderProps,>({
if (isOver && canDrop) {
setCollapsed(node.id, false);
}
}, [isOver, canDrop]);
}, [isOver, canDrop, setCollapsed, node.id]);
return (
<TreeNodeItem

View File

@@ -60,7 +60,7 @@ export const TreeView = <RenderProps,>({
document.removeEventListener('keydown', handleDirectionKeyDown);
document.removeEventListener('keydown', handleEnterKeyDown);
};
}, [data, selectedId]);
}, [data, enableKeyboardSelection, onSelect, selectedId]);
const setCollapsed: TreeNodeProps['setCollapsed'] = (id, collapsed) => {
if (disableCollapse) {

View File

@@ -4,7 +4,7 @@
"main": "./src/index.ts",
"module": "./src/index.ts",
"devDependencies": {
"@blocksuite/global": "0.0.0-20230413112150-e058f87e-nightly",
"@blocksuite/global": "0.0.0-20230413190748-4d32b79a-nightly",
"next": "=13.2.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
@@ -12,7 +12,8 @@
},
"exports": {
".": "./src/index.ts",
"./constant": "./src/constant.ts"
"./constant": "./src/constant.ts",
"./blocksuite": "./src/blocksuite.ts"
},
"peerDependencies": {
"@blocksuite/global": "0.0.0-20230409084303-221991d4-nightly"

View File

@@ -1,10 +1,14 @@
import { DebugLogger } from '@affine/debug';
import markdown from '@affine/templates/Welcome-to-AFFiNE.md';
import { ContentParser } from '@blocksuite/blocks/content-parser';
import type { Page } from '@blocksuite/store';
import type { Page, Workspace } from '@blocksuite/store';
import { nanoid } from '@blocksuite/store';
import type { BlockSuiteWorkspace } from '../shared';
declare global {
interface Window {
lastImportedMarkdown: string;
}
}
const demoTitle = markdown
.split('\n')
@@ -50,20 +54,11 @@ export function _initPageWithDemoMarkdown(page: Page): void {
const frameId = page.addBlock('affine:frame', {}, pageBlockId);
page.addBlock('affine:paragraph', {}, frameId);
const contentParser = new ContentParser(page);
contentParser.importMarkdown(demoText, frameId).then(() => {
document.dispatchEvent(
new CustomEvent('markdown:imported', {
detail: {
workspaceId: page.workspace.id,
pageId: page.id,
},
})
);
});
contentParser.importMarkdown(demoText, frameId);
page.workspace.setPageMeta(page.id, { demoTitle });
}
export function ensureRootPinboard(blockSuiteWorkspace: BlockSuiteWorkspace) {
export function ensureRootPinboard(blockSuiteWorkspace: Workspace) {
const metas = blockSuiteWorkspace.meta.pageMetas;
const rootMeta = metas.find(m => m.isRootPinboard);

Some files were not shown because too many files have changed in this diff Show More