mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-04 16:44:56 +00:00
Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec50d721ea | ||
|
|
7bbe67af43 | ||
|
|
caa292e097 | ||
|
|
73b8b805c6 | ||
|
|
084d4e043a | ||
|
|
69a9c34f11 | ||
|
|
d742cab1d5 | ||
|
|
8b3c1fb363 | ||
|
|
ec445207d6 | ||
|
|
49281e68a6 | ||
|
|
a918d6e14c | ||
|
|
7cf7187893 | ||
|
|
2383165470 | ||
|
|
43a96fe8e3 | ||
|
|
b771a2504b | ||
|
|
8d2fefb5f8 | ||
|
|
c71e5f1c96 | ||
|
|
5b96fb0db3 | ||
|
|
46cd0c5c9a | ||
|
|
261a41f8da | ||
|
|
bd387f6551 | ||
|
|
5335118e93 | ||
|
|
70313eb5ee | ||
|
|
ccd2b79d20 | ||
|
|
5ca94db5d2 | ||
|
|
d58f9db289 | ||
|
|
93e78c315c | ||
|
|
3954f309aa | ||
|
|
f902d0c324 | ||
|
|
e79fb1ae3a | ||
|
|
08d67b316c | ||
|
|
d12c00d5cb |
@@ -4,3 +4,4 @@ dist
|
||||
out
|
||||
storybook-static
|
||||
affine-out
|
||||
_next
|
||||
|
||||
@@ -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
1
.github/CLA.md
vendored
@@ -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
|
||||
|
||||
5
.github/ISSUE_TEMPLATE/BUG-REPORT.yml
vendored
5
.github/ISSUE_TEMPLATE/BUG-REPORT.yml
vendored
@@ -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
2
.github/labeler.yml
vendored
@@ -3,7 +3,7 @@ docs:
|
||||
- '**/README.md'
|
||||
- 'packages/templates/**/*'
|
||||
|
||||
tests:
|
||||
test:
|
||||
- 'tests/**/*'
|
||||
- '**/tests/**/*'
|
||||
- '**/__tests__/**/*'
|
||||
|
||||
9
.github/workflows/build-master.yml
vendored
9
.github/workflows/build-master.yml
vendored
@@ -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:
|
||||
|
||||
5
.github/workflows/build.yml
vendored
5
.github/workflows/build.yml
vendored
@@ -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
2
.gitignore
vendored
@@ -5,7 +5,7 @@
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
.yarn/versions
|
||||
|
||||
# compiled output
|
||||
*dist
|
||||
|
||||
550
.yarn/plugins/@yarnpkg/plugin-version.cjs
vendored
Normal file
550
.yarn/plugins/@yarnpkg/plugin-version.cjs
vendored
Normal file
File diff suppressed because one or more lines are too long
28
.yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
vendored
Normal file
28
.yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -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
|
||||
|
||||
10
README.md
10
README.md
@@ -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)
|
||||
[?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)
|
||||
[](https://github.com/toeverything/AFFiNE/releases/latest)
|
||||
[](https://github.com/toeverything/AFFiNE/releases/latest)
|
||||
[](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>
|
||||
|
||||
@@ -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 (əˈfʌɪn | a-fine).</em>
|
||||
</div>
|
||||
<br />
|
||||
</div>
|
||||
|
||||

|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
cacheFolder: '../../.yarn/cache'
|
||||
# deferredVersionFolder: '../../.yarn/versions'
|
||||
deferredVersionFolder: '../../.yarn/versions'
|
||||
globalFolder: '../../.yarn/global'
|
||||
installStatePath: '../../.yarn/install-state.gz'
|
||||
patchFolder: '../../.yarn/patches'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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`;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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' };
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
);
|
||||
|
||||
78
apps/web/src/atoms/root.ts
Normal file
78
apps/web/src/atoms/root.ts
Normal 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
|
||||
@@ -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');
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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]
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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={{
|
||||
|
||||
@@ -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'),
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
@@ -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]
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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']?.();
|
||||
}
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
}
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
48
apps/web/src/hooks/use-router-with-workspace-id-defense.ts
Normal file
48
apps/web/src/hooks/use-router-with-workspace-id-defense.ts
Normal 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,
|
||||
]);
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
18
apps/web/src/hooks/use-sync-router-with-current-page-id.ts
Normal file
18
apps/web/src/hooks/use-sync-router-with-current-page-id.ts
Normal 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]);
|
||||
}
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
@@ -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]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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' }}>
|
||||
@@ -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),
|
||||
[]
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -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>;
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -75,7 +75,13 @@ const ListPageInner: React.FC<{
|
||||
<SearchIcon />
|
||||
</IconButton>
|
||||
</NavContainer>
|
||||
<Suspense>
|
||||
<Suspense
|
||||
fallback={
|
||||
<StyledTableContainer>
|
||||
<ListSkeleton />
|
||||
</StyledTableContainer>
|
||||
}
|
||||
>
|
||||
<BlockSuitePublicPageList
|
||||
onOpenPage={handleClickPage}
|
||||
blockSuiteWorkspace={blockSuiteWorkspace}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 />;
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from './blocksuite';
|
||||
export * from './create-emotion-cache';
|
||||
export * from './string2color';
|
||||
export * from './toast';
|
||||
|
||||
256
docs/contributing/behind-the-code.md
Normal file
256
docs/contributing/behind-the-code.md
Normal 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
|
||||
```
|
||||
@@ -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)
|
||||
|
||||
43
docs/jobs.md
43
docs/jobs.md
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -53,6 +53,7 @@ const env = {
|
||||
API_SERVER_PROFILE: dev.server,
|
||||
PATH: process.env.PATH,
|
||||
NODE_ENV: 'development',
|
||||
PORT: 8080,
|
||||
};
|
||||
|
||||
if (dev.debugBlockSuite) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -32,7 +32,7 @@ export const PublicLinkDisableModal = ({
|
||||
portal: document.body,
|
||||
});
|
||||
onClose();
|
||||
}, []);
|
||||
}, [onClose, setIsPublic]);
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<StyledModalWrapper>
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -12,7 +12,7 @@ const SIZE_CONFIG = {
|
||||
areaSize: 20,
|
||||
},
|
||||
[SIZE_MIDDLE]: {
|
||||
iconSize: 16,
|
||||
iconSize: 20,
|
||||
areaSize: 24,
|
||||
},
|
||||
[SIZE_NORMAL]: {
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -60,4 +60,4 @@ export type PopperProps = {
|
||||
popperHandlerRef?: Ref<PopperHandler>;
|
||||
|
||||
onClickAway?: () => void;
|
||||
} & Omit<PopperUnstyledProps, 'open'>;
|
||||
} & Omit<PopperUnstyledProps, 'open' | 'content'>;
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -78,7 +78,7 @@ const TreeNodeItemWithDnd = <RenderProps,>({
|
||||
if (isOver && canDrop) {
|
||||
setCollapsed(node.id, false);
|
||||
}
|
||||
}, [isOver, canDrop]);
|
||||
}, [isOver, canDrop, setCollapsed, node.id]);
|
||||
|
||||
return (
|
||||
<TreeNodeItem
|
||||
|
||||
@@ -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) {
|
||||
|
||||
5
packages/env/package.json
vendored
5
packages/env/package.json
vendored
@@ -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"
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user