Compare commits

..

125 Commits

Author SHA1 Message Date
himself65
db3f63e8f2 v0.6.0-canary.7 2023-05-25 09:17:21 +08:00
JimmFly
e562ca1011 chore: update download tip link (#2509) 2023-05-24 16:43:45 +08:00
fourdim
f6adf93f90 feat: add simple support for pdf (#2503) 2023-05-24 16:40:20 +08:00
Chi Zhang
053eba5d98 docs: update README.md (#2506) 2023-05-24 14:46:26 +08:00
Himself65
49f1ba676f fix: regression on toast component (#2502) 2023-05-24 13:10:25 +08:00
Aditya Sharma
48c109e149 feat(component): keyboard navigation for image-viewer (#2334)
Co-authored-by: Himself65 <himself65@outlook.com>
2023-05-23 09:35:11 +00:00
LongYinan
259d7988d9 chore(native): upgrade notify to v6 (#2489) 2023-05-22 22:45:43 +08:00
fourdim
0a49258ddd docs: update build guideline (#2434)
Co-authored-by: Himself65 <himself65@outlook.com>
2023-05-22 12:18:43 +00:00
himself65
fd35d3427e fix: make editor width to 800px
Fixes: https://github.com/toeverything/AFFiNE/issues/2486
2023-05-22 17:40:51 +08:00
Himself65
ef0a20b358 fix: use data-testid (#2487) 2023-05-22 17:36:52 +08:00
Himself65
f01997f8ee refactor: remove unused code (#2484) 2023-05-22 17:11:18 +08:00
Whitewater
281a068cfb chore(i18n): remove unused dependencies (#2485) 2023-05-22 17:03:55 +08:00
Whitewater
fe5be0cb47 fix: flatten i18n keys (#2483) 2023-05-22 08:08:43 +00:00
himself65
8aab1d6459 docs: add comment on legacy affine adapter 2023-05-22 16:02:05 +08:00
Himself65
2eaaeef4a7 fix: use hook with first render (#2481) 2023-05-22 15:58:13 +08:00
Himself65
5fbfabb3b2 refactor: rename plugins to adapters (#2480) 2023-05-22 15:48:01 +08:00
himself65
ec64260b6a v0.6.0-canary.6 2023-05-22 14:25:20 +08:00
LongYinan
2e23a4830b ci: add circular import detect (#2475)
Co-authored-by: himself65 <himself65@outlook.com>
2023-05-22 04:53:55 +00:00
Himself65
41a3d6f62f fix: wrap all workspaces with Suspense (#2477) 2023-05-22 12:39:07 +08:00
Peng Xiao
752bc9ca0e fix: fav reference style issue (#2476) 2023-05-22 12:01:03 +08:00
Himself65
c08f6fdba4 chore: update blocksuite to 0.0.0-20230519102837-01acd96b-nightly (#2472) 2023-05-22 02:27:03 +00:00
Geoffrey Biggs
b23b7e896b docs: correct spelling (#2469) 2023-05-22 07:26:30 +08:00
Whitewater
d68b421a4b feat: add responvise page view (#2453) 2023-05-22 07:25:25 +08:00
Horus
1f510799e2 fix: add windows install loading gif (#2462) 2023-05-21 16:03:48 +08:00
Peng Xiao
66ea97c7c9 fix: adjust some windows style issues (#2454) 2023-05-19 09:39:51 -07:00
Shishu
ee300e7b60 docs: sign CLA (#2457) 2023-05-19 08:40:59 -07:00
Peng Xiao
ef2d135e9b fix: optimize app updater (#2452) 2023-05-19 00:07:07 -07:00
JimmFly
c82fb89d57 chore: remove unused i18n key (#2451) 2023-05-19 03:38:48 +00:00
himself65
725bf63a32 ci: remove add-to-project.yml 2023-05-18 18:35:19 -07:00
himself65
c1ca578f7d docs: add download count 2023-05-18 13:12:26 -07:00
Whitewater
530dd5ff7f feat: add new page button (#2417) 2023-05-18 13:07:05 -07:00
Himself65
11370bc07e chore: bump version (#2444) 2023-05-18 11:43:45 -07:00
JimmFly
1c53daf1c4 chore: bump icon version (#2441) 2023-05-18 10:37:40 -07:00
Peng Xiao
b2556db33b fix: adjust some styles (#2438) 2023-05-18 09:24:23 +00:00
JimmFly
89310c9b97 chore: adjust delete description style (#2437) 2023-05-18 09:17:38 +00:00
JimmFly
8e09af910f fix: create workspace card responsive (#2435) 2023-05-18 09:11:15 +00:00
himself65
885aea3425 v0.6.0-canary.5 2023-05-18 01:21:25 -07:00
ShortCipher5
a616150f2d chore: update pre-load content (#2432) 2023-05-18 00:08:35 -07:00
Himself65
d80dae8a89 fix: open non-trash page when open (#2431) 2023-05-17 23:22:31 -07:00
Himself65
34ff08b92b chore: bump blocksuite to 0.0.0-20230518051344-45970a96-nightly (#2430) 2023-05-17 22:30:45 -07:00
Peng Xiao
2f7b51d7ff feat: fav page references (#2422)
Co-authored-by: Himself65 <himself65@outlook.com>
2023-05-17 22:18:40 -07:00
ShortCipher5
b7cee3185e chore: update pre-loading page (#2429) 2023-05-17 22:16:19 -07:00
JimmFly
1c5455e6ed chore: adjust copywriting for onboarding (#2428) 2023-05-17 22:15:01 -07:00
himself65
2d303fd5d3 v0.6.0-canary.4 2023-05-17 17:46:39 -07:00
himself65
fbe2543c03 fix: version check 2023-05-17 17:44:58 -07:00
Himself65
d6b640726e refactor: remove unused code (#2425) 2023-05-17 17:15:12 -07:00
Peng Xiao
f875b37641 fix: configurable changelog url (#2418) 2023-05-17 16:16:22 -07:00
Himself65
53c4fc6dfa fix: sidebar fallback ui position (#2424) 2023-05-17 15:49:55 -07:00
Himself65
6c310249d9 chore: bump version (#2423) 2023-05-17 15:02:55 -07:00
Horus
02e1f528bf fix: add workflow to check release version match with package.json (#2420) 2023-05-17 10:37:28 -07:00
Peng Xiao
c870104370 chore: bump blocksuite to 0.0.0-20230517102216-36bda4ab-nightly (#2411) 2023-05-17 10:09:29 -07:00
himself65
627d8ef787 v0.6.0-canary.3 2023-05-17 09:48:51 -07:00
LongYinan
5563823a7a build: missing build native step in nightly build (#2421) 2023-05-17 23:52:32 +08:00
JimmFly
d6804bb0fd chore: update prompt (#2410) 2023-05-17 17:52:57 +08:00
LongYinan
1350633690 build: fix electron release build process (#2408) 2023-05-17 17:22:49 +08:00
JimmFly
50196d8fde chore: update preloading page (#2409) 2023-05-17 09:16:07 +00:00
Peng Xiao
2e0ccb53ec feat: update button enhancements (#2401) 2023-05-17 16:58:14 +08:00
Whitewater
1498ee405b feat: add dropdown button (#2407) 2023-05-17 16:32:37 +08:00
Peng Xiao
cb863c4afa chore: disable image modal by default (#2400) 2023-05-16 23:14:31 -07:00
Himself65
2629d39501 fix: infinite reloading (#2405) 2023-05-17 13:58:34 +08:00
Himself65
38305cd984 fix: hydration error (#2404) 2023-05-17 05:23:55 +00:00
LongYinan
93116c24f2 feat(electron): use affine native (#2329) 2023-05-17 12:36:51 +08:00
Whitewater
017b9c8615 feat: add block card component (#2398) 2023-05-16 18:19:28 +08:00
Whitewater
9ce3a96862 fix: unexpected undefined class in popup (#2394) 2023-05-16 10:01:27 +00:00
Peng Xiao
a0ff520ba4 fix: some style updates (#2396) 2023-05-16 09:46:51 +00:00
Whitewater
a8b8986d89 chore: disable confused storybook backgrounds addon (#2395) 2023-05-16 17:46:35 +08:00
JimmFly
8ffc096fee fix: text overflows in the header option menu (#2393) 2023-05-16 17:35:57 +08:00
JimmFly
7e457f7b4c chore: add responsive styles for workspace card (#2390) 2023-05-16 16:51:46 +08:00
xiaodong zuo
aedf2d339e Update jobs.md
Added a job posting for a full-time or internship engineer.
2023-05-16 15:35:23 +08:00
JimmFly
ffd5ae52b3 feat: add Japanese support and update translation (#2388) 2023-05-16 14:21:51 +08:00
DiamondThree
3093194da8 docs: update jobs.md (#2389) 2023-05-15 22:24:27 -07:00
Horus
68b4f792f0 fix: app updater not working for internal release (#2377) 2023-05-15 20:34:54 -07:00
himself65
e2c6e4f9fc ci: use samver 2023-05-15 09:34:04 -07:00
Whitewater
9ff7dbffb7 feat: supports sort all page (#2356) 2023-05-15 08:50:43 -07:00
JimmFly
0c561da061 chore: remove favorite page (#2372) 2023-05-15 08:41:38 -07:00
JimmFly
06951319a6 chore: remove quick search tips (#2375) 2023-05-15 08:41:10 -07:00
JimmFly
0bfcab4067 chore: add animation for tour modal (#2365) 2023-05-15 16:48:52 +08:00
himself65
2c4db4fa16 v0.6.0-canary.2 2023-05-14 23:14:36 -07:00
Himself65
23b4f9ee12 feat(electron): track router history (#2336)
Co-authored-by: Peng Xiao <pengxiao@outlook.com>
2023-05-14 23:13:30 -07:00
himself65
e5330b1917 build: add app bundle id for internal 2023-05-14 22:35:40 -07:00
Peng Xiao
183611a556 fix: some style updates (#2348) 2023-05-14 21:58:13 -07:00
Himself65
7786456ba4 chore: update blocksuite to 0.0.0-20230514141009-705c0fac-nightly (#2357) 2023-05-14 19:32:27 -07:00
Ikko Eltociear Ashimine
f4bf7e3ddf fix: typo in AFFiNE-Docs.md (#2355) 2023-05-13 22:37:42 -07:00
Doma
05d88215d1 feat(electron): app menu item and hotkey for creating new page (#2267)
Co-authored-by: Peng Xiao <pengxiao@outlook.com>
2023-05-13 15:45:12 +00:00
Himself65
b240a70e51 chore: update blocksuite to 0.0.0-20230512192655-e61e272b-nightly (#2352) 2023-05-12 15:39:05 -05:00
LongYinan
00fd468e9b chore(server): remove bcrypt to avoid node-gyp usage (#2349) 2023-05-12 13:48:38 -05:00
Himself65
b5a7f8b7eb chore: bump version (#2331) 2023-05-12 13:47:14 -05:00
himself65
f03277fd17 v0.6.0-canary.1 2023-05-12 01:30:54 -05:00
himself65
ee93071149 chore: update icons 2023-05-12 01:06:05 -05:00
Himself65
21fdced2bd fix: correct router logic (#2342) 2023-05-12 00:55:45 -05:00
Peng Xiao
10b4558947 feat: new sidebar (app shell) styles (#2303) 2023-05-11 22:13:51 -05:00
Himself65
0fbed5d9d6 ci: collect test coverage on electron (#2335) 2023-05-11 20:51:13 -05:00
Himself65
8d117123d7 fix: remove useEffect on router sync with atoms (#2241) 2023-05-11 16:37:42 -05:00
Himself65
063ffda09d refactor: rename WorkspacePlugin to WorkspaceAdapter (#2330) 2023-05-11 12:43:39 -05:00
Himself65
39c83bd25b fix: delay setAom on rootWorkspacesMetadataAtom (#2271) 2023-05-11 15:03:11 +00:00
Peng Xiao
4444c3d1a6 fix: updater issue 2023-05-11 14:44:54 +08:00
LongYinan
717dd93f37 fix(electron): close db before move db file 2023-05-11 14:41:51 +08:00
LongYinan
c58673c55f chore(native): license 2023-05-11 14:41:51 +08:00
LongYinan
768e55072d ci: rust build config 2023-05-11 14:41:51 +08:00
LongYinan
8c84daec2b feat(native): NotifyEvent types 2023-05-11 14:41:51 +08:00
LongYinan
e54a5b6128 feat(native): provide FSWatcher 2023-05-11 14:41:51 +08:00
LongYinan
ee1e50f391 refactor(native): rename folder name 2023-05-11 14:41:51 +08:00
himself65
268636c440 v0.6.0-canary.0 2023-05-11 01:09:21 -05:00
Peng Xiao
06fa0cdb60 fix: should not show open folder if it is not moved (#2299) 2023-05-11 05:36:22 +00:00
Himself65
73dbb39009 feat(component): improve fallback skeleton (#2323) 2023-05-11 00:35:42 -05:00
JimmFly
47848cb5da fix: delete modal on confirm does not close (#2322) 2023-05-11 05:19:11 +00:00
JimmFly
eff6a03a51 chore: update AFFiNE Cloud prompt (#2321) 2023-05-11 00:18:12 -05:00
himself65
08f6a41ef4 ci: fix set version scripts 2023-05-10 23:00:36 -05:00
himself65
6d1345ffe5 build: replace version 2023-05-10 22:24:34 -05:00
Himself65
689f615b11 chore: bump version (#2310) 2023-05-10 21:43:14 -05:00
Himself65
f82ea5d9c4 build(electron): add internal release channel (#2309) 2023-05-10 21:42:56 -05:00
himself65
dc4979a80c fix(electron): remove unused code 2023-05-10 15:04:13 -05:00
JimmFly
1f48bc4301 refactor: tour modal (#2297) 2023-05-10 08:11:42 +00:00
himself65
beabd1e050 v0.5.4-canary.31 2023-05-10 00:56:04 -05:00
Himself65
19e20a6a20 fix(component): toast too many times when switch page mode (#2296) 2023-05-10 00:50:51 -05:00
Peng Xiao
e4f13ddae4 fix: try to fix updater not working (#2294)
Co-authored-by: Himself65 <himself65@outlook.com>
2023-05-10 00:40:22 -05:00
Himself65
752d0545ca feat: enhance root div styles (#2295) 2023-05-10 00:39:51 -05:00
himself65
08341d3d6c ci: remove master branch build 2023-05-09 23:11:46 -05:00
himself65
ef665df330 ci: add nightly-build.yml 2023-05-09 23:04:24 -05:00
Himself65
b38017cd23 feat(component): add skeleton in page detail (#2292) 2023-05-09 22:38:30 -05:00
Peng Xiao
0c550a2827 fix: theme not being persisted issue (#2283) 2023-05-09 22:04:36 -05:00
Chi Zhang
87ffdad862 docs: update README.md (#2291) 2023-05-09 21:51:44 -05:00
himself65
c6e8024e16 ci: disable fall-test in desktop-test 2023-05-09 21:13:33 -05:00
himself65
4200b3c3e5 ci: build staging and release branches 2023-05-09 20:27:07 -05:00
Himself65
10976a9257 chore: bump version (#2287) 2023-05-09 15:40:36 -05:00
166 changed files with 2664 additions and 2416 deletions

1
.github/CLA.md vendored
View File

@@ -58,3 +58,4 @@ Example:
- Howard Do, @howarddo2208, 2023/04/20
- 三咲智子 Kevin Deng, @sxzz, 2023/04/21
- Moeyua, @moeyua, 2023/04/22
- Shishu, @shishudesu, 2023/05/19

View File

@@ -1,24 +0,0 @@
name: Add to GitHub projects
on:
issues:
types:
- opened
pull_request_target:
types:
- opened
- reopened
jobs:
add-to-project:
name: Add issues and pull requests
runs-on: ubuntu-latest
steps:
- uses: actions/add-to-project@v0.4.0
with:
# You can target a repository in a different organization
# to the issue
project-url: https://github.com/orgs/toeverything/projects/10
github-token: ${{ secrets.ADD_TO_PROJECT_PAT }}
# labeled: bug, needs-triage
# label-operator: OR

View File

@@ -35,7 +35,9 @@ jobs:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
- run: yarn lint --max-warnings=0
- run: |
yarn lint --max-warnings=0
yarn circular
build-storybook:
name: Build Storybook
@@ -365,11 +367,6 @@ jobs:
name: affine
fail_ci_if_error: true
- name: Run desktop tests
if: ${{ matrix.spec.test && matrix.spec.os != 'ubuntu-latest' }}
run: yarn test
working-directory: apps/electron
- name: Upload test results
if: ${{ failure() }}
uses: actions/upload-artifact@v3

4
.gitignore vendored
View File

@@ -28,9 +28,9 @@ node_modules
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/settings.template.json
!.vscode/launch.template.json
!.vscode/extensions.json
# misc

View File

@@ -6,6 +6,12 @@
"name": "Run Dev",
"request": "launch",
"type": "node-terminal"
},
{
"command": "yarn run dev:local",
"name": "Run Dev Locally",
"request": "launch",
"type": "node-terminal"
}
]
}

21
Cargo.lock generated
View File

@@ -305,9 +305,9 @@ dependencies = [
[[package]]
name = "notify"
version = "5.1.0"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58ea850aa68a06e48fdb069c0ec44d0d64c8dbffa49bf3b6f7f0a901fdea1ba9"
checksum = "4d9ba6c734de18ca27c8cef5cd7058aa4ac9f63596131e4c7e41e579319032a2"
dependencies = [
"bitflags 1.3.2",
"crossbeam-channel",
@@ -319,7 +319,7 @@ dependencies = [
"mio",
"serde",
"walkdir",
"windows-sys 0.42.0",
"windows-sys 0.45.0",
]
[[package]]
@@ -652,21 +652,6 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-sys"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
dependencies = [
"windows_aarch64_gnullvm 0.42.2",
"windows_aarch64_msvc 0.42.2",
"windows_i686_gnu 0.42.2",
"windows_i686_msvc 0.42.2",
"windows_x86_64_gnu 0.42.2",
"windows_x86_64_gnullvm 0.42.2",
"windows_x86_64_msvc 0.42.2",
]
[[package]]
name = "windows-sys"
version = "0.45.0"

View File

@@ -30,6 +30,7 @@ See https://github.com/all-?/all-contributors/issues/361#issuecomment-637166066
[![AFFiNE Window x64](https://img.shields.io/badge/-Windows%20%E2%86%92-blue?style=flat-square&logo=windows&logoColor=white)](https://affine.pro/download)
[![AFFiNE Linux](https://img.shields.io/badge/-Linux%20%E2%86%92-yellow?style=flat-square&logo=linux&logoColor=white)](https://affine.pro/download)
[![Releases](https://img.shields.io/github/downloads/toeverything/AFFiNE/total)](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)
@@ -69,7 +70,7 @@ See https://github.com/all-?/all-contributors/issues/361#issuecomment-637166066
Before we tell you how to get started with AFFiNE, we'd like to shamelessly plug our awesome user and developer communities across [official social platforms](https://community.affine.pro/c/start-here/)! Once youre familiar with using the software, maybe you will share your wisdom with others and even consider joining the [AFFiNE Ambassador program](https://community.affine.pro/c/start-here/affine-ambassador) to help spread AFFiNE to the world.
## Getting started & Stay tunned with us.
## Getting started & staying tuned with us.
⚠️ Please note that AFFiNE is still under active development and is not yet ready for production use. ⚠️
@@ -140,7 +141,7 @@ Thanks a lot to the community for providing such powerful and simple libraries,
We would like to express our gratitude to all the individuals who have already contributed to AFFiNE! If you have any AFFiNE-related project, documentation, tool or template, please feel free to contribute it by submitting a pull request to our curated list on GitHub: [awesome-affine](https://github.com/toeverything/awesome-affine).
<a href="https://github.com/toeverything/affine/graphs/contributors">
<img src="https://user-images.githubusercontent.com/5910926/237263745-36bb975d-83d6-4a7c-a152-d9ad020e5023.png" />
<img src="https://user-images.githubusercontent.com/5910926/240508358-93eddded-48a0-40cd-85e4-a1d172dbe1d9.svg" />
</a>
## Self-Host

View File

@@ -94,7 +94,7 @@ module.exports = {
config: {
name: 'AFFiNE',
setupIcon: icoPath,
// loadingGif: './resources/icons/loading.gif',
loadingGif: './resources/icons/affine_installing.gif',
},
},
],

View File

@@ -10,6 +10,7 @@ import { logger } from './logger';
import { restoreOrCreateWindow } from './main-window';
import { registerProtocol } from './protocol';
if (require('electron-squirrel-startup')) app.quit();
// allow tests to overwrite app name through passing args
if (process.argv.includes('--app-name')) {
const appNameIndex = process.argv.indexOf('--app-name');

View File

@@ -1,7 +1,7 @@
{
"name": "@affine/electron",
"private": true,
"version": "0.5.4-beta.1",
"version": "0.6.0-canary.7",
"author": "affine",
"repository": {
"url": "https://github.com/toeverything/AFFiNE",

Binary file not shown.

After

Width:  |  Height:  |  Size: 480 KiB

View File

@@ -1,7 +1,7 @@
{
"name": "@affine/server",
"private": true,
"version": "0.5.4-beta.1",
"version": "0.6.0-canary.7",
"description": "Affine Node.js server",
"type": "module",
"bin": {
@@ -37,7 +37,7 @@
"@types/express": "^4.17.17",
"@types/jsonwebtoken": "^9.0.2",
"@types/lodash-es": "^4.17.7",
"@types/node": "^18.16.12",
"@types/node": "^18.16.13",
"@types/supertest": "^2.0.12",
"c8": "^7.13.0",
"nodemon": "^2.0.22",

View File

@@ -10,7 +10,7 @@ First, run the development server:
pnpm run dev
```
Open [http://localhost:8080](http://localhost:3000) with your browser to see the result.
Open [http://localhost:8080](http://localhost:8080) with your browser to see the result.
You can start editing the page by modifying `src/pages/workspace/[workspaceId]/all.tsx`. The page auto-updates as you edit the file.

View File

@@ -1,13 +1,12 @@
{
"name": "@affine/web",
"private": true,
"version": "0.5.4-beta.1",
"version": "0.6.0-canary.7",
"scripts": {
"dev": "next dev",
"build": "next build",
"export": "next export",
"start": "next start",
"lint": "next lint"
"start": "next start"
},
"dependencies": {
"@affine-test/fixtures": "workspace:*",
@@ -19,12 +18,12 @@
"@affine/jotai": "workspace:*",
"@affine/templates": "workspace:*",
"@affine/workspace": "workspace:*",
"@blocksuite/blocks": "0.0.0-20230518051344-45970a96-nightly",
"@blocksuite/editor": "0.0.0-20230518051344-45970a96-nightly",
"@blocksuite/global": "0.0.0-20230518051344-45970a96-nightly",
"@blocksuite/icons": "^2.1.16",
"@blocksuite/lit": "0.0.0-20230518051344-45970a96-nightly",
"@blocksuite/store": "0.0.0-20230518051344-45970a96-nightly",
"@blocksuite/blocks": "0.0.0-20230519102837-01acd96b-nightly",
"@blocksuite/editor": "0.0.0-20230519102837-01acd96b-nightly",
"@blocksuite/global": "0.0.0-20230519102837-01acd96b-nightly",
"@blocksuite/icons": "^2.1.18",
"@blocksuite/lit": "0.0.0-20230519102837-01acd96b-nightly",
"@blocksuite/store": "0.0.0-20230519102837-01acd96b-nightly",
"@dnd-kit/core": "^6.0.8",
"@dnd-kit/sortable": "^7.0.2",
"@emotion/cache": "^11.11.0",

View File

@@ -1,10 +1,13 @@
import { AFFINE_STORAGE_KEY, config, prefixUrl } from '@affine/env';
/**
* This file has deprecated because we do not maintain legacy affine cloud,
* please use new affine cloud instead.
*/
import { AFFINE_STORAGE_KEY, config } from '@affine/env';
import { initPage } from '@affine/env/blocksuite';
import { PageNotFoundError } from '@affine/env/constant';
import { currentAffineUserAtom } from '@affine/workspace/affine/atom';
import {
clearLoginStorage,
createAffineAuth,
getLoginStorage,
isExpired,
parseIdToken,
@@ -37,9 +40,9 @@ import { PageDetailEditor } from '../../components/page-detail-editor';
import { PageLoading } from '../../components/pure/loading';
import { useAffineRefreshAuthToken } from '../../hooks/affine/use-affine-refresh-auth-token';
import { BlockSuiteWorkspace } from '../../shared';
import { affineApis } from '../../shared/apis';
import { affineApis, affineAuth } from '../../shared/apis';
import { toast } from '../../utils';
import type { WorkspaceAdapter } from '..';
import type { WorkspaceAdapter } from '../type';
import { QueryKey } from './fetcher';
const storage = createJSONStorage(() => localStorage);
@@ -79,8 +82,6 @@ const getPersistenceAllWorkspace = () => {
return allWorkspaces;
};
export const affineAuth = createAffineAuth(prefixUrl);
function AuthContext({ children }: PropsWithChildren): ReactElement {
const login = useAffineRefreshAuthToken();

View File

@@ -17,12 +17,31 @@ import {
} from '@affine/workspace/type';
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
import { nanoid } from '@blocksuite/store';
import React from 'react';
import { lazy } from 'react';
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 type { WorkspaceAdapter } from '..';
import type { WorkspaceAdapter } from '../type';
const WorkspaceSettingDetail = lazy(() =>
import('../../components/affine/workspace-setting-detail').then(
({ WorkspaceSettingDetail }) => ({
default: WorkspaceSettingDetail,
})
)
);
const BlockSuitePageList = lazy(() =>
import('../../components/blocksuite/block-suite-page-list').then(
({ BlockSuitePageList }) => ({
default: BlockSuitePageList,
})
)
);
const PageDetailEditor = lazy(() =>
import('../../components/page-detail-editor').then(
({ PageDetailEditor }) => ({
default: PageDetailEditor,
})
)
);
const logger = new DebugLogger('use-create-first-workspace');
@@ -62,7 +81,7 @@ export const LocalPlugin: WorkspaceAdapter<WorkspaceFlavour.LOCAL> = {
Provider: ({ children }) => {
return <>{children}</>;
},
PageDetail: ({ currentWorkspace, currentPageId }) => {
PageDetail: ({ currentWorkspace, currentPageId, onLoadEditor }) => {
const page = currentWorkspace.blockSuiteWorkspace.getPage(currentPageId);
if (!page) {
throw new PageNotFoundError(
@@ -75,6 +94,7 @@ export const LocalPlugin: WorkspaceAdapter<WorkspaceFlavour.LOCAL> = {
<PageDetailEditor
pageId={currentPageId}
onInit={initPage}
onLoad={onLoadEditor}
workspace={currentWorkspace}
/>
</>

View File

@@ -0,0 +1,21 @@
import type {
AppEvents,
WorkspaceCRUD,
WorkspaceUISchema,
} from '@affine/workspace/type';
import type {
LoadPriority,
ReleaseType,
WorkspaceFlavour,
} from '@affine/workspace/type';
export interface WorkspaceAdapter<Flavour extends WorkspaceFlavour> {
releaseType: ReleaseType;
flavour: Flavour;
// Plugin will be loaded according to the priority
loadPriority: LoadPriority;
Events: Partial<AppEvents>;
// Fetch necessary data for the first render
CRUD: WorkspaceCRUD<Flavour>;
UI: WorkspaceUISchema<Flavour>;
}

View File

@@ -1,8 +1,4 @@
import type {
AppEvents,
WorkspaceCRUD,
WorkspaceUISchema,
} from '@affine/workspace/type';
import type { AppEvents } from '@affine/workspace/type';
import {
LoadPriority,
ReleaseType,
@@ -11,17 +7,7 @@ import {
import { AffinePlugin } from './affine';
import { LocalPlugin } from './local';
export interface WorkspaceAdapter<Flavour extends WorkspaceFlavour> {
releaseType: ReleaseType;
flavour: Flavour;
// Plugin will be loaded according to the priority
loadPriority: LoadPriority;
Events: Partial<AppEvents>;
// Fetch necessary data for the first render
CRUD: WorkspaceCRUD<Flavour>;
UI: WorkspaceUISchema<Flavour>;
}
import type { WorkspaceAdapter } from './type';
const unimplemented = () => {
throw new Error('Not implemented');

View File

@@ -19,7 +19,7 @@ import type { Page } from '@blocksuite/store';
import { createStore } from 'jotai';
import { describe, expect, test } from 'vitest';
import { WorkspaceAdapters } from '../../plugins';
import { WorkspaceAdapters } from '../../adapters/workspace';
import { rootCurrentWorkspaceAtom } from '../root';
describe('currentWorkspace atom', () => {

View File

@@ -11,8 +11,8 @@ import type { Page } from '@blocksuite/store';
import { atom } from 'jotai';
import { atomWithStorage } from 'jotai/utils';
import { WorkspaceAdapters } from '../adapters/workspace';
import type { CreateWorkspaceMode } from '../components/affine/create-workspace-modal';
import { WorkspaceAdapters } from '../plugins';
const logger = new DebugLogger('web:atoms');
@@ -96,7 +96,13 @@ export const openDisableCloudAlertModalAtom = atom(false);
export { workspacesAtom } from './root';
type View = { id: string; mode: 'page' | 'edgeless' };
type View = {
id: string;
/**
* @deprecated Use `mode` from `useWorkspacePreferredMode` instead.
*/
mode: 'page' | 'edgeless';
};
export type WorkspaceRecentViews = Record<string, View[]>;
@@ -106,6 +112,9 @@ export const workspaceRecentViewsAtom = atomWithStorage<WorkspaceRecentViews>(
);
export type PreferredModeRecord = Record<Page['id'], 'page' | 'edgeless'>;
/**
* @deprecated Use `useWorkspacePreferredMode` instead.
*/
export const workspacePreferredModeAtom = atomWithStorage<PreferredModeRecord>(
'preferredMode',
{}

View File

@@ -13,7 +13,6 @@ import { WorkspaceFlavour } from '@affine/workspace/type';
import { assertExists } from '@blocksuite/store';
import { atom } from 'jotai';
import { WorkspaceAdapters } from '../plugins';
import type { AllWorkspace } from '../shared';
const logger = new DebugLogger('web:atoms:root');
@@ -22,6 +21,7 @@ const logger = new DebugLogger('web:atoms:root');
* Fetch all workspaces from the Plugin CRUD
*/
export const workspacesAtom = atom<Promise<AllWorkspace[]>>(async get => {
const { WorkspaceAdapters } = await import('../adapters/workspace');
const flavours: string[] = Object.values(WorkspaceAdapters).map(
plugin => plugin.flavour
);
@@ -82,6 +82,7 @@ export const workspacesAtom = atom<Promise<AllWorkspace[]>>(async get => {
*/
export const rootCurrentWorkspaceAtom = atom<Promise<AllWorkspace>>(
async get => {
const { WorkspaceAdapters } = await import('../adapters/workspace');
const metadata = get(rootWorkspacesMetadataAtom);
const targetId = get(rootCurrentWorkspaceIdAtom);
if (targetId === null) {

View File

@@ -36,6 +36,7 @@ const Editor: React.FC<{
globalThis.page = page;
// @ts-ignore
globalThis.editor = editor;
return () => void 0;
}, []);
if (!page) {

View File

@@ -1,2 +0,0 @@
export * from './pinboard-menu';
export * from './pinboard-render/';

View File

@@ -1,133 +0,0 @@
import type { PureMenuProps } from '@affine/component';
import { Input, PureMenu, TreeView } from '@affine/component';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { RemoveIcon, SearchIcon } from '@blocksuite/icons';
import type { PageMeta } from '@blocksuite/store';
import React, { useCallback, useMemo, useState } from 'react';
import { useReferenceLinkHelper } from '../../../../hooks/affine/use-reference-link-helper';
import { usePinboardData } from '../../../../hooks/use-pinboard-data';
import { usePinboardHandler } from '../../../../hooks/use-pinboard-handler';
import type { BlockSuiteWorkspace } from '../../../../shared';
import { toast } from '../../../../utils';
import { PinboardRender } from '../pinboard-render/';
import {
StyledMenuContent,
StyledMenuFooter,
StyledMenuSubTitle,
StyledPinboard,
StyledSearchContainer,
} from '../styles';
import { SearchContent } from './search-content';
export interface PinboardMenuProps extends PureMenuProps {
metas: PageMeta[];
currentMeta: PageMeta;
blockSuiteWorkspace: BlockSuiteWorkspace;
showRemovePinboard?: boolean;
onPinboardClick?: (p: { dragId: string; dropId: string }) => void;
}
export const PinboardMenu = ({
metas: propsMetas,
currentMeta,
blockSuiteWorkspace,
showRemovePinboard = false,
onPinboardClick,
...pureMenuProps
}: PinboardMenuProps) => {
const metas = useMemo(
() => propsMetas.filter(m => m.id !== currentMeta.id),
[currentMeta.id, propsMetas]
);
const t = useAFFiNEI18N();
const [query, setQuery] = useState('');
const isSearching = query.length > 0;
const searchResult = metas.filter(
meta => !meta.trash && meta.title.includes(query)
);
const { removeReferenceLink } = useReferenceLinkHelper(blockSuiteWorkspace);
const { dropPin } = usePinboardHandler({
blockSuiteWorkspace,
metas,
});
const handleClick = useCallback(
(dropId: string) => {
const targetTitle = metas.find(m => m.id === dropId)?.title;
dropPin(currentMeta.id, dropId, {
bottomLine: false,
topLine: false,
internal: true,
});
onPinboardClick?.({ dragId: currentMeta.id, dropId });
toast(`Moved "${currentMeta.title}" to "${targetTitle}"`);
},
[currentMeta.id, currentMeta.title, dropPin, metas, onPinboardClick]
);
const { data } = usePinboardData({
metas,
pinboardRender: PinboardRender,
blockSuiteWorkspace,
onClick: (e, node) => {
handleClick(node.id);
},
});
return (
<PureMenu
width={320}
height={480}
{...pureMenuProps}
data-testid="pinboard-menu"
>
<StyledSearchContainer>
<label>
<SearchIcon />
</label>
<Input
value={query}
onChange={setQuery}
placeholder={t['Move page to']()}
height={32}
noBorder={true}
onClick={e => e.stopPropagation()}
data-testid="pinboard-menu-search"
/>
</StyledSearchContainer>
<StyledMenuContent>
{isSearching && (
<SearchContent results={searchResult} onClick={handleClick} />
)}
{!isSearching && (
<>
<StyledMenuSubTitle>Suggested</StyledMenuSubTitle>
<TreeView data={data} indent={16} enableDnd={false} />
</>
)}
</StyledMenuContent>
{showRemovePinboard && (
<StyledMenuFooter>
<StyledPinboard
data-testid={'remove-from-pinboard-button'}
onClick={() => {
removeReferenceLink(currentMeta.id);
}}
>
<RemoveIcon />
{t['Remove from Pivots']()}
</StyledPinboard>
<p>{t['RFP']()}</p>
</StyledMenuFooter>
)}
</PureMenu>
);
};
export default PinboardMenu;

View File

@@ -1,63 +0,0 @@
import { FlexWrapper } from '@affine/component';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
import type { PageMeta } from '@blocksuite/store';
import { useAtomValue } from 'jotai';
import Image from 'next/legacy/image';
import React from 'react';
import { workspacePreferredModeAtom } from '../../../../atoms';
import { StyledMenuSubTitle, StyledPinboard } from '../styles';
export const SearchContent = ({
results,
onClick,
}: {
results: PageMeta[];
onClick?: (dropId: string) => void;
}) => {
const t = useAFFiNEI18N();
const record = useAtomValue(workspacePreferredModeAtom);
if (results.length) {
return (
<>
<StyledMenuSubTitle>
{t['Find results']({ number: `${results.length}` })}
</StyledMenuSubTitle>
{results.map(meta => {
return (
<StyledPinboard
key={meta.id}
onClick={() => {
onClick?.(meta.id);
}}
data-testid="pinboard-search-result"
>
{record[meta.id] === 'edgeless' ? <EdgelessIcon /> : <PageIcon />}
{meta.title}
</StyledPinboard>
);
})}
</>
);
}
return (
<>
<StyledMenuSubTitle>{t['Find 0 result']()}</StyledMenuSubTitle>
<FlexWrapper
alignItems="center"
justifyContent="center"
style={{ marginTop: 20 }}
>
<Image
src="/imgs/no-result.svg"
alt="no result"
width={150}
height={150}
/>
</FlexWrapper>
</>
);
};

View File

@@ -1,22 +0,0 @@
import { PlusIcon } from '@blocksuite/icons';
import { StyledOperationButton } from '../styles';
import type { OperationButtonProps } from './operation-button';
export const AddButton = ({
onAdd,
visible,
}: Pick<OperationButtonProps, 'onAdd' | 'visible'>) => {
return (
<StyledOperationButton
visible={visible}
size="small"
onClick={e => {
e.stopPropagation();
onAdd();
}}
>
<PlusIcon />
</StyledOperationButton>
);
};

View File

@@ -1,14 +0,0 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { StyledPinboard } from '../styles';
export const EmptyItem = () => {
const t = useAFFiNEI18N();
return (
<StyledPinboard disable={true} textWrap={true}>
{t['Organize pages to build knowledge']()}
</StyledPinboard>
);
};
export default EmptyItem;

View File

@@ -1,137 +0,0 @@
import { Input } from '@affine/component';
import {
ArrowDownSmallIcon,
EdgelessIcon,
LevelIcon,
PageIcon,
PinboardIcon,
} from '@blocksuite/icons';
import { usePageMetaHelper } from '@toeverything/hooks/use-block-suite-page-meta';
import { useAtomValue } from 'jotai';
import { useRouter } from 'next/router';
import { useMemo, useState } from 'react';
import { workspacePreferredModeAtom } from '../../../../atoms';
import type { PinboardNode } from '../../../../hooks/use-pinboard-data';
import { StyledCollapsedButton, StyledPinboard } from '../styles';
import { AddButton } from './add-button';
import EmptyItem from './empty-item';
import { OperationButton } from './operation-button';
const getIcon = (type: 'root' | 'edgeless' | 'page') => {
switch (type) {
case 'root':
return <PinboardIcon className="mode-icon" />;
case 'edgeless':
return <EdgelessIcon className="mode-icon" />;
default:
return <PageIcon className="mode-icon" />;
}
};
export const PinboardRender: PinboardNode['render'] = (
node,
{
isOver,
onAdd,
onDelete,
collapsed,
setCollapsed,
isSelected,
disableCollapse,
},
renderProps
) => {
const {
onClick,
showOperationButton = false,
currentMeta,
metas = [],
blockSuiteWorkspace,
asPath,
} = renderProps!;
const record = useAtomValue(workspacePreferredModeAtom);
const { setPageTitle } = usePageMetaHelper(blockSuiteWorkspace);
const router = useRouter();
const [isHover, setIsHover] = useState(false);
const [showRename, setShowRename] = useState(false);
const active = router.query.pageId === node.id;
const isRoot = !!currentMeta.isRootPinboard;
return (
<>
<StyledPinboard
data-testid={`pinboard-${node.id}`}
onClick={e => {
onClick?.(e, node);
}}
onMouseEnter={() => setIsHover(true)}
onMouseLeave={() => setIsHover(false)}
isOver={isOver || isSelected}
active={active}
disableCollapse={!!disableCollapse}
>
{!disableCollapse && (
<StyledCollapsedButton
collapse={collapsed}
show={!!node.children?.length}
onClick={e => {
e.stopPropagation();
setCollapsed(node.id, !collapsed);
}}
>
<ArrowDownSmallIcon />
</StyledCollapsedButton>
)}
{asPath && !isRoot ? <LevelIcon className="path-icon" /> : null}
{getIcon(isRoot ? 'root' : record[node.id])}
{showRename ? (
<Input
data-testid={`pinboard-input-${node.id}`}
value={currentMeta.title || ''}
placeholder="Untitled"
onClick={e => e.stopPropagation()}
height={32}
onBlur={() => {
setShowRename(false);
}}
onChange={value => {
// FIXME: setPageTitle would make input blur, and can't input the Chinese character
setPageTitle(node.id, value);
}}
/>
) : (
<span>{isRoot ? 'Pinboard' : currentMeta.title || 'Untitled'}</span>
)}
{showOperationButton && <AddButton onAdd={onAdd} visible={isHover} />}
{showOperationButton && (
<OperationButton
isRoot={isRoot}
onAdd={onAdd}
onDelete={onDelete}
metas={metas}
currentMeta={currentMeta!}
blockSuiteWorkspace={blockSuiteWorkspace!}
visible={isHover}
onMenuClose={() => setIsHover(false)}
onRename={() => {
setShowRename(true);
setIsHover(false);
}}
/>
)}
</StyledPinboard>
{useMemo(
() =>
isRoot &&
!metas.find(m => (currentMeta.subpageIds ?? []).includes(m.id)),
[currentMeta.subpageIds, isRoot, metas]
) && <EmptyItem />}
</>
);
};
export default PinboardRender;

View File

@@ -1,170 +0,0 @@
import { MenuItem, MuiClickAwayListener, PureMenu } from '@affine/component';
import { CopyLink, MoveToTrash } from '@affine/component/page-list';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import {
MoreVerticalIcon,
MoveToIcon,
PenIcon,
PlusIcon,
} from '@blocksuite/icons';
import type { PageMeta } from '@blocksuite/store';
import { baseTheme } from '@toeverything/theme';
import { useMemo, useRef, useState } from 'react';
import { useBlockSuiteMetaHelper } from '../../../../hooks/affine/use-block-suite-meta-helper';
import type { BlockSuiteWorkspace } from '../../../../shared';
import { toast } from '../../../../utils';
import { PinboardMenu } from '../pinboard-menu/';
import { StyledOperationButton } from '../styles';
export type OperationButtonProps = {
isRoot: boolean;
onAdd: () => void;
onDelete: () => void;
metas: PageMeta[];
currentMeta: PageMeta;
blockSuiteWorkspace: BlockSuiteWorkspace;
visible: boolean;
onRename?: () => void;
onMenuClose?: () => void;
};
export const OperationButton = ({
isRoot,
onAdd,
onDelete,
metas,
currentMeta,
blockSuiteWorkspace,
visible,
onMenuClose,
onRename,
}: OperationButtonProps) => {
const t = useAFFiNEI18N();
const timer = useRef<ReturnType<typeof setTimeout>>();
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [operationMenuOpen, setOperationMenuOpen] = useState(false);
const [pinboardMenuOpen, setPinboardMenuOpen] = useState(false);
const [confirmModalOpen, setConfirmModalOpen] = useState(false);
const menuIndex = useMemo(() => parseInt(baseTheme.zIndexModal) + 1, []);
const { removeToTrash } = useBlockSuiteMetaHelper(blockSuiteWorkspace);
return (
<MuiClickAwayListener
onClickAway={() => {
setOperationMenuOpen(false);
setPinboardMenuOpen(false);
}}
>
<div
style={{ display: 'flex' }}
onClick={e => {
e.stopPropagation();
}}
onMouseLeave={() => {
timer.current = setTimeout(() => {
setOperationMenuOpen(false);
setPinboardMenuOpen(false);
}, 150);
}}
onMouseEnter={() => {
clearTimeout(timer.current);
}}
>
<StyledOperationButton
data-testid="pinboard-operation-button"
ref={ref => setAnchorEl(ref)}
size="small"
onClick={() => {
setOperationMenuOpen(!operationMenuOpen);
}}
visible={visible}
>
<MoreVerticalIcon />
</StyledOperationButton>
<PureMenu
data-testid="pinboard-operation-menu"
width={256}
anchorEl={anchorEl}
open={operationMenuOpen}
placement="bottom"
zIndex={menuIndex}
>
<MenuItem
data-testid="pinboard-operation-add"
onClick={() => {
onAdd();
setOperationMenuOpen(false);
onMenuClose?.();
}}
icon={<PlusIcon />}
>
{t['Add a subpage inside']()}
</MenuItem>
{!isRoot && (
<MenuItem
data-testid="pinboard-operation-move-to"
onClick={() => {
setOperationMenuOpen(false);
setPinboardMenuOpen(true);
}}
icon={<MoveToIcon />}
>
{t['Move to']()}
</MenuItem>
)}
{!isRoot && (
<MenuItem
data-testid="pinboard-operation-rename"
onClick={() => {
onRename?.();
setOperationMenuOpen(false);
onMenuClose?.();
}}
icon={<PenIcon />}
>
{t['Rename']()}
</MenuItem>
)}
{!isRoot && (
<MoveToTrash
testId="pinboard-operation-move-to-trash"
onItemClick={() => {
setOperationMenuOpen(false);
setConfirmModalOpen(true);
onMenuClose?.();
}}
/>
)}
<CopyLink />
</PureMenu>
<PinboardMenu
anchorEl={anchorEl}
open={pinboardMenuOpen}
placement="bottom"
zIndex={menuIndex}
metas={metas}
currentMeta={currentMeta}
blockSuiteWorkspace={blockSuiteWorkspace}
showRemovePinboard={true}
/>
<MoveToTrash.ConfirmModal
open={confirmModalOpen}
title={currentMeta.title}
onConfirm={() => {
toast(t['Moved to Trash']());
removeToTrash(currentMeta.id);
onDelete();
}}
onCancel={() => {
setConfirmModalOpen(false);
}}
confirmButtonTestId="move-to-trash-confirm"
cancelButtonTestId="move-to-trash-cancel"
/>
</div>
</MuiClickAwayListener>
);
};

View File

@@ -1,148 +0,0 @@
import {
displayFlex,
IconButton,
styled,
textEllipsis,
} from '@affine/component';
export const StyledCollapsedButton = styled('button')<{
collapse: boolean;
show?: boolean;
}>(({ collapse, show = true }) => {
return {
width: '16px',
height: '100%',
...displayFlex('center', 'center'),
fontSize: '16px',
position: 'absolute',
left: '0',
top: '0',
bottom: '0',
margin: 'auto',
color: 'var(--affine-icon-color)',
opacity: '.6',
transition: 'opacity .15s ease-in-out',
display: show ? 'flex' : 'none',
svg: {
transform: `rotate(${collapse ? '-90' : '0'}deg)`,
},
':hover': {
opacity: '1',
},
};
});
export const StyledPinboard = styled('div')<{
disable?: boolean;
active?: boolean;
isOver?: boolean;
disableCollapse?: boolean;
textWrap?: boolean;
}>(
({
disableCollapse,
disable = false,
active = false,
isOver,
textWrap = false,
}) => {
return {
width: '100%',
lineHeight: '1.5',
minHeight: '32px',
borderRadius: '8px',
...displayFlex('flex-start', 'center'),
padding: disableCollapse ? '0 5px' : '0 2px 0 16px',
position: 'relative',
color: disable
? 'var(--affine-text-disable-color)'
: active
? 'var(--affine-primary-color)'
: 'var(--affine-text-primary-color)',
cursor: disable ? 'not-allowed' : 'pointer',
background: isOver ? 'rgba(118, 95, 254, 0.06)' : '',
fontSize: 'var(--affine-font-base)',
userSelect: 'none',
...(textWrap
? {
wordBreak: 'break-word',
whiteSpace: 'pre-wrap',
}
: {}),
span: {
flexGrow: '1',
textAlign: 'left',
...textEllipsis(1),
},
'.path-icon': {
fontSize: '16px',
transform: 'translateY(-4px)',
},
'.mode-icon': {
fontSize: '20px',
marginRight: '8px',
flexShrink: '0',
color: active
? 'var(--affine-primary-color)'
: 'var(--affine-icon-color)',
},
':hover': {
backgroundColor: disable ? '' : 'var(--affine-hover-color)',
},
};
}
);
export const StyledOperationButton = styled(IconButton, {
shouldForwardProp: prop => {
return !['visible'].includes(prop as string);
},
})<{ visible: boolean }>(({ visible }) => {
return {
visibility: visible ? 'visible' : 'hidden',
};
});
export const StyledSearchContainer = styled('div')(() => {
return {
width: 'calc(100% - 24px)',
margin: '0 auto',
...displayFlex('flex-start', 'center'),
borderBottom: '1px solid var(--affine-border-color)',
label: {
color: 'var(--affine-icon-color)',
fontSize: '20px',
height: '20px',
},
};
});
export const StyledMenuContent = styled('div')(() => {
return {
height: '266px',
overflow: 'auto',
};
});
export const StyledMenuSubTitle = styled('div')(() => {
return {
color: 'var(--affine-text-secondary-color)',
lineHeight: '36px',
padding: '0 12px',
};
});
export const StyledMenuFooter = styled('div')(() => {
return {
width: 'calc(100% - 24px)',
margin: '0 auto',
borderTop: '1px solid var(--affine-border-color)',
padding: '6px 0',
p: {
paddingLeft: '44px',
color: 'var(--affine-text-secondary-color)',
fontSize: '14px',
},
};
});

View File

@@ -6,8 +6,8 @@ import type React from 'react';
import { Suspense, useCallback, useEffect, useMemo, useRef } from 'react';
import { preload } from 'swr';
import { fetcher, QueryKey } from '../../../adapters/affine/fetcher';
import { useIsWorkspaceOwner } from '../../../hooks/affine/use-is-workspace-owner';
import { fetcher, QueryKey } from '../../../plugins/affine/fetcher';
import type { AffineOfficialWorkspace } from '../../../shared';
import * as style from './index.css';
import { CollaborationPanel } from './panel/collaboration';

View File

@@ -57,7 +57,7 @@ export const StyledButtonContent = styled('div')(() => {
export const StyledWorkspaceName = styled('span')(() => {
return {
color: '#E8178A',
fontWeight: '600',
};
});

View File

@@ -5,17 +5,14 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
import type { PageMeta } from '@blocksuite/store';
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
import dayjs from 'dayjs';
import localizedFormat from 'dayjs/plugin/localizedFormat';
import { useAtomValue } from 'jotai';
import type React from 'react';
import { useMemo } from 'react';
import { workspacePreferredModeAtom } from '../../../atoms';
import { useBlockSuiteMetaHelper } from '../../../hooks/affine/use-block-suite-meta-helper';
import type { BlockSuiteWorkspace } from '../../../shared';
import { toast } from '../../../utils';
import { pageListEmptyStyle } from './index.css';
import { formatDate, usePageHelper } from './utils';
export type BlockSuitePageListProps = {
blockSuiteWorkspace: BlockSuiteWorkspace;
@@ -34,13 +31,6 @@ const filter = {
shared: (pageMeta: PageMeta) => pageMeta.isPublic && !pageMeta.trash,
};
dayjs.extend(localizedFormat);
const formatDate = (date?: number | unknown) => {
const dateStr =
typeof date === 'number' ? dayjs(date).format('YYYY-MM-DD HH:mm') : '--';
return dateStr;
};
const PageListEmpty = (props: {
listType: BlockSuitePageListProps['listType'];
}) => {
@@ -80,12 +70,13 @@ export const BlockSuitePageList: React.FC<BlockSuitePageListProps> = ({
permanentlyDeletePage,
cancelPublicPage,
} = useBlockSuiteMetaHelper(blockSuiteWorkspace);
const { createPage, createEdgeless, isPreferredEdgeless } =
usePageHelper(blockSuiteWorkspace);
const t = useAFFiNEI18N();
const list = useMemo(
() => pageMetas.filter(pageMeta => filter[listType](pageMeta, pageMetas)),
[pageMetas, listType]
);
const record = useAtomValue(workspacePreferredModeAtom);
if (list.length === 0) {
return <PageListEmpty listType={listType} />;
}
@@ -93,8 +84,11 @@ export const BlockSuitePageList: React.FC<BlockSuitePageListProps> = ({
if (listType === 'trash') {
const pageList: TrashListData[] = list.map(pageMeta => {
return {
icon:
record[pageMeta.id] === 'edgeless' ? <EdgelessIcon /> : <PageIcon />,
icon: isPreferredEdgeless(pageMeta.id) ? (
<EdgelessIcon />
) : (
<PageIcon />
),
pageId: pageMeta.id,
title: pageMeta.title,
createDate: formatDate(pageMeta.createDate),
@@ -118,8 +112,7 @@ export const BlockSuitePageList: React.FC<BlockSuitePageListProps> = ({
const pageList: ListData[] = list.map(pageMeta => {
return {
icon:
record[pageMeta.id] === 'edgeless' ? <EdgelessIcon /> : <PageIcon />,
icon: isPreferredEdgeless(pageMeta.id) ? <EdgelessIcon /> : <PageIcon />,
pageId: pageMeta.id,
title: pageMeta.title,
favorite: !!pageMeta.favorite,
@@ -158,10 +151,10 @@ export const BlockSuitePageList: React.FC<BlockSuitePageListProps> = ({
return (
<PageList
onClickPage={onOpenPage}
onCreateNewPage={createPage}
onCreateNewEdgeless={createEdgeless}
isPublicWorkspace={isPublic}
list={pageList}
listType={listType}
/>
);
};

View File

@@ -0,0 +1,40 @@
import { useBlockSuiteWorkspaceHelper } from '@toeverything/hooks/use-block-suite-workspace-helper';
import dayjs from 'dayjs';
import localizedFormat from 'dayjs/plugin/localizedFormat';
import { useRouter } from 'next/router';
import { useWorkspacePreferredMode } from '../../../hooks/use-recent-views';
import { useRouterHelper } from '../../../hooks/use-router-helper';
import type { BlockSuiteWorkspace } from '../../../shared';
dayjs.extend(localizedFormat);
export const formatDate = (date?: number | unknown) => {
const dateStr =
typeof date === 'number' ? dayjs(date).format('MM-DD HH:mm') : '--';
return dateStr;
};
export const usePageHelper = (blockSuiteWorkspace: BlockSuiteWorkspace) => {
const router = useRouter();
const { openPage } = useRouterHelper(router);
const { createPage } = useBlockSuiteWorkspaceHelper(blockSuiteWorkspace);
const { getPreferredMode, setPreferredMode } = useWorkspacePreferredMode();
const isPreferredEdgeless = (pageId: string) => {
return getPreferredMode(pageId) === 'edgeless';
};
const createPageAndOpen = () => {
const page = createPage();
openPage(blockSuiteWorkspace.id, page.id);
};
const createEdgelessAndOpen = () => {
const page = createPage();
setPreferredMode(page.id, 'edgeless');
openPage(blockSuiteWorkspace.id, page.id);
};
return {
createPage: createPageAndOpen,
createEdgeless: createEdgelessAndOpen,
isPreferredEdgeless: isPreferredEdgeless,
};
};

View File

@@ -108,14 +108,12 @@ const PageMenu = () => {
{mode === 'page' ? t['Edgeless']() : t['Page']()}
</MenuItem>
<Export />
{!pageMeta.isRootPinboard && (
<MoveToTrash
testId="editor-option-menu-delete"
onItemClick={() => {
setOpenConfirm(true);
}}
/>
)}
<MoveToTrash
data-testid="editor-option-menu-delete"
onItemClick={() => {
setOpenConfirm(true);
}}
/>
<div className={styles.horizontalDividerContainer}>
<div className={styles.horizontalDivider} />
</div>

View File

@@ -19,8 +19,8 @@ import React, { useEffect, useState } from 'react';
import { useCurrentWorkspace } from '../../../../hooks/current/use-current-workspace';
import { useTransformWorkspace } from '../../../../hooks/use-transform-workspace';
import { affineAuth } from '../../../../plugins/affine';
import type { AffineOfficialWorkspace } from '../../../../shared';
import { affineAuth } from '../../../../shared/apis';
import { TransformWorkspaceToAffineModal } from '../../../affine/transform-workspace-to-affine-modal';
const IconWrapper = styled('div')(() => {

View File

@@ -16,6 +16,11 @@ export const headerContainer = style({
WebkitAppRegion: 'drag',
},
},
'@media': {
print: {
display: 'none',
},
},
} as ComplexStyleRule);
export const header = style({
@@ -191,10 +196,11 @@ export const windowAppControl = style({
WebkitAppRegion: 'no-drag',
cursor: 'pointer',
display: 'inline-flex',
width: '32px',
width: '42px',
height: '32px',
alignItems: 'center',
justifyContent: 'center',
borderRadius: '2px',
borderRadius: '4px',
selectors: {
'&[data-type="close"]:hover': {
background: 'var(--affine-error-color)',

View File

@@ -21,7 +21,7 @@ export type PageDetailEditorProps = {
workspace: AffineOfficialWorkspace;
pageId: string;
onInit: (page: Page, editor: Readonly<EditorContainer>) => void;
onLoad?: (page: Page, editor: EditorContainer) => void;
onLoad?: (page: Page, editor: EditorContainer) => () => void;
header?: React.ReactNode;
};
@@ -85,7 +85,10 @@ export const PageDetailEditor: React.FC<PageDetailEditorProps> = ({
updatedDate: Date.now(),
});
localStorage.setItem('last_page_id', page.id);
onLoad?.(page, editor);
if (onLoad) {
return onLoad(page, editor);
}
return () => {};
},
[onLoad, setEditor]
)}

View File

@@ -4,7 +4,7 @@ import type React from 'react';
import { memo, useEffect, useState } from 'react';
import { useAffineLogOut } from '../../../hooks/affine/use-affine-log-out';
import { affineAuth } from '../../../plugins/affine';
import { affineAuth } from '../../../shared/apis';
import { toast } from '../../../utils';
declare global {

View File

@@ -1,166 +0,0 @@
import { IconButton, Tooltip, TreeView } from '@affine/component';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import {
ArrowRightSmallIcon,
CollapseIcon,
ExpandIcon,
MoreHorizontalIcon,
} from '@blocksuite/icons';
import type { PageMeta } from '@blocksuite/store';
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
import { useRouter } from 'next/router';
import type { MouseEvent } from 'react';
import { Fragment, useCallback, useMemo, useState } from 'react';
import type { PinboardNode } from '../../../../hooks/use-pinboard-data';
import { usePinboardData } from '../../../../hooks/use-pinboard-data';
import { useRouterHelper } from '../../../../hooks/use-router-helper';
import type { BlockSuiteWorkspace } from '../../../../shared';
import { PinboardRender } from '../../../affine/pinboard';
import {
StyledNavigationPathContainer,
StyledNavPathExtendContainer,
StyledNavPathLink,
} from './styles';
import { calcHowManyPathShouldBeShown, findPath } from './utils';
export const NavigationPath = ({
blockSuiteWorkspace,
pageId: propsPageId,
onJumpToPage,
}: {
blockSuiteWorkspace: BlockSuiteWorkspace;
pageId?: string;
onJumpToPage?: (pageId: string) => void;
}) => {
const metas = useBlockSuitePageMeta(blockSuiteWorkspace);
const router = useRouter();
const t = useAFFiNEI18N();
const [openExtend, setOpenExtend] = useState(false);
const pageId = propsPageId ?? router.query.pageId;
const { jumpToPage } = useRouterHelper(router);
const pathData = useMemo(() => {
const meta = metas.find(m => m.id === pageId);
const path = meta ? findPath(metas, meta) : [];
const actualPath = calcHowManyPathShouldBeShown(path);
return {
hasEllipsis: path.length !== actualPath.length,
path: actualPath,
};
}, [metas, pageId]);
if (pathData.path.length < 2) {
// Means there is no parent page
return null;
}
return (
<>
<StyledNavigationPathContainer data-testid="navigation-path">
{openExtend ? (
<span>{t['Navigation Path']()}</span>
) : (
pathData.path.map((meta, index) => {
const isLast = index === pathData.path.length - 1;
const showEllipsis = pathData.hasEllipsis && index === 1;
return (
<Fragment key={meta.id}>
{showEllipsis && (
<>
<IconButton
size="small"
onClick={() => setOpenExtend(true)}
>
<MoreHorizontalIcon />
</IconButton>
<ArrowRightSmallIcon className="path-arrow" />
</>
)}
<StyledNavPathLink
data-testid="navigation-path-link"
active={isLast}
onClick={() => {
if (isLast) return;
jumpToPage(blockSuiteWorkspace.id, meta.id);
onJumpToPage?.(meta.id);
}}
title={meta.title}
>
{meta.title}
</StyledNavPathLink>
{!isLast && <ArrowRightSmallIcon className="path-arrow" />}
</Fragment>
);
})
)}
<Tooltip
content={
openExtend
? t['Back to Quick Search']()
: t['View Navigation Path']()
}
placement="top"
disablePortal={true}
>
<IconButton
data-testid="navigation-path-expand-btn"
size="small"
className="collapse-btn"
onClick={() => {
setOpenExtend(!openExtend);
}}
>
{openExtend ? <CollapseIcon /> : <ExpandIcon />}
</IconButton>
</Tooltip>
</StyledNavigationPathContainer>
<NavigationPathExtendPanel
open={openExtend}
blockSuiteWorkspace={blockSuiteWorkspace}
metas={metas}
onJumpToPage={onJumpToPage}
/>
</>
);
};
const NavigationPathExtendPanel = ({
open,
metas,
blockSuiteWorkspace,
onJumpToPage,
}: {
open: boolean;
metas: PageMeta[];
blockSuiteWorkspace: BlockSuiteWorkspace;
onJumpToPage?: (pageId: string) => void;
}) => {
const router = useRouter();
const { jumpToPage } = useRouterHelper(router);
const handlePinboardClick = useCallback(
(e: MouseEvent<HTMLDivElement>, node: PinboardNode) => {
jumpToPage(blockSuiteWorkspace.id, node.id);
onJumpToPage?.(node.id);
},
[blockSuiteWorkspace.id, jumpToPage, onJumpToPage]
);
const { data } = usePinboardData({
metas,
pinboardRender: PinboardRender,
blockSuiteWorkspace: blockSuiteWorkspace,
onClick: handlePinboardClick,
asPath: true,
});
return (
<StyledNavPathExtendContainer
show={open}
data-testid="navigation-path-expand-panel"
>
<TreeView data={data} indent={10} disableCollapse={true} />
</StyledNavPathExtendContainer>
);
};

View File

@@ -1,63 +0,0 @@
import { displayFlex, styled, textEllipsis } from '@affine/component';
export const StyledNavigationPathContainer = styled('div')(() => {
return {
height: '46px',
...displayFlex('flex-start', 'center'),
background: 'var(--affine-background-secondary-color)',
padding: '0 40px 0 20px',
position: 'relative',
fontSize: 'var(--affine-font-sm)',
zIndex: 2,
'.collapse-btn': {
position: 'absolute',
right: '12px',
top: 0,
bottom: 0,
margin: 'auto',
},
'.path-arrow': {
fontSize: '16px',
color: 'var(--affine-icon-color)',
},
};
});
export const StyledNavPathLink = styled('div')<{ active?: boolean }>(
({ active }) => {
return {
color: active
? 'var(--affine-text-primary-color)'
: 'var(--affine-text-secondary-color)',
cursor: active ? 'auto' : 'pointer',
maxWidth: '160px',
...textEllipsis(1),
padding: '0 4px',
transition: 'background .15s',
':hover': active
? {}
: {
background: 'var(--affine-hover-color)',
borderRadius: '4px',
},
};
}
);
export const StyledNavPathExtendContainer = styled('div')<{ show: boolean }>(
({ show }) => {
return {
position: 'absolute',
left: '0',
top: show ? '0' : '-100%',
zIndex: '1',
height: '100%',
width: '100%',
background: 'var(--affine-background-secondary-color)',
transition: 'top .15s',
fontSize: 'var(--affine-font-sm)',
color: 'var(--affine-text-secondary-color)',
padding: '46px 12px 0 15px',
};
}
);

View File

@@ -1,60 +0,0 @@
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));
if (parent) {
return helper([...group, parent]);
}
return group;
}
return helper([meta]).reverse();
}
function getPathItemWidth(content: string) {
// padding is 8px, arrow is 16px, and each char is 10px
// the max width is 160px
const charWidth = 10;
const w = content.length * charWidth + 8 + 16;
return w > 160 ? 160 : w;
}
// XXX: this is a static way to calculate the path width, not get the real width
export function calcHowManyPathShouldBeShown(path: PageMeta[]): PageMeta[] {
if (path.length === 0) {
return [];
}
const first = path[0];
const last = path[path.length - 1];
// 20 is the ellipsis icon width
const maxWidth = 550 - 20;
if (first.id === last.id) {
return [first];
}
function getMiddlePath(restWidth: number, restPath: PageMeta[]): PageMeta[] {
if (restPath.length === 0) {
return [];
}
const last = restPath[restPath.length - 1];
const w = getPathItemWidth(last.title);
if (restWidth - w > 80) {
return [
...getMiddlePath(restWidth - w, restPath.slice(0, restPath.length - 1)),
last,
];
}
return [];
}
return [
first,
...getMiddlePath(
maxWidth - getPathItemWidth(first.title),
path.slice(1, -1)
),
last,
];
}

View File

@@ -148,7 +148,7 @@ export const WorkspaceListModal = ({
{environment.isDesktop && (
<Menu
placement="auto"
trigger={['click', 'hover']}
trigger={['click']}
zIndex={1000}
content={
<StyledCreateWorkspaceCardPillContainer>

View File

@@ -62,6 +62,9 @@ export const StyledCreateWorkspaceCard = styled('div')(() => {
color: 'var(--affine-primary-color)',
},
},
'@media (max-width: 720px)': {
width: '100%',
},
};
});
export const StyledCreateWorkspaceCardPillContainer = styled('div')(() => {

View File

@@ -1,6 +1,7 @@
import { MenuLinkItem } from '@affine/component/app-sidebar';
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
import type { PageMeta, Workspace } from '@blocksuite/store';
import * as Collapsible from '@radix-ui/react-collapsible';
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
import { useBlockSuitePageReferences } from '@toeverything/hooks/use-block-suite-page-references';
import { useAtomValue } from 'jotai';
@@ -10,6 +11,7 @@ import { useMemo, useState } from 'react';
import { workspacePreferredModeAtom } from '../../../../atoms';
import type { FavoriteListProps } from '../index';
import EmptyItem from './empty-item';
import * as styles from './styles.css';
interface FavoriteMenuItemProps {
workspace: Workspace;
@@ -33,16 +35,16 @@ function FavoriteMenuItem({
return [...new Set(references.filter(ref => !parentIds.has(ref)))];
}, [references, parentIds]);
const [collapsed, setCollapsed] = useState(true);
const collapsible = referencesToShow.length > 0 && parentIds.size === 0;
const showReferences = collapsible ? !collapsed : referencesToShow.length > 0;
const collapsible = referencesToShow.length > 0;
const nestedItem = parentIds.size > 0;
const untitled = !metaMapping[pageId]?.title;
return (
<>
<Collapsible.Root
className={styles.favItemWrapper}
data-nested={nestedItem}
open={!collapsed}
>
<MenuLinkItem
style={{
marginLeft: nestedItem ? '12px' : undefined,
width: nestedItem ? 'calc(100% - 12px)' : undefined,
}}
data-type="favorite-list-item"
data-testid={`favorite-list-item-${pageId}`}
active={active}
@@ -51,21 +53,28 @@ function FavoriteMenuItem({
collapsed={collapsible ? collapsed : undefined}
onCollapsedChange={setCollapsed}
>
<span>{metaMapping[pageId]?.title || 'Untitled'}</span>
<span className={styles.label} data-untitled={untitled}>
{metaMapping[pageId]?.title || 'Untitled'}
</span>
</MenuLinkItem>
{showReferences &&
referencesToShow.map(ref => {
return (
<FavoriteMenuItem
key={ref}
workspace={workspace}
pageId={ref}
metaMapping={metaMapping}
parentIds={new Set([...parentIds, pageId])}
/>
);
})}
</>
{collapsible && (
<Collapsible.Content className={styles.collapsibleContent}>
<div className={styles.collapsibleContentInner}>
{referencesToShow.map(ref => {
return (
<FavoriteMenuItem
key={ref}
workspace={workspace}
pageId={ref}
metaMapping={metaMapping}
parentIds={new Set([...parentIds, pageId])}
/>
);
})}
</div>
</Collapsible.Content>
)}
</Collapsible.Root>
);
}

View File

@@ -0,0 +1,57 @@
import { keyframes, style } from '@vanilla-extract/css';
export const label = style({
selectors: {
'&[data-untitled="true"]': {
opacity: 0.6,
},
},
});
export const favItemWrapper = style({
display: 'flex',
flexDirection: 'column',
gap: '4px',
selectors: {
'&[data-nested="true"]': {
marginLeft: '12px',
width: 'calc(100% - 12px)',
},
},
});
const slideDown = keyframes({
'0%': {
height: '0px',
},
'100%': {
height: 'var(--radix-collapsible-content-height)',
},
});
const slideUp = keyframes({
'0%': {
height: 'var(--radix-collapsible-content-height)',
},
'100%': {
height: '0px',
},
});
export const collapsibleContent = style({
overflow: 'hidden',
selectors: {
'&[data-state="open"]': {
animation: `${slideDown} 0.2s ease-out`,
},
'&[data-state="closed"]': {
animation: `${slideUp} 0.2s ease-out`,
},
},
});
export const collapsibleContentInner = style({
display: 'flex',
flexDirection: 'column',
gap: '4px',
});

View File

@@ -1,25 +1,5 @@
import type { Page } from '@blocksuite/store';
import type { AllWorkspace } from '../../../shared';
export type FavoriteListProps = {
currentWorkspace: AllWorkspace;
};
export type WorkSpaceSliderBarProps = {
isPublicWorkspace: boolean;
onOpenQuickSearchModal: () => void;
onOpenWorkspaceListModal: () => void;
currentWorkspace: AllWorkspace | null;
currentPageId: string | null;
openPage: (pageId: string) => void;
createPage: () => Page;
currentPath: string;
paths: {
all: (workspaceId: string) => string;
favorite: (workspaceId: string) => string;
trash: (workspaceId: string) => string;
setting: (workspaceId: string) => string;
shared: (workspaceId: string) => string;
};
};

View File

@@ -1,64 +0,0 @@
import { TreeView } from '@affine/component';
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
import type { MouseEvent } from 'react';
import { useCallback } from 'react';
import type { PinboardNode } from '../../../hooks/use-pinboard-data';
import { usePinboardData } from '../../../hooks/use-pinboard-data';
import { usePinboardHandler } from '../../../hooks/use-pinboard-handler';
import type { BlockSuiteWorkspace } from '../../../shared';
import { PinboardRender } from '../../affine/pinboard';
export type PinboardProps = {
blockSuiteWorkspace: BlockSuiteWorkspace;
openPage: (pageId: string) => void;
};
/**
* @deprecated
*/
export const Pinboard = ({ blockSuiteWorkspace, openPage }: PinboardProps) => {
const allMetas = useBlockSuitePageMeta(blockSuiteWorkspace);
const handlePinboardClick = useCallback(
(e: MouseEvent<HTMLDivElement>, node: PinboardNode) => {
openPage(node.id);
},
[openPage]
);
const onAdd = useCallback(
(id: string) => {
openPage(id);
},
[openPage]
);
const { data } = usePinboardData({
metas: allMetas,
pinboardRender: PinboardRender,
blockSuiteWorkspace: blockSuiteWorkspace,
onClick: handlePinboardClick,
showOperationButton: true,
});
const { addPin, deletePin, dropPin } = usePinboardHandler({
blockSuiteWorkspace: blockSuiteWorkspace,
metas: allMetas,
onAdd,
});
if (!data.length) {
return null;
}
return (
<div data-testid="sidebar-pinboard-container">
<TreeView
data={data}
onAdd={addPin}
onDelete={deletePin}
onDrop={dropPin}
indent={16}
/>
</div>
);
};
export default Pinboard;

View File

@@ -84,7 +84,7 @@ export const RootAppSidebar = ({
const blockSuiteWorkspace = currentWorkspace?.blockSuiteWorkspace;
const t = useAFFiNEI18N();
const onClickNewPage = useCallback(async () => {
const page = await createPage();
const page = createPage();
openPage(page.id);
}, [createPage, openPage]);

View File

@@ -44,7 +44,6 @@ beforeAll(() => {
`/workspace/[workspaceId/${WorkspaceSubPath.ALL}`,
`/workspace/[workspaceId/${WorkspaceSubPath.SETTING}`,
`/workspace/[workspaceId/${WorkspaceSubPath.TRASH}`,
`/workspace/[workspaceId/${WorkspaceSubPath.FAVORITE}`,
'/workspace/[workspaceId]/[pageId]',
])
);

View File

@@ -11,14 +11,15 @@ import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models';
import type { Page } from '@blocksuite/store';
import { assertExists } from '@blocksuite/store';
import { renderHook } from '@testing-library/react';
import { createStore, Provider } from 'jotai/index';
import { createStore, Provider } from 'jotai';
import { useRouter } from 'next/router';
import routerMock from 'next-router-mock';
import { createDynamicRouteParser } from 'next-router-mock/dynamic-routes';
import type { FC, PropsWithChildren } from 'react';
import { beforeAll, beforeEach, describe, expect, test, vi } from 'vitest';
import { LocalPlugin } from '../../adapters/local';
import { workspacesAtom } from '../../atoms';
import { LocalPlugin } from '../../plugins/local';
import { BlockSuiteWorkspace } from '../../shared';
import { WorkspaceSubPath } from '../../shared';
import {
@@ -37,7 +38,6 @@ beforeAll(() => {
`/workspace/[workspaceId/${WorkspaceSubPath.ALL}`,
`/workspace/[workspaceId/${WorkspaceSubPath.SETTING}`,
`/workspace/[workspaceId/${WorkspaceSubPath.TRASH}`,
`/workspace/[workspaceId/${WorkspaceSubPath.FAVORITE}`,
'/workspace/[workspaceId]/[pageId]',
])
);
@@ -45,10 +45,11 @@ beforeAll(() => {
async function getJotaiContext() {
const store = createStore();
const ProviderWrapper: React.FC<React.PropsWithChildren> =
function ProviderWrapper({ children }) {
return <Provider store={store}>{children}</Provider>;
};
const ProviderWrapper: FC<PropsWithChildren> = function ProviderWrapper({
children,
}) {
return <Provider store={store}>{children}</Provider>;
};
const workspaces = await store.get(workspacesAtom);
expect(workspaces.length).toBe(0);
return {

View File

@@ -2,7 +2,7 @@ import { WorkspaceFlavour } from '@affine/workspace/type';
import { useRouter } from 'next/router';
import { useCallback } from 'react';
import { WorkspaceAdapters } from '../../plugins';
import { WorkspaceAdapters } from '../../adapters/workspace';
export function useAffineLogIn() {
const router = useRouter();

View File

@@ -2,7 +2,7 @@ import { WorkspaceFlavour } from '@affine/workspace/type';
import { useRouter } from 'next/router';
import { useCallback } from 'react';
import { WorkspaceAdapters } from '../../plugins';
import { WorkspaceAdapters } from '../../adapters/workspace';
export function useAffineLogOut() {
const router = useRouter();

View File

@@ -8,7 +8,7 @@ import {
} from '@affine/workspace/affine/login';
import useSWR from 'swr';
import { affineAuth } from '../../plugins/affine';
import { affineAuth } from '../../shared/apis';
const logger = new DebugLogger('auth-token');

View File

@@ -2,7 +2,7 @@ import type { Member } from '@affine/workspace/affine/api';
import { useCallback } from 'react';
import useSWR from 'swr';
import { QueryKey } from '../../plugins/affine/fetcher';
import { QueryKey } from '../../adapters/affine/fetcher';
import { affineApis } from '../../shared/apis';
export function useMembers(workspaceId: string) {

View File

@@ -1,42 +0,0 @@
import { useAtomValue } from 'jotai';
import { useEffect } from 'react';
import { currentEditorAtom } from '../../atoms';
export function useReferenceLinkEffect(props?: {
pageLinkClicked?: (params: { pageId: string }) => void;
subpageLinked?: (params: { pageId: string }) => void;
subpageUnlinked?: (params: { pageId: string }) => void;
}) {
const { pageLinkClicked, subpageLinked, subpageUnlinked } = props ?? {};
const editor = useAtomValue(currentEditorAtom);
useEffect(() => {
if (!editor) {
return;
}
const linkClickedDisposable = editor.slots.pageLinkClicked.on(
({ pageId }) => {
pageLinkClicked?.({ pageId });
}
);
// const subpageLinkedDisposable = editor.slots.subpageLinked.on(
// ({ pageId }) => {
// subpageLinked?.({ pageId });
// }
// );
// const subpageUnlinkedDisposable = editor.slots.subpageUnlinked.on(
// ({ pageId }) => {
// subpageUnlinked?.({ pageId });
// }
// );
return () => {
linkClickedDisposable.dispose();
// subpageLinkedDisposable.dispose();
// subpageUnlinkedDisposable.dispose();
};
}, [editor, pageLinkClicked, subpageLinked, subpageUnlinked]);
}

View File

@@ -3,7 +3,7 @@ import type { AffineLegacyCloudWorkspace } from '@affine/workspace/type';
import { useCallback } from 'react';
import useSWR from 'swr';
import { QueryKey } from '../../plugins/affine/fetcher';
import { QueryKey } from '../../adapters/affine/fetcher';
import { affineApis } from '../../shared/apis';
export function useToggleWorkspacePublish(

View File

@@ -1,6 +1,6 @@
import useSWR from 'swr';
import { QueryKey } from '../../plugins/affine/fetcher';
import { QueryKey } from '../../adapters/affine/fetcher';
export interface QueryEmailMember {
id: string;

View File

@@ -12,7 +12,7 @@ import { WorkspaceFlavour } from '@affine/workspace/type';
import { useSetAtom } from 'jotai';
import { useCallback } from 'react';
import { affineAuth } from '../../plugins/affine';
import { affineAuth } from '../../shared/apis';
import { useTransformWorkspace } from '../use-transform-workspace';
export function useOnTransformWorkspace() {

View File

@@ -1,92 +0,0 @@
import type { Node } from '@affine/component';
import type { PageMeta } from '@blocksuite/store';
import type { MouseEvent } from 'react';
import { useMemo } from 'react';
import type { BlockSuiteWorkspace } from '../shared';
export type RenderProps = {
blockSuiteWorkspace: BlockSuiteWorkspace;
onClick?: (e: MouseEvent<HTMLDivElement>, node: PinboardNode) => void;
showOperationButton?: boolean;
// If true, the node will be rendered with path icon at start
asPath?: boolean;
};
export type NodeRenderProps = RenderProps & {
metas: PageMeta[];
currentMeta: PageMeta;
};
export type PinboardNode = Node<NodeRenderProps>;
function flattenToTree(
metas: PageMeta[],
pinboardRender: PinboardNode['render'],
renderProps: RenderProps
): PinboardNode[] {
const rootMeta = metas.find(meta => meta.isRootPinboard);
const helper = (internalMetas: PageMeta[]): PinboardNode[] => {
return internalMetas.reduce<PinboardNode[]>(
(returnedMetas, internalMeta) => {
const { subpageIds = [] } = internalMeta;
const childrenMetas = subpageIds
.map(id => metas.find(m => m.id === id)!)
.filter(m => m);
// @ts-ignore
const returnedMeta: PinboardNode = {
...internalMeta,
children: helper(childrenMetas),
render: (node, props) =>
pinboardRender(node, props, {
...renderProps,
currentMeta: internalMeta,
metas,
}),
};
returnedMetas.push(returnedMeta);
return returnedMetas;
},
[]
);
};
// Unreachable code, we have removed the root pinboard
// @ts-expect-error
return helper(rootMeta ? [{ ...rootMeta, renderTopLine: false }] : []);
}
export function usePinboardData({
metas,
pinboardRender,
blockSuiteWorkspace,
onClick,
showOperationButton,
asPath,
}: {
metas: PageMeta[];
pinboardRender: PinboardNode['render'];
} & RenderProps) {
const data = useMemo(
() =>
flattenToTree(metas, pinboardRender, {
blockSuiteWorkspace,
onClick,
showOperationButton,
asPath,
}),
[
asPath,
blockSuiteWorkspace,
metas,
onClick,
pinboardRender,
showOperationButton,
]
);
return {
data,
};
}
export default usePinboardData;

View File

@@ -1,138 +0,0 @@
import type { TreeViewProps } from '@affine/component';
import { DebugLogger } from '@affine/debug';
import type { PageMeta } from '@blocksuite/store';
import { nanoid } from '@blocksuite/store';
import { usePageMetaHelper } from '@toeverything/hooks/use-block-suite-page-meta';
import { useBlockSuiteWorkspaceHelper } from '@toeverything/hooks/use-block-suite-workspace-helper';
import { useCallback, useMemo } from 'react';
import type { BlockSuiteWorkspace } from '../shared';
import { useBlockSuiteMetaHelper } from './affine/use-block-suite-meta-helper';
import { useReferenceLinkHelper } from './affine/use-reference-link-helper';
import type { NodeRenderProps } from './use-pinboard-data';
const logger = new DebugLogger('pinboard');
function findRootIds(metas: PageMeta[], id: string): string[] {
const parentMeta = metas.find(m => m.subpageIds?.includes(id));
if (!parentMeta) {
return [id];
}
return [parentMeta.id, ...findRootIds(metas, parentMeta.id)];
}
export function usePinboardHandler({
blockSuiteWorkspace,
metas: propsMetas,
onAdd,
onDelete,
onDrop,
}: {
blockSuiteWorkspace: BlockSuiteWorkspace;
metas?: PageMeta[];
onAdd?: (addedId: string, parentId: string) => void;
onDelete?: TreeViewProps<NodeRenderProps>['onDelete'];
onDrop?: TreeViewProps<NodeRenderProps>['onDrop'];
}) {
const metas = useMemo(
() => propsMetas || blockSuiteWorkspace.meta.pageMetas || [],
[blockSuiteWorkspace.meta.pageMetas, propsMetas]
);
const { createPage } = useBlockSuiteWorkspaceHelper(blockSuiteWorkspace);
const { setPageMeta } = usePageMetaHelper(blockSuiteWorkspace);
const { removeToTrash: removeToTrashHelper } =
useBlockSuiteMetaHelper(blockSuiteWorkspace);
const { addReferenceLink, removeReferenceLink } =
useReferenceLinkHelper(blockSuiteWorkspace);
const addPin = useCallback(
(parentId: string) => {
const id = nanoid();
createPage(id);
onAdd?.(id, parentId);
addReferenceLink(parentId, id);
},
[addReferenceLink, createPage, onAdd]
);
const deletePin = useCallback(
(deleteId: string) => {
removeToTrashHelper(deleteId);
onDelete?.(deleteId);
},
[removeToTrashHelper, onDelete]
);
const dropPin = useCallback(
(
dragId: string,
dropId: string,
position: {
topLine: boolean;
bottomLine: boolean;
internal: boolean;
}
) => {
if (dragId === dropId) {
return;
}
const dropRootIds = findRootIds(metas, dropId);
if (dropRootIds.includes(dragId)) {
return;
}
logger.info('handleDrop', {
dragId,
dropId,
position,
metas,
});
const { topLine, bottomLine } = position;
const dragParentMeta = metas.find(meta =>
meta.subpageIds?.includes(dragId)
);
if (bottomLine || topLine) {
const insertOffset = bottomLine ? 1 : 0;
const dropParentMeta = metas.find(m => m.subpageIds?.includes(dropId));
if (dropParentMeta?.id === dragParentMeta?.id) {
// same parent, resort node
const newSubpageIds = [...(dragParentMeta?.subpageIds ?? [])];
const deleteIndex = newSubpageIds.findIndex(id => id === dragId);
newSubpageIds.splice(deleteIndex, 1);
const insertIndex =
newSubpageIds.findIndex(id => id === dropId) + insertOffset;
newSubpageIds.splice(insertIndex, 0, dragId);
dragParentMeta &&
setPageMeta(dragParentMeta.id, {
subpageIds: newSubpageIds,
});
return onDrop?.(dragId, dropId, position);
}
// Old parent will delete drag node, new parent will be added
removeReferenceLink(dragId);
dropParentMeta && addReferenceLink(dropParentMeta.id, dragId);
return onDrop?.(dragId, dropId, position);
}
// drop into the node
if (dragParentMeta && dragParentMeta.id === dropId) {
return;
}
if (dragParentMeta) {
removeReferenceLink(dragId);
}
const dropMeta = metas.find(meta => meta.id === dropId)!;
addReferenceLink(dropMeta.id, dragId);
},
[addReferenceLink, metas, onDrop, removeReferenceLink, setPageMeta]
);
return {
dropPin,
addPin,
deletePin,
};
}
export default usePinboardHandler;

View File

@@ -1,6 +1,6 @@
import type { Workspace } from '@blocksuite/store';
import type { Page, Workspace } from '@blocksuite/store';
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
import { useAtomValue, useSetAtom } from 'jotai';
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
import type { NextRouter } from 'next/router';
import { useEffect } from 'react';
@@ -11,6 +11,19 @@ import {
} from '../atoms';
import { useCurrentWorkspace } from './current/use-current-workspace';
export const useWorkspacePreferredMode = () => {
const [record, setPreferred] = useAtom(workspacePreferredModeAtom);
return {
getPreferredMode: (pageId: Page['id']) => record[pageId] ?? 'page',
setPreferredMode: (pageId: Page['id'], mode: 'page' | 'edgeless') => {
setPreferred(record => ({
...record,
[pageId]: mode,
}));
},
};
};
export function useRecentlyViewed() {
const [workspace] = useCurrentWorkspace();
const workspaceId = workspace?.id || null;
@@ -30,15 +43,19 @@ export function useSyncRecentViewsWithRouter(
const meta = useBlockSuitePageMeta(blockSuiteWorkspace).find(
meta => meta.id === pageId
);
const currentMode = useAtomValue(workspacePreferredModeAtom)[
pageId as string
];
const { getPreferredMode } = useWorkspacePreferredMode();
const currentMode =
typeof pageId === 'string' ? getPreferredMode(pageId) : 'page';
useEffect(() => {
if (!workspaceId) return;
if (pageId && meta) {
set(workspaceId, {
id: pageId as string,
mode: currentMode ?? 'page',
/**
* @deprecated No necessary update `mode` at here, use `mode` from {@link useWorkspacePreferredMode} directly.
*/
mode: currentMode,
});
}
}, [pageId, meta, workspaceId, set, currentMode]);

View File

@@ -4,7 +4,7 @@ import type { WorkspaceRegistry } from '@affine/workspace/type';
import { useSetAtom } from 'jotai';
import { useCallback } from 'react';
import { WorkspaceAdapters } from '../plugins';
import { WorkspaceAdapters } from '../adapters/workspace';
/**
* Transform workspace from one flavour to another

View File

@@ -8,9 +8,9 @@ import { nanoid } from '@blocksuite/store';
import { useAtomValue, useSetAtom } from 'jotai';
import { useCallback } from 'react';
import { LocalPlugin } from '../adapters/local';
import { WorkspaceAdapters } from '../adapters/workspace';
import { workspacesAtom } from '../atoms';
import { WorkspaceAdapters } from '../plugins';
import { LocalPlugin } from '../plugins/local';
import type { AllWorkspace } from '../shared';
export function useWorkspaces(): AllWorkspace[] {

View File

@@ -26,6 +26,7 @@ import { useRouter } from 'next/router';
import type { FC, PropsWithChildren, ReactElement } from 'react';
import { lazy, Suspense, useCallback, useEffect, useMemo } from 'react';
import { WorkspaceAdapters } from '../adapters/workspace';
import { openQuickSearchModalAtom, openWorkspacesModalAtom } from '../atoms';
import { useTrackRouterHistoryEffect } from '../atoms/history';
import {
@@ -38,7 +39,6 @@ import { useCurrentWorkspace } from '../hooks/current/use-current-workspace';
import { useRouterHelper } from '../hooks/use-router-helper';
import { useRouterTitle } from '../hooks/use-router-title';
import { useWorkspaces } from '../hooks/use-workspaces';
import { WorkspaceAdapters } from '../plugins';
import { ModalProvider } from '../providers/modal-provider';
import { pathGenerator, publicPathGenerator } from '../shared';
@@ -243,12 +243,15 @@ export const WorkspaceLayout: FC<PropsWithChildren> =
(meta && WorkspaceAdapters[meta.flavour].UI.Provider) ?? DefaultProvider;
return (
<>
{/* fixme(himself65): don't re-render whole modals */}
<AllWorkspaceContext>
<CurrentWorkspaceContext>
<ModalProvider key={currentWorkspaceId} />
</CurrentWorkspaceContext>
</AllWorkspaceContext>
{/* load all workspaces is costly, do not block the whole UI */}
<Suspense fallback={null}>
<AllWorkspaceContext>
<CurrentWorkspaceContext>
{/* fixme(himself65): don't re-render whole modals */}
<ModalProvider key={currentWorkspaceId} />
</CurrentWorkspaceContext>
</AllWorkspaceContext>
</Suspense>
<CurrentWorkspaceContext>
<Suspense fallback={<WorkspaceFallback />}>
<Provider>
@@ -284,11 +287,6 @@ export const WorkspaceLayoutInner: FC<PropsWithChildren> = ({ children }) => {
void jumpToPage(currentWorkspace.id, pageId);
}
}
// fixme: pinboard has been removed,
// the related code should be removed in the future.
// no matter the workspace is empty, ensure the root pinboard exists
// ensureRootPinboard(currentWorkspace.blockSuiteWorkspace);
//#endregion
useEffect(() => {

View File

@@ -11,9 +11,9 @@ import { useRouter } from 'next/router';
import { Suspense } from 'react';
import useSWR from 'swr';
import { QueryKey } from '../../adapters/affine/fetcher';
import { PageLoading } from '../../components/pure/loading';
import { RouteLogic, useRouterHelper } from '../../hooks/use-router-helper';
import { QueryKey } from '../../plugins/affine/fetcher';
import type { NextPageWithLayout } from '../../shared';
import { WorkspaceSubPath } from '../../shared';

View File

@@ -9,7 +9,7 @@ import { useAtom, useAtomValue } from 'jotai';
import Link from 'next/link';
import { useRouter } from 'next/router';
import type { ReactElement } from 'react';
import { Suspense, useCallback, useEffect } from 'react';
import { Suspense, useEffect } from 'react';
import {
publicPageBlockSuiteAtom,
@@ -19,7 +19,6 @@ import {
import { PageDetailEditor } from '../../../components/page-detail-editor';
import { WorkspaceAvatar } from '../../../components/pure/footer';
import { PageLoading } from '../../../components/pure/loading';
import { useReferenceLinkEffect } from '../../../hooks/affine/use-reference-link-effect';
import { useRouterHelper } from '../../../hooks/use-router-helper';
import {
PublicQuickSearch,
@@ -64,14 +63,6 @@ const PublicWorkspaceDetailPageInner = (): ReactElement => {
}
const router = useRouter();
const { openPage } = useRouterHelper(router);
useReferenceLinkEffect({
pageLinkClicked: useCallback(
({ pageId }: { pageId: string }) => {
return openPage(blockSuiteWorkspace.id, pageId);
},
[blockSuiteWorkspace.id, openPage]
),
});
const t = useAFFiNEI18N();
const [name] = useBlockSuiteWorkspaceName(blockSuiteWorkspace);
const [avatar] = useBlockSuiteWorkspaceAvatarUrl(blockSuiteWorkspace);
@@ -86,6 +77,12 @@ const PublicWorkspaceDetailPageInner = (): ReactElement => {
onLoad={(_, editor) => {
const { page } = editor;
page.awarenessStore.setReadonly(page, true);
const dispose = editor.slots.pageLinkClicked.on(({ pageId }) => {
return openPage(blockSuiteWorkspace.id, pageId);
});
return () => {
dispose.dispose();
};
}}
onInit={initPage}
header={

View File

@@ -4,25 +4,21 @@ import { config } from '@affine/env';
import { Unreachable } from '@affine/env/constant';
import { rootCurrentPageIdAtom } from '@affine/workspace/atom';
import { WorkspaceFlavour } from '@affine/workspace/type';
import type { EditorContainer } from '@blocksuite/editor';
import type { Page } from '@blocksuite/store';
import { assertExists } from '@blocksuite/store';
import {
useBlockSuitePageMeta,
usePageMetaHelper,
} from '@toeverything/hooks/use-block-suite-page-meta';
import { useBlockSuiteWorkspacePage } from '@toeverything/hooks/use-block-suite-workspace-page';
import { useAtomValue } from 'jotai';
import { useRouter } from 'next/router';
import type React from 'react';
import { useCallback, useEffect } from 'react';
import { WorkspaceAdapters } from '../../../adapters/workspace';
import { rootCurrentWorkspaceAtom } from '../../../atoms/root';
import { useReferenceLinkEffect } from '../../../hooks/affine/use-reference-link-effect';
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
import { usePinboardHandler } from '../../../hooks/use-pinboard-handler';
import { useSyncRecentViewsWithRouter } from '../../../hooks/use-recent-views';
import { useRouterHelper } from '../../../hooks/use-router-helper';
import { WorkspaceLayout } from '../../../layouts/workspace-layout';
import { WorkspaceAdapters } from '../../../plugins';
import type { BlockSuiteWorkspace, NextPageWithLayout } from '../../../shared';
function setEditorFlags(blockSuiteWorkspace: BlockSuiteWorkspace) {
@@ -42,41 +38,19 @@ const WorkspaceDetail: React.FC = () => {
assertExists(currentWorkspace);
assertExists(currentPageId);
const blockSuiteWorkspace = currentWorkspace.blockSuiteWorkspace;
const { setPageMeta, getPageMeta } = usePageMetaHelper(blockSuiteWorkspace);
const { deletePin } = usePinboardHandler({
blockSuiteWorkspace,
metas: useBlockSuitePageMeta(currentWorkspace.blockSuiteWorkspace),
});
useSyncRecentViewsWithRouter(router, blockSuiteWorkspace);
useReferenceLinkEffect({
pageLinkClicked: useCallback(
({ pageId }: { pageId: string }) => {
assertExists(currentWorkspace);
return openPage(currentWorkspace.id, pageId);
},
[currentWorkspace, openPage]
),
subpageUnlinked: useCallback(
({ pageId }: { pageId: string }) => {
deletePin(pageId);
},
[deletePin]
),
subpageLinked: useCallback(
({ pageId }: { pageId: string }) => {
const meta = currentPageId && getPageMeta(currentPageId);
if (!meta || meta.subpageIds?.includes(pageId)) {
return;
}
setPageMeta(currentPageId, {
subpageIds: [...(meta.subpageIds ?? []), pageId],
});
},
[currentPageId, getPageMeta, setPageMeta]
),
});
const onLoad = useCallback(
(page: Page, editor: EditorContainer) => {
const dispose = editor.slots.pageLinkClicked.on(({ pageId }) => {
return openPage(blockSuiteWorkspace.id, pageId);
});
return () => {
dispose.dispose();
};
},
[blockSuiteWorkspace.id, openPage]
);
useEffect(() => {
if (currentWorkspace) {
@@ -90,6 +64,7 @@ const WorkspaceDetail: React.FC = () => {
<PageDetail
currentWorkspace={currentWorkspace}
currentPageId={currentPageId}
onLoadEditor={onLoad}
/>
);
} else if (currentWorkspace.flavour === WorkspaceFlavour.LOCAL) {
@@ -99,6 +74,7 @@ const WorkspaceDetail: React.FC = () => {
<PageDetail
currentWorkspace={currentWorkspace}
currentPageId={currentPageId}
onLoadEditor={onLoad}
/>
);
}

View File

@@ -7,12 +7,12 @@ import Head from 'next/head';
import { useRouter } from 'next/router';
import React, { useCallback } from 'react';
import { WorkspaceAdapters } from '../../../adapters/workspace';
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 { WorkspaceLayout } from '../../../layouts/workspace-layout';
import { WorkspaceAdapters } from '../../../plugins';
import type { NextPageWithLayout } from '../../../shared';
const AllPage: NextPageWithLayout = () => {

View File

@@ -16,13 +16,13 @@ import type { NextRouter } from 'next/router';
import { useRouter } from 'next/router';
import React, { useCallback, useEffect } from 'react';
import { WorkspaceAdapters } from '../../../adapters/workspace';
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 { useAppHelper } from '../../../hooks/use-workspaces';
import { WorkspaceLayout } from '../../../layouts/workspace-layout';
import { WorkspaceAdapters } from '../../../plugins';
import type { NextPageWithLayout } from '../../../shared';
import { toast } from '../../../utils';

View File

@@ -3,7 +3,7 @@ import { memo } from 'react';
import type { SWRConfiguration } from 'swr';
import { SWRConfig } from 'swr';
import { fetcher } from '../plugins/affine/fetcher';
import { fetcher } from '../adapters/affine/fetcher';
const config: SWRConfiguration = {
suspense: true,

View File

@@ -4,18 +4,16 @@ import { memo, useRef } from 'react';
const themes = ['dark', 'light'];
// a workaround to sync theme to electron
let firstRender = true;
const DesktopThemeSync = memo(function DesktopThemeSync() {
const { theme } = useTheme();
const lastThemeRef = useRef(theme);
if (lastThemeRef.current !== theme || firstRender) {
const onceRef = useRef(false);
if (lastThemeRef.current !== theme || !onceRef.current) {
if (environment.isDesktop && theme) {
window.apis?.ui.handleThemeChange(theme as 'dark' | 'light' | 'system');
}
lastThemeRef.current = theme;
firstRender = false;
onceRef.current = true;
}
return null;
});

View File

@@ -6,9 +6,14 @@ import {
} from '@affine/workspace/affine/api';
import { currentAffineUserAtom } from '@affine/workspace/affine/atom';
import type { LoginResponse } from '@affine/workspace/affine/login';
import { parseIdToken, setLoginStorage } from '@affine/workspace/affine/login';
import {
createAffineAuth,
parseIdToken,
setLoginStorage,
} from '@affine/workspace/affine/login';
import { rootStore } from '@affine/workspace/atom';
export const affineAuth = createAffineAuth(prefixUrl);
const affineApis = {} as ReturnType<typeof createUserApis> &
ReturnType<typeof createWorkspaceApis>;
Object.assign(affineApis, createUserApis(prefixUrl));

View File

@@ -1,17 +1,10 @@
import type { ToastOptions } from '@affine/component';
import { toast as basicToast } from '@affine/component';
import { DebugLogger } from '@affine/debug';
const logger = new DebugLogger('toast');
export const toast = (message: string, options?: ToastOptions) => {
const mainContainer = document.querySelector(
'.main-container'
) as HTMLElement;
logger.debug(`toast with message: "${message}"`, options);
window.dispatchEvent(
new CustomEvent('affine-toast:emit', { detail: message })
);
return basicToast(message, {
portal: mainContainer || document.body,
...options,

View File

@@ -31,13 +31,13 @@ nvm use 18
## Setup Environment
This setup requires modern yarn (currently `3.5.0`), run this if your yarn version is `1.x`
This setup requires modern yarn (currently `3.x`), run this if your yarn version is `1.x`
Reference: [Yarn installation doc](https://yarnpkg.com/getting-started/install)
```sh
corepack enable
corepack prepare yarn@3.5.0 --activate
corepack prepare yarn@stable --activate
```
```sh
@@ -52,7 +52,7 @@ yarn install
```shell
# Run OctoBase container in background
docker pull ghcr.io/toeverything/cloud-self-hosted:nightly-latest
docker run --env=SIGN_KEY=test123 --env=RUST_LOG=debug --env=JWST_DEV=1 --env=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin --workdir=/app -p 3000:3000 --runtime=runc -d ghcr.io/toeverything/cloud-self-hosted:nightly-latest
docker run --env=SIGN_KEY=test123 --env=RUST_LOG=debug --env=JWST_DEV=1 --env=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin --workdir=/app -p 127.0.0.1:3000:3000 --runtime=runc -d ghcr.io/toeverything/cloud-self-hosted:nightly-latest
```
```shell

View File

@@ -1,6 +1,6 @@
{
"name": "AFFiNE",
"version": "0.5.4-beta.1",
"version": "0.6.0-canary.7",
"private": true,
"author": "toeverything",
"license": "MPL-2.0",
@@ -13,12 +13,13 @@
"scripts": {
"dev": "dev-web",
"dev:ac": "API_SERVER_PROFILE=ac yarn workspace @affine/web dev",
"dev:local": "API_SERVER_PROFILE=local yarn workspace @affine/web dev",
"dev:local": "PORT=8080 API_SERVER_PROFILE=local yarn workspace @affine/web dev",
"dev:app": "yarn workspace @affine/electron dev:app",
"build": "yarn workspace @affine/web build",
"build:client": "yarn workspace @affine/client-app build:app",
"build:storybook": "yarn workspace @affine/component build-storybook",
"bump:nightly": "./scripts/bump-blocksuite.sh",
"circular": "madge --circular --ts-config ./tsconfig.json ./apps/web/src/pages/**/*.tsx",
"export": "yarn workspace @affine/web export",
"start": "yarn workspace @affine/web start",
"start:storybook": "yarn exec serve packages/component/storybook-static -l 6006",
@@ -54,7 +55,7 @@
"@taplo/cli": "^0.5.2",
"@testing-library/react": "^14.0.0",
"@types/eslint": "^8.37.0",
"@types/node": "^18.16.12",
"@types/node": "^18.16.13",
"@typescript-eslint/eslint-plugin": "^5.59.6",
"@typescript-eslint/parser": "^5.59.6",
"@vanilla-extract/vite-plugin": "^3.8.1",
@@ -75,6 +76,7 @@
"happy-dom": "^9.18.3",
"husky": "^8.0.3",
"lint-staged": "^13.2.2",
"madge": "^6.0.0",
"msw": "^1.2.1",
"nanoid": "^4.0.2",
"nyc": "^15.1.0",
@@ -83,7 +85,7 @@
"react-dom": "18.3.0-canary-16d053d59-20230506",
"serve": "^14.2.0",
"typescript": "^5.0.4",
"vite": "^4.3.7",
"vite": "^4.3.8",
"vite-plugin-istanbul": "^4.0.1",
"vite-tsconfig-paths": "^4.2.0",
"vitest": "^0.31.1",

View File

@@ -15,5 +15,5 @@
"dependencies": {
"dotenv": "^16.0.3"
},
"version": "0.5.4-beta.1"
"version": "0.6.0-canary.7"
}

View File

@@ -36,6 +36,7 @@
"@mui/material": "^5.13.1",
"@popperjs/core": "^2.11.7",
"@radix-ui/react-avatar": "^1.0.2",
"@radix-ui/react-collapsible": "^1.0.2",
"@toeverything/hooks": "workspace:*",
"@toeverything/theme": "^0.5.8",
"@vanilla-extract/dynamic": "^2.0.3",
@@ -50,12 +51,12 @@
"rxjs": "^7.8.1"
},
"devDependencies": {
"@blocksuite/blocks": "0.0.0-20230518051344-45970a96-nightly",
"@blocksuite/editor": "0.0.0-20230518051344-45970a96-nightly",
"@blocksuite/global": "0.0.0-20230518051344-45970a96-nightly",
"@blocksuite/icons": "^2.1.16",
"@blocksuite/lit": "0.0.0-20230518051344-45970a96-nightly",
"@blocksuite/store": "0.0.0-20230518051344-45970a96-nightly",
"@blocksuite/blocks": "0.0.0-20230519102837-01acd96b-nightly",
"@blocksuite/editor": "0.0.0-20230519102837-01acd96b-nightly",
"@blocksuite/global": "0.0.0-20230519102837-01acd96b-nightly",
"@blocksuite/icons": "^2.1.18",
"@blocksuite/lit": "0.0.0-20230519102837-01acd96b-nightly",
"@blocksuite/store": "0.0.0-20230519102837-01acd96b-nightly",
"@storybook/addon-actions": "^7.0.12",
"@storybook/addon-coverage": "^0.0.8",
"@storybook/addon-essentials": "^7.0.12",
@@ -80,9 +81,9 @@
"storybook": "^7.0.12",
"storybook-dark-mode": "^3.0.0",
"typescript": "^5.0.4",
"vite": "^4.3.7",
"vite": "^4.3.8",
"wait-on": "^7.0.1",
"yjs": "^13.6.1"
},
"version": "0.5.4-beta.1"
"version": "0.6.0-canary.7"
}

View File

@@ -1,3 +1,4 @@
import { Trans } from '@affine/i18n';
import { AffineLogoSimSBlue1_1Icon, CloseIcon } from '@blocksuite/icons';
import {
@@ -18,16 +19,18 @@ export const DownloadTips = ({ onClose }: { onClose: () => void }) => {
<div className={downloadTipStyle}>
<AffineLogoSimSBlue1_1Icon className={downloadTipIconStyle} />
<div className={downloadMessageStyle}>
Enjoying the demo? &nbsp;
<a
className={linkStyle}
href="https://github.com/toeverything/AFFiNE/releases"
target="_blank"
rel="noreferrer"
>
Download the AFFiNE Client
</a>
&nbsp;for the full experience.
<Trans i18nKey="com.affine.banner.content">
Enjoying the demo?
<a
className={linkStyle}
href="https://affine.pro/download"
target="_blank"
rel="noreferrer"
>
Download the AFFiNE Client
</a>
for the full experience.
</Trans>
</div>
</div>
<div

View File

@@ -12,7 +12,6 @@ interface AddPageButtonProps {
style?: React.CSSProperties;
}
// Although it is called an input, it is actually a button.
export function AddPageButton({
onClick,
className,

View File

@@ -62,11 +62,17 @@ export const closeIcon = style({
cursor: 'pointer',
transition: '0.1s',
borderRadius: '50%',
transform: 'scale(0.6)',
zIndex: 1,
opacity: 0,
selectors: {
'&:hover': {
transform: 'scale(1.1)',
},
[`${root}:hover &`]: {
opacity: 1,
transform: 'scale(1)',
},
},
});
@@ -153,28 +159,32 @@ export const particles = style({
backgroundRepeat: 'no-repeat, repeat',
backgroundPosition: 'center, center top 100%',
backgroundSize: '100%, 130%',
WebkitMaskImage:
'linear-gradient(to top, transparent, black, black, transparent)',
maskImage: 'linear-gradient(to top, transparent, black, black, transparent)',
width: '100%',
height: '100%',
position: 'absolute',
left: 0,
pointerEvents: 'none',
});
export const particlesBefore = style({
content: '""',
display: 'block',
position: 'absolute',
width: '100%',
height: '100%',
background: `var(--svg-dot-animation), var(--svg-dot-animation), var(--svg-dot-animation)`,
backgroundRepeat: 'no-repeat, repeat, repeat',
backgroundPosition: 'center, center top 100%, center center',
backgroundSize: '100% 120%, 150%, 120%',
filter: 'blur(1px)',
willChange: 'filter',
pointerEvents: 'none',
display: 'none',
selectors: {
[`${root}:hover &`]: {
display: 'block',
},
'&:before': {
content: '""',
display: 'block',
position: 'absolute',
width: '100%',
height: '100%',
background: `var(--svg-dot-animation), var(--svg-dot-animation), var(--svg-dot-animation)`,
backgroundRepeat: 'no-repeat, repeat, repeat',
backgroundPosition: 'center, center top 100%, center center',
backgroundSize: '100% 120%, 150%, 120%',
filter: 'blur(1px)',
willChange: 'filter',
pointerEvents: 'none',
},
},
});
export const halo = style({

View File

@@ -85,6 +85,7 @@ export function AppUpdaterButton({ className, style }: AddPageButtonProps) {
}
} else if (currentChangelogUnread) {
window.open(config.changelogUrl, '_blank');
onDismissCurrentChangelog();
} else {
throw new Unreachable();
}

View File

@@ -1,18 +0,0 @@
import { Skeleton } from '@mui/material';
import type { ReactElement } from 'react';
import { fallbackHeaderStyle, fallbackStyle } from './fallback.css';
import { AppSidebar } from './index';
export const AppSidebarFallback = (): ReactElement | null => {
return (
<AppSidebar>
<div className={fallbackStyle}>
<div className={fallbackHeaderStyle}>
<Skeleton variant="circular" width={40} height={40} />
<Skeleton variant="rectangular" width={150} height={40} />
</div>
</div>
</AppSidebar>
);
};

View File

@@ -14,6 +14,7 @@ export const navWrapperStyle = style({
minWidth: navWidthVar,
height: '100%',
zIndex: 2,
paddingBottom: '8px',
backgroundColor: 'transparent',
'@media': {
[`(max-width: ${floatingMaxWidth}px)`]: {
@@ -26,6 +27,10 @@ export const navWrapperStyle = style({
},
},
},
print: {
display: 'none',
zIndex: -1,
},
},
selectors: {
'&[data-open="false"]': {
@@ -97,5 +102,8 @@ export const sidebarFloatMaskStyle = style({
},
},
},
print: {
display: 'none',
},
},
});

View File

@@ -1,9 +1,11 @@
import { getEnvironment } from '@affine/env';
import { Skeleton } from '@mui/material';
import { assignInlineVars } from '@vanilla-extract/dynamic';
import { useAtom, useAtomValue } from 'jotai';
import type { PropsWithChildren, ReactElement } from 'react';
import { useEffect, useRef, useState } from 'react';
import { fallbackHeaderStyle, fallbackStyle } from './fallback.css';
import {
floatingMaxWidth,
navBodyStyle,
@@ -114,10 +116,22 @@ export function AppSidebar(props: AppSidebarProps): ReactElement {
);
}
export const AppSidebarFallback = (): ReactElement | null => {
return (
<AppSidebar>
<div className={fallbackStyle}>
<div className={fallbackHeaderStyle}>
<Skeleton variant="circular" width={40} height={40} />
<Skeleton variant="rectangular" width={150} height={40} />
</div>
</div>
</AppSidebar>
);
};
export * from './add-page-button';
export * from './app-updater-button';
export * from './category-divider';
export { AppSidebarFallback } from './fallback';
export * from './menu-item';
export * from './quick-search-input';
export * from './sidebar-containers';

View File

@@ -69,6 +69,7 @@ export const iconsContainer = style({
alignItems: 'center',
justifyContent: 'flex-start',
width: '28px',
flexShrink: 0,
selectors: {
'&[data-collapsible="true"]': {
width: '40px',

View File

@@ -39,28 +39,29 @@ export function MenuItem({
data-disabled={disabled}
data-collapsible={collapsible}
>
<div className={styles.iconsContainer} data-collapsible={collapsible}>
{collapsible && (
<div
onClick={e => {
e.stopPropagation();
e.preventDefault(); // for links
onCollapsedChange?.(!collapsed);
}}
data-testid="fav-collapsed-button"
className={styles.collapsedIconContainer}
>
<ArrowDownSmallIcon
className={styles.collapsedIcon}
data-collapsed={collapsed}
/>
</div>
)}
{icon &&
React.cloneElement(icon, {
{icon && (
<div className={styles.iconsContainer} data-collapsible={collapsible}>
{collapsible && (
<div
onClick={e => {
e.stopPropagation();
e.preventDefault(); // for links
onCollapsedChange?.(!collapsed);
}}
data-testid="fav-collapsed-button"
className={styles.collapsedIconContainer}
>
<ArrowDownSmallIcon
className={styles.collapsedIcon}
data-collapsed={collapsed}
/>
</div>
)}
{React.cloneElement(icon, {
className: clsx([styles.icon, icon.props.className]),
})}
</div>
</div>
)}
<div className={styles.content}>{children}</div>
</div>

View File

@@ -1,7 +1,7 @@
import { globalStyle, style } from '@vanilla-extract/css';
export const baseContainer = style({
padding: '12px 16px',
padding: '4px 16px',
display: 'flex',
flexFlow: 'column nowrap',
rowGap: '4px',
@@ -13,17 +13,27 @@ export const scrollableContainerRoot = style({
vars: {
'--scrollbar-width': '10px',
},
transition: 'all .3s .2s',
borderTop: '1px solid transparent',
});
export const scrollTopBorder = style({
position: 'absolute',
top: 0,
left: '16px',
right: '16px',
height: '1px',
transition: 'opacity .3s .2s',
opacity: 0,
background: 'var(--affine-black-10)',
selectors: {
'&[data-has-scroll-top="true"]': {
boxShadow: 'inset 0 8px 8px -8px var(--affine-black-10)',
opacity: 1,
},
},
});
export const scrollableViewport = style({
height: '100%',
marginTop: '4px',
});
globalStyle(`${scrollableViewport} > div`, {

View File

@@ -39,10 +39,11 @@ function useHasScrollTop() {
export function SidebarScrollableContainer({ children }: PropsWithChildren) {
const [hasScrollTop, ref] = useHasScrollTop();
return (
<ScrollArea.Root
data-has-scroll-top={hasScrollTop}
className={styles.scrollableContainerRoot}
>
<ScrollArea.Root className={styles.scrollableContainerRoot}>
<div
data-has-scroll-top={hasScrollTop}
className={styles.scrollTopBorder}
/>
<ScrollArea.Viewport
className={clsx([styles.scrollableViewport])}
ref={ref}

View File

@@ -22,15 +22,11 @@ export type SidebarHeaderProps = {
export const SidebarHeader = (props: SidebarHeaderProps) => {
const [open, setOpen] = useAtom(appSidebarOpenAtom);
const environment = getEnvironment();
const isMacosDesktop = environment.isDesktop && environment.isMacOs;
return (
<div
className={navHeaderStyle}
data-is-macos-electron={isMacosDesktop}
data-open={open}
>
{isMacosDesktop && (
<div className={navHeaderStyle} data-open={open}>
{environment.isDesktop && (
<>
{environment.isMacOs && <div style={{ flex: 1 }} />}
<IconButton
size="middle"
data-testid="app-sidebar-arrow-button-back"
@@ -57,6 +53,8 @@ export const SidebarHeader = (props: SidebarHeaderProps) => {
>
<ArrowRightSmallIcon />
</IconButton>
{!environment.isMacOs && <div style={{ flex: 1 }} />}
</>
)}
<IconButton

View File

@@ -16,7 +16,7 @@ export const spotlight = style({
background: `radial-gradient(${spotlightSize} circle at ${spotlightX} ${spotlightY}, var(--affine-text-primary-color), transparent)`,
inset: '0px',
pointerEvents: 'none',
willChange: 'background',
willChange: 'background, opacity',
opacity: spotlightOpacity,
zIndex: 1,
transition: 'all 0.2s',

View File

@@ -0,0 +1,30 @@
import type { Meta, StoryFn } from '@storybook/react';
import { type PropsWithChildren } from 'react';
import { Spotlight } from '.';
export default {
title: 'Components/AppSidebar/Spotlight',
component: Spotlight,
} satisfies Meta;
const Container = ({ children }: PropsWithChildren) => (
<main
style={{
position: 'relative',
width: '320px',
height: '320px',
border: '1px solid #ccc',
}}
>
{children}
</main>
);
export const Default: StoryFn = () => {
return (
<Container>
<Spotlight />
</Container>
);
};

View File

@@ -12,10 +12,10 @@ function useMouseOffset() {
useEffect(() => {
if (ref.current && ref.current.parentElement) {
const el = ref.current.parentElement;
const bound = el.getBoundingClientRect();
// debounce?
const onMouseMove = (e: MouseEvent) => {
const bound = el.getBoundingClientRect();
setOffset({ x: e.clientX - bound.x, y: e.clientY - bound.y });
setOutside(false);
};

View File

@@ -21,7 +21,7 @@ export type EditorProps = {
page: Page;
mode: 'page' | 'edgeless';
onInit: (page: Page, editor: Readonly<EditorContainer>) => void;
onLoad?: (page: Page, editor: EditorContainer) => void;
onLoad?: (page: Page, editor: EditorContainer) => () => void;
style?: CSSProperties;
};
@@ -67,7 +67,7 @@ const BlockSuiteEditorImpl = (props: EditorProps): ReactElement => {
if (page.root === null) {
props.onInit(page, editor);
}
props.onLoad?.(page, editor);
return props.onLoad?.(page, editor);
}
}, [props.page, props.onInit, props.onLoad, editor, props, page]);

View File

@@ -0,0 +1,25 @@
import { forwardRef, type HTMLAttributes, type ReactNode } from 'react';
import * as styles from './styles.css';
export const BlockCard = forwardRef<
HTMLDivElement,
{
left?: ReactNode;
title: string;
desc?: string;
right?: ReactNode;
} & HTMLAttributes<HTMLDivElement>
>(({ left, title, desc, right, ...props }, ref) => {
return (
<div ref={ref} className={styles.blockCard} {...props}>
{left && <div className={styles.blockCardAround}>{left}</div>}
<div className={styles.blockCardContent}>
<div>{title}</div>
<div className={styles.blockCardDesc}>{desc}</div>
</div>
{right && <div className={styles.blockCardAround}>{right}</div>}
</div>
);
});
BlockCard.displayName = 'BlockCard';

View File

@@ -0,0 +1,36 @@
import { style } from '@vanilla-extract/css';
export const blockCard = style({
display: 'flex',
gap: '12px',
padding: '8px 12px',
color: 'var(--affine-text-primary-color)',
backgroundColor: 'var(--affine-background-primary-color)',
borderRadius: '4px',
userSelect: 'none',
cursor: 'pointer',
textAlign: 'start',
selectors: {
'&:hover': {
boxShadow: 'var(--affine-shadow-1)',
},
// TODO active styles
},
});
export const blockCardAround = style({
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
});
export const blockCardContent = style({
display: 'flex',
flexDirection: 'column',
flex: 1,
});
export const blockCardDesc = style({
color: 'var(--affine-text-secondary-color)',
fontSize: 'var(--affine-font-xs)',
});

View File

@@ -10,7 +10,7 @@ import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-
import type { FC } from 'react';
import { useCallback } from 'react';
import { WorkspaceAvatar } from '../workspace-avatar';
import { WorkspaceAvatar } from '../../workspace-avatar';
import {
StyledCard,
StyledSettingLink,

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