Compare commits

...

43 Commits

Author SHA1 Message Date
李华桥
dc279d062b v0.10.3-beta.5 2023-11-30 16:49:55 +08:00
Joooye_34
47d5f9e1c2 fix(infra): use blocksuite api to check compatibility (#5137) 2023-11-30 08:48:13 +00:00
Joooye_34
a226eb8d5f fix(core): expose catched editor load error (#5133) 2023-11-29 20:31:35 +08:00
Joooye_34
908c4e1a6f ci: add sentry env when frontend assets build (#5131) 2023-11-29 10:03:49 +00:00
李华桥
1d0bcc80a0 v0.10.3-beta.4 2023-11-29 16:14:06 +08:00
Joooye_34
50010bd824 fix(core): implement editor timeout and report error from boundary (#5105) 2023-11-29 08:10:38 +00:00
liuyi
c0ede1326d fix(server): wrong OTEL config (#5084) 2023-11-29 11:19:13 +08:00
李华桥
89197bacef Revert "Merge remote-tracking branch 'origin/canary' into stable"
This reverts commit 992ed89a89, reversing
changes made to d272d7922d.
2023-11-29 11:18:45 +08:00
李华桥
f97d323ab5 Revert "Revert "refactor(server): standarderlize metrics and trace with OTEL (#5054)""
This reverts commit c1cd1713b9.
2023-11-29 11:07:28 +08:00
EYHN
2acb219dcc fix(workspace): filter awareness from other workspace (#5093) 2023-11-28 16:47:45 +08:00
LongYinan
992ed89a89 Merge remote-tracking branch 'origin/canary' into stable 2023-11-28 15:12:52 +08:00
李华桥
d272d7922d v0.10.3-beta.2 2023-11-25 23:50:40 +08:00
李华桥
c1cd1713b9 Revert "refactor(server): standarderlize metrics and trace with OTEL (#5054)"
This reverts commit 91efca107a.
2023-11-25 23:50:39 +08:00
李华桥
b20e91bee0 v0.10.3-beta.1 2023-11-25 14:14:40 +08:00
李华桥
9a4e5ec8c3 Merge branch 'canary' into stable 2023-11-25 14:14:14 +08:00
李华桥
2019838ae7 v0.10.3-beta.0 2023-11-24 11:39:23 +08:00
李华桥
30ff25f400 Merge branch 'canary' into stable 2023-11-23 23:40:32 +08:00
李华桥
e766208c18 chore: reset merge wrong codes 2023-11-23 22:53:06 +08:00
李华桥
8742f28148 Merge branch 'canary' into stable 2023-11-23 21:31:42 +08:00
LongYinan
cd291bb60e build: remove useless source-map-loader to speedup webpack (#4910) 2023-11-20 10:52:28 +08:00
LongYinan
62c0efcfd1 fix(core): handle the getSession network error properly (#4909)
If network offline or API error happens, the `session` returned by the `useSession` hook will be null, so we can't assume it is not null.

There should be following changes:
1. create a page in ErrorBoundary to let the user refetch the session.
2. The `SessionProvider` stop to pull the new session once the session is null, we need to figure out a way to pull the new session when the network is back or the user click the refetch button.
2023-11-17 16:50:48 +08:00
liuyi
87248b3337 fix(server): all viewers can share public link (#4968) 2023-11-17 12:34:15 +08:00
Joooye_34
00c940f7df chore: bump affine version to 0.10.2 (#4959) 2023-11-16 15:48:37 +08:00
Flrande
931b459fbd chore: bump blocksuite (#4958) 2023-11-16 14:27:39 +08:00
LongYinan
51e71f4a0a ci: prevent error if rust build is cached by nx (#4951)
If Rust build was cached by nx, only the output file will be presented. The chmod command will be failed in this case like: https://github.com/toeverything/AFFiNE/actions/runs/6874496337/job/18697360212
2023-11-16 10:31:51 +08:00
Peng Xiao
9b631f2328 fix(infra): page id compat fix for page ids in workspace.meta (#4950)
since we strip `page:` in keys of workspacedoc.spaces, we should also strip the prefix in meta.pages as well.
2023-11-15 17:36:08 +08:00
LongYinan
01f481a9b6 ci: only disable postinstall on macOS in nightly desktop build (#4938) 2023-11-14 23:00:30 +08:00
Joooye_34
0177ab5c87 fix(infra): workspace migration without blockVersions (#4936) 2023-11-14 14:38:11 +01:00
Peng Xiao
4db35d341c perf(component): use png instead of svg for rendering noise svg (#4935) 2023-11-14 11:52:51 +00:00
DarkSky
3c4a803c97 fix: change password token check (#4934) (#4932) 2023-11-14 11:15:54 +00:00
LongYinan
05154dc7ca ci: disable postinstall in nightly desktop build (#4930)
Should be part of https://github.com/toeverything/AFFiNE/pull/4885
2023-11-14 14:13:55 +08:00
Peng Xiao
c90b477f60 fix(core): change server url of stable to insider (#4902) (#4926) 2023-11-14 12:05:52 +08:00
李华桥
6f18ddbe85 v0.10.1 2023-11-13 19:49:26 +08:00
LongYinan
dde779a71d test(e2e): add subdoc migration test (#4921)
test(e2e): add subdoc migration test

fix: remove .only
2023-11-13 18:00:40 +08:00
Peng Xiao
bd9f66fbc7 fix(infra): compatibility fix for space prefix (#4912)
It seems there are some cases that [this upstream PR](https://github.com/toeverything/blocksuite/pull/4747) will cause data loss.

Because of some historical reasons, the page id could be different with its doc id.
It might be caused by subdoc migration in the following (not 100% sure if all white screen issue is caused by it) 0714c12703/packages/common/infra/src/blocksuite/index.ts (L538-L540)

In version 0.10, page id in spaces no longer has prefix "space:"
The data flow for fetching a doc's updates is:
- page id in `meta.pages` -> find `${page-id}` in `doc.spaces` -> `doc` -> `doc.guid`
if `doc` is not found in `doc.spaces`, a new doc will be created and its `doc.guid` is the same with its pageId
- because of guid logic change, the doc that previously prefixed with `space:` will not be found in `doc.spaces`
- when fetching the rows of this doc using the doc id === page id,
  it will return EMPTY since there is no updates associated with the page id

The provided fix in the PR will patch the `spaces` field of the root doc so that after 0.10 the page doc can still be found in the `spaces` map. It shall apply to both of the idb & sqlite datasources.

Special thanks to @lawvs 's db file for investigation!
2023-11-13 17:57:56 +08:00
liuyi
92f1f40bfa fix(server): wrap updates applying in a transaction (#4922) 2023-11-13 08:49:30 +00:00
LongYinan
48dc1049b3 Merge pull request #4913 from toeverything/darksky/cleanup-depolyment
chore: cleanup deployment
2023-11-12 11:20:02 +08:00
DarkSky
9add530370 chore: cleanup deployment 2023-11-12 11:03:25 +08:00
LongYinan
b77460d871 Merge pull request #4908 from toeverything/61/hotfix-websocket-payload
fix(server): increase server acceptable websocket payload size
2023-11-10 22:01:48 +08:00
forehalo
42db41776b fix(server): increase server acceptable websocket payload size 2023-11-10 21:31:45 +08:00
李华桥
075439c74f fix(core): change server url of stable to insider 2023-11-10 18:32:53 +08:00
Yifeng Wang
fc6c553ece chore: bump theme (#4904)
Co-authored-by: 李华桥 <joooye1991@gmail.com>
2023-11-10 15:40:38 +08:00
Joooye_34
59cb3d5df1 fix(core): change server url of stable to insider (#4902) 2023-11-10 14:50:57 +08:00
125 changed files with 1221 additions and 2494 deletions

View File

@@ -35,7 +35,7 @@ jobs:
build-core:
name: Build @affine/core
runs-on: ubuntu-latest
environment: ${{ github.event.inputs.flavor }}
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
@@ -52,6 +52,10 @@ jobs:
SHOULD_REPORT_TRACE: true
TRACE_REPORT_ENDPOINT: ${{ secrets.TRACE_REPORT_ENDPOINT }}
CAPTCHA_SITE_KEY: ${{ secrets.CAPTCHA_SITE_KEY }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
- name: Upload core artifact
uses: actions/upload-artifact@v3
with:

View File

@@ -70,8 +70,8 @@ jobs:
env:
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
NEXT_PUBLIC_SENTRY_DSN: ${{ secrets.NEXT_PUBLIC_SENTRY_DSN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
RELEASE_VERSION: ${{ needs.set-build-version.outputs.version }}
- name: Upload core artifact

View File

@@ -40,6 +40,7 @@ env:
jobs:
before-make:
runs-on: ubuntu-latest
environment: ${{ github.event.inputs.build-type || (github.ref_type == 'tag' && contains(github.ref, 'canary') && 'canary') }}
outputs:
RELEASE_VERSION: ${{ steps.get-canary-version.outputs.RELEASE_VERSION }}
steps:
@@ -65,6 +66,7 @@ jobs:
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
RELEASE_VERSION: ${{ github.event.inputs.version || steps.get-canary-version.outputs.RELEASE_VERSION }}
SKIP_PLUGIN_BUILD: 'true'

144
Cargo.lock generated
View File

@@ -83,15 +83,14 @@ dependencies = [
[[package]]
name = "ahash"
version = "0.8.6"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a"
checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f"
dependencies = [
"cfg-if",
"getrandom",
"once_cell",
"version_check",
"zerocopy",
]
[[package]]
@@ -241,16 +240,6 @@ dependencies = [
"num-traits",
]
[[package]]
name = "atomic-write-file"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c232177ba50b16fe7a4588495bd474a62a9e45a8e4ca6fd7d0b7ac29d164631e"
dependencies = [
"nix",
"rand",
]
[[package]]
name = "autocfg"
version = "1.1.0"
@@ -952,7 +941,7 @@ version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e"
dependencies = [
"ahash 0.8.6",
"ahash 0.8.3",
]
[[package]]
@@ -961,7 +950,7 @@ version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156"
dependencies = [
"ahash 0.8.6",
"ahash 0.8.3",
"allocator-api2",
]
@@ -1123,7 +1112,7 @@ dependencies = [
[[package]]
name = "jwst-codec"
version = "0.1.0"
source = "git+https://github.com/toeverything/OctoBase.git?rev=49a6b7a#49a6b7af25ce1fe54e8383e10980e9536821d286"
source = "git+https://github.com/toeverything/OctoBase.git?rev=aad9e5b#aad9e5b7e9d6f479e6cf7555f5845bbbaaadbc66"
dependencies = [
"arbitrary",
"bitvec",
@@ -1144,7 +1133,7 @@ dependencies = [
[[package]]
name = "jwst-core"
version = "0.1.0"
source = "git+https://github.com/toeverything/OctoBase.git?rev=49a6b7a#49a6b7af25ce1fe54e8383e10980e9536821d286"
source = "git+https://github.com/toeverything/OctoBase.git?rev=aad9e5b#aad9e5b7e9d6f479e6cf7555f5845bbbaaadbc66"
dependencies = [
"async-trait",
"base64",
@@ -1162,7 +1151,7 @@ dependencies = [
[[package]]
name = "jwst-logger"
version = "0.1.0"
source = "git+https://github.com/toeverything/OctoBase.git?rev=49a6b7a#49a6b7af25ce1fe54e8383e10980e9536821d286"
source = "git+https://github.com/toeverything/OctoBase.git?rev=aad9e5b#aad9e5b7e9d6f479e6cf7555f5845bbbaaadbc66"
dependencies = [
"chrono",
"nu-ansi-term 0.49.0",
@@ -1175,7 +1164,7 @@ dependencies = [
[[package]]
name = "jwst-storage"
version = "0.1.0"
source = "git+https://github.com/toeverything/OctoBase.git?rev=49a6b7a#49a6b7af25ce1fe54e8383e10980e9536821d286"
source = "git+https://github.com/toeverything/OctoBase.git?rev=aad9e5b#aad9e5b7e9d6f479e6cf7555f5845bbbaaadbc66"
dependencies = [
"anyhow",
"async-trait",
@@ -1200,7 +1189,7 @@ dependencies = [
[[package]]
name = "jwst-storage-migration"
version = "0.1.0"
source = "git+https://github.com/toeverything/OctoBase.git?rev=49a6b7a#49a6b7af25ce1fe54e8383e10980e9536821d286"
source = "git+https://github.com/toeverything/OctoBase.git?rev=aad9e5b#aad9e5b7e9d6f479e6cf7555f5845bbbaaadbc66"
dependencies = [
"sea-orm-migration",
"tokio",
@@ -1268,9 +1257,9 @@ checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"
[[package]]
name = "libsqlite3-sys"
version = "0.27.0"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716"
checksum = "afc22eff61b133b115c6e8c74e818c628d6d5e7a502afea6f64dee076dd94326"
dependencies = [
"cc",
"pkg-config",
@@ -1348,15 +1337,6 @@ version = "2.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167"
[[package]]
name = "memoffset"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4"
dependencies = [
"autocfg",
]
[[package]]
name = "minimal-lexical"
version = "0.2.1"
@@ -1395,9 +1375,9 @@ dependencies = [
[[package]]
name = "napi"
version = "2.14.1"
version = "2.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1133249c46e92da921bafc8aba4912bf84d6c475f7625183772ed2d0844dc3a7"
checksum = "f9d90182620f32fe34b6ac9b52cba898af26e94c7f5abc01eb4094c417ae2e6c"
dependencies = [
"anyhow",
"bitflags 2.4.1",
@@ -1419,9 +1399,9 @@ checksum = "d4b4532cf86bfef556348ac65e561e3123879f0e7566cca6d43a6ff5326f13df"
[[package]]
name = "napi-derive"
version = "2.14.2"
version = "2.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0cca5738c6e81eb5ffd2c8ff2b4f05ece9c4c60c7e2b36cec6524492cf7f330"
checksum = "3619fa472d23cd5af94d63a2bae454a77a8863251f40230fbf59ce20eafa8a86"
dependencies = [
"cfg-if",
"convert_case",
@@ -1433,9 +1413,9 @@ dependencies = [
[[package]]
name = "napi-derive-backend"
version = "1.0.55"
version = "1.0.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35960e5f33228192a9b661447d0dfe8f5a3790ff5b4058c4d67680ded4f65b91"
checksum = "ecd3ea4b54020c73d591a49cd192f6334c5f37f71a63ead54dbc851fa991ef00"
dependencies = [
"convert_case",
"once_cell",
@@ -1455,19 +1435,6 @@ dependencies = [
"libloading",
]
[[package]]
name = "nix"
version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b"
dependencies = [
"bitflags 1.3.2",
"cfg-if",
"libc",
"memoffset",
"pin-utils",
]
[[package]]
name = "no-std-compat"
version = "0.4.1"
@@ -2325,18 +2292,18 @@ checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090"
[[package]]
name = "serde"
version = "1.0.193"
version = "1.0.192"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89"
checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.193"
version = "1.0.192"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3"
checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1"
dependencies = [
"proc-macro2",
"quote",
@@ -2483,9 +2450,9 @@ dependencies = [
[[package]]
name = "sqlx"
version = "0.7.3"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dba03c279da73694ef99763320dea58b51095dfe87d001b1d4b5fe78ba8763cf"
checksum = "0e50c216e3624ec8e7ecd14c6a6a6370aad6ee5d8cfc3ab30b5162eeeef2ed33"
dependencies = [
"sqlx-core",
"sqlx-macros",
@@ -2496,11 +2463,11 @@ dependencies = [
[[package]]
name = "sqlx-core"
version = "0.7.3"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d84b0a3c3739e220d94b3239fd69fb1f74bc36e16643423bd99de3b43c21bfbd"
checksum = "8d6753e460c998bbd4cd8c6f0ed9a64346fcca0723d6e75e52fdc351c5d2169d"
dependencies = [
"ahash 0.8.6",
"ahash 0.8.3",
"atoi",
"bigdecimal",
"byteorder",
@@ -2544,9 +2511,9 @@ dependencies = [
[[package]]
name = "sqlx-macros"
version = "0.7.3"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89961c00dc4d7dffb7aee214964b065072bff69e36ddb9e2c107541f75e4f2a5"
checksum = "9a793bb3ba331ec8359c1853bd39eed32cdd7baaf22c35ccf5c92a7e8d1189ec"
dependencies = [
"proc-macro2",
"quote",
@@ -2557,11 +2524,10 @@ dependencies = [
[[package]]
name = "sqlx-macros-core"
version = "0.7.3"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0bd4519486723648186a08785143599760f7cc81c52334a55d6a83ea1e20841"
checksum = "0a4ee1e104e00dedb6aa5ffdd1343107b0a4702e862a84320ee7cc74782d96fc"
dependencies = [
"atomic-write-file",
"dotenvy",
"either",
"heck",
@@ -2584,9 +2550,9 @@ dependencies = [
[[package]]
name = "sqlx-mysql"
version = "0.7.3"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e37195395df71fd068f6e2082247891bc11e3289624bbc776a0cdfa1ca7f1ea4"
checksum = "864b869fdf56263f4c95c45483191ea0af340f9f3e3e7b4d57a61c7c87a970db"
dependencies = [
"atoi",
"base64",
@@ -2631,9 +2597,9 @@ dependencies = [
[[package]]
name = "sqlx-postgres"
version = "0.7.3"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6ac0ac3b7ccd10cc96c7ab29791a7dd236bd94021f31eec7ba3d46a74aa1c24"
checksum = "eb7ae0e6a97fb3ba33b23ac2671a5ce6e3cabe003f451abd5a56e7951d975624"
dependencies = [
"atoi",
"base64",
@@ -2676,9 +2642,9 @@ dependencies = [
[[package]]
name = "sqlx-sqlite"
version = "0.7.3"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "210976b7d948c7ba9fced8ca835b11cbb2d677c59c79de41ac0d397e14547490"
checksum = "d59dc83cf45d89c555a577694534fcd1b55c545a816c816ce51f20bbe56a4f3f"
dependencies = [
"atoi",
"chrono",
@@ -2696,7 +2662,6 @@ dependencies = [
"time",
"tracing",
"url",
"urlencoding",
"uuid",
]
@@ -3059,12 +3024,6 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "urlencoding"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
[[package]]
name = "utf8parse"
version = "0.2.1"
@@ -3073,9 +3032,9 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]]
name = "uuid"
version = "1.6.1"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560"
checksum = "c58fe91d841bc04822c9801002db4ea904b9e4b8e6bbad25127b46eff8dc516b"
dependencies = [
"getrandom",
"rand",
@@ -3182,9 +3141,12 @@ dependencies = [
[[package]]
name = "webpki-roots"
version = "0.25.3"
version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1778a42e8b3b90bff8d0f5032bf22250792889a5cdc752aa0020c84abe3aaf10"
checksum = "b291546d5d9d1eab74f069c77749f2cb8504a12caa20f0f2de93ddbf6f411888"
dependencies = [
"rustls-webpki",
]
[[package]]
name = "whoami"
@@ -3316,26 +3278,6 @@ dependencies = [
"tap",
]
[[package]]
name = "zerocopy"
version = "0.7.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e97e415490559a91254a2979b4829267a57d2fcd741a98eee8b722fb57289aa0"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.7.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd7e48ccf166952882ca8bd778a43502c64f33bf94c12ebe2a7f08e5a0f6689f"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.38",
]
[[package]]
name = "zeroize"
version = "1.6.0"

View File

@@ -1,28 +0,0 @@
# Issues Triaging
When we receive your issue, we will first triaging it. Triaging an issue usually takes around one business day but may take longer. Goal of triaging is to provide you with a clear understanding of what will happen to your issue. For example, after your feature request was triaged you know whether we plan to tackle the issue or whether we'll wait to hear what the broader community thinks about this request.
Here are issue states and their descriptions:
| State | Description |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Untriaged | The team has not yet reviewed the issue. We usually do it within one business day. |
| As designed | The behavior described in the issue is intentional. If you find it seriously disruptive or if weve misunderstood you, please let us know in the issues comments section. |
| Blocked | We cant work on this issue until another one (linked) is resolved. |
| Cant Reproduce | We have been unable to reproduce the issue on our side. It could be flaky or fixed already, or we may not have had all the details we needed. If youre still experiencing the issue and have any further details, please share them. |
| Duplicate | The issue is the same (or has the same cause) as another one (linked). |
| Fixed | If the issue was a bug, its been fixed; if it was a missing feature, its been implemented. |
| Fixed In Branch | If the issue was a bug, its been fixed; if it was a missing feature, its been implemented; the changes are now in a separate branch and havent been merged into the default branch yet. |
| In Progress | Were currently working on the issue. |
| Incomplete | Unfortunately we dont have enough information to proceed. If youre willing to share any further details about the issue, please do so in the comments. |
| Obsolete | The part of the product that was causing this issue has been removed or significantly reworked since it was created. |
| Upvoting | We are currently evaluating demand for the issue and checking whether it requires complicated or risky changes. Please leave a vote or comment if you think it should be prioritized. |
| Open | We want to implement the fix or feature in the near future. We cant promise it will appear in the next public release, but its on our short list. |
| Shelved | We have reviewed the issue and decided that, even though it has merit, we cannot currently include it in our near-term plan. |
| Third Party Problem | The issue is caused by a third party. We've done our best to inform them about it. |
| To be Discussed | We need some time to discuss the issue. |
| To Reproduce | We will try to find the steps needed to reproduce the issue on our side. |
| Under Investigation | Weve triaged the issue, but now we need to investigate it more thoroughly. This may require processing additional information like logs or dumps. |
| Waiting for Info | Weve requested additional information from the person who created the issue and are waiting for them to get back to us. |
| Declined | Weve reviewed the suggestion and, while we appreciate its value, we unfortunately do not have the resources to implement it. |
| Answered | The issue actually turned out to be a question or a misunderstanding, and it has been answered or resolved. |

View File

@@ -56,7 +56,7 @@
"env": "SENTRY_AUTH_TOKEN"
},
{
"env": "NEXT_PUBLIC_SENTRY_DSN"
"env": "SENTRY_DSN"
},
{
"env": "DISTRIBUTION"

View File

@@ -1,6 +1,6 @@
{
"name": "@affine/monorepo",
"version": "0.10.3-canary.2",
"version": "0.10.3-beta.5",
"private": true,
"author": "toeverything",
"license": "MIT",

View File

@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "optimized_blobs" ADD COLUMN "deleted_at" TIMESTAMPTZ(6);

View File

@@ -1,7 +1,7 @@
{
"name": "@affine/server",
"private": true,
"version": "0.10.3-canary.2",
"version": "0.10.3-beta.5",
"description": "Affine Node.js server",
"type": "module",
"bin": {

View File

@@ -178,15 +178,13 @@ model Blob {
}
model OptimizedBlob {
id Int @id @default(autoincrement()) @db.Integer
hash String @db.VarChar
workspaceId String @map("workspace_id") @db.VarChar
params String @db.VarChar
blob Bytes @db.ByteA
id Int @id @default(autoincrement()) @db.Integer
hash String @db.VarChar
workspaceId String @map("workspace_id") @db.VarChar
params String @db.VarChar
blob Bytes @db.ByteA
length BigInt
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
// not for keeping, but for snapshot history
deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6)
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
@@unique([workspaceId, hash, params])
@@map("optimized_blobs")

View File

@@ -1,30 +0,0 @@
import { Module } from '@nestjs/common';
import { Field, ObjectType, Query } from '@nestjs/graphql';
import { SERVER_FLAVOR } from '../modules';
@ObjectType()
export class ServerConfigType {
@Field({ description: 'server version' })
version!: string;
@Field({ description: 'server flavor' })
flavor!: string;
}
export class ServerConfigResolver {
@Query(() => ServerConfigType, {
description: 'server config',
})
serverConfig(): ServerConfigType {
return {
version: AFFiNE.version,
flavor: SERVER_FLAVOR || 'allinone',
};
}
}
@Module({
providers: [ServerConfigResolver],
})
export class ServerConfigModule {}

View File

@@ -89,7 +89,7 @@ export class DocHistoryManager {
workspaceId,
id,
timestamp: {
lt: before,
lte: before,
},
// only include the ones has not expired
expiredAt: {

View File

@@ -3,10 +3,9 @@ import { EventEmitterModule } from '@nestjs/event-emitter';
import { ScheduleModule } from '@nestjs/schedule';
import { GqlModule } from '../graphql.module';
import { ServerConfigModule } from './config';
import { AuthModule } from './auth';
import { DocModule } from './doc';
import { PaymentModule } from './payment';
import { SelfHostedModule } from './self-hosted';
import { SyncModule } from './sync';
import { UsersModule } from './users';
import { WorkspaceModule } from './workspaces';
@@ -23,25 +22,13 @@ switch (SERVER_FLAVOR) {
case 'sync':
BusinessModules.push(SyncModule, DocModule.forSync());
break;
case 'selfhosted':
BusinessModules.push(
ServerConfigModule,
SelfHostedModule,
ScheduleModule.forRoot(),
GqlModule,
WorkspaceModule,
UsersModule,
SyncModule,
DocModule.forRoot()
);
break;
case 'graphql':
BusinessModules.push(
ServerConfigModule,
ScheduleModule.forRoot(),
GqlModule,
WorkspaceModule,
UsersModule,
AuthModule,
DocModule.forRoot(),
PaymentModule
);
@@ -49,11 +36,11 @@ switch (SERVER_FLAVOR) {
case 'allinone':
default:
BusinessModules.push(
ServerConfigModule,
ScheduleModule.forRoot(),
GqlModule,
WorkspaceModule,
UsersModule,
AuthModule,
SyncModule,
DocModule.forRoot(),
PaymentModule
@@ -61,4 +48,4 @@ switch (SERVER_FLAVOR) {
break;
}
export { BusinessModules, SERVER_FLAVOR };
export { BusinessModules };

View File

@@ -52,7 +52,7 @@ class SubscriptionPrice {
}
@ObjectType('UserSubscription')
export class UserSubscriptionType implements Partial<UserSubscription> {
class UserSubscriptionType implements Partial<UserSubscription> {
@Field({ name: 'id' })
stripeSubscriptionId!: string;

View File

@@ -30,7 +30,6 @@ export enum SubscriptionPlan {
Pro = 'pro',
Team = 'team',
Enterprise = 'enterprise',
SelfHosted = 'selfhosted',
}
export function encodeLookupKey(

View File

@@ -1,38 +0,0 @@
import { Module } from '@nestjs/common';
import { ResolveField, Resolver } from '@nestjs/graphql';
import { UserSubscriptionType } from './payment/resolver';
import {
SubscriptionPlan,
SubscriptionRecurring,
SubscriptionStatus,
} from './payment/service';
import { UserType } from './users';
const YEAR = 1000 * 60 * 60 * 24 * 30 * 12;
@Resolver(() => UserType)
export class SelfHostedDummyResolver {
private readonly start = new Date();
private readonly end = new Date(Number(this.start) + YEAR);
constructor() {}
@ResolveField(() => UserSubscriptionType)
async subscription() {
return {
stripeSubscriptionId: 'dummy',
plan: SubscriptionPlan.SelfHosted,
recurring: SubscriptionRecurring.Yearly,
status: SubscriptionStatus.Active,
start: this.start,
end: this.end,
createdAt: this.start,
updatedAt: this.start,
};
}
}
@Module({
providers: [SelfHostedDummyResolver],
})
export class SelfHostedModule {}

View File

@@ -2,14 +2,6 @@
# THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY)
# ------------------------------------------------------
type ServerConfigType {
"""server version"""
version: String!
"""server flavor"""
flavor: String!
}
type UserType {
id: ID!
@@ -81,7 +73,6 @@ enum SubscriptionPlan {
Pro
Team
Enterprise
SelfHosted
}
type UserSubscription {
@@ -249,9 +240,6 @@ type DocHistoryType {
}
type Query {
"""server config"""
serverConfig: ServerConfigType!
"""Get is owner of workspace"""
isOwner(workspaceId: String!): Boolean!

View File

@@ -8,9 +8,9 @@ crate-type = ["cdylib"]
[dependencies]
chrono = "0.4"
jwst-codec = { git = "https://github.com/toeverything/OctoBase.git", rev = "49a6b7a" }
jwst-core = { git = "https://github.com/toeverything/OctoBase.git", rev = "49a6b7a" }
jwst-storage = { git = "https://github.com/toeverything/OctoBase.git", rev = "49a6b7a" }
jwst-codec = { git = "https://github.com/toeverything/OctoBase.git", rev = "aad9e5b" }
jwst-core = { git = "https://github.com/toeverything/OctoBase.git", rev = "aad9e5b" }
jwst-storage = { git = "https://github.com/toeverything/OctoBase.git", rev = "aad9e5b" }
napi = { version = "2", default-features = false, features = [
"napi5",
"async",

View File

@@ -1,6 +1,6 @@
{
"name": "@affine/storage",
"version": "0.10.3-canary.2",
"version": "0.10.3-beta.5",
"engines": {
"node": ">= 10.16.0 < 11 || >= 11.8.0"
},

View File

@@ -8,5 +8,5 @@
"react": "18.2.0",
"react-dom": "18.2.0"
},
"version": "0.10.3-canary.2"
"version": "0.10.3-beta.5"
}

View File

@@ -9,5 +9,5 @@
"@types/debug": "^4.1.9",
"vitest": "0.34.6"
},
"version": "0.10.3-canary.2"
"version": "0.10.3-beta.5"
}

View File

@@ -3,8 +3,8 @@
"private": true,
"type": "module",
"devDependencies": {
"@blocksuite/global": "0.0.0-20231124123613-7c06e95d-nightly",
"@blocksuite/store": "0.0.0-20231124123613-7c06e95d-nightly",
"@blocksuite/global": "0.0.0-20231122113751-6bf81eb3-nightly",
"@blocksuite/store": "0.0.0-20231122113751-6bf81eb3-nightly",
"react": "18.2.0",
"react-dom": "18.2.0",
"vitest": "0.34.6",
@@ -27,5 +27,5 @@
"dependencies": {
"lit": "^3.0.2"
},
"version": "0.10.3-canary.2"
"version": "0.10.3-beta.5"
}

View File

@@ -32,7 +32,6 @@ export const runtimeFlagsSchema = z.object({
enableCaptcha: z.boolean(),
enableEnhanceShareMode: z.boolean(),
enablePayment: z.boolean(),
enablePageHistory: z.boolean(),
// this is for the electron app
serverUrlPrefix: z.string(),
enableMoveDatabase: z.boolean(),

View File

@@ -55,9 +55,9 @@
},
"dependencies": {
"@affine/sdk": "workspace:*",
"@blocksuite/blocks": "0.0.0-20231124123613-7c06e95d-nightly",
"@blocksuite/global": "0.0.0-20231124123613-7c06e95d-nightly",
"@blocksuite/store": "0.0.0-20231124123613-7c06e95d-nightly",
"@blocksuite/blocks": "0.0.0-20231122113751-6bf81eb3-nightly",
"@blocksuite/global": "0.0.0-20231122113751-6bf81eb3-nightly",
"@blocksuite/store": "0.0.0-20231122113751-6bf81eb3-nightly",
"jotai": "^2.5.1",
"jotai-effect": "^0.2.3",
"tinykeys": "^2.1.0",
@@ -66,8 +66,8 @@
"devDependencies": {
"@affine-test/fixtures": "workspace:*",
"@affine/templates": "workspace:*",
"@blocksuite/editor": "0.0.0-20231124123613-7c06e95d-nightly",
"@blocksuite/lit": "0.0.0-20231124123613-7c06e95d-nightly",
"@blocksuite/editor": "0.0.0-20231122113751-6bf81eb3-nightly",
"@blocksuite/lit": "0.0.0-20231122113751-6bf81eb3-nightly",
"@testing-library/react": "^14.0.0",
"async-call-rpc": "^6.3.1",
"electron": "link:../../frontend/electron/node_modules/electron",
@@ -107,5 +107,5 @@
"optional": true
}
},
"version": "0.10.3-canary.2"
"version": "0.10.3-beta.5"
}

View File

@@ -3,6 +3,7 @@ import type { Page, PageMeta, Workspace } from '@blocksuite/store';
import type { createStore, WritableAtom } from 'jotai/vanilla';
import { nanoid } from 'nanoid';
import { checkWorkspaceCompatibility, MigrationPoint } from '..';
import { migratePages } from '../migration/blocksuite';
export async function initEmptyPage(page: Page, title?: string) {
@@ -244,46 +245,48 @@ export async function buildShowcaseWorkspace(
{} as Record<string, string>
);
});
await Promise.all(
data.map(async ([id, promise, newId]) => {
const { default: template } = await promise;
let json = JSON.stringify(template);
Object.entries(idMap).forEach(([oldId, newId]) => {
json = json.replaceAll(oldId, newId);
});
json = JSON.parse(json);
await workspace
.importPageSnapshot(structuredClone(json), newId)
.catch(error => {
console.error('error importing page', id, error);
});
const page = workspace.getPage(newId);
assertExists(page);
await page.load();
workspace.schema.upgradePage(
0,
{
'affine:note': 1,
'affine:bookmark': 1,
'affine:database': 2,
'affine:divider': 1,
'affine:image': 1,
'affine:list': 1,
'affine:code': 1,
'affine:page': 2,
'affine:paragraph': 1,
'affine:surface': 3,
},
page.spaceDoc
);
// The showcase building will create multiple pages once, and may skip the version writing.
// https://github.com/toeverything/blocksuite/blob/master/packages/store/src/workspace/page.ts#L662
if (!workspace.meta.blockVersions) {
await migratePages(workspace.doc, workspace.schema);
}
})
);
// Import page one by one to prevent workspace meta race condition problem.
for (const [id, promise, newId] of data) {
const { default: template } = await promise;
let json = JSON.stringify(template);
Object.entries(idMap).forEach(([oldId, newId]) => {
json = json.replaceAll(oldId, newId);
});
json = JSON.parse(json);
await workspace
.importPageSnapshot(structuredClone(json), newId)
.catch(error => {
console.error('error importing page', id, error);
});
const page = workspace.getPage(newId);
assertExists(page);
await page.load();
workspace.schema.upgradePage(
0,
{
'affine:note': 1,
'affine:bookmark': 1,
'affine:database': 2,
'affine:divider': 1,
'affine:image': 1,
'affine:list': 1,
'affine:code': 1,
'affine:page': 2,
'affine:paragraph': 1,
'affine:surface': 3,
},
page.spaceDoc
);
}
// The showcase building will create multiple pages once, and may skip the version writing.
// https://github.com/toeverything/blocksuite/blob/master/packages/store/src/workspace/page.ts#L662
const compatibilityResult = checkWorkspaceCompatibility(workspace);
if (compatibilityResult === MigrationPoint.BlockVersion) {
await migratePages(workspace.doc, workspace.schema);
}
Object.entries(pageMetas).forEach(([oldId, meta]) => {
const newId = idMap[oldId];
workspace.setPageMeta(newId, meta);

View File

@@ -20,13 +20,18 @@ export async function migratePages(
const meta = rootDoc.getMap('meta') as YMap<unknown>;
const versions = meta.get('blockVersions') as YMap<number>;
const oldVersions = versions?.toJSON() ?? {};
spaces.forEach((space: YDoc) => {
try {
schema.upgradePage(0, oldVersions, space);
} catch (e) {
console.error(`page ${space.guid} upgrade failed`, e);
}
schema.upgradePage(0, oldVersions, space);
});
schema.upgradeWorkspace(rootDoc);
// Hard code to upgrade page version to 2.
// Let e2e to ensure the data version is correct.
const pageVersion = meta.get('pageVersion');
if (typeof pageVersion !== 'number' || pageVersion < 2) {
meta.set('pageVersion', 2);
}
const newVersions = getLatestVersions(schema);
meta.set('blockVersions', new YMap(Object.entries(newVersions)));

View File

@@ -43,3 +43,25 @@ export function guidCompatibilityFix(rootDoc: YDoc) {
});
return changed;
}
/**
* Hard code to fix workspace version to be compatible with legacy data.
* Let e2e to ensure the data version is correct.
*/
export function fixWorkspaceVersion(rootDoc: YDoc) {
const meta = rootDoc.getMap('meta') as YMap<unknown>;
/**
* It doesn't matter to upgrade workspace version from 1 or undefined to 2.
* Blocksuite just set the value, do nothing else.
*/
const workspaceVersion = meta.get('workspaceVersion');
if (typeof workspaceVersion !== 'number' || workspaceVersion < 2) {
meta.set('workspaceVersion', 2);
const pageVersion = meta.get('pageVersion');
if (typeof pageVersion !== 'number') {
meta.set('pageVersion', 1);
}
}
}

View File

@@ -58,19 +58,16 @@ export function checkWorkspaceCompatibility(
return MigrationPoint.SubDoc;
}
// Sometimes, blocksuite will not write blockVersions to meta.
// Just fix it when user open the workspace.
const blockVersions = workspace.meta.blockVersions;
if (!blockVersions) {
const hasVersion = workspace.meta.hasVersion;
if (!hasVersion) {
return MigrationPoint.BlockVersion;
}
// From v2, we depend on blocksuite to check and migrate data.
for (const [flavour, version] of Object.entries(blockVersions)) {
const schema = workspace.schema.flavourSchemaMap.get(flavour);
if (schema?.version !== version) {
return MigrationPoint.BlockVersion;
}
try {
workspace.meta.validateVersion(workspace);
} catch (e) {
console.info('validateVersion error', e);
return MigrationPoint.BlockVersion;
}
return null;

View File

@@ -1,6 +1,6 @@
{
"name": "@affine/sdk",
"version": "0.10.3-canary.2",
"version": "0.10.3-beta.5",
"type": "module",
"scripts": {
"build": "vite build",
@@ -22,11 +22,11 @@
"dist"
],
"dependencies": {
"@blocksuite/block-std": "0.0.0-20231124123613-7c06e95d-nightly",
"@blocksuite/blocks": "0.0.0-20231124123613-7c06e95d-nightly",
"@blocksuite/editor": "0.0.0-20231124123613-7c06e95d-nightly",
"@blocksuite/global": "0.0.0-20231124123613-7c06e95d-nightly",
"@blocksuite/store": "0.0.0-20231124123613-7c06e95d-nightly",
"@blocksuite/block-std": "0.0.0-20231122113751-6bf81eb3-nightly",
"@blocksuite/blocks": "0.0.0-20231122113751-6bf81eb3-nightly",
"@blocksuite/editor": "0.0.0-20231122113751-6bf81eb3-nightly",
"@blocksuite/global": "0.0.0-20231122113751-6bf81eb3-nightly",
"@blocksuite/store": "0.0.0-20231122113751-6bf81eb3-nightly",
"jotai": "^2.5.1",
"zod": "^3.22.4"
},

View File

@@ -1,7 +1,7 @@
{
"name": "@toeverything/y-indexeddb",
"type": "module",
"version": "0.10.3-canary.2",
"version": "0.10.3-beta.5",
"description": "IndexedDB database adapter for Yjs",
"repository": "toeverything/AFFiNE",
"author": "toeverything",
@@ -37,8 +37,8 @@
"y-provider": "workspace:*"
},
"devDependencies": {
"@blocksuite/blocks": "0.0.0-20231124123613-7c06e95d-nightly",
"@blocksuite/store": "0.0.0-20231124123613-7c06e95d-nightly",
"@blocksuite/blocks": "0.0.0-20231122113751-6bf81eb3-nightly",
"@blocksuite/store": "0.0.0-20231122113751-6bf81eb3-nightly",
"fake-indexeddb": "^5.0.0",
"vite": "^4.4.11",
"vite-plugin-dts": "3.6.0",

View File

@@ -1,7 +1,7 @@
{
"name": "y-provider",
"type": "module",
"version": "0.10.3-canary.2",
"version": "0.10.3-beta.5",
"description": "Yjs provider protocol for multi document support",
"exports": {
".": "./src/index.ts"
@@ -24,7 +24,7 @@
"build": "vite build"
},
"devDependencies": {
"@blocksuite/store": "0.0.0-20231124123613-7c06e95d-nightly",
"@blocksuite/store": "0.0.0-20231122113751-6bf81eb3-nightly",
"vite": "^4.4.11",
"vite-plugin-dts": "3.6.0",
"vitest": "0.34.6",

View File

@@ -64,12 +64,12 @@
"uuid": "^9.0.1"
},
"devDependencies": {
"@blocksuite/blocks": "0.0.0-20231124123613-7c06e95d-nightly",
"@blocksuite/editor": "0.0.0-20231124123613-7c06e95d-nightly",
"@blocksuite/global": "0.0.0-20231124123613-7c06e95d-nightly",
"@blocksuite/blocks": "0.0.0-20231122113751-6bf81eb3-nightly",
"@blocksuite/editor": "0.0.0-20231122113751-6bf81eb3-nightly",
"@blocksuite/global": "0.0.0-20231122113751-6bf81eb3-nightly",
"@blocksuite/icons": "2.1.36",
"@blocksuite/lit": "0.0.0-20231124123613-7c06e95d-nightly",
"@blocksuite/store": "0.0.0-20231124123613-7c06e95d-nightly",
"@blocksuite/lit": "0.0.0-20231122113751-6bf81eb3-nightly",
"@blocksuite/store": "0.0.0-20231122113751-6bf81eb3-nightly",
"@storybook/jest": "^0.2.3",
"@storybook/testing-library": "^0.2.2",
"@testing-library/react": "^14.0.0",
@@ -85,5 +85,5 @@
"vitest": "0.34.6",
"yjs": "^13.6.10"
},
"version": "0.10.3-canary.2"
"version": "0.10.3-beta.5"
}

View File

@@ -7,14 +7,11 @@ import type { CSSProperties, ReactElement } from 'react';
import {
memo,
Suspense,
useCallback,
useEffect,
useLayoutEffect,
useRef,
useState,
} from 'react';
import type { FallbackProps } from 'react-error-boundary';
import { ErrorBoundary } from 'react-error-boundary';
import { Skeleton } from '../../ui/skeleton';
import {
@@ -77,6 +74,63 @@ const useBlockElementById = (
return blockElement;
};
/**
* TODO: Define error to unexpected state together in the future.
*/
export class NoPageRootError extends Error {
constructor(public page: Page) {
super('Page root not found when render editor!');
// Log info to let sentry collect more message
const spaceVectors = Array.from(page.doc.spaces.entries()).map(
([pageId, doc]) => `${pageId} > ${doc.guid}`
);
const blocks = page.doc.getMap('blocks');
console.info(
'NoPageRootError current data: %s',
JSON.stringify({
expectPageId: page.id,
expectGuid: page.spaceDoc.guid,
spaceVectors,
blockSize: blocks.size,
})
);
}
}
/**
* TODO: Defined async cache to support suspense, instead of reflect symbol to provider persistent error cache.
*/
const PAGE_LOAD_KEY = Symbol('PAGE_LOAD');
const PAGE_ROOT_KEY = Symbol('PAGE_ROOT');
function usePageRoot(page: Page) {
let load$ = Reflect.get(page, PAGE_LOAD_KEY);
if (!load$) {
load$ = page.load();
Reflect.set(page, PAGE_LOAD_KEY, load$);
}
use(load$);
if (!page.root) {
let root$: Promise<void> | undefined = Reflect.get(page, PAGE_ROOT_KEY);
if (!root$) {
root$ = new Promise((resolve, reject) => {
const disposable = page.slots.rootAdded.once(() => {
resolve();
});
window.setTimeout(() => {
disposable.dispose();
reject(new NoPageRootError(page));
}, 20 * 1000);
});
Reflect.set(page, PAGE_ROOT_KEY, root$);
}
use(root$);
}
return page.root;
}
const BlockSuiteEditorImpl = ({
mode,
page,
@@ -86,9 +140,8 @@ const BlockSuiteEditorImpl = ({
onModeChange,
style,
}: EditorProps): ReactElement => {
if (!page.loaded) {
use(page.waitForLoaded());
}
usePageRoot(page);
assertExists(page, 'page should not be null');
const editorRef = useRef<EditorContainer | null>(null);
if (editorRef.current === null) {
@@ -176,27 +229,7 @@ const BlockSuiteEditorImpl = ({
);
};
const BlockSuiteErrorFallback = (
props: FallbackProps & ErrorBoundaryProps
): ReactElement => {
return (
<div>
<h1>Sorry.. there was an error</h1>
<div>{props.error.message}</div>
<button
data-testid="error-fallback-reset-button"
onClick={() => {
props.onReset?.();
props.resetErrorBoundary();
}}
>
Try again
</button>
</div>
);
};
export const BlockSuiteFallback = memo(function BlockSuiteFallback() {
export const EditorLoading = memo(function EditorLoading() {
return (
<div className={blockSuiteEditorStyle}>
<Skeleton
@@ -210,21 +243,12 @@ export const BlockSuiteFallback = memo(function BlockSuiteFallback() {
});
export const BlockSuiteEditor = memo(function BlockSuiteEditor(
props: EditorProps & ErrorBoundaryProps
props: EditorProps
): ReactElement {
return (
<ErrorBoundary
fallbackRender={useCallback(
(fallbackProps: FallbackProps) => (
<BlockSuiteErrorFallback {...fallbackProps} onReset={props.onReset} />
),
[props.onReset]
)}
>
<Suspense fallback={<BlockSuiteFallback />}>
<BlockSuiteEditorImpl key={props.page.id} {...props} />
</Suspense>
</ErrorBoundary>
<Suspense fallback={<EditorLoading />}>
<BlockSuiteEditorImpl key={props.page.id} {...props} />
</Suspense>
);
});

View File

@@ -1,4 +1,4 @@
import { BlockSuiteFallback } from '../block-suite-editor';
import { EditorLoading } from '../block-suite-editor';
import {
pageDetailSkeletonStyle,
pageDetailSkeletonTitleStyle,
@@ -8,7 +8,7 @@ export const PageDetailSkeleton = () => {
return (
<div className={pageDetailSkeletonStyle}>
<div className={pageDetailSkeletonTitleStyle} />
<BlockSuiteFallback />
<EditorLoading />
</div>
);
};

View File

@@ -1,6 +1,4 @@
import { createVar, style } from '@vanilla-extract/css';
export const hoverMaxWidth = createVar();
import { style } from '@vanilla-extract/css';
export const root = style({
position: 'relative',
@@ -17,8 +15,7 @@ export const tagsContainer = style({
export const tagsScrollContainer = style([
tagsContainer,
{
overflowX: 'hidden',
position: 'relative',
overflow: 'auto',
height: '100%',
gap: '8px',
},
@@ -44,7 +41,7 @@ export const innerContainer = style({
transition: 'all 0.2s 0.3s ease-in-out',
selectors: {
[`${root}:hover &`]: {
maxWidth: hoverMaxWidth,
maxWidth: 'var(--hover-max-width)',
},
},
});
@@ -69,16 +66,6 @@ export const innerBackdrop = style({
export const tag = style({
height: '20px',
display: 'flex',
minWidth: 0,
alignItems: 'center',
justifyContent: 'space-between',
':last-child': {
minWidth: 'max-content',
},
});
export const tagInnerWrapper = style({
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
@@ -87,7 +74,7 @@ export const tagInnerWrapper = style({
});
export const tagSticky = style([
tagInnerWrapper,
tag,
{
fontSize: 'var(--affine-font-xs)',
borderRadius: '10px',
@@ -95,8 +82,10 @@ export const tagSticky = style([
border: '1px solid var(--affine-border-color)',
background: 'var(--affine-background-primary-color)',
maxWidth: '128px',
position: 'sticky',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
left: 0,
},
]);

View File

@@ -1,9 +1,8 @@
import type { Tag } from '@affine/env/filter';
import { MoreHorizontalIcon } from '@blocksuite/icons';
import { Menu } from '@toeverything/components/menu';
import { assignInlineVars } from '@vanilla-extract/dynamic';
import clsx from 'clsx';
import { useMemo } from 'react';
import { useEffect, useMemo, useRef } from 'react';
import * as styles from './page-tags.css';
import { stopPropagation } from './utils';
@@ -43,22 +42,18 @@ const TagItem = ({ tag, idx, mode, style }: TagItemProps) => {
return (
<div
data-testid="page-tag"
className={styles.tag}
className={mode === 'sticky' ? styles.tagSticky : styles.tagListItem}
data-idx={idx}
title={tag.value}
style={style}
>
<div
className={mode === 'sticky' ? styles.tagSticky : styles.tagListItem}
>
<div
className={styles.tagIndicator}
style={{
backgroundColor: tagColorMap(tag.color),
}}
/>
<div className={styles.tagLabel}>{tag.value}</div>
</div>
className={styles.tagIndicator}
style={{
backgroundColor: tagColorMap(tag.color),
}}
/>
<div className={styles.tagLabel}>{tag.value}</div>
</div>
);
};
@@ -74,6 +69,26 @@ export const PageTags = ({
? widthOnHover
: `${widthOnHover}px`
: 'auto';
const tagsContainerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (tagsContainerRef.current) {
const tagsContainer = tagsContainerRef.current;
const listener = () => {
// on mouseleave, reset scroll position to the hoverExpandDirection
tagsContainer.scrollTo({
left: hoverExpandDirection === 'left' ? Number.MAX_SAFE_INTEGER : 0,
behavior: 'smooth',
});
};
listener();
tagsContainerRef.current.addEventListener('mouseleave', listener);
return () => {
tagsContainer.removeEventListener('mouseleave', listener);
};
}
return;
}, [hoverExpandDirection]);
const tagsInPopover = useMemo(() => {
const lastTags = tags.slice(maxItems);
@@ -92,17 +107,36 @@ export const PageTags = ({
// sort tags by length
nTags.sort((a, b) => a.value.length - b.value.length);
const tagRightCharLength = nTags.reduceRight<number[]>(
(acc, tag) => {
const curr = acc[0] + Math.min(tag.value.length, 10);
return [curr, ...acc];
},
[0]
);
tagRightCharLength.shift();
return nTags.map((tag, idx) => (
<TagItem key={tag.id} tag={tag} idx={idx} mode="sticky" />
<TagItem
key={tag.id}
tag={tag}
idx={idx}
mode="sticky"
style={{
right: `calc(${tagRightCharLength[idx]}em)`,
}}
/>
));
}, [maxItems, tags]);
return (
<div
data-testid="page-tags"
className={styles.root}
style={assignInlineVars({
[styles.hoverMaxWidth]: sanitizedWidthOnHover,
})}
style={{
// @ts-expect-error it's fine
'--hover-max-width': sanitizedWidthOnHover,
}}
>
<div
style={{
@@ -112,7 +146,9 @@ export const PageTags = ({
className={clsx(styles.innerContainer)}
>
<div className={styles.innerBackdrop} />
<div className={styles.tagsScrollContainer}>{tagsNormal}</div>
<div className={styles.tagsScrollContainer} ref={tagsContainerRef}>
{tagsNormal}
</div>
{maxItems && tags.length > maxItems ? (
<Menu
items={tagsInPopover}

View File

@@ -11,7 +11,6 @@ import * as styles from './share.css';
export interface StorageProgressProgress {
max: number;
value: number;
upgradable?: boolean;
onUpgrade: () => void;
plan: SubscriptionPlan;
}
@@ -24,7 +23,6 @@ enum ButtonType {
export const StorageProgress = ({
max: upperLimit,
value,
upgradable = true,
onUpgrade,
plan,
}: StorageProgressProgress) => {
@@ -65,24 +63,22 @@ export const StorageProgress = ({
</div>
</div>
{upgradable ? (
<Tooltip
options={{ hidden: percent < 100 }}
content={t['com.affine.storage.maximum-tips']()}
>
<span tabIndex={0}>
<Button
type={buttonType}
onClick={onUpgrade}
className={styles.storageButton}
>
{plan === 'Free'
? t['com.affine.storage.upgrade']()
: t['com.affine.storage.change-plan']()}
</Button>
</span>
</Tooltip>
) : null}
<Tooltip
options={{ hidden: percent < 100 }}
content={t['com.affine.storage.maximum-tips']()}
>
<span tabIndex={0}>
<Button
type={buttonType}
onClick={onUpgrade}
className={styles.storageButton}
>
{plan === 'Free'
? t['com.affine.storage.upgrade']()
: t['com.affine.storage.change-plan']()}
</Button>
</span>
</Tooltip>
</div>
);
};

View File

@@ -351,6 +351,8 @@ export const createConfiguration: (
'process.env.CAPTCHA_SITE_KEY': JSON.stringify(
process.env.CAPTCHA_SITE_KEY
),
'process.env.SENTRY_DSN': JSON.stringify(process.env.SENTRY_DSN),
'process.env.BUILD_TYPE': JSON.stringify(process.env.BUILD_TYPE),
runtimeConfig: JSON.stringify(runtimeConfig),
}),
new CopyPlugin({

View File

@@ -33,7 +33,6 @@ export function getRuntimeConfig(buildFlags: BuildFlags): RuntimeConfig {
enableCaptcha: true,
enableEnhanceShareMode: false,
enablePayment: true,
enablePageHistory: false,
serverUrlPrefix: 'https://insider.affine.pro', // Let insider be stable environment temporarily.
editorFlags,
appVersion: packageJson.version,
@@ -42,7 +41,6 @@ export function getRuntimeConfig(buildFlags: BuildFlags): RuntimeConfig {
get beta() {
return {
...this.stable,
enablePageHistory: false,
serverUrlPrefix: 'https://insider.affine.pro',
};
},
@@ -77,7 +75,6 @@ export function getRuntimeConfig(buildFlags: BuildFlags): RuntimeConfig {
enableCaptcha: true,
enableEnhanceShareMode: false,
enablePayment: true,
enablePageHistory: true,
serverUrlPrefix: 'https://affine.fail',
editorFlags,
appVersion: packageJson.version,
@@ -145,11 +142,6 @@ export function getRuntimeConfig(buildFlags: BuildFlags): RuntimeConfig {
: buildFlags.mode === 'development'
? true
: currentBuildPreset.enablePayment,
enablePageHistory: process.env.ENABLE_PAGE_HISTORY
? process.env.ENABLE_PAGE_HISTORY === 'true'
: buildFlags.mode === 'development'
? true
: currentBuildPreset.enablePageHistory,
};
if (buildFlags.mode === 'development') {

View File

@@ -2,7 +2,7 @@
"name": "@affine/core",
"type": "module",
"private": true,
"version": "0.10.3-canary.2",
"version": "0.10.3-beta.5",
"scripts": {
"build": "yarn -T run build-core",
"dev": "yarn -T run dev-core",
@@ -25,14 +25,14 @@
"@affine/i18n": "workspace:*",
"@affine/templates": "workspace:*",
"@affine/workspace": "workspace:*",
"@blocksuite/block-std": "0.0.0-20231124123613-7c06e95d-nightly",
"@blocksuite/blocks": "0.0.0-20231124123613-7c06e95d-nightly",
"@blocksuite/editor": "0.0.0-20231124123613-7c06e95d-nightly",
"@blocksuite/global": "0.0.0-20231124123613-7c06e95d-nightly",
"@blocksuite/block-std": "0.0.0-20231122113751-6bf81eb3-nightly",
"@blocksuite/blocks": "0.0.0-20231122113751-6bf81eb3-nightly",
"@blocksuite/editor": "0.0.0-20231122113751-6bf81eb3-nightly",
"@blocksuite/global": "0.0.0-20231122113751-6bf81eb3-nightly",
"@blocksuite/icons": "2.1.36",
"@blocksuite/lit": "0.0.0-20231124123613-7c06e95d-nightly",
"@blocksuite/store": "0.0.0-20231124123613-7c06e95d-nightly",
"@blocksuite/virgo": "0.0.0-20231124123613-7c06e95d-nightly",
"@blocksuite/lit": "0.0.0-20231122113751-6bf81eb3-nightly",
"@blocksuite/store": "0.0.0-20231122113751-6bf81eb3-nightly",
"@blocksuite/virgo": "0.0.0-20231122113751-6bf81eb3-nightly",
"@dnd-kit/core": "^6.0.8",
"@dnd-kit/sortable": "^8.0.0",
"@emotion/cache": "^11.11.0",
@@ -45,6 +45,8 @@
"@radix-ui/react-scroll-area": "^1.0.5",
"@radix-ui/react-select": "^2.0.0",
"@react-hookz/web": "^23.1.0",
"@sentry/integrations": "^7.83.0",
"@sentry/react": "^7.83.0",
"@toeverything/components": "^0.0.46",
"@toeverything/theme": "^0.7.20",
"@vanilla-extract/css": "^1.13.0",

View File

@@ -38,7 +38,7 @@
"env": "SENTRY_AUTH_TOKEN"
},
{
"env": "NEXT_PUBLIC_SENTRY_DSN"
"env": "SENTRY_DSN"
},
{
"env": "DISTRIBUTION"

View File

@@ -1,7 +0,0 @@
import { atom } from 'jotai';
// make page history controllable by atom to make it easier to use in CMDK
export const pageHistoryModalAtom = atom({
open: false,
pageId: '',
});

View File

@@ -6,7 +6,15 @@ import {
rootWorkspacesMetadataAtom,
workspaceAdaptersAtom,
} from '@affine/workspace/atom';
import * as Sentry from '@sentry/react';
import type { createStore } from 'jotai/vanilla';
import { useEffect } from 'react';
import {
createRoutesFromChildren,
matchRoutes,
useLocation,
useNavigationType,
} from 'react-router-dom';
import { WorkspaceAdapters } from '../adapters/workspace';
import { performanceLogger } from '../shared';
@@ -51,6 +59,31 @@ export async function setup(store: ReturnType<typeof createStore>) {
performanceSetupLogger.info('setup global');
setupGlobal();
if (window.SENTRY_RELEASE) {
// https://docs.sentry.io/platforms/javascript/guides/react/#configure
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.BUILD_TYPE ?? 'development',
integrations: [
new Sentry.BrowserTracing({
routingInstrumentation: Sentry.reactRouterV6Instrumentation(
useEffect,
useLocation,
useNavigationType,
createRoutesFromChildren,
matchRoutes
),
}),
new Sentry.Replay(),
new Sentry.BrowserTracing(),
],
});
Sentry.setTags({
appVersion: runtimeConfig.appVersion,
editorVersion: runtimeConfig.editorVersion,
});
}
performanceSetupLogger.info('get root workspace meta');
// do not read `rootWorkspacesMetadataAtom` before migration
await store.get(rootWorkspacesMetadataAtom);

View File

@@ -1,191 +0,0 @@
import type {
QueryParamError,
Unreachable,
WorkspaceNotFoundError,
} from '@affine/env/constant';
import { PageNotFoundError } from '@affine/env/constant';
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import { Button } from '@toeverything/components/button';
import {
currentPageIdAtom,
currentWorkspaceIdAtom,
getCurrentStore,
} from '@toeverything/infra/atom';
import { useAtomValue } from 'jotai/react';
import { Provider } from 'jotai/react';
import type { ErrorInfo, ReactElement, ReactNode } from 'react';
import type React from 'react';
import { Component, useEffect } from 'react';
import { useLocation, useParams } from 'react-router-dom';
import {
RecoverableError,
type SessionFetchErrorRightAfterLoginOrSignUp,
} from '../../unexpected-application-state/errors';
import {
errorDescription,
errorDetailStyle,
errorDivider,
errorImage,
errorLayout,
errorRetryButton,
errorTitle,
} from './affine-error-boundary.css';
import errorBackground from './error-status.assets.svg';
export type AffineErrorBoundaryProps = React.PropsWithChildren & {
height?: number | string;
};
type AffineError =
| QueryParamError
| Unreachable
| WorkspaceNotFoundError
| PageNotFoundError
| Error
| SessionFetchErrorRightAfterLoginOrSignUp;
interface AffineErrorBoundaryState {
error: AffineError | null;
canRetryRecoveredError: boolean;
}
export const DumpInfo = () => {
const location = useLocation();
const metadata = useAtomValue(rootWorkspacesMetadataAtom);
const currentWorkspaceId = useAtomValue(currentWorkspaceIdAtom);
const currentPageId = useAtomValue(currentPageIdAtom);
const path = location.pathname;
const query = useParams();
useEffect(() => {
console.info('DumpInfo', {
path,
query,
currentWorkspaceId,
currentPageId,
metadata,
});
}, [path, query, currentWorkspaceId, currentPageId, metadata]);
return null;
};
export class AffineErrorBoundary extends Component<
AffineErrorBoundaryProps,
AffineErrorBoundaryState
> {
override state: AffineErrorBoundaryState = {
error: null,
canRetryRecoveredError: true,
};
private readonly handleRecoverableRetry = () => {
if (this.state.error instanceof RecoverableError) {
if (this.state.error.canRetry()) {
this.state.error.retry();
this.setState({
error: null,
canRetryRecoveredError: this.state.error.canRetry(),
});
} else {
document.location.reload();
}
}
};
private readonly handleRefresh = () => {
this.setState({ error: null });
};
static getDerivedStateFromError(
error: AffineError
): AffineErrorBoundaryState {
return {
error,
canRetryRecoveredError:
error instanceof RecoverableError ? error.canRetry() : true,
};
}
override componentDidCatch(error: AffineError, errorInfo: ErrorInfo) {
console.error('Uncaught error:', error, errorInfo);
}
override render(): ReactNode {
if (this.state.error) {
let errorDetail: ReactElement | null = null;
const error = this.state.error;
if (error instanceof PageNotFoundError) {
errorDetail = (
<>
<h1>Sorry.. there was an error</h1>
<>
<span> Page error </span>
<span>
Cannot find page {error.pageId} in workspace{' '}
{error.workspace.id}
</span>
</>
</>
);
} else if (error instanceof RecoverableError) {
const retryButtonDesc = this.state.canRetryRecoveredError
? 'Refetch'
: 'Reload';
errorDetail = (
<>
<h1 className={errorTitle}>Sorry.. there was an error</h1>
<span className={errorDescription}> {error.message} </span>
<span className={errorDescription}>
If you are still experiencing this issue, please{' '}
<a
style={{ color: 'var(--affine-primary-color)' }}
href="https://community.affine.pro"
target="__blank"
>
contact us through the community.
</a>
</span>
<Button
className={errorRetryButton}
onClick={this.handleRecoverableRetry}
type="primary"
>
{retryButtonDesc}
</Button>
</>
);
} else {
errorDetail = (
<>
<h1 className={errorTitle}>Sorry.. there was an error</h1>
<code className={errorDescription}>
{error.message ?? error.toString()}
</code>
<Button
onClick={this.handleRefresh}
className={errorRetryButton}
type="primary"
>
Refresh
</Button>
</>
);
}
return (
<div className={errorLayout} style={{ height: this.props.height }}>
<div className={errorDetailStyle}>{errorDetail}</div>
<span className={errorDivider} />
<div
className={errorImage}
style={{ backgroundImage: `url(${errorBackground})` }}
/>
<Provider key="JotaiProvider" store={getCurrentStore()}>
<DumpInfo />
</Provider>
</div>
);
}
return this.props.children;
}
}

View File

@@ -0,0 +1,6 @@
import { style } from '@vanilla-extract/css';
export const viewport = style({
height: '100%',
width: '100%',
});

View File

@@ -0,0 +1,53 @@
import { getCurrentStore } from '@toeverything/infra/atom';
import { Provider } from 'jotai/react';
import type { FC } from 'react';
import { useMemo } from 'react';
import * as styles from './affine-error-fallback.css';
import {
ERROR_REFLECT_KEY,
type FallbackProps,
} from './error-basic/fallback-creator';
import { DumpInfo } from './error-basic/info-logger';
import { AnyErrorFallback } from './error-fallbacks/any-error-fallback';
import { NoPageRootFallback } from './error-fallbacks/no-page-root-fallback';
import { PageNotFoundDetail } from './error-fallbacks/page-not-found-fallback';
import { RecoverableErrorFallback } from './error-fallbacks/recoverable-error-fallback';
/**
* Register all fallback components here.
* If have new one just add it to the set.
*/
const fallbacks = new Set([
PageNotFoundDetail,
RecoverableErrorFallback,
NoPageRootFallback,
]);
function getErrorFallbackComponent(error: any): FC<FallbackProps> {
for (const Component of fallbacks) {
const ErrorConstructor = Reflect.get(Component, ERROR_REFLECT_KEY);
if (ErrorConstructor && error instanceof ErrorConstructor) {
return Component as FC<FallbackProps>;
}
}
return AnyErrorFallback;
}
export interface AffineErrorFallbackProps extends FallbackProps {
height?: number | string;
}
export const AffineErrorFallback: FC<AffineErrorFallbackProps> = props => {
const { error, resetError, height } = props;
const Component = useMemo(() => getErrorFallbackComponent(error), [error]);
return (
<div className={styles.viewport} style={{ height }}>
<Component error={error} resetError={resetError} />
<Provider key="JotaiProvider" store={getCurrentStore()}>
<DumpInfo error={error} />
</Provider>
</div>
);
};

View File

@@ -0,0 +1,43 @@
<svg width="490" height="242" viewBox="0 0 490 242" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16.5098 155.545C16.4625 163.027 18.0723 169.655 21.3393 175.432C24.6064 181.208 29.1282 185.73 34.9047 188.997C40.6812 192.264 47.3337 193.898 54.8621 193.898C63.2428 193.898 70.6765 191.838 77.1632 187.719C83.65 183.599 88.7399 177.989 92.4331 170.886C96.1263 163.784 97.9965 155.735 98.0439 146.739C98.0912 137.364 96.1026 129.196 92.078 122.236C88.1007 115.228 82.7977 109.807 76.1689 105.972C69.5875 102.089 62.3905 100.148 54.578 100.148C48.7541 100.148 43.4274 101.166 38.5979 103.202C33.7683 105.19 29.5306 107.866 25.8848 111.227H25.3166L32.703 51H93.2143" stroke="black" stroke-width="2"/>
<ellipse cx="94.4026" cy="50.8894" rx="3.89381" ry="3.88938" fill="#121212"/>
<ellipse cx="16.4026" cy="155.889" rx="3.89381" ry="3.88938" fill="#121212"/>
<ellipse cx="32.4026" cy="50.8894" rx="3.89381" ry="3.88938" fill="#121212"/>
<ellipse cx="25.4026" cy="110.889" rx="3.89381" ry="3.88938" fill="#121212"/>
<path d="M151.592 59.1235L275.951 183.342" stroke="#E3E2E4"/>
<rect x="151.626" y="59.1582" width="124.291" height="124.149" stroke="#E3E2E4"/>
<path d="M275.573 121.465C276.129 117.824 276.417 114.095 276.417 110.299C276.417 69.7024 243.469 36.792 202.826 36.792C162.183 36.792 129.235 69.7024 129.235 110.299C129.235 150.897 162.183 183.807 202.826 183.807C206.465 183.807 210.041 183.543 213.539 183.034" stroke="#E3E2E4"/>
<path d="M213.539 182.964C217.184 183.519 220.917 183.807 224.717 183.807C265.36 183.807 298.308 150.897 298.308 110.299C298.308 69.7024 265.36 36.792 224.717 36.792C184.074 36.792 151.126 69.7024 151.126 110.299C151.126 114.095 151.414 117.824 151.97 121.465" stroke="#E3E2E4"/>
<path d="M151.434 121.465C150.924 124.958 150.66 128.531 150.66 132.166C150.66 172.763 183.608 205.673 224.251 205.673C264.894 205.673 297.842 172.763 297.842 132.166C297.842 91.5686 264.894 58.6582 224.251 58.6582C220.613 58.6582 217.036 58.922 213.539 59.4314" stroke="#E3E2E4"/>
<path d="M213.539 59.4314C210.041 58.922 206.465 58.6582 202.826 58.6582C162.183 58.6582 129.235 91.5686 129.235 132.166C129.235 172.763 162.183 205.673 202.826 205.673C243.469 205.673 276.417 172.763 276.417 132.166C276.417 128.531 276.153 124.958 275.643 121.465" stroke="#E3E2E4"/>
<path d="M275.951 59.1235L151.592 183.342" stroke="#E3E2E4"/>
<path d="M151.126 121.465H275.951" stroke="#E3E2E4"/>
<path d="M213.539 58.6582V183.807" stroke="#E3E2E4"/>
<ellipse cx="275.951" cy="121.465" rx="3.72614" ry="3.7219" fill="#121212"/>
<ellipse cx="275.951" cy="59.1233" rx="3.72614" ry="3.7219" fill="#121212"/>
<ellipse cx="275.951" cy="183.807" rx="3.72614" ry="3.7219" fill="#121212"/>
<ellipse cx="151.592" cy="183.807" rx="3.72613" ry="3.7219" fill="#121212"/>
<ellipse cx="213.539" cy="121.465" rx="3.72613" ry="3.7219" fill="#121212"/>
<ellipse cx="213.539" cy="59.1233" rx="3.72613" ry="3.7219" fill="#121212"/>
<ellipse cx="151.592" cy="121.465" rx="3.72613" ry="3.7219" fill="#121212"/>
<ellipse cx="151.592" cy="59.1233" rx="3.72613" ry="3.7219" fill="#121212"/>
<ellipse cx="213.539" cy="183.807" rx="3.72613" ry="3.7219" fill="#121212"/>
<path d="M338.583 59.1235L462.943 183.342" stroke="#E3E2E4"/>
<rect x="338.617" y="59.1582" width="124.291" height="124.149" stroke="#E3E2E4"/>
<path d="M462.565 121.465C463.12 117.824 463.408 114.095 463.408 110.299C463.408 69.7024 430.46 36.792 389.817 36.792C349.174 36.792 316.226 69.7024 316.226 110.299C316.226 150.897 349.174 183.807 389.817 183.807C393.456 183.807 397.033 183.543 400.53 183.034" stroke="#E3E2E4"/>
<path d="M400.53 182.964C404.175 183.519 407.908 183.807 411.708 183.807C452.351 183.807 485.299 150.897 485.299 110.299C485.299 69.7024 452.351 36.792 411.708 36.792C371.065 36.792 338.117 69.7024 338.117 110.299C338.117 114.095 338.405 117.824 338.961 121.465" stroke="#E3E2E4"/>
<path d="M338.425 121.465C337.915 124.958 337.651 128.531 337.651 132.166C337.651 172.763 370.599 205.673 411.242 205.673C451.886 205.673 484.833 172.763 484.833 132.166C484.833 91.5686 451.886 58.6582 411.242 58.6582C407.604 58.6582 404.027 58.922 400.53 59.4314" stroke="#E3E2E4"/>
<path d="M400.53 59.4314C397.033 58.922 393.456 58.6582 389.817 58.6582C349.174 58.6582 316.226 91.5686 316.226 132.166C316.226 172.763 349.174 205.673 389.817 205.673C430.46 205.673 463.408 172.763 463.408 132.166C463.408 128.531 463.144 124.958 462.634 121.465" stroke="#E3E2E4"/>
<path d="M462.943 59.1235L338.583 183.342" stroke="#E3E2E4"/>
<path d="M338.117 121.465H462.943" stroke="#E3E2E4"/>
<path d="M400.53 58.6582V183.807" stroke="#E3E2E4"/>
<ellipse cx="462.942" cy="121.465" rx="3.72614" ry="3.7219" fill="#121212"/>
<ellipse cx="462.942" cy="59.1233" rx="3.72614" ry="3.7219" fill="#121212"/>
<ellipse cx="462.942" cy="183.807" rx="3.72614" ry="3.7219" fill="#121212"/>
<ellipse cx="338.583" cy="183.807" rx="3.72613" ry="3.7219" fill="#121212"/>
<ellipse cx="400.53" cy="121.465" rx="3.72613" ry="3.7219" fill="#121212"/>
<ellipse cx="400.53" cy="59.1233" rx="3.72613" ry="3.7219" fill="#121212"/>
<ellipse cx="338.583" cy="121.465" rx="3.72613" ry="3.7219" fill="#121212"/>
<ellipse cx="338.583" cy="59.1233" rx="3.72613" ry="3.7219" fill="#121212"/>
<ellipse cx="400.53" cy="183.807" rx="3.72613" ry="3.7219" fill="#121212"/>
</svg>

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@@ -6,6 +6,7 @@ export const errorLayout = style({
alignItems: 'center',
height: '100%',
width: '100%',
gap: '20px',
});
export const errorDetailStyle = style({
@@ -24,15 +25,15 @@ export const errorImage = style({
height: '178px',
maxWidth: '400px',
flexGrow: 1,
backgroundSize: 'cover',
});
export const errorDescription = style({
marginTop: '24px',
});
export const errorRetryButton = style({
export const errorFooter = style({
marginTop: '24px',
width: '94px',
});
export const errorDivider = style({

View File

@@ -0,0 +1,106 @@
import { Trans } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { Button } from '@toeverything/components/button';
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
import {
type FC,
type PropsWithChildren,
type ReactNode,
useState,
} from 'react';
import imageUrlFor404 from '../error-assets/404-status.assets.svg';
import imageUrlFor500 from '../error-assets/500-status.assets.svg';
import * as styles from './error-detail.css';
export enum ErrorStatus {
NotFound = 404,
Unexpected = 500,
}
export interface ErrorDetailProps extends PropsWithChildren {
status?: ErrorStatus;
direction?: 'column' | 'row';
title: string;
description: ReactNode | Array<ReactNode>;
buttonText?: string;
onButtonClick?: () => void | Promise<void>;
resetError?: () => void;
withoutImage?: boolean;
}
const imageMap = new Map([
[ErrorStatus.NotFound, imageUrlFor404],
[ErrorStatus.Unexpected, imageUrlFor500],
]);
/**
* TODO: Unify with NotFoundPage.
*/
export const ErrorDetail: FC<ErrorDetailProps> = props => {
const {
status = ErrorStatus.Unexpected,
direction = 'row',
description,
onButtonClick,
resetError,
withoutImage,
} = props;
const descriptions = Array.isArray(description) ? description : [description];
const [isBtnLoading, setBtnLoading] = useState(false);
const t = useAFFiNEI18N();
const onBtnClick = useAsyncCallback(async () => {
try {
setBtnLoading(true);
await onButtonClick?.();
resetError?.(); // Only reset when retry success.
} finally {
setBtnLoading(false);
}
}, [onButtonClick, resetError]);
return (
<div className={styles.errorLayout} style={{ flexDirection: direction }}>
<div className={styles.errorDetailStyle}>
<h1 className={styles.errorTitle}>{props.title}</h1>
{descriptions.map((item, i) => (
<p key={i} className={styles.errorDescription}>
{item}
</p>
))}
<div className={styles.errorFooter}>
<Button
type="primary"
onClick={onBtnClick}
loading={isBtnLoading}
size="extraLarge"
>
{props.buttonText ?? t['com.affine.error.retry']()}
</Button>
</div>
</div>
{withoutImage ? null : (
<div
className={styles.errorImage}
style={{ backgroundImage: `url(${imageMap.get(status)})` }}
/>
)}
</div>
);
};
export function ContactUS() {
return (
<Trans>
If you are still experiencing this issue, please{' '}
<a
style={{ color: 'var(--affine-primary-color)' }}
href="https://community.affine.pro"
target="__blank"
>
contact us through the community.
</a>
</Trans>
);
}

View File

@@ -0,0 +1,16 @@
import type { FC } from 'react';
export interface FallbackProps<T extends Error = Error> {
error: T;
resetError: () => void;
}
export const ERROR_REFLECT_KEY = Symbol('ERROR_REFLECT_KEY');
export function createErrorFallback<T extends Error>(
ErrorConstructor: abstract new (...args: any[]) => T,
Component: FC<FallbackProps<T>>
): FC<FallbackProps<T>> {
Reflect.set(Component, ERROR_REFLECT_KEY, ErrorConstructor);
return Component;
}

View File

@@ -0,0 +1,31 @@
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import {
currentPageIdAtom,
currentWorkspaceIdAtom,
} from '@toeverything/infra/atom';
import { useAtomValue } from 'jotai/react';
import { useEffect } from 'react';
import { useLocation, useParams } from 'react-router-dom';
export interface DumpInfoProps {
error: any;
}
export const DumpInfo = (_props: DumpInfoProps) => {
const location = useLocation();
const metadata = useAtomValue(rootWorkspacesMetadataAtom);
const currentWorkspaceId = useAtomValue(currentWorkspaceIdAtom);
const currentPageId = useAtomValue(currentPageIdAtom);
const path = location.pathname;
const query = useParams();
useEffect(() => {
console.info('DumpInfo', {
path,
query,
currentWorkspaceId,
currentPageId,
metadata,
});
}, [path, query, currentWorkspaceId, currentPageId, metadata]);
return null;
};

View File

@@ -0,0 +1,26 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { type FC, useCallback } from 'react';
import { ErrorDetail } from '../error-basic/error-detail';
import type { FallbackProps } from '../error-basic/fallback-creator';
/**
* TODO: Support reload and retry two reset actions in page error and area error.
*/
export const AnyErrorFallback: FC<FallbackProps> = props => {
const { error } = props;
const t = useAFFiNEI18N();
const reloadPage = useCallback(() => {
document.location.reload();
}, []);
return (
<ErrorDetail
title={t['com.affine.error.unexpected-error.title']()}
resetError={reloadPage}
buttonText={t['com.affine.error.reload']()}
description={error.message ?? error.toString()}
/>
);
};

View File

@@ -0,0 +1,21 @@
import { NoPageRootError } from '@affine/component/block-suite-editor';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { ContactUS, ErrorDetail } from '../error-basic/error-detail';
import { createErrorFallback } from '../error-basic/fallback-creator';
export const NoPageRootFallback = createErrorFallback(
NoPageRootError,
props => {
const { resetError } = props;
const t = useAFFiNEI18N();
return (
<ErrorDetail
title={t['com.affine.error.no-page-root.title']()}
description={<ContactUS />}
resetError={resetError}
/>
);
}
);

View File

@@ -0,0 +1,30 @@
import { PageNotFoundError } from '@affine/env/constant';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useCallback } from 'react';
import {
RouteLogic,
useNavigateHelper,
} from '../../../../hooks/use-navigate-helper';
import { ErrorDetail, ErrorStatus } from '../error-basic/error-detail';
import { createErrorFallback } from '../error-basic/fallback-creator';
export const PageNotFoundDetail = createErrorFallback(PageNotFoundError, () => {
const t = useAFFiNEI18N();
const { jumpToIndex } = useNavigateHelper();
const onBtnClick = useCallback(
() => jumpToIndex(RouteLogic.REPLACE),
[jumpToIndex]
);
return (
<ErrorDetail
title={t['com.affine.notFoundPage.title']()}
description={t['404.hint']()}
buttonText={t['404.back']()}
onButtonClick={onBtnClick}
status={ErrorStatus.NotFound}
/>
);
});

View File

@@ -0,0 +1,41 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useCallback, useMemo, useState } from 'react';
import { RecoverableError } from '../../../../unexpected-application-state/errors';
import { ContactUS, ErrorDetail } from '../error-basic/error-detail';
import { createErrorFallback } from '../error-basic/fallback-creator';
export const RecoverableErrorFallback = createErrorFallback(
RecoverableError,
props => {
const { error, resetError } = props;
const t = useAFFiNEI18N();
const [count, rerender] = useState(0);
const canRetry = error.canRetry();
const buttonDesc = useMemo(() => {
if (canRetry) {
return t['com.affine.error.refetch']();
}
return t['com.affine.error.reload']();
}, [canRetry, t]);
const onRetry = useCallback(async () => {
if (canRetry) {
rerender(count + 1);
await error.retry();
} else {
document.location.reload();
}
}, [error, count, canRetry]);
return (
<ErrorDetail
title={t['com.affine.error.unexpected-error.title']()}
resetError={resetError}
buttonText={buttonDesc}
onButtonClick={onRetry}
description={[error.message, <ContactUS key="contact-us" />]}
/>
);
}
);

View File

@@ -0,0 +1,25 @@
<svg width="402" height="178" viewBox="0 0 402 178" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M93.7434 129.308H1L71.7965 15.1021V167.142" stroke="#121212" stroke-width="2" stroke-linecap="square" stroke-linejoin="bevel" />
<ellipse cx="71.4426" cy="14.7483" rx="3.89381" ry="3.88938" fill="#121212" />
<ellipse cx="93.3894" cy="129.308" rx="3.89381" ry="3.88938" fill="#121212" />
<path d="M140.357 27.1235L264.717 151.342" stroke="#E3E2E4" />
<rect x="140.392" y="27.1582" width="124.291" height="124.149" stroke="#E3E2E4" />
<path d="M264.339 89.4652C264.895 85.8242 265.183 82.0954 265.183 78.2995C265.183 37.7024 232.235 4.79199 191.592 4.79199C150.948 4.79199 118 37.7024 118 78.2995C118 118.897 150.948 151.807 191.592 151.807C195.23 151.807 198.807 151.543 202.304 151.034" stroke="#E3E2E4" />
<path d="M202.304 150.964C205.949 151.519 209.682 151.807 213.483 151.807C254.126 151.807 287.074 118.897 287.074 78.2995C287.074 37.7024 254.126 4.79199 213.483 4.79199C172.839 4.79199 139.892 37.7024 139.892 78.2995C139.892 82.0955 140.18 85.8242 140.735 89.4652" stroke="#E3E2E4" />
<path d="M140.2 89.4652C139.69 92.9584 139.426 96.5312 139.426 100.166C139.426 140.763 172.374 173.673 213.017 173.673C253.66 173.673 286.608 140.763 286.608 100.166C286.608 59.5686 253.66 26.6582 213.017 26.6582C209.378 26.6582 205.801 26.922 202.304 27.4314" stroke="#E3E2E4" />
<path d="M202.304 27.4314C198.807 26.922 195.23 26.6582 191.592 26.6582C150.948 26.6582 118 59.5686 118 100.166C118 140.763 150.948 173.673 191.592 173.673C232.235 173.673 265.183 140.763 265.183 100.166C265.183 96.5312 264.919 92.9584 264.409 89.4652" stroke="#E3E2E4" />
<path d="M264.717 27.1235L140.357 151.342" stroke="#E3E2E4" />
<path d="M139.892 89.4653H264.717" stroke="#E3E2E4" />
<path d="M202.304 26.6582V151.807" stroke="#E3E2E4" />
<ellipse cx="264.717" cy="89.4651" rx="3.72614" ry="3.7219" fill="#121212" />
<ellipse cx="264.717" cy="27.1233" rx="3.72614" ry="3.7219" fill="#121212" />
<ellipse cx="264.717" cy="151.807" rx="3.72614" ry="3.7219" fill="#121212" />
<ellipse cx="140.357" cy="151.807" rx="3.72613" ry="3.7219" fill="#121212" />
<ellipse cx="202.304" cy="89.4651" rx="3.72613" ry="3.7219" fill="#121212" />
<ellipse cx="202.304" cy="27.1233" rx="3.72613" ry="3.7219" fill="#121212" />
<ellipse cx="140.357" cy="89.4651" rx="3.72613" ry="3.7219" fill="#121212" />
<ellipse cx="140.357" cy="27.1233" rx="3.72613" ry="3.7219" fill="#121212" />
<ellipse cx="202.304" cy="151.807" rx="3.72613" ry="3.7219" fill="#121212" />
<path d="M401 127.187H308.257L379.053 12.9805V165.02" stroke="#121212" stroke-width="2" stroke-linecap="square" stroke-linejoin="bevel" />
<ellipse cx="379.407" cy="127.187" rx="3.89381" ry="3.88938" fill="#121212" />
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -0,0 +1,34 @@
import { ErrorBoundary } from '@sentry/react';
import type { FC, PropsWithChildren } from 'react';
import { useCallback } from 'react';
import { AffineErrorFallback } from './affine-error-fallback';
import { type FallbackProps } from './error-basic/fallback-creator';
export { type FallbackProps } from './error-basic/fallback-creator';
export interface AffineErrorBoundaryProps extends PropsWithChildren {
height?: number | string;
}
/**
* TODO: Unify with SWRErrorBoundary
*/
export const AffineErrorBoundary: FC<AffineErrorBoundaryProps> = props => {
const fallbackRender = useCallback(
(fallbackProps: FallbackProps) => {
return <AffineErrorFallback {...fallbackProps} height={props.height} />;
},
[props.height]
);
const onError = useCallback((error: Error, componentStack: string) => {
console.error('Uncaught error:', error, componentStack);
}, []);
return (
<ErrorBoundary fallback={fallbackRender} onError={onError}>
{props.children}
</ErrorBoundary>
);
};

View File

@@ -1,11 +0,0 @@
import type { ReactElement } from 'react';
import type { FallbackProps } from 'react-error-boundary';
export const AnyErrorBoundary = (props: FallbackProps): ReactElement => {
return (
<div>
<p>Something went wrong:</p>
<p>{props.error.toString()}</p>
</div>
);
};

View File

@@ -28,11 +28,6 @@ const UserPlanButtonWithData = () => {
const t = useAFFiNEI18N();
if (plan === SubscriptionPlan.SelfHosted) {
// Self hosted version doesn't have a payment apis.
return <div className={styles.userPlanButton}>{plan}</div>;
}
return (
<Tooltip content={t['com.affine.payment.tag-tooltips']()} side="top">
<div className={styles.userPlanButton} onClick={handleClick}>

View File

@@ -7,7 +7,6 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
import { useMemo } from 'react';
import { useSelfHosted } from '../../../hooks/affine/use-server-flavor';
import { useWorkspace } from '../../../hooks/use-workspace';
import { DeleteLeaveWorkspace } from './delete-leave-workspace';
import { ExportPanel } from './export';
@@ -21,7 +20,6 @@ import type { WorkspaceSettingDetailProps } from './types';
export const WorkspaceSettingDetail = (props: WorkspaceSettingDetailProps) => {
const { workspaceId } = props;
const t = useAFFiNEI18N();
const isSelfHosted = useSelfHosted();
const workspace = useWorkspace(workspaceId);
const [name] = useBlockSuiteWorkspaceName(workspace.blockSuiteWorkspace);
@@ -58,11 +56,7 @@ export const WorkspaceSettingDetail = (props: WorkspaceSettingDetailProps) => {
</SettingWrapper>
<SettingWrapper title={t['com.affine.brand.affineCloud']()}>
<PublishPanel workspace={workspace} {...props} />
<MembersPanel
workspace={workspace}
upgradable={!isSelfHosted}
{...props}
/>
<MembersPanel workspace={workspace} {...props} />
</SettingWrapper>
{storageAndExportSetting}
<SettingWrapper>

View File

@@ -29,7 +29,6 @@ import {
useRef,
useState,
} from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { openSettingModalAtom } from '../../../atoms';
import type { CheckedUser } from '../../../hooks/affine/use-current-user';
@@ -39,7 +38,7 @@ import { useMemberCount } from '../../../hooks/affine/use-member-count';
import { type Member, useMembers } from '../../../hooks/affine/use-members';
import { useRevokeMemberPermission } from '../../../hooks/affine/use-revoke-member-permission';
import { useUserSubscription } from '../../../hooks/use-subscription';
import { AnyErrorBoundary } from '../any-error-boundary';
import { AffineErrorBoundary } from '../affine-error-boundary';
import * as style from './style.css';
import type { WorkspaceSettingDetailProps } from './types';
@@ -51,7 +50,6 @@ enum MemberLimitCount {
const COUNT_PER_PAGE = 8;
export interface MembersPanelProps extends WorkspaceSettingDetailProps {
upgradable: boolean;
workspace: AffineOfficialWorkspace;
}
type OnRevoke = (memberId: string) => void;
@@ -71,7 +69,6 @@ const MembersPanelLocal = () => {
export const CloudWorkspaceMembersPanel = ({
workspace,
isOwner,
upgradable,
}: MembersPanelProps) => {
const workspaceId = workspace.id;
const memberCount = useMemberCount(workspaceId);
@@ -167,20 +164,16 @@ export const CloudWorkspaceMembersPanel = ({
planName: plan,
memberLimit,
})}
{upgradable ? (
<>
,
<div className={style.goUpgradeWrapper} onClick={handleUpgrade}>
<span className={style.goUpgrade}>
{t['com.affine.payment.member.description.go-upgrade']()}
</span>
<ArrowRightBigIcon className={style.arrowRight} />
</div>
</>
) : null}
,
<div className={style.goUpgradeWrapper} onClick={handleUpgrade}>
<span className={style.goUpgrade}>
{t['com.affine.payment.member.description.go-upgrade']()}
</span>
<ArrowRightBigIcon className={style.arrowRight} />
</div>
</span>
);
}, [handleUpgrade, memberLimit, plan, t, upgradable]);
}, [handleUpgrade, memberLimit, plan, t]);
return (
<>
@@ -362,10 +355,10 @@ export const MembersPanel = (props: MembersPanelProps): ReactElement | null => {
return <MembersPanelLocal />;
}
return (
<ErrorBoundary FallbackComponent={AnyErrorBoundary}>
<AffineErrorBoundary>
<Suspense>
<CloudWorkspaceMembersPanel {...props} />
</Suspense>
</ErrorBoundary>
</AffineErrorBoundary>
);
};

View File

@@ -1,251 +0,0 @@
import { DebugLogger } from '@affine/debug';
import {
fetchWithTraceReport,
type ListHistoryQuery,
listHistoryQuery,
recoverDocMutation,
} from '@affine/graphql';
import {
useMutateQueryResource,
useMutation,
useQueryInfinite,
} from '@affine/workspace/affine/gql';
import { createAffineCloudBlobEngine } from '@affine/workspace/blob';
import { globalBlockSuiteSchema } from '@affine/workspace/manager';
import { assertEquals } from '@blocksuite/global/utils';
import { Workspace } from '@blocksuite/store';
import { usePageMetaHelper } from '@toeverything/hooks/use-block-suite-page-meta';
import { useBlockSuiteWorkspacePage } from '@toeverything/hooks/use-block-suite-workspace-page';
import { revertUpdate } from '@toeverything/y-indexeddb';
import { useMemo } from 'react';
import useSWRImmutable from 'swr/immutable';
import { applyUpdate } from 'yjs';
const logger = new DebugLogger('page-history');
type DocHistory = ListHistoryQuery['workspace']['histories'][number];
export const usePageSnapshotList = (workspaceId: string, pageDocId: string) => {
const pageSize = 10;
const { data, loadingMore, loadMore } = useQueryInfinite({
query: listHistoryQuery,
getVariables: (_, previousPageData) => {
// use the timestamp of the last history as the cursor
const before = previousPageData?.workspace.histories.at(-1)?.timestamp;
const vars = {
pageDocId: pageDocId,
workspaceId: workspaceId,
before: before,
take: pageSize,
};
return vars;
},
});
const shouldLoadMore = useMemo(() => {
if (!data) {
return false;
}
const lastPage = data.at(-1);
if (!lastPage) {
return false;
}
return lastPage.workspace.histories.length === pageSize;
}, [data]);
const histories = useMemo(() => {
if (!data) {
return [];
}
return data.flatMap(page => page.workspace.histories);
}, [data]);
return [
histories,
shouldLoadMore ? loadMore : undefined,
loadingMore,
] as const;
};
const snapshotFetcher = async (
[workspaceId, pageDocId, ts]: [
workspaceId: string,
pageDocId: string,
ts: string,
] // timestamp is the key to the history/snapshot
) => {
if (!ts) {
return null;
}
const res = await fetchWithTraceReport(
runtimeConfig.serverUrlPrefix +
`/api/workspaces/${workspaceId}/docs/${pageDocId}/histories/${ts}`,
{
priority: 'high',
}
);
if (!res.ok) {
throw new Error('Failed to fetch snapshot');
}
const snapshot = await res.arrayBuffer();
if (!snapshot) {
throw new Error('Invalid snapshot');
}
return snapshot;
};
// attach the Page shown in the modal to a temporary workspace
// so that we do not need to worry about providers etc
// todo: fix references to the page (the referenced page will shown as deleted)
// if we simply clone the current workspace, it maybe time consuming right?
const workspaceMap = new Map<string, Workspace>();
// assume the workspace is a cloud workspace since the history feature is only enabled for cloud workspace
const getOrCreateWorkspace = (workspaceId: string) => {
let workspace = workspaceMap.get(workspaceId);
if (!workspace) {
const blobEngine = createAffineCloudBlobEngine(workspaceId);
workspace = new Workspace({
id: workspaceId,
providerCreators: [],
blobStorages: [
() => ({
crud: {
async get(key) {
return (await blobEngine.get(key)) ?? null;
},
async set(key, value) {
await blobEngine.set(key, value);
return key;
},
async delete(key) {
return blobEngine.delete(key);
},
async list() {
return blobEngine.list();
},
},
}),
],
schema: globalBlockSuiteSchema,
});
workspaceMap.set(workspaceId, workspace);
}
return workspace;
};
// workspace id + page id + timestamp -> snapshot (update binary)
export const usePageHistory = (
workspaceId: string,
pageDocId: string,
ts?: string
) => {
// snapshot should be immutable. so we use swr immutable to disable revalidation
const { data } = useSWRImmutable<ArrayBuffer | null>(
[workspaceId, pageDocId, ts],
{
fetcher: snapshotFetcher,
suspense: false,
}
);
return data ?? undefined;
};
// workspace id + page id + timestamp + snapshot -> Page (to be used for rendering in blocksuite editor)
export const useSnapshotPage = (
workspaceId: string,
pageDocId: string,
ts?: string,
snapshot?: ArrayBuffer
) => {
const page = useMemo(() => {
if (!ts) {
return null;
}
const pageId = pageDocId + '-' + ts;
const historyShellWorkspace = getOrCreateWorkspace(workspaceId);
let page = historyShellWorkspace.getPage(pageId);
if (!page && snapshot) {
page = historyShellWorkspace.createPage({
id: pageId,
});
page.awarenessStore.setReadonly(page, true);
const spaceDoc = page.spaceDoc;
page
.load(() => applyUpdate(spaceDoc, new Uint8Array(snapshot)))
.catch(console.error); // must load before applyUpdate
}
return page;
}, [pageDocId, snapshot, ts, workspaceId]);
return page;
};
export const historyListGroupByDay = (histories: DocHistory[]) => {
const map = new Map<string, DocHistory[]>();
for (const history of histories) {
const day = new Date(history.timestamp).toLocaleDateString();
const list = map.get(day) ?? [];
list.push(history);
map.set(day, list);
}
return [...map.entries()];
};
export const useRestorePage = (workspace: Workspace, pageId: string) => {
const page = useBlockSuiteWorkspacePage(workspace, pageId);
const mutateQueryResource = useMutateQueryResource();
const { trigger: recover, isMutating } = useMutation({
mutation: recoverDocMutation,
});
const { getPageMeta, setPageTitle } = usePageMetaHelper(workspace);
const onRestore = useMemo(() => {
return async (version: string, update: Uint8Array) => {
if (!page) {
return;
}
const pageDocId = page.spaceDoc.guid;
revertUpdate(page.spaceDoc, update, key => {
assertEquals(key, 'blocks'); // only expect this value is 'blocks'
return 'Map';
});
// should also update the page title, since it may be changed in the history
const title = page.meta.title;
if (getPageMeta(pageDocId)?.title !== title) {
setPageTitle(pageDocId, title);
}
await recover({
docId: pageDocId,
timestamp: version,
workspaceId: workspace.id,
});
await mutateQueryResource(listHistoryQuery, vars => {
return (
vars.pageDocId === pageDocId && vars.workspaceId === workspace.id
);
});
logger.info('Page restored', pageDocId, version);
};
}, [
getPageMeta,
mutateQueryResource,
page,
recover,
setPageTitle,
workspace.id,
]);
return {
onRestore,
isMutating,
};
};

View File

@@ -1,119 +0,0 @@
export const EmptyHistoryShape = () => (
<svg
width="200"
height="174"
viewBox="0 0 200 174"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect
x="51.724"
y="38.4615"
width="96.5517"
height="96.5517"
stroke="var(--affine-text-disable-color)"
/>
<path
d="M51.8339 86.7374L99.9999 38.5714L148.166 86.7374L99.9999 134.903L51.8339 86.7374Z"
stroke="var(--affine-text-disable-color)"
/>
<path
d="M99.6052 38.1966C107.662 33.4757 117.043 30.7695 127.055 30.7695C157.087 30.7695 181.432 55.1148 181.432 85.1462C181.432 107.547 167.887 126.783 148.541 135.113"
stroke="var(--affine-text-disable-color)"
/>
<path
d="M148.375 86.4722C153.096 94.5291 155.802 103.91 155.802 113.922C155.802 143.954 131.457 168.299 101.426 168.299C79.0254 168.299 59.7886 154.754 51.4587 135.408"
stroke="var(--affine-text-disable-color)"
/>
<path
d="M100.394 135.113C92.3373 139.834 82.9568 142.54 72.9441 142.54C42.9127 142.54 18.5675 118.195 18.5675 88.1634C18.5675 65.763 32.1124 46.5261 51.4587 38.1963"
stroke="var(--affine-text-disable-color)"
/>
<path
d="M51.4585 87.1318C46.7377 79.0749 44.0315 69.6943 44.0315 59.6817C44.0315 29.6503 68.3767 5.30503 98.4081 5.30503C120.809 5.30503 140.045 18.8499 148.375 38.1963"
stroke="var(--affine-text-disable-color)"
/>
<path
d="M51.4587 38.1963L148.541 135.279"
stroke="var(--affine-text-disable-color)"
/>
<path
d="M148.541 38.1963L51.4587 135.279"
stroke="var(--affine-text-disable-color)"
/>
<path
d="M99.9998 38.1963V135.279"
stroke="var(--affine-text-disable-color)"
/>
<path
d="M148.541 86.7378L51.4588 86.7378"
stroke="var(--affine-text-disable-color)"
/>
<ellipse
cx="148.275"
cy="38.4617"
rx="3.97878"
ry="3.97878"
fill="var(--affine-text-primary-color)"
/>
<ellipse
cx="148.275"
cy="135.013"
rx="3.97878"
ry="3.97878"
fill="var(--affine-text-primary-color)"
/>
<ellipse
cx="148.275"
cy="86.7376"
rx="3.97878"
ry="3.97878"
fill="var(--affine-text-primary-color)"
/>
<ellipse
cx="51.7241"
cy="38.4617"
rx="3.97878"
ry="3.97878"
fill="var(--affine-text-primary-color)"
/>
<ellipse
cx="51.7241"
cy="135.013"
rx="3.97878"
ry="3.97878"
fill="var(--affine-text-primary-color)"
/>
<ellipse
cx="51.7241"
cy="86.7376"
rx="3.97878"
ry="3.97878"
fill="var(--affine-text-primary-color)"
/>
<ellipse
cx="100"
cy="38.4617"
rx="3.97878"
ry="3.97878"
transform="rotate(-90 100 38.4617)"
fill="var(--affine-text-primary-color)"
/>
<ellipse
cx="100"
cy="86.2073"
rx="3.97878"
ry="3.97878"
transform="rotate(-90 100 86.2073)"
fill="var(--affine-text-primary-color)"
/>
<ellipse
cx="100"
cy="135.013"
rx="3.97878"
ry="3.97878"
transform="rotate(-90 100 135.013)"
fill="var(--affine-text-primary-color)"
/>
</svg>
);

View File

@@ -1,446 +0,0 @@
import { Scrollable } from '@affine/component';
import {
BlockSuiteEditor,
BlockSuiteFallback,
} from '@affine/component/block-suite-editor';
import type { PageMode } from '@affine/core/atoms';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { Workspace } from '@blocksuite/store';
import type { DialogContentProps } from '@radix-ui/react-dialog';
import { Button } from '@toeverything/components/button';
import { ConfirmModal, Modal } from '@toeverything/components/modal';
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
import { useAtom, useAtomValue } from 'jotai';
import {
type PropsWithChildren,
Suspense,
useCallback,
useLayoutEffect,
useMemo,
useState,
} from 'react';
import { currentModeAtom } from '../../../atoms/mode';
import { pageHistoryModalAtom } from '../../../atoms/page-history';
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
import { StyledEditorModeSwitch } from '../../blocksuite/block-suite-mode-switch/style';
import {
EdgelessSwitchItem,
PageSwitchItem,
} from '../../blocksuite/block-suite-mode-switch/switch-items';
import { AffineErrorBoundary } from '../affine-error-boundary';
import {
historyListGroupByDay,
usePageHistory,
usePageSnapshotList,
useRestorePage,
useSnapshotPage,
} from './data';
import { EmptyHistoryShape } from './empty-history-shape';
import * as styles from './styles.css';
export interface PageHistoryModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
workspace: Workspace;
pageId: string;
}
const contentOptions: DialogContentProps = {
['data-testid' as string]: 'page-history-modal',
onPointerDownOutside: e => {
e.preventDefault();
},
style: {
padding: 0,
maxWidth: 944,
backgroundColor: 'var(--affine-background-primary-color)',
overflow: 'hidden',
},
};
const ModalContainer = ({
onOpenChange,
open,
children,
}: PropsWithChildren<{
open: boolean;
onOpenChange: (open: boolean) => void;
}>) => {
return (
<Modal
open={open}
onOpenChange={onOpenChange}
width="calc(100% - 64px)"
height="80%"
withoutCloseButton
contentOptions={contentOptions}
>
<AffineErrorBoundary>{children}</AffineErrorBoundary>
</Modal>
);
};
const localTimeFormatter = new Intl.DateTimeFormat('en', {
timeStyle: 'short',
});
const timestampToLocalTime = (ts: string) => {
return localTimeFormatter.format(new Date(ts));
};
interface HistoryEditorPreviewProps {
workspaceId: string;
pageDocId: string;
ts?: string;
snapshot?: ArrayBuffer;
mode: PageMode;
onModeChange: (mode: PageMode) => void;
title: string;
}
const HistoryEditorPreview = ({
ts,
snapshot,
onModeChange,
mode,
workspaceId,
pageDocId,
title,
}: HistoryEditorPreviewProps) => {
const onSwitchToPageMode = useCallback(() => {
onModeChange('page');
}, [onModeChange]);
const onSwitchToEdgelessMode = useCallback(() => {
onModeChange('edgeless');
}, [onModeChange]);
const page = useSnapshotPage(workspaceId, pageDocId, ts, snapshot);
return (
<div className={styles.previewWrapper}>
<div className={styles.previewHeader}>
<StyledEditorModeSwitch switchLeft={mode === 'page'}>
<PageSwitchItem
data-testid="switch-page-mode-button"
active={mode === 'page'}
onClick={onSwitchToPageMode}
/>
<EdgelessSwitchItem
data-testid="switch-edgeless-mode-button"
active={mode === 'edgeless'}
onClick={onSwitchToEdgelessMode}
/>
</StyledEditorModeSwitch>
<div className={styles.previewHeaderTitle}>{title}</div>
<div className={styles.previewHeaderTimestamp}>
{ts ? timestampToLocalTime(ts) : null}
</div>
</div>
{page ? (
<BlockSuiteEditor
className={styles.editor}
mode={mode}
page={page}
onModeChange={onModeChange}
/>
) : (
<BlockSuiteFallback />
)}
</div>
);
};
const PageHistoryList = ({
pageDocId,
workspaceId,
activeVersion,
onVersionChange,
}: {
workspaceId: string;
pageDocId: string;
activeVersion?: string;
onVersionChange: (version: string) => void;
}) => {
const [historyList, loadMore, loadingMore] = usePageSnapshotList(
workspaceId,
pageDocId
);
const historyListByDay = useMemo(() => {
return historyListGroupByDay(historyList);
}, [historyList]);
const t = useAFFiNEI18N();
useLayoutEffect(() => {
if (historyList.length > 0 && !activeVersion) {
onVersionChange(historyList[0].timestamp);
}
}, [activeVersion, historyList, onVersionChange]);
return (
<div className={styles.historyList}>
<div className={styles.historyListHeader}>
{t['com.affine.history.version-history']()}
</div>
<Scrollable.Root className={styles.historyListScrollable}>
<Scrollable.Viewport className={styles.historyListScrollableInner}>
{historyListByDay.map(([day, list]) => {
return (
<div key={day} className={styles.historyItemGroup}>
<div className={styles.historyItemGroupTitle}>{day}</div>
{list.map(history => (
<div
className={styles.historyItem}
key={history.timestamp}
data-testid="version-history-item"
onClick={e => {
e.stopPropagation();
onVersionChange(history.timestamp);
}}
data-active={activeVersion === history.timestamp}
>
<button>{timestampToLocalTime(history.timestamp)}</button>
</div>
))}
</div>
);
})}
{loadMore ? (
<Button
type="plain"
loading={loadingMore}
disabled={loadingMore}
className={styles.historyItemLoadMore}
onClick={loadMore}
>
Load More
</Button>
) : null}
</Scrollable.Viewport>
<Scrollable.Scrollbar />
</Scrollable.Root>
</div>
);
};
interface ConfirmRestoreModalProps {
open: boolean;
onConfirm: (res: boolean) => void;
isMutating: boolean;
}
const ConfirmRestoreModal = ({
isMutating,
open,
onConfirm,
}: ConfirmRestoreModalProps) => {
const t = useAFFiNEI18N();
const handleConfirm = useCallback(() => {
onConfirm(true);
}, [onConfirm]);
const handleCancel = useCallback(() => {
onConfirm(false);
}, [onConfirm]);
return (
<ConfirmModal
open={open}
onOpenChange={handleCancel}
title={t['com.affine.history.restore-current-version']()}
description={t['com.affine.history.confirm-restore-modal.hint']()}
cancelText={t['Cancel']()}
contentOptions={{
['data-testid' as string]: 'confirm-restore-history-modal',
style: {
padding: '20px 26px',
},
}}
confirmButtonOptions={{
loading: isMutating,
type: 'primary',
['data-testid' as string]: 'confirm-restore-history-button',
children: t['com.affine.history.confirm-restore-modal.restore'](),
}}
onConfirm={handleConfirm}
></ConfirmModal>
);
};
const EmptyHistoryPrompt = () => {
const t = useAFFiNEI18N();
return (
<div
className={styles.emptyHistoryPrompt}
data-testid="empty-history-prompt"
>
<EmptyHistoryShape />
<div className={styles.emptyHistoryPromptTitle}>
{t['com.affine.history.empty-prompt.title']()}
</div>
<div className={styles.emptyHistoryPromptDescription}>
{t['com.affine.history.empty-prompt.description']()}
</div>
</div>
);
};
const PageHistoryManager = ({
workspace,
pageId,
onClose,
}: {
workspace: Workspace;
pageId: string;
onClose: () => void;
}) => {
const workspaceId = workspace.id;
const [activeVersion, setActiveVersion] = useState<string>();
const pageDocId = useMemo(() => {
return workspace.getPage(pageId)?.spaceDoc.guid ?? pageId;
}, [pageId, workspace]);
const snapshot = usePageHistory(workspaceId, pageDocId, activeVersion);
const t = useAFFiNEI18N();
const { onRestore, isMutating } = useRestorePage(workspace, pageId);
const handleRestore = useMemo(
() => async () => {
if (!activeVersion || !snapshot) {
return;
}
await onRestore(activeVersion, new Uint8Array(snapshot));
// close the modal after restore
onClose();
},
[activeVersion, onClose, onRestore, snapshot]
);
const defaultPreviewPageMode = useAtomValue(currentModeAtom);
const [mode, setMode] = useState<PageMode>(defaultPreviewPageMode);
const title = useMemo(
() => workspace.getPage(pageId)?.meta.title || t['Untitled'](),
[pageId, t, workspace]
);
const [showRestoreConfirmModal, setShowRestoreConfirmModal] = useState(false);
const showRestoreConfirm = useCallback(() => {
setShowRestoreConfirmModal(true);
}, []);
const onConfirmRestore = useAsyncCallback(
async res => {
if (res) {
await handleRestore();
}
setShowRestoreConfirmModal(false);
},
[handleRestore]
);
return (
<div className={styles.root}>
<div className={styles.modalContent} data-empty={!activeVersion}>
<HistoryEditorPreview
workspaceId={workspaceId}
pageDocId={pageDocId}
ts={activeVersion}
snapshot={snapshot}
mode={mode}
onModeChange={setMode}
title={title}
/>
<PageHistoryList
workspaceId={workspaceId}
pageDocId={pageDocId}
activeVersion={activeVersion}
onVersionChange={setActiveVersion}
/>
</div>
{!activeVersion ? (
<div className={styles.modalContent}>
<EmptyHistoryPrompt />
</div>
) : null}
<div className={styles.historyFooter}>
<Button type="plain" onClick={onClose}>
{t['com.affine.history.back-to-page']()}
</Button>
<div className={styles.spacer} />
<Button
type="primary"
onClick={showRestoreConfirm}
disabled={isMutating || !activeVersion}
>
{t['com.affine.history.restore-current-version']()}
</Button>
</div>
<ConfirmRestoreModal
open={showRestoreConfirmModal}
isMutating={isMutating}
onConfirm={onConfirmRestore}
/>
</div>
);
};
export const PageHistoryModal = ({
onOpenChange,
open,
pageId,
workspace,
}: PageHistoryModalProps) => {
const onClose = useCallback(() => {
onOpenChange(false);
}, [onOpenChange]);
return (
<ModalContainer onOpenChange={onOpenChange} open={open}>
<Suspense fallback={<BlockSuiteFallback />}>
<PageHistoryManager
onClose={onClose}
pageId={pageId}
workspace={workspace}
/>
</Suspense>
</ModalContainer>
);
};
export const GlobalPageHistoryModal = () => {
const [{ open, pageId }, setState] = useAtom(pageHistoryModalAtom);
const [workspace] = useCurrentWorkspace();
const handleOpenChange = useCallback(
(open: boolean) => {
setState(prev => ({
...prev,
open,
}));
},
[setState]
);
return (
<PageHistoryModal
open={open}
onOpenChange={handleOpenChange}
pageId={pageId}
workspace={workspace.blockSuiteWorkspace}
/>
);
};

View File

@@ -1 +0,0 @@
export * from './history-modal';

View File

@@ -1,185 +0,0 @@
import { createVar, globalStyle, style } from '@vanilla-extract/css';
const headerHeight = createVar('header-height');
const footerHeight = createVar('footer-height');
const historyListWidth = createVar('history-list-width');
export const root = style({
height: '100%',
width: '100%',
vars: {
[headerHeight]: '52px',
[footerHeight]: '68px',
[historyListWidth]: '160px',
},
});
export const modalContent = style({
display: 'flex',
height: `calc(100% - ${footerHeight})`,
width: '100%',
position: 'absolute',
selectors: {
'&[data-empty="true"]': {
opacity: 0,
pointerEvents: 'none',
},
},
});
export const previewWrapper = style({
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
height: '100%',
width: `calc(100% - ${historyListWidth})`,
backgroundColor: 'var(--affine-background-secondary-color)',
});
export const previewHeader = style({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: headerHeight,
borderBottom: '1px solid var(--affine-border-color)',
backgroundColor: 'var(--affine-background-primary-color)',
padding: '0 12px',
flexShrink: 0,
gap: 12,
});
export const previewHeaderTitle = style({
fontSize: 'var(--affine-font-xs)',
fontWeight: 600,
maxWidth: 400, // better responsiveness
});
export const previewHeaderTimestamp = style({
color: 'var(--affine-text-secondary-color)',
backgroundColor: 'var(--affine-background-secondary-color)',
padding: '0 10px',
borderRadius: 4,
fontSize: 'var(--affine-font-xs)',
});
export const editor = style({
height: '100%',
flexGrow: 1,
overflow: 'hidden',
});
export const historyList = style({
overflow: 'hidden',
height: '100%',
width: historyListWidth,
flexShrink: 0,
borderLeft: '1px solid var(--affine-border-color)',
});
export const historyListScrollable = style({
height: `calc(100% - ${headerHeight})`,
});
export const historyListScrollableInner = style({
display: 'flex',
gap: 16,
flexDirection: 'column',
});
export const historyListHeader = style({
display: 'flex',
alignItems: 'center',
height: 52,
borderBottom: '1px solid var(--affine-border-color)',
fontWeight: 'bold',
flexShrink: 0,
padding: '0 12px',
});
export const historyItemGroup = style({
display: 'flex',
flexDirection: 'column',
rowGap: 6,
});
export const historyItemGroupTitle = style({
display: 'flex',
alignItems: 'center',
padding: '12px',
fontWeight: 'bold',
backgroundColor: 'var(--affine-background-primary-color)',
position: 'sticky',
top: 0,
});
export const historyItem = style({
display: 'flex',
alignItems: 'center',
padding: '0 12px',
height: 32,
cursor: 'pointer',
selectors: {
'&:hover, &[data-active=true]': {
backgroundColor: 'var(--affine-hover-color)',
},
},
});
export const historyItemLoadMore = style([
historyItem,
{
cursor: 'pointer',
color: 'var(--affine-text-secondary-color)',
flexShrink: 0,
borderRadius: 0,
selectors: {
'&:hover': {
backgroundColor: 'var(--affine-hover-color)',
},
},
},
]);
globalStyle(`${historyItem} button`, {
color: 'inherit',
});
export const historyFooter = style({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: 68,
borderTop: '1px solid var(--affine-border-color)',
padding: '0 24px',
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
});
export const spacer = style({
flexGrow: 1,
});
export const emptyHistoryPrompt = style({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
height: '100%',
width: '100%',
zIndex: 1,
gap: 20,
});
export const emptyHistoryPromptTitle = style({
fontWeight: 600,
fontSize: 'var(--affine-font-h-5)',
});
export const emptyHistoryPromptDescription = style({
width: 320,
textAlign: 'center',
fontSize: 'var(--affine-font-xs)',
color: 'var(--affine-text-secondary-color)',
});

View File

@@ -35,7 +35,6 @@ import {
openSignOutModalAtom,
} from '../../../../atoms';
import { useCurrentUser } from '../../../../hooks/affine/use-current-user';
import { useSelfHosted } from '../../../../hooks/affine/use-server-flavor';
import { useUserSubscription } from '../../../../hooks/use-subscription';
import { Upload } from '../../../pure/file-upload';
import * as style from './style.css';
@@ -168,7 +167,6 @@ export const AvatarAndName = () => {
const StoragePanel = () => {
const t = useAFFiNEI18N();
const isSelfHosted = useSelfHosted();
const { data } = useQuery({
query: allBlobSizesQuery,
@@ -177,7 +175,6 @@ const StoragePanel = () => {
const [subscription] = useUserSubscription();
const plan = subscription?.plan ?? SubscriptionPlan.Free;
// TODO(@JimmFly): get limit from user usage query directly after #4720 is merged
const maxLimit = useMemo(() => {
return bytes.parse(plan === SubscriptionPlan.Free ? '10GB' : '100GB');
}, [plan]);
@@ -202,7 +199,6 @@ const StoragePanel = () => {
plan={plan}
value={data.collectAllBlobSizes.size}
onUpgrade={onUpgrade}
upgradable={!isSelfHosted}
/>
</SettingRow>
);

View File

@@ -8,7 +8,6 @@ import {
import type { ReactElement, SVGProps } from 'react';
import { useCurrentLoginStatus } from '../../../../hooks/affine/use-current-login-status';
import { useSelfHosted } from '../../../../hooks/affine/use-server-flavor';
import { AboutAffine } from './about';
import { AppearanceSettings } from './appearance';
import { BillingSettings } from './billing';
@@ -37,7 +36,6 @@ export type GeneralSettingList = GeneralSettingListItem[];
export const useGeneralSettingList = (): GeneralSettingList => {
const t = useAFFiNEI18N();
const status = useCurrentLoginStatus();
const isSelfHosted = useSelfHosted();
const settings: GeneralSettingListItem[] = [
{
@@ -52,6 +50,13 @@ export const useGeneralSettingList = (): GeneralSettingList => {
icon: KeyboardIcon,
testId: 'shortcuts-panel-trigger',
},
{
key: 'plans',
title: t['com.affine.payment.title'](),
icon: UpgradeIcon,
testId: 'plans-panel-trigger',
},
{
key: 'plugins',
title: 'Plugins',
@@ -66,21 +71,13 @@ export const useGeneralSettingList = (): GeneralSettingList => {
},
];
if (!isSelfHosted) {
if (status === 'authenticated') {
settings.splice(3, 0, {
key: 'plans',
title: t['com.affine.payment.title'](),
icon: UpgradeIcon,
testId: 'plans-panel-trigger',
key: 'billing',
title: t['com.affine.payment.billing-setting.title'](),
icon: PaymentIcon,
testId: 'billing-panel-trigger',
});
if (status === 'authenticated') {
settings.splice(3, 0, {
key: 'billing',
title: t['com.affine.payment.billing-setting.title'](),
icon: PaymentIcon,
testId: 'billing-panel-trigger',
});
}
}
return settings;

View File

@@ -1,6 +1,5 @@
import { FlexWrapper } from '@affine/component';
import { Export, MoveToTrash } from '@affine/component/page-list';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { assertExists } from '@blocksuite/global/utils';
import {
@@ -9,7 +8,6 @@ import {
EditIcon,
FavoritedIcon,
FavoriteIcon,
HistoryIcon,
ImportIcon,
PageIcon,
} from '@blocksuite/icons';
@@ -27,7 +25,7 @@ import {
} from '@toeverything/hooks/use-block-suite-page-meta';
import { useBlockSuiteWorkspaceHelper } from '@toeverything/hooks/use-block-suite-workspace-helper';
import { useAtomValue, useSetAtom } from 'jotai';
import { useCallback, useRef, useState } from 'react';
import { useCallback, useRef } from 'react';
import { applyUpdate, encodeStateAsUpdate } from 'yjs';
import { setPageModeAtom } from '../../../atoms';
@@ -38,7 +36,6 @@ import { useTrashModalHelper } from '../../../hooks/affine/use-trash-modal-helpe
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
import { toast } from '../../../utils';
import { PageHistoryModal } from '../../affine/page-history-modal/history-modal';
import { HeaderDropDownButton } from '../../pure/header-drop-down-button';
import { usePageHelper } from '../block-suite-page-list/utils';
@@ -71,12 +68,6 @@ export const PageMenu = ({ rename, pageId }: PageMenuProps) => {
const { createPage } = useBlockSuiteWorkspaceHelper(blockSuiteWorkspace);
const { setTrashModal } = useTrashModalHelper(blockSuiteWorkspace);
const [historyModalOpen, setHistoryModalOpen] = useState(false);
const openHistoryModal = useCallback(() => {
setHistoryModalOpen(true);
}, []);
const handleOpenTrashModal = useCallback(() => {
setTrashModal({
open: true,
@@ -216,23 +207,6 @@ export const PageMenu = ({ rename, pageId }: PageMenuProps) => {
>
{t['Import']()}
</MenuItem>
{workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD &&
runtimeConfig.enablePageHistory ? (
<MenuItem
preFix={
<MenuIcon>
<HistoryIcon />
</MenuIcon>
}
data-testid="editor-option-menu-history"
onSelect={openHistoryModal}
style={menuItemStyle}
>
{t['com.affine.history.view-history-version']()}
</MenuItem>
) : null}
<Export exportHandler={exportHandler} />
<MenuSeparator />
<MoveToTrash
@@ -254,14 +228,6 @@ export const PageMenu = ({ rename, pageId }: PageMenuProps) => {
>
<HeaderDropDownButton />
</Menu>
{workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD ? (
<PageHistoryModal
workspace={workspace.blockSuiteWorkspace}
open={historyModalOpen}
pageId={pageId}
onOpenChange={setHistoryModalOpen}
/>
) : null}
</FlexWrapper>
);
};

View File

@@ -30,8 +30,8 @@ import { useAppSettingHelper } from '../../hooks/affine/use-app-setting-helper';
import { useDeleteCollectionInfo } from '../../hooks/affine/use-delete-collection-info';
import { useGeneralShortcuts } from '../../hooks/affine/use-shortcuts';
import { useTrashModalHelper } from '../../hooks/affine/use-trash-modal-helper';
import { useRegisterBrowserHistoryCommands } from '../../hooks/use-browser-history-commands';
import { useNavigateHelper } from '../../hooks/use-navigate-helper';
import { useRegisterBlocksuiteEditorCommands } from '../../hooks/use-shortcut-commands';
import type { AllWorkspace } from '../../shared';
import { CollectionsList } from '../pure/workspace-slider-bar/collections';
import { AddCollectionButton } from '../pure/workspace-slider-bar/collections/add-collection-button';
@@ -169,7 +169,7 @@ export const RootAppSidebar = ({
const closeUserWorkspaceList = useCallback(() => {
setOpenUserWorkspaceList(false);
}, [setOpenUserWorkspaceList]);
useRegisterBrowserHistoryCommands(router.back, router.forward);
useRegisterBlocksuiteEditorCommands(router.back, router.forward);
const userInfo = useDeleteCollectionInfo();
return (
<AppSidebar

View File

@@ -19,6 +19,7 @@ export const upgradeTips = style({
fontStyle: 'normal',
fontWeight: '400',
lineHeight: '20px',
textAlign: 'center',
});
const rotate = keyframes({

View File

@@ -44,7 +44,7 @@ interface WorkspaceUpgradeProps {
export const WorkspaceUpgrade = function WorkspaceUpgrade(
props: WorkspaceUpgradeProps
) {
const [upgradeState, , upgradeWorkspace, newWorkspaceId] =
const [upgradeState, error, upgradeWorkspace, newWorkspaceId] =
useUpgradeWorkspace(props.migration);
const t = useAFFiNEI18N();
@@ -75,7 +75,7 @@ export const WorkspaceUpgrade = function WorkspaceUpgrade(
<div className={styles.upgradeBox}>
<AffineShapeIcon width={180} height={180} />
<p className={styles.upgradeTips}>
{t[UPGRADE_TIPS_KEYS[upgradeState]]()}
{error ? error.message : t[UPGRADE_TIPS_KEYS[upgradeState]]()}
</p>
<Button
data-testid="upgrade-workspace-button"

View File

@@ -4,7 +4,6 @@ import { useSWRConfig } from 'swr';
export function useMutateCloud() {
const { mutate } = useSWRConfig();
return useCallback(async () => {
// todo: should not mutate all graphql cache
return mutate(key => {
if (Array.isArray(key)) {
return key[0] === 'cloud';

View File

@@ -1,29 +1,25 @@
import { toast } from '@affine/component';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { assertExists } from '@blocksuite/global/utils';
import { EdgelessIcon, HistoryIcon, PageIcon } from '@blocksuite/icons';
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
import type { Workspace } from '@blocksuite/store';
import { usePageMetaHelper } from '@toeverything/hooks/use-block-suite-page-meta';
import {
PreconditionStrategy,
registerAffineCommand,
} from '@toeverything/infra/command';
import { useSetAtom } from 'jotai';
import { useCallback, useEffect } from 'react';
import { pageHistoryModalAtom } from '../../atoms/page-history';
import { useCurrentWorkspace } from '../current/use-current-workspace';
import { useBlockSuiteMetaHelper } from './use-block-suite-meta-helper';
import { useExportPage } from './use-export-page';
import { useTrashModalHelper } from './use-trash-modal-helper';
export function useRegisterBlocksuiteEditorCommands(
blockSuiteWorkspace: Workspace,
pageId: string,
mode: 'page' | 'edgeless'
) {
const t = useAFFiNEI18N();
const [workspace] = useCurrentWorkspace();
const blockSuiteWorkspace = workspace.blockSuiteWorkspace;
const { getPageMeta } = usePageMetaHelper(blockSuiteWorkspace);
const currentPage = blockSuiteWorkspace.getPage(pageId);
assertExists(currentPage);
@@ -32,15 +28,6 @@ export function useRegisterBlocksuiteEditorCommands(
const favorite = pageMeta.favorite ?? false;
const trash = pageMeta.trash ?? false;
const setPageHistoryModalState = useSetAtom(pageHistoryModalAtom);
const openHistoryModal = useCallback(() => {
setPageHistoryModalState(() => ({
pageId,
open: true,
}));
}, [pageId, setPageHistoryModalState]);
const { togglePageMode, toggleFavorite, restoreFromTrash } =
useBlockSuiteMetaHelper(blockSuiteWorkspace);
const exportHandler = useExportPage(currentPage);
@@ -53,14 +40,12 @@ export function useRegisterBlocksuiteEditorCommands(
});
}, [pageId, pageMeta.title, setTrashModal]);
const isCloudWorkspace = workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD;
useEffect(() => {
const unsubs: Array<() => void> = [];
const preconditionStrategy = () =>
PreconditionStrategy.InPaperOrEdgeless && !trash;
// TODO: add back when edgeless presentation is ready
//TODO: add back when edgeless presentation is ready
// this is pretty hack and easy to break. need a better way to communicate with blocksuite editor
// unsubs.push(
@@ -204,20 +189,6 @@ export function useRegisterBlocksuiteEditorCommands(
})
);
if (runtimeConfig.enablePageHistory && isCloudWorkspace) {
unsubs.push(
registerAffineCommand({
id: `editor:${mode}-page-history`,
category: `editor:${mode}`,
icon: <HistoryIcon />,
label: t['com.affine.cmdk.affine.editor.reveal-page-history-modal'](),
run() {
openHistoryModal();
},
})
);
}
return () => {
unsubs.forEach(unsub => unsub());
};
@@ -227,12 +198,11 @@ export function useRegisterBlocksuiteEditorCommands(
onClickDelete,
exportHandler,
pageId,
pageMeta.title,
restoreFromTrash,
t,
toggleFavorite,
togglePageMode,
trash,
isCloudWorkspace,
openHistoryModal,
]);
}

View File

@@ -1,34 +0,0 @@
import { serverConfigQuery } from '@affine/graphql';
import { useQuery } from '@affine/workspace/affine/gql';
import type { BareFetcher, Middleware } from 'swr';
const wrappedFetcher = (fetcher: BareFetcher<any> | null, ...args: any[]) =>
fetcher?.(...args).catch(() => null);
const errorHandler: Middleware = useSWRNext => (key, fetcher, config) => {
return useSWRNext(key, wrappedFetcher.bind(null, fetcher), config);
};
export const useServerFlavor = () => {
const { data: config, error } = useQuery(
{ query: serverConfigQuery },
{
use: [errorHandler],
revalidateOnFocus: false,
revalidateOnMount: false,
revalidateIfStale: false,
}
);
if (error || !config) {
return 'local';
}
return config.serverConfig.flavor;
};
export const useSelfHosted = () => {
const serverFlavor = useServerFlavor();
return ['local', 'selfhosted'].includes(serverFlavor);
};

View File

@@ -4,7 +4,7 @@ import {
} from '@toeverything/infra/command';
import { useEffect } from 'react';
export function useRegisterBrowserHistoryCommands(
export function useRegisterBlocksuiteEditorCommands(
back: () => unknown,
forward: () => unknown
) {

View File

@@ -2,8 +2,6 @@ import { type SubscriptionQuery, subscriptionQuery } from '@affine/graphql';
import { useQuery } from '@affine/workspace/affine/gql';
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
import { useSelfHosted } from './affine/use-server-flavor';
export type Subscription = NonNullable<
NonNullable<SubscriptionQuery['currentUser']>['subscription']
>;
@@ -14,7 +12,6 @@ const selector = (data: SubscriptionQuery) =>
data.currentUser?.subscription ?? null;
export const useUserSubscription = () => {
const isSelfHosted = useSelfHosted();
const { data, mutate } = useQuery({
query: subscriptionQuery,
});
@@ -39,9 +36,5 @@ export const useUserSubscription = () => {
[mutate]
);
if (isSelfHosted) {
return [selector(data), () => {}] as const;
}
return [selector(data), set] as const;
};

View File

@@ -24,7 +24,6 @@ import { setPageModeAtom } from '../../atoms';
import { collectionsCRUDAtom } from '../../atoms/collections';
import { currentModeAtom } from '../../atoms/mode';
import { AffineErrorBoundary } from '../../components/affine/affine-error-boundary';
import { GlobalPageHistoryModal } from '../../components/affine/page-history-modal';
import { WorkspaceHeader } from '../../components/workspace-header';
import { useRegisterBlocksuiteEditorCommands } from '../../hooks/affine/use-register-blocksuite-editor-commands';
import { useCurrentSyncEngineStatus } from '../../hooks/current/use-current-sync-engine';
@@ -42,7 +41,7 @@ const DetailPageImpl = (): ReactElement => {
const { setTemporaryFilter } = useCollectionManager(collectionsCRUDAtom);
const mode = useAtomValue(currentModeAtom);
const setPageMode = useSetAtom(setPageModeAtom);
useRegisterBlocksuiteEditorCommands(currentPageId, mode);
useRegisterBlocksuiteEditorCommands(blockSuiteWorkspace, currentPageId, mode);
const onLoad = useCallback(
(page: Page, editor: EditorContainer) => {
try {
@@ -102,8 +101,6 @@ const DetailPageImpl = (): ReactElement => {
currentPageId={currentPageId}
onLoadEditor={onLoad}
/>
<GlobalPageHistoryModal />
</>
);
};
@@ -113,7 +110,6 @@ export const DetailPage = (): ReactElement => {
const currentSyncEngineStatus = useCurrentSyncEngineStatus();
const currentPageId = useAtomValue(currentPageIdAtom);
const [page, setPage] = useState<Page | null>(null);
const [pageLoaded, setPageLoaded] = useState<boolean>(false);
// load page by current page id
useEffect(() => {
@@ -158,30 +154,7 @@ export const DetailPage = (): ReactElement => {
return;
}, [currentSyncEngineStatus, navigate, page]);
// wait for page to be loaded
useEffect(() => {
if (page) {
if (!page.isEmpty) {
setPageLoaded(true);
} else {
setPageLoaded(false);
// call waitForLoaded to trigger load
page
.load(() => {})
.catch(() => {
// do nothing
});
return page.slots.ready.on(() => {
setPageLoaded(true);
}).dispose;
}
} else {
setPageLoaded(false);
}
return;
}, [page]);
if (!currentPageId || !page || !pageLoaded) {
if (!currentPageId || !page) {
return <PageDetailSkeleton key="current-page-is-null" />;
}
@@ -191,7 +164,8 @@ export const DetailPage = (): ReactElement => {
});
}
return <DetailPageImpl />;
// Add a key to force rerender when page changed, to avoid some lifecycle issues.
return <DetailPageImpl key={currentPageId} />;
};
export const loader: LoaderFunction = async () => {

View File

@@ -8,6 +8,7 @@ import {
import type { MigrationPoint } from '@toeverything/infra/blocksuite';
import {
checkWorkspaceCompatibility,
fixWorkspaceVersion,
guidCompatibilityFix,
} from '@toeverything/infra/blocksuite';
import { useSetAtom } from 'jotai';
@@ -54,6 +55,7 @@ export const loader: LoaderFunction = async args => {
workspaceLoaderLogger.info('workspace loaded');
guidCompatibilityFix(workspace.doc);
fixWorkspaceVersion(workspace.doc);
return checkWorkspaceCompatibility(workspace);
};

View File

@@ -1,5 +1,6 @@
import * as Sentry from '@sentry/react';
import type { RouteObject } from 'react-router-dom';
import { createBrowserRouter } from 'react-router-dom';
import { createBrowserRouter as reactRouterCreateBrowserRouter } from 'react-router-dom';
export const routes = [
{
@@ -70,6 +71,9 @@ export const routes = [
},
] satisfies [RouteObject, ...RouteObject[]];
const createBrowserRouter = Sentry.wrapCreateBrowserRouter(
reactRouterCreateBrowserRouter
);
export const router = createBrowserRouter(routes, {
future: {
v7_normalizeFormMethod: true,

View File

@@ -5,7 +5,7 @@ export abstract class RecoverableError extends Error {
return this.ttl > 0;
}
abstract retry(): void;
abstract retry(): void | Promise<void>;
}
// the first session request failed after login or signup succeed.
@@ -24,8 +24,6 @@ export class SessionFetchErrorRightAfterLoginOrSignUp extends RecoverableError {
}
try {
this.onRetry();
} catch (e) {
console.error('Retry error', e);
} finally {
this.ttl--;
}

View File

@@ -1,7 +1,7 @@
{
"name": "@affine/electron",
"private": true,
"version": "0.10.3-canary.2",
"version": "0.10.3-beta.5",
"author": "toeverything",
"repository": {
"url": "https://github.com/toeverything/AFFiNE",
@@ -32,10 +32,10 @@
"@affine/sdk": "workspace:*",
"@affine/templates": "workspace:*",
"@affine/vue-hello-world-plugin": "workspace:*",
"@blocksuite/blocks": "0.0.0-20231124123613-7c06e95d-nightly",
"@blocksuite/editor": "0.0.0-20231124123613-7c06e95d-nightly",
"@blocksuite/lit": "0.0.0-20231124123613-7c06e95d-nightly",
"@blocksuite/store": "0.0.0-20231124123613-7c06e95d-nightly",
"@blocksuite/blocks": "0.0.0-20231122113751-6bf81eb3-nightly",
"@blocksuite/editor": "0.0.0-20231122113751-6bf81eb3-nightly",
"@blocksuite/lit": "0.0.0-20231122113751-6bf81eb3-nightly",
"@blocksuite/store": "0.0.0-20231122113751-6bf81eb3-nightly",
"@electron-forge/cli": "^7.1.0",
"@electron-forge/core": "^7.1.0",
"@electron-forge/core-utils": "^7.1.0",

View File

@@ -1,11 +1,10 @@
import { spawn } from 'node:child_process';
import { resolve } from 'node:path';
import type { ChildProcessWithoutNullStreams } from 'child_process';
import type { BuildContext } from 'esbuild';
import * as esbuild from 'esbuild';
import { config, electronDir, rootDir } from './common';
import { config, electronDir } from './common';
// this means we don't spawn electron windows, mainly for testing
const watchMode = process.argv.includes('--watch');
@@ -30,10 +29,7 @@ function spawnOrReloadElectron() {
spawnProcess = null;
}
const ext = process.platform === 'win32' ? '.cmd' : '';
const exe = resolve(rootDir, 'node_modules', '.bin', `electron${ext}`);
spawnProcess = spawn(exe, ['.'], {
spawnProcess = spawn('electron', ['.'], {
cwd: electronDir,
env: process.env,
});

View File

@@ -1,6 +1,6 @@
{
"name": "@affine/graphql",
"version": "0.10.3-canary.2",
"version": "0.10.3-beta.5",
"description": "Autogenerated GraphQL client for affine.pro",
"license": "MIT",
"type": "module",

View File

@@ -1,13 +0,0 @@
query listHistory(
$workspaceId: String!
$pageDocId: String!
$take: Int
$before: DateTime
) {
workspace(id: $workspaceId) {
histories(guid: $pageDocId, take: $take, before: $before) {
id
timestamp
}
}
}

View File

@@ -362,22 +362,6 @@ query getWorkspaces {
}`,
};
export const listHistoryQuery = {
id: 'listHistoryQuery' as const,
operationName: 'listHistory',
definitionName: 'workspace',
containsFile: false,
query: `
query listHistory($workspaceId: String!, $pageDocId: String!, $take: Int, $before: DateTime) {
workspace(id: $workspaceId) {
histories(guid: $pageDocId, take: $take, before: $before) {
id
timestamp
}
}
}`,
};
export const getInvoicesCountQuery = {
id: 'getInvoicesCountQuery' as const,
operationName: 'getInvoicesCount',
@@ -461,17 +445,6 @@ mutation publishPage($workspaceId: String!, $pageId: String!, $mode: PublicPageM
}`,
};
export const recoverDocMutation = {
id: 'recoverDocMutation' as const,
operationName: 'recoverDoc',
definitionName: 'recoverDoc',
containsFile: false,
query: `
mutation recoverDoc($workspaceId: String!, $docId: String!, $timestamp: DateTime!) {
recoverDoc(workspaceId: $workspaceId, guid: $docId, timestamp: $timestamp)
}`,
};
export const removeAvatarMutation = {
id: 'removeAvatarMutation' as const,
operationName: 'removeAvatar',
@@ -572,20 +545,6 @@ mutation sendVerifyChangeEmail($token: String!, $email: String!, $callbackUrl: S
}`,
};
export const serverConfigQuery = {
id: 'serverConfigQuery' as const,
operationName: 'serverConfig',
definitionName: 'serverConfig',
containsFile: false,
query: `
query serverConfig {
serverConfig {
version
flavor
}
}`,
};
export const setWorkspacePublicByIdMutation = {
id: 'setWorkspacePublicByIdMutation' as const,
operationName: 'setWorkspacePublicById',

View File

@@ -1,7 +0,0 @@
mutation recoverDoc(
$workspaceId: String!
$docId: String!
$timestamp: DateTime!
) {
recoverDoc(workspaceId: $workspaceId, guid: $docId, timestamp: $timestamp)
}

View File

@@ -1,6 +0,0 @@
query serverConfig {
serverConfig {
version
flavor
}
}

View File

@@ -62,7 +62,6 @@ export enum SubscriptionPlan {
Enterprise = 'Enterprise',
Free = 'Free',
Pro = 'Pro',
SelfHosted = 'SelfHosted',
Team = 'Team',
}
@@ -373,25 +372,6 @@ export type GetWorkspacesQuery = {
workspaces: Array<{ __typename?: 'WorkspaceType'; id: string }>;
};
export type ListHistoryQueryVariables = Exact<{
workspaceId: Scalars['String']['input'];
pageDocId: Scalars['String']['input'];
take: InputMaybe<Scalars['Int']['input']>;
before: InputMaybe<Scalars['DateTime']['input']>;
}>;
export type ListHistoryQuery = {
__typename?: 'Query';
workspace: {
__typename?: 'WorkspaceType';
histories: Array<{
__typename?: 'DocHistoryType';
id: string;
timestamp: string;
}>;
};
};
export type GetInvoicesCountQueryVariables = Exact<{ [key: string]: never }>;
export type GetInvoicesCountQuery = {
@@ -464,17 +444,6 @@ export type PublishPageMutation = {
};
};
export type RecoverDocMutationVariables = Exact<{
workspaceId: Scalars['String']['input'];
docId: Scalars['String']['input'];
timestamp: Scalars['DateTime']['input'];
}>;
export type RecoverDocMutation = {
__typename?: 'Mutation';
recoverDoc: string;
};
export type RemoveAvatarMutationVariables = Exact<{ [key: string]: never }>;
export type RemoveAvatarMutation = {
@@ -564,17 +533,6 @@ export type SendVerifyChangeEmailMutation = {
sendVerifyChangeEmail: boolean;
};
export type ServerConfigQueryVariables = Exact<{ [key: string]: never }>;
export type ServerConfigQuery = {
__typename?: 'Query';
serverConfig: {
__typename?: 'ServerConfigType';
version: string;
flavor: string;
};
};
export type SetWorkspacePublicByIdMutationVariables = Exact<{
id: Scalars['ID']['input'];
public: Scalars['Boolean']['input'];
@@ -759,11 +717,6 @@ export type Queries =
variables: GetWorkspacesQueryVariables;
response: GetWorkspacesQuery;
}
| {
name: 'listHistoryQuery';
variables: ListHistoryQueryVariables;
response: ListHistoryQuery;
}
| {
name: 'getInvoicesCountQuery';
variables: GetInvoicesCountQueryVariables;
@@ -779,11 +732,6 @@ export type Queries =
variables: PricesQueryVariables;
response: PricesQuery;
}
| {
name: 'serverConfigQuery';
variables: ServerConfigQueryVariables;
response: ServerConfigQuery;
}
| {
name: 'subscriptionQuery';
variables: SubscriptionQueryVariables;
@@ -851,11 +799,6 @@ export type Mutations =
variables: PublishPageMutationVariables;
response: PublishPageMutation;
}
| {
name: 'recoverDocMutation';
variables: RecoverDocMutationVariables;
response: RecoverDocMutation;
}
| {
name: 'removeAvatarMutation';
variables: RemoveAvatarMutationVariables;

View File

@@ -18,12 +18,12 @@
"devDependencies": {
"@affine/debug": "workspace:*",
"@affine/env": "workspace:*",
"@blocksuite/block-std": "0.0.0-20231124123613-7c06e95d-nightly",
"@blocksuite/blocks": "0.0.0-20231124123613-7c06e95d-nightly",
"@blocksuite/editor": "0.0.0-20231124123613-7c06e95d-nightly",
"@blocksuite/global": "0.0.0-20231124123613-7c06e95d-nightly",
"@blocksuite/lit": "0.0.0-20231124123613-7c06e95d-nightly",
"@blocksuite/store": "0.0.0-20231124123613-7c06e95d-nightly",
"@blocksuite/block-std": "0.0.0-20231122113751-6bf81eb3-nightly",
"@blocksuite/blocks": "0.0.0-20231122113751-6bf81eb3-nightly",
"@blocksuite/editor": "0.0.0-20231122113751-6bf81eb3-nightly",
"@blocksuite/global": "0.0.0-20231122113751-6bf81eb3-nightly",
"@blocksuite/lit": "0.0.0-20231122113751-6bf81eb3-nightly",
"@blocksuite/store": "0.0.0-20231122113751-6bf81eb3-nightly",
"@testing-library/react": "^14.0.0",
"@types/image-blob-reduce": "^4.1.3",
"@types/lodash.debounce": "^4.0.7",
@@ -62,5 +62,5 @@
"optional": true
}
},
"version": "0.10.3-canary.2"
"version": "0.10.3-beta.5"
}

View File

@@ -36,5 +36,5 @@
"ts-node": "^10.9.1",
"typescript": "^5.2.2"
},
"version": "0.10.3-canary.2"
"version": "0.10.3-beta.5"
}

View File

@@ -518,7 +518,6 @@
"com.affine.cmdk.affine.switch-state.on": "ON",
"com.affine.cmdk.affine.translucent-ui-on-the-sidebar.to": "Change Translucent UI On The Sidebar to",
"com.affine.cmdk.affine.whats-new": "What's New",
"com.affine.cmdk.affine.editor.reveal-page-history-modal": "Reveal Page History Modal",
"com.affine.cmdk.placeholder": "Type a command or search anything...",
"com.affine.collection-bar.action.tooltip.delete": "Delete",
"com.affine.collection-bar.action.tooltip.edit": "Edit",
@@ -677,7 +676,7 @@
"com.affine.new_edgeless": "New Edgeless",
"com.affine.new_import": "Import",
"com.affine.notFoundPage.backButton": "Back Home",
"com.affine.notFoundPage.title": "404 - Page Not Found",
"com.affine.notFoundPage.title": "Page Not Found",
"com.affine.onboarding.title1": "Hyper merged whiteboard and docs",
"com.affine.onboarding.title2": "Intuitive & robust block-based editing",
"com.affine.onboarding.videoDescription1": "Easily switch between Page mode for structured document creation and Whiteboard mode for the freeform visual expression of creative ideas.",
@@ -951,6 +950,13 @@
"com.affine.workspaceType.offline": "Available Offline",
"com.affine.write_with_a_blank_page": "Write with a blank page",
"com.affine.yesterday": "Yesterday",
"com.affine.error.retry": "Refresh",
"com.affine.error.refetch": "Refetch",
"com.affine.error.reload": "Reload",
"com.affine.error.page-not-found.title": "Refresh",
"com.affine.error.unexpected-error.title": "Something is wrong...",
"com.affine.error.contact.description": "If you are still experiencing this issue, please <1>contact us through the community.</1>",
"com.affine.error.no-page-root.title": "Page content is missing",
"core": "core",
"dark": "Dark",
"emptyAllPages": "Click on the <1>$t(New Page)</1> button to create your first page.",
@@ -973,13 +979,5 @@
"system": "System",
"upgradeBrowser": "Please upgrade to the latest version of Chrome for the best experience.",
"will be moved to Trash": "{{title}} will be moved to Trash",
"will delete member": "will delete member",
"com.affine.history.version-history": "Version History",
"com.affine.history.view-history-version": "View History Version",
"com.affine.history.restore-current-version": "Restore current version",
"com.affine.history.back-to-page": "Back to page",
"com.affine.history.empty-prompt.title": "Empty",
"com.affine.history.empty-prompt.description": "This document is such a spring chicken, it hasn't sprouted a single historical sprig yet!",
"com.affine.history.confirm-restore-modal.restore": "Restore",
"com.affine.history.confirm-restore-modal.hint": "You are about to restore the current version of the page to the latest version available. This action will overwrite any changes made prior to the latest version."
"will delete member": "will delete member"
}

View File

@@ -25,7 +25,7 @@ rand = "0.8"
serde = "1"
serde_json = "1"
sha3 = "0.10"
sqlx = { version = "0.7.3", default-features = false, features = [
sqlx = { version = "0.7.2", default-features = false, features = [
"sqlite",
"migrate",
"runtime-tokio",
@@ -44,7 +44,7 @@ uuid = { version = "1", default-features = false, features = [
affine_schema = { path = "./schema" }
dotenv = "0.15"
napi-build = "2"
sqlx = { version = "0.7.3", default-features = false, features = [
sqlx = { version = "0.7.2", default-features = false, features = [
"sqlite",
"runtime-tokio",
"tls-rustls",

View File

@@ -58,5 +58,5 @@
"test": "ava",
"version": "napi version"
},
"version": "0.10.3-canary.2"
"version": "0.10.3-beta.5"
}

View File

@@ -7,5 +7,5 @@
"./v1/*.json": "./v1/*.json",
"./preloading.json": "./preloading.json"
},
"version": "0.10.3-canary.2"
"version": "0.10.3-beta.5"
}

View File

@@ -4,7 +4,6 @@
"exports": {
"./atom": "./src/atom.ts",
"./manager": "./src/manager/index.ts",
"./blob": "./src/blob/index.ts",
"./local/crud": "./src/local/crud.ts",
"./affine/*": "./src/affine/*.ts",
"./providers": "./src/providers/index.ts"
@@ -49,5 +48,5 @@
"vitest": "0.34.6",
"ws": "^8.14.2"
},
"version": "0.10.3-canary.2"
"version": "0.10.3-beta.5"
}

View File

@@ -5,15 +5,12 @@ import type {
QueryOptions,
QueryResponse,
QueryVariables,
RecursiveMaybeFields,
} from '@affine/graphql';
import { gqlFetcherFactory } from '@affine/graphql';
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
import type { GraphQLError } from 'graphql';
import { useMemo } from 'react';
import type { Key, SWRConfiguration, SWRResponse } from 'swr';
import useSWR, { useSWRConfig } from 'swr';
import useSWRInfinite from 'swr/infinite';
import useSWR from 'swr';
import type {
SWRMutationConfiguration,
SWRMutationResponse,
@@ -89,63 +86,6 @@ export function useQuery<Query extends GraphQLQuery>(
);
}
export function useQueryInfinite<Query extends GraphQLQuery>(
options: Omit<QueryOptions<Query>, 'variables'> & {
getVariables: (
pageIndex: number,
previousPageData: QueryResponse<Query>
) => QueryOptions<Query>['variables'];
},
config?: Omit<
SWRConfiguration<
QueryResponse<Query>,
GraphQLError | GraphQLError[],
typeof fetcher<Query>
>,
'fetcher'
>
) {
const configWithSuspense: SWRConfiguration = useMemo(
() => ({
suspense: true,
...config,
}),
[config]
);
const { data, setSize, size, error } = useSWRInfinite<
QueryResponse<Query>,
GraphQLError | GraphQLError[]
>(
(pageIndex: number, previousPageData: QueryResponse<Query>) => [
'cloud',
options.query.id,
options.getVariables(pageIndex, previousPageData),
],
async ([_, __, variables]) => {
const params = { ...options, variables } as QueryOptions<Query>;
return fetcher(params);
},
configWithSuspense
);
const loadingMore = size > 0 && data && !data[size - 1];
// todo: find a generic way to know whether or not there are more items to load
const loadMore = useAsyncCallback(async () => {
if (loadingMore) {
return;
}
await setSize(size => size + 1);
}, [loadingMore, setSize]);
return {
data,
error,
loadingMore,
loadMore,
};
}
/**
* A useSWRMutation wrapper for sending graphql mutations
*
@@ -198,32 +138,3 @@ export function useMutation(
}
export const gql = fetcher;
// use this to revalidate all queries that match the filter
export const useMutateQueryResource = () => {
const { mutate } = useSWRConfig();
const revalidateResource = useMemo(
() =>
<Q extends GraphQLQuery>(
query: Q,
varsFilter: (
vars: RecursiveMaybeFields<QueryVariables<Q>>
) => boolean = _vars => true
) => {
return mutate(key => {
const res =
Array.isArray(key) &&
key[0] === 'cloud' &&
key[1] === query.id &&
varsFilter(key[2]);
if (res) {
console.debug('revalidate resource', key);
}
return res;
});
},
[mutate]
);
return revalidateResource;
};

View File

@@ -24,13 +24,13 @@ export function createAffineAwarenessProvider(
const socket = getIoManager().socket('/');
const awarenessBroadcast = ({
workspaceId,
workspaceId: remoteWorkspaceId,
awarenessUpdate,
}: {
workspaceId: string;
awarenessUpdate: string;
}) => {
if (workspaceId !== workspaceId) {
if (remoteWorkspaceId !== workspaceId) {
return;
}
applyAwarenessUpdate(

View File

@@ -38,5 +38,5 @@
"react": "*",
"react-dom": "*"
},
"version": "0.10.3-canary.2"
"version": "0.10.3-beta.5"
}

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