mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-05 00:54:56 +00:00
Compare commits
47 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b066a4b39 | ||
|
|
cc0462e7fe | ||
|
|
b87c3840f3 | ||
|
|
47243247b9 | ||
|
|
8689465e00 | ||
|
|
b0ca3c6d58 | ||
|
|
2857568f03 | ||
|
|
2e94944d2b | ||
|
|
afa0e31ecd | ||
|
|
3390fbc5db | ||
|
|
cd2c2b7fdb | ||
|
|
3f5dadb4f5 | ||
|
|
401106203c | ||
|
|
e200e0a1a0 | ||
|
|
56a3f054f9 | ||
|
|
abaea9e605 | ||
|
|
1c2b23b160 | ||
|
|
9642566086 | ||
|
|
bd7c422c46 | ||
|
|
bf093710b7 | ||
|
|
ffa4d5422d | ||
|
|
a97ee60502 | ||
|
|
84cfcb193f | ||
|
|
1f71e87460 | ||
|
|
54c51225ed | ||
|
|
5fade7aaf5 | ||
|
|
df99e2ca97 | ||
|
|
4610f1e934 | ||
|
|
78ef9fee34 | ||
|
|
18089c7369 | ||
|
|
991e0b9b63 | ||
|
|
129cceade9 | ||
|
|
055fa0a8b4 | ||
|
|
9f3dceb220 | ||
|
|
6a64055886 | ||
|
|
c712e87114 | ||
|
|
343152e162 | ||
|
|
97d6f53932 | ||
|
|
44e00f67c4 | ||
|
|
39cb1b7714 | ||
|
|
6f5c61b8b6 | ||
|
|
87520e9bf9 | ||
|
|
181b213a3e | ||
|
|
a8938ab403 | ||
|
|
ca8bb6dc90 | ||
|
|
21c7d1810d | ||
|
|
2fa843b960 |
2
.github/actions/deploy/deploy.mjs
vendored
2
.github/actions/deploy/deploy.mjs
vendored
@@ -41,7 +41,7 @@ const isBeta = buildType === 'beta';
|
||||
const isInternal = buildType === 'internal';
|
||||
|
||||
const replicaConfig = {
|
||||
production: {
|
||||
stable: {
|
||||
web: 3,
|
||||
graphql: Number(process.env.PRODUCTION_GRAPHQL_REPLICA) || 3,
|
||||
sync: Number(process.env.PRODUCTION_SYNC_REPLICA) || 3,
|
||||
|
||||
28
.github/actions/setup-rust/action.yml
vendored
Normal file
28
.github/actions/setup-rust/action.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
name: 'Rust setup'
|
||||
description: 'Rust setup, including cache configuration'
|
||||
inputs:
|
||||
components:
|
||||
description: 'Cargo components'
|
||||
required: false
|
||||
targets:
|
||||
description: 'Cargo target'
|
||||
required: false
|
||||
toolchain:
|
||||
description: 'Rustup toolchain'
|
||||
required: false
|
||||
default: 'stable'
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: ${{ inputs.toolchain }}
|
||||
targets: ${{ inputs.targets }}
|
||||
components: ${{ inputs.components }}
|
||||
- name: Add Targets
|
||||
if: ${{ inputs.targets }}
|
||||
run: rustup target add ${{ inputs.targets }}
|
||||
shell: bash
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
2
.github/helm/affine/Chart.yaml
vendored
2
.github/helm/affine/Chart.yaml
vendored
@@ -3,4 +3,4 @@ name: affine
|
||||
description: AFFiNE cloud chart
|
||||
type: application
|
||||
version: 0.0.0
|
||||
appVersion: "0.17.0"
|
||||
appVersion: "0.18.0"
|
||||
|
||||
@@ -3,7 +3,7 @@ name: graphql
|
||||
description: AFFiNE GraphQL server
|
||||
type: application
|
||||
version: 0.0.0
|
||||
appVersion: "0.17.0"
|
||||
appVersion: "0.18.0"
|
||||
dependencies:
|
||||
- name: gcloud-sql-proxy
|
||||
version: 0.0.0
|
||||
|
||||
2
.github/helm/affine/charts/sync/Chart.yaml
vendored
2
.github/helm/affine/charts/sync/Chart.yaml
vendored
@@ -3,7 +3,7 @@ name: sync
|
||||
description: AFFiNE Sync Server
|
||||
type: application
|
||||
version: 0.0.0
|
||||
appVersion: "0.17.0"
|
||||
appVersion: "0.18.0"
|
||||
dependencies:
|
||||
- name: gcloud-sql-proxy
|
||||
version: 0.0.0
|
||||
|
||||
25
.github/workflows/build-test.yml
vendored
25
.github/workflows/build-test.yml
vendored
@@ -223,7 +223,7 @@ jobs:
|
||||
run: yarn nx test:coverage @affine/monorepo
|
||||
|
||||
- name: Upload unit test coverage results
|
||||
uses: codecov/codecov-action@v4
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./.coverage/store/lcov.info
|
||||
@@ -371,7 +371,7 @@ jobs:
|
||||
COPILOT_OPENAI_API_KEY: 'use_fake_openai_api_key'
|
||||
|
||||
- name: Upload server test coverage results
|
||||
uses: codecov/codecov-action@v4
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./packages/backend/server/.coverage/lcov.info
|
||||
@@ -379,6 +379,23 @@ jobs:
|
||||
name: affine
|
||||
fail_ci_if_error: false
|
||||
|
||||
server-native-test:
|
||||
name: Run server native tests
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUSTFLAGS: -D warnings
|
||||
CARGO_TERM_COLOR: always
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Rust
|
||||
uses: ./.github/actions/setup-rust
|
||||
|
||||
- name: Install latest nextest release
|
||||
uses: taiki-e/install-action@nextest
|
||||
|
||||
- name: Run tests
|
||||
run: cargo nextest run --release
|
||||
|
||||
copilot-api-test:
|
||||
name: Server Copilot Api Test
|
||||
runs-on: ubuntu-latest
|
||||
@@ -454,7 +471,7 @@ jobs:
|
||||
|
||||
- name: Upload server test coverage results
|
||||
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.filter.outputs.backend == 'true' }}
|
||||
uses: codecov/codecov-action@v4
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./packages/backend/server/.coverage/lcov.info
|
||||
@@ -755,6 +772,8 @@ jobs:
|
||||
- build-server-native
|
||||
- build-electron-renderer
|
||||
- server-test
|
||||
- server-native-test
|
||||
- copilot-api-test
|
||||
- copilot-e2e-test
|
||||
- server-e2e-test
|
||||
- desktop-test
|
||||
|
||||
2
.github/workflows/copilot-test.yml
vendored
2
.github/workflows/copilot-test.yml
vendored
@@ -86,7 +86,7 @@ jobs:
|
||||
COPILOT_FAL_API_KEY: ${{ secrets.COPILOT_FAL_API_KEY }}
|
||||
|
||||
- name: Upload server test coverage results
|
||||
uses: codecov/codecov-action@v4
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./packages/backend/server/.coverage/lcov.info
|
||||
|
||||
50
.github/workflows/deploy.yml
vendored
50
.github/workflows/deploy.yml
vendored
@@ -173,41 +173,31 @@ jobs:
|
||||
BLOCKSUITE_REPO_PATH: ${{ github.workspace }}/blocksuite
|
||||
- name: Post Failed event to a Slack channel
|
||||
id: failed-slack
|
||||
uses: slackapi/slack-github-action@v1.27.0
|
||||
uses: slackapi/slack-github-action@v2.0.0
|
||||
if: ${{ always() && contains(needs.*.result, 'failure') }}
|
||||
with:
|
||||
channel-id: ${{ secrets.RELEASE_SLACK_CHNNEL_ID }}
|
||||
method: chat.postMessage
|
||||
token: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||
payload: |
|
||||
{
|
||||
"blocks": [
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"text": "<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|Backend deploy failed `${{ github.event.inputs.flavor }}`>",
|
||||
"type": "mrkdwn"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
env:
|
||||
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||
channel: ${{ secrets.RELEASE_SLACK_CHNNEL_ID }}
|
||||
text: "<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|Backend deploy failed `${{ github.event.inputs.flavor }}`>"
|
||||
blocks:
|
||||
- type: section
|
||||
text:
|
||||
type: mrkdwn
|
||||
text: "<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|Backend deploy failed `${{ github.event.inputs.flavor }}`>"
|
||||
- name: Post Cancel event to a Slack channel
|
||||
id: cancel-slack
|
||||
uses: slackapi/slack-github-action@v1.27.0
|
||||
uses: slackapi/slack-github-action@v2.0.0
|
||||
if: ${{ always() && contains(needs.*.result, 'cancelled') && !contains(needs.*.result, 'failure') }}
|
||||
with:
|
||||
channel-id: ${{ secrets.RELEASE_SLACK_CHNNEL_ID }}
|
||||
token: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||
method: chat.postMessage
|
||||
payload: |
|
||||
{
|
||||
"blocks": [
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"text": "<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|Backend deploy cancelled `${{ github.event.inputs.flavor }}`>",
|
||||
"type": "mrkdwn"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
env:
|
||||
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||
channel: ${{ secrets.RELEASE_SLACK_CHNNEL_ID }}
|
||||
text: "<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|Backend deploy cancelled `${{ github.event.inputs.flavor }}`>"
|
||||
blocks:
|
||||
- type: section
|
||||
text:
|
||||
type: mrkdwn
|
||||
text: "<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|Backend deploy cancelled `${{ github.event.inputs.flavor }}`>"
|
||||
|
||||
2
.github/workflows/release-desktop.yml
vendored
2
.github/workflows/release-desktop.yml
vendored
@@ -97,6 +97,7 @@ jobs:
|
||||
SENTRY_PROJECT: 'affine'
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
SENTRY_RELEASE: ${{ needs.before-make.outputs.RELEASE_VERSION }}
|
||||
MIXPANEL_TOKEN: ${{ secrets.MIXPANEL_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -213,6 +214,7 @@ jobs:
|
||||
SENTRY_PROJECT: 'affine'
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
|
||||
SENTRY_RELEASE: ${{ needs.before-make.outputs.RELEASE_VERSION }}
|
||||
MIXPANEL_TOKEN: ${{ secrets.MIXPANEL_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
55
Cargo.lock
generated
55
Cargo.lock
generated
@@ -17,10 +17,21 @@ version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
|
||||
|
||||
[[package]]
|
||||
name = "affine_common"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"rand",
|
||||
"rayon",
|
||||
"sha3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "affine_native"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"affine_common",
|
||||
"affine_schema",
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@@ -32,6 +43,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"parking_lot",
|
||||
"rand",
|
||||
"rayon",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha3",
|
||||
@@ -48,6 +60,7 @@ version = "0.0.0"
|
||||
name = "affine_server_native"
|
||||
version = "1.0.0"
|
||||
dependencies = [
|
||||
"affine_common",
|
||||
"chrono",
|
||||
"file-format",
|
||||
"mimalloc",
|
||||
@@ -55,6 +68,7 @@ dependencies = [
|
||||
"napi-build",
|
||||
"napi-derive",
|
||||
"rand",
|
||||
"rayon",
|
||||
"sha3",
|
||||
"tiktoken-rs",
|
||||
"tokio",
|
||||
@@ -331,6 +345,25 @@ version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-deque"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d"
|
||||
dependencies = [
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-epoch"
|
||||
version = "0.9.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-queue"
|
||||
version = "0.3.11"
|
||||
@@ -1013,7 +1046,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-targets 0.48.5",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1514,6 +1547,26 @@ dependencies = [
|
||||
"rand",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
|
||||
dependencies = [
|
||||
"either",
|
||||
"rayon-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon-core"
|
||||
version = "1.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
|
||||
dependencies = [
|
||||
"crossbeam-deque",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.7"
|
||||
|
||||
51
Cargo.toml
51
Cargo.toml
@@ -1,29 +1,36 @@
|
||||
[workspace]
|
||||
members = ["./packages/backend/native", "./packages/frontend/native", "./packages/frontend/native/schema"]
|
||||
members = [
|
||||
"./packages/backend/native",
|
||||
"./packages/common/native",
|
||||
"./packages/frontend/native",
|
||||
"./packages/frontend/native/schema"
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "1"
|
||||
chrono = "0.4"
|
||||
dotenv = "0.15"
|
||||
file-format = { version = "0.26", features = ["reader"] }
|
||||
mimalloc = "0.1"
|
||||
napi = { version = "3.0.0-alpha.12", features = ["async", "chrono_date", "error_anyhow", "napi9", "serde"] }
|
||||
napi-build = { version = "2" }
|
||||
napi-derive = { version = "3.0.0-alpha.12" }
|
||||
notify = { version = "7", features = ["serde"] }
|
||||
once_cell = "1"
|
||||
parking_lot = "0.12"
|
||||
rand = "0.8"
|
||||
serde = "1"
|
||||
serde_json = "1"
|
||||
sha3 = "0.10"
|
||||
sqlx = { version = "0.8", default-features = false, features = ["chrono", "macros", "migrate", "runtime-tokio", "sqlite", "tls-rustls"] }
|
||||
tiktoken-rs = "0.6"
|
||||
tokio = "1.37"
|
||||
uuid = "1.8"
|
||||
v_htmlescape = "0.15"
|
||||
y-octo = { git = "https://github.com/y-crdt/y-octo.git", branch = "main" }
|
||||
affine_common = { path = "./packages/common/native" }
|
||||
anyhow = "1"
|
||||
chrono = "0.4"
|
||||
dotenv = "0.15"
|
||||
file-format = { version = "0.26", features = ["reader"] }
|
||||
mimalloc = "0.1"
|
||||
napi = { version = "3.0.0-alpha.12", features = ["async", "chrono_date", "error_anyhow", "napi9", "serde"] }
|
||||
napi-build = { version = "2" }
|
||||
napi-derive = { version = "3.0.0-alpha.12" }
|
||||
notify = { version = "7", features = ["serde"] }
|
||||
once_cell = "1"
|
||||
parking_lot = "0.12"
|
||||
rand = "0.8"
|
||||
rayon = "1.10"
|
||||
serde = "1"
|
||||
serde_json = "1"
|
||||
sha3 = "0.10"
|
||||
sqlx = { version = "0.8", default-features = false, features = ["chrono", "macros", "migrate", "runtime-tokio", "sqlite", "tls-rustls"] }
|
||||
tiktoken-rs = "0.6"
|
||||
tokio = "1.37"
|
||||
uuid = "1.8"
|
||||
v_htmlescape = "0.15"
|
||||
y-octo = { git = "https://github.com/y-crdt/y-octo.git", branch = "main" }
|
||||
|
||||
[profile.dev.package.sqlx-macros]
|
||||
opt-level = 3
|
||||
|
||||
@@ -19,5 +19,5 @@
|
||||
],
|
||||
"ext": "ts,md,json"
|
||||
},
|
||||
"version": "0.17.0"
|
||||
"version": "0.18.0"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@affine/monorepo",
|
||||
"version": "0.17.0",
|
||||
"version": "0.18.0",
|
||||
"private": true,
|
||||
"author": "toeverything",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -7,15 +7,16 @@ version = "1.0.0"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
chrono = { workspace = true }
|
||||
file-format = { workspace = true }
|
||||
napi = { workspace = true }
|
||||
napi-derive = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
sha3 = { workspace = true }
|
||||
tiktoken-rs = { workspace = true }
|
||||
v_htmlescape = { workspace = true }
|
||||
y-octo = { workspace = true }
|
||||
affine_common = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
file-format = { workspace = true }
|
||||
napi = { workspace = true }
|
||||
napi-derive = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
sha3 = { workspace = true }
|
||||
tiktoken-rs = { workspace = true }
|
||||
v_htmlescape = { workspace = true }
|
||||
y-octo = { workspace = true }
|
||||
|
||||
[target.'cfg(not(target_os = "linux"))'.dependencies]
|
||||
mimalloc = { workspace = true }
|
||||
@@ -24,7 +25,8 @@ mimalloc = { workspace = true }
|
||||
mimalloc = { workspace = true, features = ["local_dynamic_tls"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = "1"
|
||||
rayon = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
napi-build = { workspace = true }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@affine/server-native",
|
||||
"version": "0.17.0",
|
||||
"version": "0.18.0",
|
||||
"engines": {
|
||||
"node": ">= 10.16.0 < 11 || >= 11.8.0"
|
||||
},
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
../../../frontend/native/src/hashcash.rs
|
||||
69
packages/backend/native/src/hashcash.rs
Normal file
69
packages/backend/native/src/hashcash.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use affine_common::hashcash::Stamp;
|
||||
use napi::{bindgen_prelude::AsyncTask, Env, JsBoolean, JsString, Result as NapiResult, Task};
|
||||
use napi_derive::napi;
|
||||
|
||||
pub struct AsyncVerifyChallengeResponse {
|
||||
response: String,
|
||||
bits: u32,
|
||||
resource: String,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl Task for AsyncVerifyChallengeResponse {
|
||||
type Output = bool;
|
||||
type JsValue = JsBoolean;
|
||||
|
||||
fn compute(&mut self) -> NapiResult<Self::Output> {
|
||||
Ok(if let Ok(stamp) = Stamp::try_from(self.response.as_str()) {
|
||||
stamp.check(self.bits, &self.resource)
|
||||
} else {
|
||||
false
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve(&mut self, env: Env, output: bool) -> NapiResult<Self::JsValue> {
|
||||
env.get_boolean(output)
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn verify_challenge_response(
|
||||
response: String,
|
||||
bits: u32,
|
||||
resource: String,
|
||||
) -> AsyncTask<AsyncVerifyChallengeResponse> {
|
||||
AsyncTask::new(AsyncVerifyChallengeResponse {
|
||||
response,
|
||||
bits,
|
||||
resource,
|
||||
})
|
||||
}
|
||||
|
||||
pub struct AsyncMintChallengeResponse {
|
||||
bits: Option<u32>,
|
||||
resource: String,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl Task for AsyncMintChallengeResponse {
|
||||
type Output = String;
|
||||
type JsValue = JsString;
|
||||
|
||||
fn compute(&mut self) -> NapiResult<Self::Output> {
|
||||
Ok(Stamp::mint(self.resource.clone(), self.bits).format())
|
||||
}
|
||||
|
||||
fn resolve(&mut self, env: Env, output: String) -> NapiResult<Self::JsValue> {
|
||||
env.create_string(&output)
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn mint_challenge_response(
|
||||
resource: String,
|
||||
bits: Option<u32>,
|
||||
) -> AsyncTask<AsyncMintChallengeResponse> {
|
||||
AsyncTask::new(AsyncMintChallengeResponse { bits, resource })
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
provider = "postgresql"
|
||||
provider = "postgresql"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@affine/server",
|
||||
"private": true,
|
||||
"version": "0.17.0",
|
||||
"version": "0.18.0",
|
||||
"description": "Affine Node.js server",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
@@ -12,9 +12,9 @@
|
||||
"start": "node --loader ts-node/esm/transpile-only.mjs ./src/index.ts",
|
||||
"dev": "nodemon ./src/index.ts",
|
||||
"test": "ava --concurrency 1 --serial",
|
||||
"test:copilot": "ava --concurrency 1 --serial \"tests/**/copilot-*.e2e.ts\"",
|
||||
"test:copilot": "ava \"tests/**/copilot-*.spec.ts\"",
|
||||
"test:coverage": "c8 ava --concurrency 1 --serial",
|
||||
"test:copilot:coverage": "c8 ava --timeout=5m --concurrency 1 --serial \"tests/**/copilot-*.e2e.ts\"",
|
||||
"test:copilot:coverage": "c8 ava --timeout=5m \"tests/**/copilot-*.spec.ts\"",
|
||||
"postinstall": "prisma generate",
|
||||
"data-migration": "node --loader ts-node/esm/transpile-only.mjs ./src/data/index.ts",
|
||||
"predeploy": "yarn prisma migrate deploy && node --import ./scripts/register.js ./dist/data/index.js run",
|
||||
|
||||
@@ -147,9 +147,11 @@ export class SelfhostModule implements OnModuleInit {
|
||||
|
||||
// fallback all unknown routes
|
||||
app.get([basePath, basePath + '/*'], this.check.use, (req, res) => {
|
||||
const mobile = isMobile({
|
||||
ua: req.headers['user-agent'] ?? undefined,
|
||||
});
|
||||
const mobile =
|
||||
this.config.AFFINE_ENV === 'dev' &&
|
||||
isMobile({
|
||||
ua: req.headers['user-agent'] ?? undefined,
|
||||
});
|
||||
|
||||
return res.sendFile(
|
||||
join(
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
import { defineStartupConfig, ModuleConfig } from '../../fundamentals/config';
|
||||
import {
|
||||
defineRuntimeConfig,
|
||||
defineStartupConfig,
|
||||
ModuleConfig,
|
||||
} from '../../fundamentals/config';
|
||||
import { CaptchaConfig } from './types';
|
||||
|
||||
declare module '../config' {
|
||||
interface PluginsConfig {
|
||||
captcha: ModuleConfig<CaptchaConfig>;
|
||||
captcha: ModuleConfig<
|
||||
CaptchaConfig,
|
||||
{
|
||||
enable: boolean;
|
||||
}
|
||||
>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,3 +30,10 @@ defineStartupConfig('plugins.captcha', {
|
||||
bits: 20,
|
||||
},
|
||||
});
|
||||
|
||||
defineRuntimeConfig('plugins.captcha', {
|
||||
enable: {
|
||||
desc: 'Check captcha challenge when user authenticating the app.',
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
Config,
|
||||
getRequestResponseFromContext,
|
||||
GuardProvider,
|
||||
} from '../../fundamentals';
|
||||
@@ -18,11 +19,18 @@ export class CaptchaGuardProvider
|
||||
{
|
||||
name = 'captcha' as const;
|
||||
|
||||
constructor(private readonly captcha: CaptchaService) {
|
||||
constructor(
|
||||
private readonly captcha: CaptchaService,
|
||||
private readonly config: Config
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async canActivate(context: ExecutionContext) {
|
||||
if (!(await this.config.runtime.fetch('plugins.captcha/enable'))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const { req } = getRequestResponseFromContext(context);
|
||||
|
||||
// require headers, old client send through query string
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import './config';
|
||||
|
||||
import { AuthModule } from '../../core/auth';
|
||||
import { ServerFeature } from '../../core/config';
|
||||
import { Plugin } from '../registry';
|
||||
|
||||
@@ -88,15 +88,14 @@ export class CaptchaService {
|
||||
|
||||
async verifyRequest(credential: Credential, req: Request) {
|
||||
const challenge = credential.challenge;
|
||||
let resource: string | null = null;
|
||||
if (typeof challenge === 'string' && challenge) {
|
||||
const resource = await this.token
|
||||
resource = await this.token
|
||||
.getToken(TokenType.Challenge, challenge)
|
||||
.then(token => token?.credential);
|
||||
|
||||
if (!resource) {
|
||||
throw new CaptchaVerificationFailed('Invalid Challenge');
|
||||
}
|
||||
.then(token => token?.credential || null);
|
||||
}
|
||||
|
||||
if (resource) {
|
||||
const isChallengeVerified = await this.verifyChallengeResponse(
|
||||
credential.token,
|
||||
resource
|
||||
|
||||
@@ -63,7 +63,7 @@ const runIfCopilotConfigured = test.macro(
|
||||
}
|
||||
);
|
||||
|
||||
test.beforeEach(async t => {
|
||||
test.serial.before(async t => {
|
||||
const module = await createTestingModule({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
@@ -101,7 +101,7 @@ test.beforeEach(async t => {
|
||||
};
|
||||
});
|
||||
|
||||
test.beforeEach(async t => {
|
||||
test.serial.before(async t => {
|
||||
const { prompt, executors } = t.context;
|
||||
|
||||
executors.image.register();
|
||||
@@ -121,12 +121,12 @@ test.beforeEach(async t => {
|
||||
}
|
||||
});
|
||||
|
||||
test.afterEach.always(async _ => {
|
||||
test.after(async _ => {
|
||||
unregisterCopilotProvider(OpenAIProvider.type);
|
||||
unregisterCopilotProvider(FalProvider.type);
|
||||
});
|
||||
|
||||
test.afterEach.always(async t => {
|
||||
test.after(async t => {
|
||||
await t.context.module.close();
|
||||
});
|
||||
|
||||
@@ -143,7 +143,7 @@ const assertNotWrappedInCodeBlock = (
|
||||
|
||||
const checkMDList = (text: string) => {
|
||||
const lines = text.split('\n');
|
||||
const listItemRegex = /^( {2})*(-|\*|\+) .+$/;
|
||||
const listItemRegex = /^( {2})*(-|\u2010-\u2015|\*|\+)? .+$/;
|
||||
let prevIndent = null;
|
||||
|
||||
for (const line of lines) {
|
||||
@@ -166,7 +166,9 @@ const checkMDList = (text: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
prevIndent = currentIndent;
|
||||
if (line.trim().startsWith('-')) {
|
||||
prevIndent = currentIndent;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -190,14 +192,14 @@ const retry = async (
|
||||
while (i--) {
|
||||
const ret = await t.try(callback);
|
||||
if (ret.passed) {
|
||||
ret.commit();
|
||||
break;
|
||||
return ret.commit();
|
||||
} else {
|
||||
ret.discard();
|
||||
t.log(ret.errors.map(e => e.message).join('\n'));
|
||||
t.log(`retrying ${action} ${3 - i}/3 ...`);
|
||||
}
|
||||
}
|
||||
t.fail(`failed to run ${action}`);
|
||||
};
|
||||
|
||||
// ==================== utils ====================
|
||||
@@ -248,6 +250,16 @@ test('should validate markdown list', t => {
|
||||
- item 1.1.1.1
|
||||
`)
|
||||
);
|
||||
t.true(
|
||||
checkMDList(`
|
||||
- item 1
|
||||
- item 1.1
|
||||
- item 1.1.1.1
|
||||
item 1.1.1.1 line breaks
|
||||
- item 1.1.1.2
|
||||
`),
|
||||
'should allow line breaks'
|
||||
);
|
||||
});
|
||||
|
||||
// ==================== action ====================
|
||||
@@ -447,14 +459,14 @@ const workflows = [
|
||||
{
|
||||
name: 'brainstorm',
|
||||
content: 'apple company',
|
||||
verifier: (t: ExecutionContext<Tester>, result: string) => {
|
||||
verifier: (t: ExecutionContext, result: string) => {
|
||||
t.assert(checkMDList(result), 'should be a markdown list');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'presentation',
|
||||
content: 'apple company',
|
||||
verifier: (t: ExecutionContext<Tester>, result: string) => {
|
||||
verifier: (t: ExecutionContext, result: string) => {
|
||||
for (const l of result.split('\n')) {
|
||||
t.notThrows(() => {
|
||||
JSON.parse(l.trim());
|
||||
@@ -475,11 +487,11 @@ for (const { name, content, verifier } of workflows) {
|
||||
let result = '';
|
||||
for await (const ret of workflow.runGraph({ content }, name)) {
|
||||
if (ret.status === GraphExecutorState.EnterNode) {
|
||||
console.log('enter node:', ret.node.name);
|
||||
t.log('enter node:', ret.node.name);
|
||||
} else if (ret.status === GraphExecutorState.ExitNode) {
|
||||
console.log('exit node:', ret.node.name);
|
||||
t.log('exit node:', ret.node.name);
|
||||
} else if (ret.status === GraphExecutorState.EmitAttachment) {
|
||||
console.log('stream attachment:', ret);
|
||||
t.log('stream attachment:', ret);
|
||||
} else {
|
||||
result += ret.content;
|
||||
}
|
||||
@@ -9,5 +9,5 @@
|
||||
"@types/debug": "^4.1.12",
|
||||
"vitest": "2.1.4"
|
||||
},
|
||||
"version": "0.17.0"
|
||||
"version": "0.18.0"
|
||||
}
|
||||
|
||||
4
packages/common/env/package.json
vendored
4
packages/common/env/package.json
vendored
@@ -3,7 +3,7 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@blocksuite/affine": "0.17.32",
|
||||
"@blocksuite/affine": "0.17.33",
|
||||
"vitest": "2.1.4"
|
||||
},
|
||||
"exports": {
|
||||
@@ -21,5 +21,5 @@
|
||||
"dependencies": {
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"version": "0.17.0"
|
||||
"version": "0.18.0"
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"@affine/debug": "workspace:*",
|
||||
"@affine/env": "workspace:*",
|
||||
"@affine/templates": "workspace:*",
|
||||
"@blocksuite/affine": "0.17.32",
|
||||
"@blocksuite/affine": "0.17.33",
|
||||
"@datastructures-js/binary-search-tree": "^5.3.2",
|
||||
"eventemitter2": "^6.4.9",
|
||||
"foxact": "^0.2.33",
|
||||
@@ -60,5 +60,5 @@
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"version": "0.17.0"
|
||||
"version": "0.18.0"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import { Unreachable } from '@affine/env/constant';
|
||||
import {
|
||||
type AffineTextAttributes,
|
||||
@@ -16,6 +17,8 @@ import { DocScope } from '../scopes/doc';
|
||||
import type { DocsStore } from '../stores/docs';
|
||||
import { DocService } from './doc';
|
||||
|
||||
const logger = new DebugLogger('DocsService');
|
||||
|
||||
export class DocsService extends Service {
|
||||
list = this.framework.createEntity(DocRecordList);
|
||||
|
||||
@@ -52,6 +55,15 @@ export class DocsService extends Service {
|
||||
record: docRecord,
|
||||
});
|
||||
|
||||
try {
|
||||
blockSuiteDoc.load();
|
||||
} catch (e) {
|
||||
logger.error('Failed to load doc', {
|
||||
docId,
|
||||
error: e,
|
||||
});
|
||||
}
|
||||
|
||||
const doc = docScope.get(DocService).doc;
|
||||
|
||||
const { obj, release } = this.pool.put(docId, doc);
|
||||
|
||||
@@ -206,16 +206,6 @@ export const AFFINE_FLAGS = {
|
||||
configurable: false,
|
||||
defaultState: isMobile,
|
||||
},
|
||||
enable_snapshot_import_export: {
|
||||
category: 'affine',
|
||||
displayName:
|
||||
'com.affine.settings.workspace.experimental-features.enable-snapshot-import-export.name',
|
||||
description:
|
||||
'com.affine.settings.workspace.experimental-features.enable-snapshot-import-export.description',
|
||||
hide: true,
|
||||
configurable: true,
|
||||
defaultState: false,
|
||||
},
|
||||
} satisfies { [key in string]: FlagInfo };
|
||||
|
||||
export type AFFINE_FLAGS = typeof AFFINE_FLAGS;
|
||||
|
||||
12
packages/common/native/Cargo.toml
Normal file
12
packages/common/native/Cargo.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
edition = "2021"
|
||||
name = "affine_common"
|
||||
version = "0.1.0"
|
||||
|
||||
[dependencies]
|
||||
chrono = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
sha3 = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
rayon = { workspace = true }
|
||||
204
packages/common/native/src/hashcash.rs
Normal file
204
packages/common/native/src/hashcash.rs
Normal file
@@ -0,0 +1,204 @@
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use chrono::{DateTime, Duration, NaiveDateTime, Utc};
|
||||
use rand::{
|
||||
distributions::{Alphanumeric, Distribution},
|
||||
thread_rng,
|
||||
};
|
||||
use sha3::{Digest, Sha3_256};
|
||||
|
||||
const SALT_LENGTH: usize = 16;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Stamp {
|
||||
version: String,
|
||||
claim: u32,
|
||||
ts: String,
|
||||
resource: String,
|
||||
ext: String,
|
||||
rand: String,
|
||||
counter: String,
|
||||
}
|
||||
|
||||
impl Stamp {
|
||||
fn check_expiration(&self) -> bool {
|
||||
NaiveDateTime::parse_from_str(&self.ts, "%Y%m%d%H%M%S")
|
||||
.ok()
|
||||
.map(|ts| DateTime::<Utc>::from_naive_utc_and_offset(ts, Utc))
|
||||
.and_then(|utc| {
|
||||
utc
|
||||
.checked_add_signed(Duration::minutes(5))
|
||||
.map(|utc| Utc::now() <= utc)
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn check<S: AsRef<str>>(&self, bits: u32, resource: S) -> bool {
|
||||
if self.version == "1"
|
||||
&& bits <= self.claim
|
||||
&& self.check_expiration()
|
||||
&& self.resource == resource.as_ref()
|
||||
{
|
||||
let hex_digits = ((self.claim as f32) / 4.).floor() as usize;
|
||||
|
||||
// check challenge
|
||||
let mut hasher = Sha3_256::new();
|
||||
hasher.update(self.format().as_bytes());
|
||||
let result = format!("{:x}", hasher.finalize());
|
||||
result[..hex_digits] == String::from_utf8(vec![b'0'; hex_digits]).unwrap()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn format(&self) -> String {
|
||||
format!(
|
||||
"{}:{}:{}:{}:{}:{}:{}",
|
||||
self.version, self.claim, self.ts, self.resource, self.ext, self.rand, self.counter
|
||||
)
|
||||
}
|
||||
|
||||
/// Mint a new hashcash stamp.
|
||||
pub fn mint(resource: String, bits: Option<u32>) -> Self {
|
||||
let version = "1";
|
||||
let now = Utc::now();
|
||||
let ts = now.format("%Y%m%d%H%M%S");
|
||||
let bits = bits.unwrap_or(20);
|
||||
let rand = String::from_iter(
|
||||
Alphanumeric
|
||||
.sample_iter(thread_rng())
|
||||
.take(SALT_LENGTH)
|
||||
.map(char::from),
|
||||
);
|
||||
let challenge = format!("{}:{}:{}:{}:{}:{}", version, bits, ts, &resource, "", rand);
|
||||
|
||||
Stamp {
|
||||
version: version.to_string(),
|
||||
claim: bits,
|
||||
ts: ts.to_string(),
|
||||
resource,
|
||||
ext: "".to_string(),
|
||||
rand,
|
||||
counter: {
|
||||
let mut hasher = Sha3_256::new();
|
||||
let mut counter = 0;
|
||||
let hex_digits = ((bits as f32) / 4.).ceil() as usize;
|
||||
let zeros = String::from_utf8(vec![b'0'; hex_digits]).unwrap();
|
||||
loop {
|
||||
hasher.update(format!("{}:{:x}", challenge, counter).as_bytes());
|
||||
let result = format!("{:x}", hasher.finalize_reset());
|
||||
if result[..hex_digits] == zeros {
|
||||
break format!("{:x}", counter);
|
||||
};
|
||||
counter += 1
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for Stamp {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||
let stamp_vec = value.split(':').collect::<Vec<&str>>();
|
||||
if stamp_vec.len() != 7
|
||||
|| stamp_vec
|
||||
.iter()
|
||||
.enumerate()
|
||||
.any(|(i, s)| i != 4 && s.is_empty())
|
||||
{
|
||||
return Err(format!(
|
||||
"Malformed stamp, expected 6 parts, got {}",
|
||||
stamp_vec.len()
|
||||
));
|
||||
}
|
||||
Ok(Stamp {
|
||||
version: stamp_vec[0].to_string(),
|
||||
claim: stamp_vec[1]
|
||||
.parse()
|
||||
.map_err(|_| "Malformed stamp".to_string())?,
|
||||
ts: stamp_vec[2].to_string(),
|
||||
resource: stamp_vec[3].to_string(),
|
||||
ext: stamp_vec[4].to_string(),
|
||||
rand: stamp_vec[5].to_string(),
|
||||
counter: stamp_vec[6].to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Stamp;
|
||||
use rand::{distributions::Alphanumeric, Rng};
|
||||
use rayon::prelude::*;
|
||||
|
||||
#[test]
|
||||
fn test_mint() {
|
||||
{
|
||||
let response = Stamp::mint("test".into(), Some(20)).format();
|
||||
assert!(
|
||||
Stamp::try_from(response.as_str())
|
||||
.unwrap()
|
||||
.check(20, "test"),
|
||||
"should pass"
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let response = Stamp::mint("test".into(), Some(19)).format();
|
||||
assert!(
|
||||
!Stamp::try_from(response.as_str())
|
||||
.unwrap()
|
||||
.check(20, "test"),
|
||||
"should fail with lower bits"
|
||||
);
|
||||
}
|
||||
{
|
||||
let response = Stamp::mint("test".into(), Some(20)).format();
|
||||
assert!(
|
||||
!Stamp::try_from(response.as_str())
|
||||
.unwrap()
|
||||
.check(20, "test2"),
|
||||
"should fail with different resource"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_expiration() {
|
||||
let response = Stamp::mint("test".into(), Some(20));
|
||||
assert!(response.check_expiration());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format() {
|
||||
let response = Stamp::mint("test".into(), Some(20));
|
||||
assert_eq!(
|
||||
response.format(),
|
||||
format!(
|
||||
"1:20:{}:test::{}:{}",
|
||||
response.ts, response.rand, response.counter
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fuzz() {
|
||||
(0..1000).into_par_iter().for_each(|_| {
|
||||
let bit = rand::random::<u32>() % 20 + 1;
|
||||
let resource = rand::thread_rng()
|
||||
.sample_iter(&Alphanumeric)
|
||||
.take(7)
|
||||
.map(char::from)
|
||||
.collect::<String>();
|
||||
let response = Stamp::mint(resource.clone(), Some(bit)).format();
|
||||
assert!(
|
||||
Stamp::try_from(response.as_str())
|
||||
.unwrap()
|
||||
.check(bit, resource),
|
||||
"should pass"
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
1
packages/common/native/src/lib.rs
Normal file
1
packages/common/native/src/lib.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod hashcash;
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@affine/admin",
|
||||
"version": "0.17.0",
|
||||
"version": "0.18.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@affine/core": "workspace:*",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@affine/android",
|
||||
"version": "0.17.0",
|
||||
"version": "0.18.0",
|
||||
"description": "AFFiNE Desktop Web application",
|
||||
"private": true,
|
||||
"browser": "src/index.tsx",
|
||||
@@ -13,8 +13,8 @@
|
||||
"@affine/component": "workspace:*",
|
||||
"@affine/core": "workspace:*",
|
||||
"@affine/i18n": "workspace:*",
|
||||
"@blocksuite/affine": "0.17.32",
|
||||
"@blocksuite/icons": "^2.1.69",
|
||||
"@blocksuite/affine": "0.17.33",
|
||||
"@blocksuite/icons": "^2.1.70",
|
||||
"@capacitor/android": "^6.1.2",
|
||||
"@capacitor/core": "^6.1.2",
|
||||
"@sentry/react": "^8.0.0",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AffineContext } from '@affine/core/components/context';
|
||||
import { AppFallback } from '@affine/core/mobile/components';
|
||||
import { AppContainer } from '@affine/core/desktop/components/app-container';
|
||||
import { configureMobileModules } from '@affine/core/mobile/modules';
|
||||
import { router } from '@affine/core/mobile/router';
|
||||
import { configureCommonModules } from '@affine/core/modules';
|
||||
@@ -47,7 +47,7 @@ export function App() {
|
||||
<I18nProvider>
|
||||
<AffineContext store={getCurrentStore()}>
|
||||
<RouterProvider
|
||||
fallbackElement={<AppFallback />}
|
||||
fallbackElement={<AppContainer fallback />}
|
||||
router={router}
|
||||
future={future}
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@affine/electron",
|
||||
"private": true,
|
||||
"version": "0.17.0",
|
||||
"version": "0.18.0",
|
||||
"author": "toeverything",
|
||||
"repository": {
|
||||
"url": "https://github.com/toeverything/AFFiNE",
|
||||
@@ -28,7 +28,7 @@
|
||||
"@affine/core": "workspace:*",
|
||||
"@affine/i18n": "workspace:*",
|
||||
"@affine/native": "workspace:*",
|
||||
"@blocksuite/affine": "0.17.32",
|
||||
"@blocksuite/affine": "0.17.33",
|
||||
"@electron-forge/cli": "^7.3.0",
|
||||
"@electron-forge/core": "^7.3.0",
|
||||
"@electron-forge/core-utils": "^7.3.0",
|
||||
|
||||
20
packages/frontend/apps/electron/src/main/cleanup.ts
Normal file
20
packages/frontend/apps/electron/src/main/cleanup.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { app } from 'electron';
|
||||
|
||||
import { logger } from './logger';
|
||||
|
||||
const cleanupRegistry: (() => void)[] = [];
|
||||
|
||||
export function beforeAppQuit(fn: () => void) {
|
||||
cleanupRegistry.push(fn);
|
||||
}
|
||||
|
||||
app.on('before-quit', () => {
|
||||
cleanupRegistry.forEach(fn => {
|
||||
// some cleanup functions might throw on quit and crash the app
|
||||
try {
|
||||
fn();
|
||||
} catch (err) {
|
||||
logger.warn('cleanup error on quit', err);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,8 @@
|
||||
import { app, BrowserWindow, WebContentsView } from 'electron';
|
||||
import { BrowserWindow, WebContentsView } from 'electron';
|
||||
|
||||
import { AFFINE_EVENT_CHANNEL_NAME } from '../shared/type';
|
||||
import { applicationMenuEvents } from './application-menu';
|
||||
import { beforeAppQuit } from './cleanup';
|
||||
import { logger } from './logger';
|
||||
import { sharedStorageEvents } from './shared-storage';
|
||||
import { uiEvents } from './ui/events';
|
||||
@@ -56,14 +57,10 @@ export function registerEvents() {
|
||||
unsubs.push(unsubscribe);
|
||||
}
|
||||
}
|
||||
app.on('before-quit', () => {
|
||||
// subscription on quit sometimes crashes the app
|
||||
unsubs.forEach(unsub => {
|
||||
try {
|
||||
unsub();
|
||||
} catch (err) {
|
||||
logger.warn('unsubscribe error on quit', err);
|
||||
}
|
||||
|
||||
unsubs.forEach(unsub => {
|
||||
beforeAppQuit(() => {
|
||||
unsub();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
|
||||
import type { HelperToMain, MainToHelper } from '../shared/type';
|
||||
import { MessageEventChannel } from '../shared/utils';
|
||||
import { beforeAppQuit } from './cleanup';
|
||||
import { logger } from './logger';
|
||||
|
||||
const HELPER_PROCESS_PATH = path.join(__dirname, './helper.js');
|
||||
@@ -65,7 +66,7 @@ class HelperProcessManager {
|
||||
});
|
||||
});
|
||||
|
||||
app.on('before-quit', () => {
|
||||
beforeAppQuit(() => {
|
||||
this.#process.kill();
|
||||
});
|
||||
}
|
||||
@@ -78,8 +79,12 @@ class HelperProcessManager {
|
||||
renderer.postMessage('helper-connection', null, [rendererPort]);
|
||||
|
||||
return () => {
|
||||
helperPort.close();
|
||||
rendererPort.close();
|
||||
try {
|
||||
helperPort.close();
|
||||
rendererPort.close();
|
||||
} catch (err) {
|
||||
logger.error('[helper] close port error', err);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { app, nativeTheme, shell } from 'electron';
|
||||
import { getLinkPreview } from 'link-preview-js';
|
||||
|
||||
import { isMacOS } from '../../shared/utils';
|
||||
import { persistentConfig } from '../config-storage/persist';
|
||||
import { logger } from '../logger';
|
||||
import type { WorkbenchViewMeta } from '../shared-state-schema';
|
||||
@@ -220,4 +221,15 @@ export const uiHandlers = {
|
||||
app.relaunch();
|
||||
app.quit();
|
||||
},
|
||||
onLanguageChange: async (e, language: string) => {
|
||||
// only works for win/linux
|
||||
// see https://www.electronjs.org/docs/latest/tutorial/spellchecker#how-to-set-the-languages-the-spellchecker-uses
|
||||
if (isMacOS()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.sender.session.availableSpellCheckerLanguages.includes(language)) {
|
||||
e.sender.session.setSpellCheckerLanguages([language, 'en-US']);
|
||||
}
|
||||
},
|
||||
} satisfies NamespaceHandlers;
|
||||
|
||||
@@ -5,6 +5,7 @@ import electronWindowState from 'electron-window-state';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
import { isLinux, isMacOS, isWindows } from '../../shared/utils';
|
||||
import { beforeAppQuit } from '../cleanup';
|
||||
import { buildType } from '../config';
|
||||
import { mainWindowOrigin } from '../constants';
|
||||
import { ensureHelperProcess } from '../helper-process';
|
||||
@@ -116,11 +117,17 @@ export class MainWindowManager {
|
||||
uiSubjects.onFullScreen$.next(mainWindow.isFullScreen());
|
||||
});
|
||||
|
||||
beforeAppQuit(() => {
|
||||
this.cleanupWindows();
|
||||
});
|
||||
|
||||
mainWindow.on('close', e => {
|
||||
// TODO(@pengx17): gracefully close the app, for example, ask user to save unsaved changes
|
||||
e.preventDefault();
|
||||
if (!isMacOS()) {
|
||||
closeAllWindows();
|
||||
this.mainWindowReady = undefined;
|
||||
this.mainWindow$.next(undefined);
|
||||
} else {
|
||||
// hide window on macOS
|
||||
// application quit will be handled by closing the hidden window
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
} from 'rxjs';
|
||||
|
||||
import { isMacOS } from '../../shared/utils';
|
||||
import { beforeAppQuit } from '../cleanup';
|
||||
import { CLOUD_BASE_URL, isDev } from '../config';
|
||||
import { mainWindowOrigin, shellViewUrl } from '../constants';
|
||||
import { ensureHelperProcess } from '../helper-process';
|
||||
@@ -749,8 +750,10 @@ export class WebContentViewsManager {
|
||||
})
|
||||
);
|
||||
|
||||
app.on('before-quit', () => {
|
||||
disposables.forEach(d => d.unsubscribe());
|
||||
disposables.forEach(d => {
|
||||
beforeAppQuit(() => {
|
||||
d.unsubscribe();
|
||||
});
|
||||
});
|
||||
|
||||
const focusActiveView = () => {
|
||||
|
||||
@@ -157,7 +157,11 @@ const createMessagePortChannel = (port: MessagePort): EventBasedChannel => {
|
||||
port.start();
|
||||
return () => {
|
||||
port.onmessage = null;
|
||||
port.close();
|
||||
try {
|
||||
port.close();
|
||||
} catch (err) {
|
||||
console.error('[helper] close port error', err);
|
||||
}
|
||||
};
|
||||
},
|
||||
send(data) {
|
||||
|
||||
@@ -44,4 +44,4 @@ SPEC CHECKSUMS:
|
||||
|
||||
PODFILE CHECKSUM: 1b0d3fe81862c0e9ce712ddd0c5a0accd0097698
|
||||
|
||||
COCOAPODS: 1.16.1
|
||||
COCOAPODS: 1.16.2
|
||||
|
||||
@@ -4,7 +4,17 @@ AFFiNE iOS app.
|
||||
|
||||
## Build
|
||||
|
||||
- yarn install
|
||||
- BUILD_TYPE=canary PUBLIC_PATH="/" yarn workspace @affine/ios build
|
||||
- yarn workspace @affine/ios cap sync
|
||||
- yarn workspace @affine/ios cap open ios
|
||||
- `yarn install`
|
||||
- `BUILD_TYPE=canary PUBLIC_PATH="/" yarn workspace @affine/ios build`
|
||||
- `yarn workspace @affine/ios cap sync`
|
||||
- `yarn workspace @affine/ios cap open ios`
|
||||
|
||||
## Live Reload
|
||||
|
||||
> Capacitor doc: https://capacitorjs.com/docs/guides/live-reload#using-with-framework-clis
|
||||
|
||||
- `yarn install`
|
||||
- `yarn dev`
|
||||
- select `ios` for the "Distribution" option
|
||||
- `yarn workspace @affine/ios sync:dev`
|
||||
- `yarn workspace @affine/ios cap open ios`
|
||||
|
||||
@@ -24,4 +24,12 @@ const config: CapacitorConfig = {
|
||||
},
|
||||
};
|
||||
|
||||
if (process.env.CAP_SERVER_URL) {
|
||||
Object.assign(config, {
|
||||
server: {
|
||||
url: process.env.CAP_SERVER_URL,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export default config;
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
{
|
||||
"name": "@affine/ios",
|
||||
"version": "0.17.0",
|
||||
"version": "0.18.0",
|
||||
"description": "AFFiNE Desktop Web application",
|
||||
"private": true,
|
||||
"browser": "src/index.tsx",
|
||||
"scripts": {
|
||||
"build": "cross-env DISTRIBUTION=ios yarn workspace @affine/cli bundle",
|
||||
"dev": "yarn workspace @affine/cli dev",
|
||||
"sync": "yarn cap sync",
|
||||
"sync:dev": "CAP_SERVER_URL=http://localhost:8080 yarn cap sync",
|
||||
"static-server": "cross-env DISTRIBUTION=ios yarn workspace @affine/cli dev --static"
|
||||
},
|
||||
"dependencies": {
|
||||
"@affine/component": "workspace:*",
|
||||
"@affine/core": "workspace:*",
|
||||
"@affine/i18n": "workspace:*",
|
||||
"@blocksuite/affine": "0.17.32",
|
||||
"@blocksuite/icons": "^2.1.69",
|
||||
"@blocksuite/affine": "0.17.33",
|
||||
"@blocksuite/icons": "^2.1.70",
|
||||
"@capacitor/app": "^6.0.1",
|
||||
"@capacitor/browser": "^6.0.3",
|
||||
"@capacitor/core": "^6.1.2",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { AffineContext } from '@affine/core/components/context';
|
||||
import { AppFallback } from '@affine/core/mobile/components';
|
||||
import { AppContainer } from '@affine/core/desktop/components/app-container';
|
||||
import { configureMobileModules } from '@affine/core/mobile/modules';
|
||||
import { VirtualKeyboardProvider } from '@affine/core/mobile/modules/virtual-keyboard';
|
||||
import { router } from '@affine/core/mobile/router';
|
||||
import { configureCommonModules } from '@affine/core/modules';
|
||||
import {
|
||||
@@ -20,6 +21,7 @@ import {
|
||||
} from '@affine/core/modules/workspace-engine';
|
||||
import { App as CapacitorApp } from '@capacitor/app';
|
||||
import { Browser } from '@capacitor/browser';
|
||||
import { Keyboard } from '@capacitor/keyboard';
|
||||
import {
|
||||
Framework,
|
||||
FrameworkRoot,
|
||||
@@ -76,6 +78,14 @@ framework.impl(ValidatorProvider, {
|
||||
return res.value;
|
||||
},
|
||||
});
|
||||
framework.impl(VirtualKeyboardProvider, {
|
||||
addEventListener: (event, callback) => {
|
||||
Keyboard.addListener(event as any, callback as any);
|
||||
},
|
||||
removeAllListeners: () => {
|
||||
Keyboard.removeAllListeners();
|
||||
},
|
||||
});
|
||||
const frameworkProvider = framework.provider();
|
||||
|
||||
// setup application lifecycle events, and emit application start event
|
||||
@@ -123,7 +133,7 @@ export function App() {
|
||||
<I18nProvider>
|
||||
<AffineContext store={getCurrentStore()}>
|
||||
<RouterProvider
|
||||
fallbackElement={<AppFallback />}
|
||||
fallbackElement={<AppContainer fallback />}
|
||||
router={router}
|
||||
future={future}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@affine/mobile",
|
||||
"version": "0.17.0",
|
||||
"version": "0.18.0",
|
||||
"description": "AFFiNE Desktop Web application",
|
||||
"private": true,
|
||||
"browser": "src/index.tsx",
|
||||
@@ -13,8 +13,8 @@
|
||||
"@affine/component": "workspace:*",
|
||||
"@affine/core": "workspace:*",
|
||||
"@affine/i18n": "workspace:*",
|
||||
"@blocksuite/affine": "0.17.32",
|
||||
"@blocksuite/icons": "^2.1.69",
|
||||
"@blocksuite/affine": "0.17.33",
|
||||
"@blocksuite/icons": "^2.1.70",
|
||||
"@sentry/react": "^8.0.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AffineContext } from '@affine/core/components/context';
|
||||
import { AppFallback } from '@affine/core/mobile/components';
|
||||
import { AppContainer } from '@affine/core/desktop/components/app-container';
|
||||
import { configureMobileModules } from '@affine/core/mobile/modules';
|
||||
import { router } from '@affine/core/mobile/router';
|
||||
import { configureCommonModules } from '@affine/core/modules';
|
||||
@@ -67,7 +67,7 @@ export function App() {
|
||||
<I18nProvider>
|
||||
<AffineContext store={getCurrentStore()}>
|
||||
<RouterProvider
|
||||
fallbackElement={<AppFallback />}
|
||||
fallbackElement={<AppContainer fallback />}
|
||||
router={router}
|
||||
future={future}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@affine/web",
|
||||
"version": "0.17.0",
|
||||
"version": "0.18.0",
|
||||
"description": "AFFiNE Desktop Web application",
|
||||
"private": true,
|
||||
"browser": "src/index.tsx",
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@blocksuite/affine": "*",
|
||||
"@blocksuite/icons": "2.1.67"
|
||||
"@blocksuite/icons": "2.1.68"
|
||||
},
|
||||
"dependencies": {
|
||||
"@affine/cli": "workspace:*",
|
||||
@@ -24,7 +24,7 @@
|
||||
"@affine/i18n": "workspace:*",
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.2.1",
|
||||
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.0.3",
|
||||
"@blocksuite/icons": "2.1.69",
|
||||
"@blocksuite/icons": "2.1.70",
|
||||
"@emotion/react": "^11.11.4",
|
||||
"@emotion/styled": "^11.11.5",
|
||||
"@radix-ui/react-avatar": "^1.0.4",
|
||||
@@ -41,7 +41,7 @@
|
||||
"@radix-ui/react-toast": "^1.1.5",
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"@radix-ui/react-visually-hidden": "^1.1.0",
|
||||
"@toeverything/theme": "^1.0.17",
|
||||
"@toeverything/theme": "^1.0.18",
|
||||
"@vanilla-extract/dynamic": "^2.1.0",
|
||||
"check-password-strength": "^2.0.10",
|
||||
"clsx": "^2.1.0",
|
||||
@@ -63,8 +63,8 @@
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@blocksuite/affine": "0.17.32",
|
||||
"@blocksuite/icons": "2.1.69",
|
||||
"@blocksuite/affine": "0.17.33",
|
||||
"@blocksuite/icons": "2.1.70",
|
||||
"@chromatic-com/storybook": "^3.0.0",
|
||||
"@storybook/addon-essentials": "^8.2.9",
|
||||
"@storybook/addon-interactions": "^8.2.9",
|
||||
@@ -82,5 +82,5 @@
|
||||
"vite": "^5.2.8",
|
||||
"vitest": "2.1.4"
|
||||
},
|
||||
"version": "0.17.0"
|
||||
"version": "0.18.0"
|
||||
}
|
||||
|
||||
@@ -9,10 +9,6 @@ export const useNavConfig = () => {
|
||||
title: t['com.affine.other-page.nav.official-website'](),
|
||||
path: 'https://affine.pro',
|
||||
},
|
||||
{
|
||||
title: t['com.affine.other-page.nav.affine-community'](),
|
||||
path: 'https://community.affine.pro/home',
|
||||
},
|
||||
{
|
||||
title: t['com.affine.other-page.nav.blog'](),
|
||||
path: 'https://affine.pro/blog',
|
||||
|
||||
@@ -21,7 +21,7 @@ export const useAutoFocus = <T extends HTMLElement = HTMLElement>(
|
||||
export const useAutoSelect = <T extends HTMLInputElement = HTMLInputElement>(
|
||||
autoSelect?: boolean
|
||||
) => {
|
||||
const ref = useAutoFocus<T>(autoSelect);
|
||||
const ref = useRef<T | null>(null);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (ref.current && autoSelect) {
|
||||
|
||||
@@ -45,17 +45,20 @@ export const RowInput = forwardRef<HTMLInputElement, RowInputProps>(
|
||||
const focusRef = useAutoFocus<HTMLInputElement>(autoFocus);
|
||||
const selectRef = useAutoSelect<HTMLInputElement>(autoSelect);
|
||||
|
||||
const inputRef = (el: HTMLInputElement | null) => {
|
||||
focusRef.current = el;
|
||||
selectRef.current = el;
|
||||
if (upstreamRef) {
|
||||
if (typeof upstreamRef === 'function') {
|
||||
upstreamRef(el);
|
||||
} else {
|
||||
upstreamRef.current = el;
|
||||
const inputRef = useCallback(
|
||||
(el: HTMLInputElement | null) => {
|
||||
focusRef.current = el;
|
||||
selectRef.current = el;
|
||||
if (upstreamRef) {
|
||||
if (typeof upstreamRef === 'function') {
|
||||
upstreamRef(el);
|
||||
} else {
|
||||
upstreamRef.current = el;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
[focusRef, selectRef, upstreamRef]
|
||||
);
|
||||
|
||||
// use native blur event to get event after unmount
|
||||
// don't use useLayoutEffect here, because the cleanup function will be called before unmount
|
||||
|
||||
@@ -68,7 +68,7 @@ export const modalOverlay = style({
|
||||
animation: 'none',
|
||||
},
|
||||
'&.mobile': {
|
||||
backgroundColor: cssVarV2('layer/mobile/modal'),
|
||||
backgroundColor: cssVarV2('layer/background/mobile/modal'),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@affine/core",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"version": "0.17.0",
|
||||
"version": "0.18.0",
|
||||
"exports": {
|
||||
"./*": "./src/*",
|
||||
"./bootstrap": "./src/bootstrap/index.ts"
|
||||
@@ -16,8 +16,8 @@
|
||||
"@affine/i18n": "workspace:*",
|
||||
"@affine/templates": "workspace:*",
|
||||
"@affine/track": "workspace:*",
|
||||
"@blocksuite/affine": "0.17.32",
|
||||
"@blocksuite/icons": "2.1.69",
|
||||
"@blocksuite/affine": "0.17.33",
|
||||
"@blocksuite/icons": "2.1.70",
|
||||
"@capacitor/app": "^6.0.1",
|
||||
"@capacitor/browser": "^6.0.3",
|
||||
"@dnd-kit/core": "^6.1.0",
|
||||
@@ -37,7 +37,7 @@
|
||||
"@radix-ui/react-toolbar": "^1.0.4",
|
||||
"@sentry/react": "^8.0.0",
|
||||
"@toeverything/pdf-viewer": "^0.1.1",
|
||||
"@toeverything/theme": "^1.0.17",
|
||||
"@toeverything/theme": "^1.0.18",
|
||||
"@vanilla-extract/dynamic": "^2.1.0",
|
||||
"animejs": "^3.2.2",
|
||||
"bytes": "^3.1.2",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { polyfillDispose } from './dispose';
|
||||
import { polyfillIteratorHelpers } from './iterator-helpers';
|
||||
import { polyfillPromise } from './promise-with-resolvers';
|
||||
import { polyfillEventLoop } from './request-idle-callback';
|
||||
import { polyfillResizeObserver } from './resize-observer';
|
||||
@@ -7,3 +8,4 @@ polyfillResizeObserver();
|
||||
polyfillEventLoop();
|
||||
await polyfillPromise();
|
||||
await polyfillDispose();
|
||||
await polyfillIteratorHelpers();
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
export async function polyfillIteratorHelpers() {
|
||||
if (typeof globalThis['Iterator'] !== 'function') {
|
||||
// @ts-expect-error ignore
|
||||
// https://github.com/zloirock/core-js/blob/master/packages/core-js/proposals/iterator-helpers-stage-3.js
|
||||
await import('core-js/proposals/iterator-helpers-stage-3');
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { Skeleton } from '@affine/component';
|
||||
import { Button } from '@affine/component/ui/button';
|
||||
import { UrlService } from '@affine/core/modules/url';
|
||||
import { OAuthProviderType } from '@affine/graphql';
|
||||
import track from '@affine/track';
|
||||
import { GithubIcon, GoogleDuotoneIcon } from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { type ReactElement, useCallback } from 'react';
|
||||
@@ -86,6 +87,8 @@ function OAuthProvider({
|
||||
const oauthUrl =
|
||||
BUILD_CONFIG.serverUrlPrefix + `/oauth/login?${params.toString()}`;
|
||||
|
||||
track.$.$.auth.signIn({ method: 'oauth', provider });
|
||||
|
||||
popupWindow(oauthUrl);
|
||||
}, [popupWindow, provider, redirectUrl, scheme]);
|
||||
|
||||
|
||||
@@ -255,9 +255,7 @@ export function setupTracker() {
|
||||
});
|
||||
|
||||
AIProvider.slots.requestLogin.on(() => {
|
||||
track.$.$.auth.signIn({
|
||||
control: 'aiAction',
|
||||
});
|
||||
track.doc.editor.aiActions.requestSignIn();
|
||||
});
|
||||
|
||||
AIProvider.slots.actions.on(event => {
|
||||
|
||||
@@ -32,10 +32,6 @@ export type EditorProps = {
|
||||
};
|
||||
|
||||
function usePageRoot(page: Doc) {
|
||||
if (!page.ready) {
|
||||
page.load();
|
||||
}
|
||||
|
||||
if (!page.root) {
|
||||
use(
|
||||
new Promise<void>((resolve, reject) => {
|
||||
|
||||
@@ -40,6 +40,7 @@ import type {
|
||||
PeekOptions,
|
||||
PeekViewService as BSPeekViewService,
|
||||
QuickSearchResult,
|
||||
ReferenceNodeConfig,
|
||||
RootBlockConfig,
|
||||
RootService,
|
||||
} from '@blocksuite/affine/blocks';
|
||||
@@ -57,6 +58,7 @@ import {
|
||||
PeekViewExtension,
|
||||
QuickSearchExtension,
|
||||
ReferenceNodeConfigExtension,
|
||||
ReferenceNodeConfigIdentifier,
|
||||
} from '@blocksuite/affine/blocks';
|
||||
import { type BlockSnapshot, Text } from '@blocksuite/affine/store';
|
||||
import {
|
||||
@@ -167,10 +169,10 @@ export function patchNotificationService({
|
||||
<div>
|
||||
<span style={{ marginBottom: 12 }}>{toReactNode(message)}</span>
|
||||
<Input
|
||||
autoSelect={true}
|
||||
placeholder={placeholder}
|
||||
defaultValue={value}
|
||||
onChange={e => (value = e)}
|
||||
ref={input => input?.select()}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -188,6 +190,7 @@ export function patchNotificationService({
|
||||
onCancel: () => {
|
||||
resolve(null);
|
||||
},
|
||||
autoFocusConfirm: false,
|
||||
});
|
||||
abort?.addEventListener('abort', () => {
|
||||
resolve(null);
|
||||
@@ -567,6 +570,17 @@ export function patchForMobile() {
|
||||
});
|
||||
}
|
||||
|
||||
// Hide reference popup on mobile.
|
||||
{
|
||||
const prev = di.getFactory(ReferenceNodeConfigIdentifier);
|
||||
di.override(ReferenceNodeConfigIdentifier, provider => {
|
||||
return {
|
||||
...prev?.(provider),
|
||||
hidePopup: true,
|
||||
} satisfies ReferenceNodeConfig;
|
||||
});
|
||||
}
|
||||
|
||||
// Disable some toolbar widgets for mobile.
|
||||
{
|
||||
di.override(WidgetViewMapIdentifier('affine:page'), () => {
|
||||
|
||||
@@ -11,11 +11,7 @@ import { useBlockSuiteMetaHelper } from '@affine/core/components/hooks/affine/us
|
||||
import { useEnableCloud } from '@affine/core/components/hooks/affine/use-enable-cloud';
|
||||
import { useExportPage } from '@affine/core/components/hooks/affine/use-export-page';
|
||||
import { useDocMetaHelper } from '@affine/core/components/hooks/use-block-suite-page-meta';
|
||||
import {
|
||||
Export,
|
||||
MoveToTrash,
|
||||
Snapshot,
|
||||
} from '@affine/core/components/page-list';
|
||||
import { Export, MoveToTrash } from '@affine/core/components/page-list';
|
||||
import { IsFavoriteIcon } from '@affine/core/components/pure/icons';
|
||||
import { useDetailPageHeaderResponsive } from '@affine/core/desktop/pages/workspace/detail-page/use-header-responsive';
|
||||
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
@@ -44,7 +40,6 @@ import {
|
||||
TocIcon,
|
||||
} from '@blocksuite/icons/rc';
|
||||
import {
|
||||
FeatureFlagService,
|
||||
useLiveData,
|
||||
useService,
|
||||
useServiceOptional,
|
||||
@@ -84,10 +79,6 @@ export const PageHeaderMenuButton = ({
|
||||
const primaryMode = useLiveData(editorService.editor.doc.primaryMode$);
|
||||
|
||||
const workbench = useService(WorkbenchService).workbench;
|
||||
const featureFlagService = useService(FeatureFlagService);
|
||||
const enableSnapshotImportExport = useLiveData(
|
||||
featureFlagService.flags.enable_snapshot_import_export.$
|
||||
);
|
||||
const openInAppService = useServiceOptional(OpenInAppService);
|
||||
|
||||
const { favorite, toggleFavorite } = useFavorite(pageId);
|
||||
@@ -402,7 +393,6 @@ export const PageHeaderMenuButton = ({
|
||||
{t['Import']()}
|
||||
</MenuItem>
|
||||
<Export exportHandler={exportHandler} pageMode={currentMode} />
|
||||
{enableSnapshotImportExport && <Snapshot />}
|
||||
<MenuSeparator />
|
||||
<MoveToTrash
|
||||
data-testid="editor-option-menu-delete"
|
||||
|
||||
@@ -131,11 +131,13 @@ export const DocPropertyRow = ({
|
||||
typeInfo && 'value' in typeInfo ? typeInfo.value : undefined;
|
||||
|
||||
const handleChange = useCallback(
|
||||
(value: any) => {
|
||||
if (typeof value !== 'string') {
|
||||
throw new Error('only allow string value');
|
||||
(value: any, skipCommit?: boolean) => {
|
||||
if (!skipCommit) {
|
||||
if (typeof value !== 'string') {
|
||||
throw new Error('only allow string value');
|
||||
}
|
||||
docService.doc.record.setCustomProperty(propertyInfo.id, value);
|
||||
}
|
||||
docService.doc.record.setCustomProperty(propertyInfo.id, value);
|
||||
onChange?.(value);
|
||||
},
|
||||
[docService, onChange, propertyInfo]
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { TagService, useDeleteTagConfirmModal } from '@affine/core/modules/tag';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import track from '@affine/track';
|
||||
import { TagsIcon } from '@blocksuite/icons/rc';
|
||||
import {
|
||||
LiveData,
|
||||
@@ -26,6 +25,7 @@ interface TagsEditorProps {
|
||||
interface TagsInlineEditorProps extends TagsEditorProps {
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
onChange?: (value: unknown) => void;
|
||||
}
|
||||
|
||||
export const TagsInlineEditor = ({
|
||||
@@ -33,19 +33,18 @@ export const TagsInlineEditor = ({
|
||||
readonly,
|
||||
placeholder,
|
||||
className,
|
||||
onChange,
|
||||
}: TagsInlineEditorProps) => {
|
||||
const workspace = useService(WorkspaceService);
|
||||
const tagService = useService(TagService);
|
||||
const tagIds = useLiveData(tagService.tagList.tagIdsByPageId$(pageId));
|
||||
const tagIds$ = tagService.tagList.tagIdsByPageId$(pageId);
|
||||
const tagIds = useLiveData(tagIds$);
|
||||
const tags = useLiveData(tagService.tagList.tags$);
|
||||
const tagColors = tagService.tagColors;
|
||||
|
||||
const onCreateTag = useCallback(
|
||||
(name: string, color: string) => {
|
||||
const newTag = tagService.tagList.createTag(name, color);
|
||||
track.doc.inlineDocInfo.property.editProperty({
|
||||
type: 'tags',
|
||||
});
|
||||
return {
|
||||
id: newTag.id,
|
||||
value: newTag.value$.value,
|
||||
@@ -58,21 +57,17 @@ export const TagsInlineEditor = ({
|
||||
const onSelectTag = useCallback(
|
||||
(tagId: string) => {
|
||||
tagService.tagList.tagByTagId$(tagId).value?.tag(pageId);
|
||||
track.doc.inlineDocInfo.property.editProperty({
|
||||
type: 'tags',
|
||||
});
|
||||
onChange?.(tagIds$.value);
|
||||
},
|
||||
[pageId, tagService.tagList]
|
||||
[onChange, pageId, tagIds$, tagService.tagList]
|
||||
);
|
||||
|
||||
const onDeselectTag = useCallback(
|
||||
(tagId: string) => {
|
||||
tagService.tagList.tagByTagId$(tagId).value?.untag(pageId);
|
||||
track.doc.inlineDocInfo.property.editProperty({
|
||||
type: 'tags',
|
||||
});
|
||||
onChange?.(tagIds$.value);
|
||||
},
|
||||
[pageId, tagService.tagList]
|
||||
[onChange, pageId, tagIds$, tagService.tagList]
|
||||
);
|
||||
|
||||
const onTagChange = useCallback(
|
||||
@@ -82,11 +77,9 @@ export const TagsInlineEditor = ({
|
||||
} else if (property === 'color') {
|
||||
tagService.tagList.tagByTagId$(id).value?.changeColor(value);
|
||||
}
|
||||
track.doc.inlineDocInfo.property.editProperty({
|
||||
type: 'tags',
|
||||
});
|
||||
onChange?.(tagIds$.value);
|
||||
},
|
||||
[tagService.tagList]
|
||||
[onChange, tagIds$, tagService.tagList]
|
||||
);
|
||||
|
||||
const deleteTags = useDeleteTagConfirmModal();
|
||||
@@ -94,11 +87,9 @@ export const TagsInlineEditor = ({
|
||||
const onTagDelete = useAsyncCallback(
|
||||
async (id: string) => {
|
||||
await deleteTags([id]);
|
||||
track.doc.inlineDocInfo.property.editProperty({
|
||||
type: 'tags',
|
||||
});
|
||||
onChange?.(tagIds$.value);
|
||||
},
|
||||
[deleteTags]
|
||||
[onChange, tagIds$, deleteTags]
|
||||
);
|
||||
|
||||
const adaptedTags = useLiveData(
|
||||
|
||||
@@ -10,8 +10,9 @@ import { DocService, useLiveData, useService } from '@toeverything/infra';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import * as styles from './doc-primary-mode.css';
|
||||
import type { PropertyValueProps } from './types';
|
||||
|
||||
export const DocPrimaryModeValue = () => {
|
||||
export const DocPrimaryModeValue = ({ onChange }: PropertyValueProps) => {
|
||||
const t = useI18n();
|
||||
const doc = useService(DocService).doc;
|
||||
|
||||
@@ -44,8 +45,9 @@ export const DocPrimaryModeValue = () => {
|
||||
? t['com.affine.toastMessage.defaultMode.page.message']()
|
||||
: t['com.affine.toastMessage.defaultMode.edgeless.message'](),
|
||||
});
|
||||
onChange?.(mode, true);
|
||||
},
|
||||
[doc, t]
|
||||
[doc, t, onChange]
|
||||
);
|
||||
return (
|
||||
<PropertyValue className={styles.container} hoverable={false}>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { DocService, useLiveData, useService } from '@toeverything/infra';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import * as styles from './edgeless-theme.css';
|
||||
import type { PropertyValueProps } from './types';
|
||||
|
||||
const getThemeOptions = (t: ReturnType<typeof useI18n>) =>
|
||||
[
|
||||
@@ -21,7 +22,7 @@ const getThemeOptions = (t: ReturnType<typeof useI18n>) =>
|
||||
},
|
||||
] satisfies RadioItem[];
|
||||
|
||||
export const EdgelessThemeValue = () => {
|
||||
export const EdgelessThemeValue = ({ onChange }: PropertyValueProps) => {
|
||||
const t = useI18n();
|
||||
const doc = useService(DocService).doc;
|
||||
const edgelessTheme = useLiveData(doc.properties$).edgelessColorTheme;
|
||||
@@ -29,8 +30,9 @@ export const EdgelessThemeValue = () => {
|
||||
const handleChange = useCallback(
|
||||
(theme: string) => {
|
||||
doc.record.setProperty('edgelessColorTheme', theme);
|
||||
onChange?.(theme, true);
|
||||
},
|
||||
[doc]
|
||||
[doc, onChange]
|
||||
);
|
||||
const themeItems = useMemo<RadioItem[]>(() => getThemeOptions(t), [t]);
|
||||
|
||||
|
||||
@@ -13,8 +13,9 @@ import dayjs from 'dayjs';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import * as styles from './journal.css';
|
||||
import type { PropertyValueProps } from './types';
|
||||
|
||||
export const JournalValue = () => {
|
||||
export const JournalValue = ({ onChange }: PropertyValueProps) => {
|
||||
const t = useI18n();
|
||||
|
||||
const journalService = useService(JournalService);
|
||||
@@ -50,8 +51,9 @@ export const JournalValue = () => {
|
||||
const date = dayjs(day).format('YYYY-MM-DD');
|
||||
setSelectedDate(date);
|
||||
journalService.setJournalDate(doc.id, date);
|
||||
onChange?.(date, true);
|
||||
},
|
||||
[journalService, doc.id]
|
||||
[journalService, doc.id, onChange]
|
||||
);
|
||||
|
||||
const handleCheck = useCallback(
|
||||
@@ -59,11 +61,12 @@ export const JournalValue = () => {
|
||||
if (!v) {
|
||||
journalService.removeJournalDate(doc.id);
|
||||
setShowDatePicker(false);
|
||||
onChange?.(null, true);
|
||||
} else {
|
||||
handleDateSelect(selectedDate);
|
||||
}
|
||||
},
|
||||
[handleDateSelect, journalService, doc.id, selectedDate]
|
||||
[onChange, journalService, doc.id, handleDateSelect, selectedDate]
|
||||
);
|
||||
|
||||
const workbench = useService(WorkbenchService).workbench;
|
||||
@@ -101,11 +104,7 @@ export const JournalValue = () => {
|
||||
onClick={toggle}
|
||||
>
|
||||
<div className={styles.root}>
|
||||
<Checkbox
|
||||
className={styles.checkbox}
|
||||
checked={checked}
|
||||
onChange={handleCheck}
|
||||
/>
|
||||
<Checkbox className={styles.checkbox} checked={checked} />
|
||||
{checked ? (
|
||||
<Menu
|
||||
contentOptions={{
|
||||
|
||||
@@ -5,9 +5,9 @@ import { DocService, useLiveData, useService } from '@toeverything/infra';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { container } from './page-width.css';
|
||||
import type { PageLayoutMode } from './types';
|
||||
import type { PageLayoutMode, PropertyValueProps } from './types';
|
||||
|
||||
export const PageWidthValue = () => {
|
||||
export const PageWidthValue = ({ onChange }: PropertyValueProps) => {
|
||||
const t = useI18n();
|
||||
const editorSetting = useService(EditorSettingService).editorSetting;
|
||||
const defaultPageWidth = useLiveData(editorSetting.settings$).fullWidthLayout;
|
||||
@@ -44,8 +44,9 @@ export const PageWidthValue = () => {
|
||||
const handleChange = useCallback(
|
||||
(value: PageLayoutMode) => {
|
||||
doc.record.setProperty('pageWidth', value);
|
||||
onChange?.(value, true);
|
||||
},
|
||||
[doc]
|
||||
[doc, onChange]
|
||||
);
|
||||
return (
|
||||
<PropertyValue className={container} hoverable={false}>
|
||||
|
||||
@@ -5,8 +5,9 @@ import { DocService, useLiveData, useService } from '@toeverything/infra';
|
||||
|
||||
import { TagsInlineEditor } from '../tags-inline-editor';
|
||||
import * as styles from './tags.css';
|
||||
import type { PropertyValueProps } from './types';
|
||||
|
||||
export const TagsValue = () => {
|
||||
export const TagsValue = ({ onChange }: PropertyValueProps) => {
|
||||
const t = useI18n();
|
||||
|
||||
const doc = useService(DocService).doc;
|
||||
@@ -27,6 +28,7 @@ export const TagsValue = () => {
|
||||
'com.affine.page-properties.property-value-placeholder'
|
||||
]()}
|
||||
pageId={doc.id}
|
||||
onChange={value => onChange(value, true)}
|
||||
/>
|
||||
</PropertyValue>
|
||||
);
|
||||
|
||||
@@ -69,7 +69,6 @@ export const textInvisible = style({
|
||||
visibility: 'hidden',
|
||||
fontSize: cssVar('fontSm'),
|
||||
lineHeight: '22px',
|
||||
padding: `6px`,
|
||||
});
|
||||
|
||||
export const mobileTextInvisible = style([textInvisible, mobileTextareaBase]);
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { DocCustomPropertyInfo } from '@toeverything/infra';
|
||||
export interface PropertyValueProps {
|
||||
propertyInfo?: DocCustomPropertyInfo;
|
||||
value: any;
|
||||
onChange: (value: any) => void;
|
||||
onChange: (value: any, skipCommit?: boolean) => void; // if skipCommit is true, the change will be handled in the component itself
|
||||
}
|
||||
|
||||
export type PageLayoutMode = 'standard' | 'fullWidth';
|
||||
|
||||
@@ -7,14 +7,21 @@ import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import type { Editor } from '@affine/core/modules/editor';
|
||||
import { EditorSettingService } from '@affine/core/modules/editor-setting';
|
||||
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/favorite';
|
||||
import { OpenInAppService } from '@affine/core/modules/open-in-app';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import { EdgelessIcon, HistoryIcon, PageIcon } from '@blocksuite/icons/rc';
|
||||
import {
|
||||
EdgelessIcon,
|
||||
HistoryIcon,
|
||||
LocalWorkspaceIcon,
|
||||
PageIcon,
|
||||
} from '@blocksuite/icons/rc';
|
||||
import {
|
||||
DocService,
|
||||
useLiveData,
|
||||
useService,
|
||||
useServiceOptional,
|
||||
WorkspaceService,
|
||||
} from '@toeverything/infra';
|
||||
import { useSetAtom } from 'jotai';
|
||||
@@ -73,6 +80,8 @@ export function useRegisterBlocksuiteEditorCommands(editor: Editor) {
|
||||
|
||||
const isCloudWorkspace = workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD;
|
||||
|
||||
const openInAppService = useServiceOptional(OpenInAppService);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubs: Array<() => void> = [];
|
||||
const preconditionStrategy = () =>
|
||||
@@ -251,6 +260,23 @@ export function useRegisterBlocksuiteEditorCommands(editor: Editor) {
|
||||
})
|
||||
);
|
||||
|
||||
unsubs.push(
|
||||
registerAffineCommand({
|
||||
id: `editor:${mode}-export-to-snapshot`,
|
||||
preconditionStrategy,
|
||||
category: `editor:${mode}`,
|
||||
icon: mode === 'page' ? <PageIcon /> : <EdgelessIcon />,
|
||||
label: t['Export to Snapshot'](),
|
||||
async run() {
|
||||
track.$.cmdk.editor.export({
|
||||
type: 'snapshot',
|
||||
});
|
||||
|
||||
exportHandler('snapshot');
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
unsubs.push(
|
||||
registerAffineCommand({
|
||||
id: `editor:${mode}-move-to-trash`,
|
||||
@@ -298,6 +324,20 @@ export function useRegisterBlocksuiteEditorCommands(editor: Editor) {
|
||||
);
|
||||
}
|
||||
|
||||
if (isCloudWorkspace && BUILD_CONFIG.isWeb) {
|
||||
unsubs.push(
|
||||
registerAffineCommand({
|
||||
id: 'editor:open-in-app',
|
||||
category: `editor:${mode}`,
|
||||
icon: <LocalWorkspaceIcon />,
|
||||
label: t['com.affine.header.option.open-in-desktop'](),
|
||||
run() {
|
||||
openInAppService?.showOpenInAppPage();
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
unsubs.push(
|
||||
registerAffineCommand({
|
||||
id: 'alert-ctrl-s',
|
||||
@@ -335,5 +375,6 @@ export function useRegisterBlocksuiteEditorCommands(editor: Editor) {
|
||||
pageWidth,
|
||||
defaultPageWidth,
|
||||
checked,
|
||||
openInAppService,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ globalStyle(`${root} > :last-child`, {
|
||||
paddingRight: '8px',
|
||||
});
|
||||
export const titleIconsWrapper = style({
|
||||
padding: '0 5px',
|
||||
width: 34,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '10px',
|
||||
@@ -119,6 +119,8 @@ export const iconCell = style({
|
||||
fontSize: cssVar('fontH3'),
|
||||
color: cssVar('iconColor'),
|
||||
flexShrink: 0,
|
||||
width: 24,
|
||||
height: 24,
|
||||
});
|
||||
export const tagsCell = style({
|
||||
display: 'flex',
|
||||
|
||||
@@ -6,11 +6,11 @@ import {
|
||||
Scrollable,
|
||||
useConfirmModal,
|
||||
} from '@affine/component';
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import { useNavigateHelper } from '@affine/core/components/hooks/use-navigate-helper';
|
||||
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import type { Tag } from '@affine/core/modules/tag';
|
||||
import { TagService } from '@affine/core/modules/tag';
|
||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||
import { isNewTabTrigger } from '@affine/core/utils';
|
||||
import type { Collection } from '@affine/env/filter';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
@@ -41,31 +41,54 @@ import { PageListNewPageButton } from './page-list-new-page-button';
|
||||
|
||||
export const PageListHeader = () => {
|
||||
const t = useI18n();
|
||||
const { workspaceService } = useServices({
|
||||
WorkspaceService,
|
||||
});
|
||||
const { workspaceService, workspaceDialogService, workbenchService } =
|
||||
useServices({
|
||||
WorkspaceService,
|
||||
WorkspaceDialogService,
|
||||
WorkbenchService,
|
||||
});
|
||||
|
||||
const workbench = workbenchService.workbench;
|
||||
const workspace = workspaceService.workspace;
|
||||
const { importFile, createEdgeless, createPage } = usePageHelper(
|
||||
workspace.docCollection
|
||||
);
|
||||
const { createEdgeless, createPage } = usePageHelper(workspace.docCollection);
|
||||
|
||||
const title = useMemo(() => {
|
||||
return t['com.affine.all-pages.header']();
|
||||
}, [t]);
|
||||
|
||||
const onImportFile = useAsyncCallback(async () => {
|
||||
const options = await importFile();
|
||||
if (options.isWorkspaceFile) {
|
||||
track.allDocs.header.actions.createWorkspace({
|
||||
control: 'import',
|
||||
});
|
||||
} else {
|
||||
track.allDocs.header.actions.createDoc({
|
||||
control: 'import',
|
||||
});
|
||||
}
|
||||
}, [importFile]);
|
||||
const handleOpenDocs = useCallback(
|
||||
(result: {
|
||||
docIds: string[];
|
||||
entryId?: string;
|
||||
isWorkspaceFile?: boolean;
|
||||
}) => {
|
||||
const { docIds, entryId, isWorkspaceFile } = result;
|
||||
// If the imported file is a workspace file, open the entry page.
|
||||
if (isWorkspaceFile && entryId) {
|
||||
workbench.openDoc(entryId);
|
||||
} else if (!docIds.length) {
|
||||
return;
|
||||
}
|
||||
// Open all the docs when there are multiple docs imported.
|
||||
if (docIds.length > 1) {
|
||||
workbench.openAll();
|
||||
} else {
|
||||
// Otherwise, open the only doc.
|
||||
workbench.openDoc(docIds[0]);
|
||||
}
|
||||
},
|
||||
[workbench]
|
||||
);
|
||||
|
||||
const onImportFile = useCallback(() => {
|
||||
track.$.header.importModal.open();
|
||||
workspaceDialogService.open('import', undefined, payload => {
|
||||
if (!payload) {
|
||||
return;
|
||||
}
|
||||
handleOpenDocs(payload);
|
||||
});
|
||||
}, [workspaceDialogService, handleOpenDocs]);
|
||||
|
||||
return (
|
||||
<div className={styles.docListHeader}>
|
||||
|
||||
@@ -72,9 +72,10 @@ globalStyle(`${root} > :last-child`, {
|
||||
paddingRight: '8px',
|
||||
});
|
||||
export const titleIconsWrapper = style({
|
||||
padding: '0 5px',
|
||||
width: 34,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '10px',
|
||||
});
|
||||
export const selectionCell = style({
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
ExportToHtmlIcon,
|
||||
ExportToMarkdownIcon,
|
||||
ExportToPngIcon,
|
||||
PageIcon,
|
||||
PrinterIcon,
|
||||
} from '@blocksuite/icons/rc';
|
||||
import type { ReactNode } from 'react';
|
||||
@@ -22,7 +23,9 @@ interface ExportMenuItemProps<T> {
|
||||
}
|
||||
|
||||
interface ExportProps {
|
||||
exportHandler: (type: 'pdf' | 'html' | 'png' | 'markdown') => void;
|
||||
exportHandler: (
|
||||
type: 'pdf' | 'html' | 'png' | 'markdown' | 'snapshot'
|
||||
) => void;
|
||||
pageMode?: 'page' | 'edgeless';
|
||||
className?: string;
|
||||
}
|
||||
@@ -94,6 +97,13 @@ export const ExportMenuItems = ({
|
||||
icon={<ExportToMarkdownIcon />}
|
||||
label={t['Export to Markdown']()}
|
||||
/>
|
||||
<ExportMenuItem
|
||||
onSelect={() => exportHandler('snapshot')}
|
||||
className={className}
|
||||
type="snapshot"
|
||||
icon={<PageIcon />}
|
||||
label={t['Export to Snapshot']()}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,4 +2,3 @@ export * from './disable-public-sharing';
|
||||
export * from './export';
|
||||
// export * from './MoveTo';
|
||||
export * from './move-to-trash';
|
||||
export * from './snapshot';
|
||||
|
||||
@@ -1,223 +0,0 @@
|
||||
import { MenuItem, MenuSeparator, MenuSub, notify } from '@affine/component';
|
||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import track from '@affine/track';
|
||||
import { openFileOrFiles, ZipTransformer } from '@blocksuite/affine/blocks';
|
||||
import type { Doc } from '@blocksuite/affine/store';
|
||||
import { ExportIcon, ImportIcon, ToneIcon } from '@blocksuite/icons/rc';
|
||||
import {
|
||||
FeatureFlagService,
|
||||
useService,
|
||||
WorkspaceService,
|
||||
} from '@toeverything/infra';
|
||||
import { type ReactNode, useCallback } from 'react';
|
||||
|
||||
import { useExportPage } from '../../hooks/affine/use-export-page';
|
||||
import { useAsyncCallback } from '../../hooks/affine-async-hooks';
|
||||
import { transitionStyle } from './index.css';
|
||||
|
||||
interface SnapshotMenuItemsProps {
|
||||
snapshotActionHandler: (action: 'import' | 'export' | 'disable') => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface SnapshotMenuItemProps<T> {
|
||||
onSelect: () => void;
|
||||
className?: string;
|
||||
type: T;
|
||||
icon: ReactNode;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface SnapshotProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function SnapshotMenuItem<T>({
|
||||
onSelect,
|
||||
className,
|
||||
type,
|
||||
icon,
|
||||
label,
|
||||
}: SnapshotMenuItemProps<T>) {
|
||||
return (
|
||||
<MenuItem
|
||||
className={className}
|
||||
data-testid={`snapshot-${type}`}
|
||||
onSelect={onSelect}
|
||||
block
|
||||
prefixIcon={icon}
|
||||
>
|
||||
{label}
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
export const DisableSnapshotMenuItems = ({
|
||||
snapshotActionHandler,
|
||||
className = transitionStyle,
|
||||
}: SnapshotMenuItemsProps) => {
|
||||
const t = useI18n();
|
||||
return (
|
||||
<SnapshotMenuItem
|
||||
onSelect={() => snapshotActionHandler('disable')}
|
||||
className={className}
|
||||
type="disable"
|
||||
icon={<ToneIcon />}
|
||||
label={t['Disable Snapshot']()}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const SnapshotMenuItems = ({
|
||||
snapshotActionHandler,
|
||||
className = transitionStyle,
|
||||
}: SnapshotMenuItemsProps) => {
|
||||
const t = useI18n();
|
||||
return (
|
||||
<>
|
||||
<SnapshotMenuItem
|
||||
onSelect={() => snapshotActionHandler('import')}
|
||||
className={className}
|
||||
type="import"
|
||||
icon={<ImportIcon />}
|
||||
label={t['Import']()}
|
||||
/>
|
||||
<SnapshotMenuItem
|
||||
onSelect={() => snapshotActionHandler('export')}
|
||||
className={className}
|
||||
type="export"
|
||||
icon={<ExportIcon />}
|
||||
label={t['Export']()}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const Snapshot = ({ className }: SnapshotProps) => {
|
||||
const t = useI18n();
|
||||
const workspace = useService(WorkspaceService).workspace;
|
||||
const docCollection = workspace.docCollection;
|
||||
const workbench = useService(WorkbenchService).workbench;
|
||||
const exportHandler = useExportPage();
|
||||
const featureFlagService = useService(FeatureFlagService);
|
||||
|
||||
const importSnapshot = useCallback(async () => {
|
||||
try {
|
||||
const file = await openFileOrFiles({ acceptType: 'Zip' });
|
||||
if (!file) return null;
|
||||
|
||||
track.$.header.snapshot.import({
|
||||
type: 'snapshot',
|
||||
status: 'importing',
|
||||
});
|
||||
|
||||
const importedDocs = (
|
||||
await ZipTransformer.importDocs(docCollection, file)
|
||||
).filter(doc => doc !== undefined);
|
||||
if (importedDocs.length === 0) {
|
||||
notify.error({
|
||||
title: 'Import Snapshot Failed',
|
||||
message: 'No valid documents found in the imported file.',
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
notify.success({
|
||||
title: 'Imported Snapshot Successfully',
|
||||
message: `Imported ${importedDocs.length} doc(s)`,
|
||||
});
|
||||
track.$.header.snapshot.import({
|
||||
type: 'snapshot',
|
||||
status: 'success',
|
||||
result: {
|
||||
docCount: importedDocs.length,
|
||||
},
|
||||
});
|
||||
return importedDocs;
|
||||
} catch (error) {
|
||||
console.error('Error importing snapshot:', error);
|
||||
notify.error({
|
||||
title: 'Import Snapshot Failed',
|
||||
message: 'Failed to import snapshot. Please try again.',
|
||||
});
|
||||
track.$.header.snapshot.import({
|
||||
type: 'snapshot',
|
||||
status: 'failed',
|
||||
error: error instanceof Error ? error.message : undefined,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
}, [docCollection]);
|
||||
|
||||
const openImportedDocs = useCallback(
|
||||
(importedDocs: Doc[]) => {
|
||||
if (importedDocs.length > 1) {
|
||||
workbench.openAll();
|
||||
} else if (importedDocs[0]?.id) {
|
||||
workbench.openDoc(importedDocs[0].id);
|
||||
}
|
||||
},
|
||||
[workbench]
|
||||
);
|
||||
|
||||
const handleImportSnapshot = useAsyncCallback(async () => {
|
||||
const importedDocs = await importSnapshot();
|
||||
if (importedDocs) {
|
||||
openImportedDocs(importedDocs);
|
||||
track.$.header.docOptions.import();
|
||||
track.$.header.actions.createDoc({
|
||||
control: 'import',
|
||||
});
|
||||
}
|
||||
}, [importSnapshot, openImportedDocs]);
|
||||
|
||||
const disableSnapshotActionOption = useCallback(() => {
|
||||
featureFlagService.flags.enable_snapshot_import_export.set(false);
|
||||
}, [featureFlagService]);
|
||||
|
||||
const snapshotActionHandler = useCallback(
|
||||
(action: 'import' | 'export' | 'disable') => {
|
||||
switch (action) {
|
||||
case 'import':
|
||||
return handleImportSnapshot();
|
||||
case 'export':
|
||||
track.$.header.snapshot.export({
|
||||
type: 'snapshot',
|
||||
});
|
||||
return exportHandler('snapshot');
|
||||
case 'disable':
|
||||
return disableSnapshotActionOption();
|
||||
}
|
||||
},
|
||||
[handleImportSnapshot, exportHandler, disableSnapshotActionOption]
|
||||
);
|
||||
|
||||
const items = (
|
||||
<>
|
||||
<SnapshotMenuItems
|
||||
snapshotActionHandler={snapshotActionHandler}
|
||||
className={className}
|
||||
/>
|
||||
<MenuSeparator />
|
||||
<DisableSnapshotMenuItems
|
||||
snapshotActionHandler={snapshotActionHandler}
|
||||
className={className}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<MenuSub
|
||||
items={items}
|
||||
triggerOptions={{
|
||||
className: transitionStyle,
|
||||
prefixIcon: <ToneIcon />,
|
||||
'data-testid': 'snapshot-menu',
|
||||
}}
|
||||
subOptions={{}}
|
||||
>
|
||||
{t['Snapshot']()}
|
||||
</MenuSub>
|
||||
);
|
||||
};
|
||||
@@ -1,30 +0,0 @@
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import { MenuItem } from '@affine/core/modules/app-sidebar/views';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import type { DocCollection } from '@blocksuite/affine/store';
|
||||
import { ImportIcon } from '@blocksuite/icons/rc';
|
||||
|
||||
import { usePageHelper } from '../blocksuite/block-suite-page-list/utils';
|
||||
|
||||
const ImportPage = ({ docCollection }: { docCollection: DocCollection }) => {
|
||||
const t = useI18n();
|
||||
const { importFile } = usePageHelper(docCollection);
|
||||
|
||||
const onImportFile = useAsyncCallback(async () => {
|
||||
const options = await importFile();
|
||||
track.$.navigationPanel.workspaceList[
|
||||
options.isWorkspaceFile ? 'createWorkspace' : 'createDoc'
|
||||
]({
|
||||
control: 'import',
|
||||
});
|
||||
}, [importFile]);
|
||||
|
||||
return (
|
||||
<MenuItem icon={<ImportIcon />} onClick={onImportFile}>
|
||||
{t['Import']()}
|
||||
</MenuItem>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImportPage;
|
||||
@@ -27,7 +27,7 @@ export const SignInItem = () => {
|
||||
const t = useI18n();
|
||||
|
||||
const onClickSignIn = useCallback(() => {
|
||||
track.$.navigationPanel.workspaceList.signIn();
|
||||
track.$.navigationPanel.workspaceList.requestSignIn();
|
||||
setOpen(state => ({
|
||||
...state,
|
||||
openModal: true,
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
type ReactElement,
|
||||
} from 'react';
|
||||
|
||||
import { AppFallback } from './mobile';
|
||||
import * as styles from './styles.css';
|
||||
|
||||
export const AppContainer = ({
|
||||
@@ -102,7 +103,23 @@ const BrowserLayout = ({
|
||||
);
|
||||
};
|
||||
|
||||
const LayoutComponent = BUILD_CONFIG.isElectron ? DesktopLayout : BrowserLayout;
|
||||
const MobileLayout = ({
|
||||
children,
|
||||
fallback = false,
|
||||
}: PropsWithChildren<{ fallback?: boolean }>) => {
|
||||
return (
|
||||
<div className={styles.browserAppViewContainer}>
|
||||
{fallback ? <AppFallback /> : null}
|
||||
<MainContainer>{children}</MainContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const LayoutComponent = BUILD_CONFIG.isElectron
|
||||
? DesktopLayout
|
||||
: BUILD_CONFIG.isMobileEdition
|
||||
? MobileLayout
|
||||
: BrowserLayout;
|
||||
|
||||
const MainContainer = forwardRef<
|
||||
HTMLDivElement,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { SafeArea, Skeleton } from '@affine/component';
|
||||
|
||||
import { WorkspaceSelector } from '../workspace-selector';
|
||||
import { WorkspaceSelector } from '@affine/core/mobile/components';
|
||||
|
||||
const SectionTitleFallback = () => {
|
||||
return (
|
||||
@@ -72,6 +72,15 @@ export const InfoTable = ({
|
||||
});
|
||||
}, []);
|
||||
|
||||
const onPropertyChange = useCallback(
|
||||
(property: DocCustomPropertyInfo, _value: unknown) => {
|
||||
track.$.docInfoPanel.property.editProperty({
|
||||
type: property.type,
|
||||
});
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{backlinks && backlinks.length > 0 ? (
|
||||
@@ -122,6 +131,7 @@ export const InfoTable = ({
|
||||
key={property.id}
|
||||
propertyInfo={property}
|
||||
defaultOpenEditMenu={newPropertyId === property.id}
|
||||
onChange={value => onPropertyChange(property, value)}
|
||||
/>
|
||||
))}
|
||||
<Menu
|
||||
|
||||
@@ -8,6 +8,8 @@ export const container = style({
|
||||
padding: '20px 0',
|
||||
alignSelf: 'start',
|
||||
marginTop: '120px',
|
||||
maxHeight: 'calc(100dvh - 240px)',
|
||||
overflow: 'auto',
|
||||
});
|
||||
|
||||
export const titleContainer = style({
|
||||
|
||||
@@ -8,6 +8,7 @@ import { UrlService } from '@affine/core/modules/url';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import track from '@affine/track';
|
||||
import {
|
||||
HtmlTransformer,
|
||||
MarkdownTransformer,
|
||||
NotionHtmlTransformer,
|
||||
openFileOrFiles,
|
||||
@@ -15,24 +16,22 @@ import {
|
||||
} from '@blocksuite/affine/blocks';
|
||||
import type { DocCollection } from '@blocksuite/affine/store';
|
||||
import {
|
||||
ExportToHtmlIcon,
|
||||
ExportToMarkdownIcon,
|
||||
HelpIcon,
|
||||
NotionIcon,
|
||||
PageIcon,
|
||||
ZipIcon,
|
||||
} from '@blocksuite/icons/rc';
|
||||
import {
|
||||
FeatureFlagService,
|
||||
useLiveData,
|
||||
useService,
|
||||
WorkspaceService,
|
||||
} from '@toeverything/infra';
|
||||
import { useService, WorkspaceService } from '@toeverything/infra';
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { type ReactElement, useCallback, useState } from 'react';
|
||||
|
||||
import * as style from './styles.css';
|
||||
|
||||
type ImportType = 'markdown' | 'markdownZip' | 'notion' | 'snapshot';
|
||||
type AcceptType = 'Markdown' | 'Zip';
|
||||
type ImportType = 'markdown' | 'markdownZip' | 'notion' | 'snapshot' | 'html';
|
||||
type AcceptType = 'Markdown' | 'Zip' | 'Html';
|
||||
type Status = 'idle' | 'importing' | 'success' | 'error';
|
||||
type ImportResult = {
|
||||
docIds: string[];
|
||||
@@ -68,14 +67,31 @@ const importOptions = [
|
||||
key: 'markdownZip',
|
||||
label: 'com.affine.import.markdown-with-media-files',
|
||||
prefixIcon: (
|
||||
<ExportToMarkdownIcon
|
||||
<ZipIcon color={cssVarV2('icon/primary')} width={20} height={20} />
|
||||
),
|
||||
suffixIcon: (
|
||||
<HelpIcon color={cssVarV2('icon/primary')} width={20} height={20} />
|
||||
),
|
||||
suffixTooltip: 'com.affine.import.markdown-with-media-files.tooltip',
|
||||
testId: 'editor-option-menu-import-markdown-with-media',
|
||||
type: 'markdownZip' as ImportType,
|
||||
},
|
||||
{
|
||||
key: 'html',
|
||||
label: 'com.affine.import.html-files',
|
||||
prefixIcon: (
|
||||
<ExportToHtmlIcon
|
||||
color={cssVarV2('icon/primary')}
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
),
|
||||
testId: 'editor-option-menu-import-markdown-with-media',
|
||||
type: 'markdownZip' as ImportType,
|
||||
suffixIcon: (
|
||||
<HelpIcon color={cssVarV2('icon/primary')} width={20} height={20} />
|
||||
),
|
||||
suffixTooltip: 'com.affine.import.html-files.tooltip',
|
||||
testId: 'editor-option-menu-import-html',
|
||||
type: 'html' as ImportType,
|
||||
},
|
||||
{
|
||||
key: 'notion',
|
||||
@@ -88,6 +104,15 @@ const importOptions = [
|
||||
testId: 'editor-option-menu-import-notion',
|
||||
type: 'notion' as ImportType,
|
||||
},
|
||||
{
|
||||
key: 'snapshot',
|
||||
label: 'com.affine.import.snapshot',
|
||||
prefixIcon: (
|
||||
<PageIcon color={cssVarV2('icon/primary')} width={20} height={20} />
|
||||
),
|
||||
testId: 'editor-option-menu-import-snapshot',
|
||||
type: 'snapshot' as ImportType,
|
||||
},
|
||||
];
|
||||
|
||||
const importConfigs: Record<ImportType, ImportConfig> = {
|
||||
@@ -128,6 +153,28 @@ const importConfigs: Record<ImportType, ImportConfig> = {
|
||||
};
|
||||
},
|
||||
},
|
||||
html: {
|
||||
fileOptions: { acceptType: 'Html', multiple: true },
|
||||
importFunction: async (docCollection, files) => {
|
||||
if (!Array.isArray(files)) {
|
||||
throw new Error('Expected an array of files for html files import');
|
||||
}
|
||||
const docIds: string[] = [];
|
||||
for (const file of files) {
|
||||
const text = await file.text();
|
||||
const fileName = file.name.split('.').slice(0, -1).join('.');
|
||||
const docId = await HtmlTransformer.importHTMLToDoc({
|
||||
collection: docCollection,
|
||||
html: text,
|
||||
fileName,
|
||||
});
|
||||
if (docId) docIds.push(docId);
|
||||
}
|
||||
return {
|
||||
docIds,
|
||||
};
|
||||
},
|
||||
},
|
||||
notion: {
|
||||
fileOptions: { acceptType: 'Zip', multiple: false },
|
||||
importFunction: async (docCollection, file) => {
|
||||
@@ -200,10 +247,6 @@ const ImportOptions = ({
|
||||
onImport: (type: ImportType) => void;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const featureFlagService = useService(FeatureFlagService);
|
||||
const enableSnapshotImportExport = useLiveData(
|
||||
featureFlagService.flags.enable_snapshot_import_export.$
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -232,14 +275,6 @@ const ImportOptions = ({
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
{enableSnapshotImportExport && (
|
||||
<div className={style.importModalTip}>
|
||||
{t['Import']()}{' '}
|
||||
<span className={style.link} onClick={() => onImport('snapshot')}>
|
||||
{t['Snapshot']()}.
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className={style.importModalTip}>
|
||||
{t['com.affine.import.modal.tip']()}{' '}
|
||||
<a
|
||||
@@ -373,6 +408,9 @@ export const ImportDialog = ({
|
||||
docCount: docIds.length,
|
||||
},
|
||||
});
|
||||
track.$.importModal.$.createDoc({
|
||||
control: 'import',
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Unknown error occurred';
|
||||
|
||||
@@ -31,6 +31,7 @@ export const AIUsagePanel = ({
|
||||
useEffect(() => {
|
||||
// revalidate latest subscription status
|
||||
subscriptionService.subscription.revalidate();
|
||||
subscriptionService.prices.revalidate();
|
||||
}, [subscriptionService]);
|
||||
const copilotQuotaService = useService(UserCopilotQuotaService);
|
||||
useEffect(() => {
|
||||
|
||||
@@ -10,11 +10,7 @@ import { appIconMap, appNames } from '@affine/core/utils/channel';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { mixpanel } from '@affine/track';
|
||||
import { ArrowRightSmallIcon, OpenInNewIcon } from '@blocksuite/icons/rc';
|
||||
import {
|
||||
FeatureFlagService,
|
||||
useLiveData,
|
||||
useServices,
|
||||
} from '@toeverything/infra';
|
||||
import { useServices } from '@toeverything/infra';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useAppSettingHelper } from '../../../../../components/hooks/affine/use-app-setting-helper';
|
||||
@@ -29,13 +25,9 @@ export const AboutAffine = () => {
|
||||
const channel = BUILD_CONFIG.appBuildType;
|
||||
const appIcon = appIconMap[channel];
|
||||
const appName = appNames[channel];
|
||||
const { urlService, featureFlagService } = useServices({
|
||||
const { urlService } = useServices({
|
||||
UrlService,
|
||||
FeatureFlagService,
|
||||
});
|
||||
const enableSnapshotImportExport = useLiveData(
|
||||
featureFlagService.flags.enable_snapshot_import_export.$
|
||||
);
|
||||
|
||||
const onSwitchAutoCheck = useCallback(
|
||||
(checked: boolean) => {
|
||||
@@ -65,13 +57,6 @@ export const AboutAffine = () => {
|
||||
[updateSettings]
|
||||
);
|
||||
|
||||
const onSwitchSnapshotImportExport = useCallback(
|
||||
(checked: boolean) => {
|
||||
featureFlagService.flags.enable_snapshot_import_export.set(checked);
|
||||
},
|
||||
[featureFlagService]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingHeader
|
||||
@@ -155,16 +140,6 @@ export const AboutAffine = () => {
|
||||
{t['com.affine.aboutAFFiNE.contact.community']()}
|
||||
<OpenInNewIcon className="icon" />
|
||||
</a>
|
||||
<SettingRow
|
||||
name={t['com.affine.snapshot.import-export.enable']()}
|
||||
desc={t['com.affine.snapshot.import-export.enable.desc']()}
|
||||
className={styles.snapshotImportExportRow}
|
||||
>
|
||||
<Switch
|
||||
checked={enableSnapshotImportExport}
|
||||
onChange={onSwitchSnapshotImportExport}
|
||||
/>
|
||||
</SettingRow>
|
||||
</SettingWrapper>
|
||||
<SettingWrapper title={t['com.affine.aboutAFFiNE.community.title']()}>
|
||||
<div className={styles.communityWrapper}>
|
||||
|
||||
@@ -74,7 +74,12 @@ export const AISubscribe = ({
|
||||
onBeforeCheckout={onBeforeCheckout}
|
||||
checkoutOptions={checkoutOptions}
|
||||
renderer={props => (
|
||||
<Button variant="primary" {...props} {...btnProps}>
|
||||
<Button
|
||||
variant="primary"
|
||||
{...props}
|
||||
{...btnProps}
|
||||
data-testid="ai-subscribe-button"
|
||||
>
|
||||
{btnProps.children ?? `${priceReadable} / ${priceFrequency}`}
|
||||
{displayedFrequency === 'monthly' ? (
|
||||
<span
|
||||
|
||||
@@ -135,8 +135,8 @@ const SettingModalInner = ({
|
||||
className={style.wrapper}
|
||||
ref={modalContentWrapperRef}
|
||||
>
|
||||
<div ref={modalContentRef} className={style.centerContainer}>
|
||||
<div className={style.content}>
|
||||
<div className={style.centerContainer}>
|
||||
<div ref={modalContentRef} className={style.content}>
|
||||
<Suspense fallback={<WorkspaceDetailSkeleton />}>
|
||||
{}
|
||||
{settingState.activeTab === 'account' &&
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { usePageHelper } from '@affine/core/components/blocksuite/block-suite-page-list/utils';
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import {
|
||||
AllPageListOperationsMenu,
|
||||
PageDisplayMenu,
|
||||
@@ -7,12 +6,15 @@ import {
|
||||
} from '@affine/core/components/page-list';
|
||||
import { Header } from '@affine/core/components/pure/header';
|
||||
import { WorkspaceModeFilterTab } from '@affine/core/components/pure/workspace-mode-filter-tab';
|
||||
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||
import { isNewTabTrigger } from '@affine/core/utils';
|
||||
import type { Filter } from '@affine/env/filter';
|
||||
import { track } from '@affine/track';
|
||||
import { PlusIcon } from '@blocksuite/icons/rc';
|
||||
import { useServices, WorkspaceService } from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import * as styles from './all-page.css';
|
||||
|
||||
@@ -25,26 +27,49 @@ export const AllPageHeader = ({
|
||||
filters: Filter[];
|
||||
onChangeFilters: (filters: Filter[]) => void;
|
||||
}) => {
|
||||
const { workspaceService } = useServices({
|
||||
WorkspaceService,
|
||||
});
|
||||
const { workspaceService, workspaceDialogService, workbenchService } =
|
||||
useServices({
|
||||
WorkspaceService,
|
||||
WorkspaceDialogService,
|
||||
WorkbenchService,
|
||||
});
|
||||
const workbench = workbenchService.workbench;
|
||||
const workspace = workspaceService.workspace;
|
||||
const { importFile, createEdgeless, createPage } = usePageHelper(
|
||||
workspace.docCollection
|
||||
const { createEdgeless, createPage } = usePageHelper(workspace.docCollection);
|
||||
|
||||
const handleOpenDocs = useCallback(
|
||||
(result: {
|
||||
docIds: string[];
|
||||
entryId?: string;
|
||||
isWorkspaceFile?: boolean;
|
||||
}) => {
|
||||
const { docIds, entryId, isWorkspaceFile } = result;
|
||||
// If the imported file is a workspace file, open the entry page.
|
||||
if (isWorkspaceFile && entryId) {
|
||||
workbench.openDoc(entryId);
|
||||
} else if (!docIds.length) {
|
||||
return;
|
||||
}
|
||||
// Open all the docs when there are multiple docs imported.
|
||||
if (docIds.length > 1) {
|
||||
workbench.openAll();
|
||||
} else {
|
||||
// Otherwise, open the only doc.
|
||||
workbench.openDoc(docIds[0]);
|
||||
}
|
||||
},
|
||||
[workbench]
|
||||
);
|
||||
|
||||
const onImportFile = useAsyncCallback(async () => {
|
||||
const options = await importFile();
|
||||
if (options.isWorkspaceFile) {
|
||||
track.allDocs.header.actions.createWorkspace({
|
||||
control: 'import',
|
||||
});
|
||||
} else {
|
||||
track.allDocs.header.actions.createDoc({
|
||||
control: 'import',
|
||||
});
|
||||
}
|
||||
}, [importFile]);
|
||||
const onImportFile = useCallback(() => {
|
||||
track.$.header.importModal.open();
|
||||
workspaceDialogService.open('import', undefined, payload => {
|
||||
if (!payload) {
|
||||
return;
|
||||
}
|
||||
handleOpenDocs(payload);
|
||||
});
|
||||
}, [workspaceDialogService, handleOpenDocs]);
|
||||
|
||||
return (
|
||||
<Header
|
||||
|
||||
@@ -44,3 +44,7 @@ export const affineDocViewport = style({
|
||||
export const scrollbar = style({
|
||||
marginRight: '4px',
|
||||
});
|
||||
|
||||
export const sidebarScrollArea = style({
|
||||
height: '100%',
|
||||
});
|
||||
|
||||
@@ -302,19 +302,39 @@ const DetailPageImpl = memo(function DetailPageImpl() {
|
||||
)}
|
||||
|
||||
<ViewSidebarTab tabId="properties" icon={<PropertyIcon />}>
|
||||
<DocPropertySidebar />
|
||||
<Scrollable.Root className={styles.sidebarScrollArea}>
|
||||
<Scrollable.Viewport>
|
||||
<DocPropertySidebar />
|
||||
</Scrollable.Viewport>
|
||||
<Scrollable.Scrollbar />
|
||||
</Scrollable.Root>
|
||||
</ViewSidebarTab>
|
||||
|
||||
<ViewSidebarTab tabId="journal" icon={<TodayIcon />}>
|
||||
<EditorJournalPanel />
|
||||
<Scrollable.Root className={styles.sidebarScrollArea}>
|
||||
<Scrollable.Viewport>
|
||||
<EditorJournalPanel />
|
||||
</Scrollable.Viewport>
|
||||
<Scrollable.Scrollbar />
|
||||
</Scrollable.Root>
|
||||
</ViewSidebarTab>
|
||||
|
||||
<ViewSidebarTab tabId="outline" icon={<TocIcon />}>
|
||||
<EditorOutlinePanel editor={editorContainer} />
|
||||
<Scrollable.Root className={styles.sidebarScrollArea}>
|
||||
<Scrollable.Viewport>
|
||||
<EditorOutlinePanel editor={editorContainer} />
|
||||
</Scrollable.Viewport>
|
||||
<Scrollable.Scrollbar />
|
||||
</Scrollable.Root>
|
||||
</ViewSidebarTab>
|
||||
|
||||
<ViewSidebarTab tabId="frame" icon={<FrameIcon />}>
|
||||
<EditorFramePanel editor={editorContainer} />
|
||||
<Scrollable.Root className={styles.sidebarScrollArea}>
|
||||
<Scrollable.Viewport>
|
||||
<EditorFramePanel editor={editorContainer} />
|
||||
</Scrollable.Viewport>
|
||||
<Scrollable.Scrollbar />
|
||||
</Scrollable.Root>
|
||||
</ViewSidebarTab>
|
||||
|
||||
<GlobalPageHistoryModal />
|
||||
|
||||
@@ -9,6 +9,7 @@ import { assignInlineVars } from '@vanilla-extract/dynamic';
|
||||
import React from 'react';
|
||||
import type { Location } from 'react-router-dom';
|
||||
|
||||
import { VirtualKeyboardService } from '../../modules/virtual-keyboard/services/virtual-keyboard';
|
||||
import { AppTabJournal } from './journal';
|
||||
import * as styles from './styles.css';
|
||||
|
||||
@@ -54,14 +55,20 @@ const routes: Route[] = [
|
||||
];
|
||||
|
||||
export const AppTabs = ({ background }: { background?: string }) => {
|
||||
const virtualKeyboardService = useService(VirtualKeyboardService);
|
||||
const virtualKeyboardVisible = useLiveData(virtualKeyboardService.show$);
|
||||
|
||||
return (
|
||||
<SafeArea
|
||||
bottom
|
||||
className={styles.appTabs}
|
||||
bottomOffset={2}
|
||||
style={assignInlineVars({
|
||||
[styles.appTabsBackground]: background,
|
||||
})}
|
||||
style={{
|
||||
...assignInlineVars({
|
||||
[styles.appTabsBackground]: background,
|
||||
}),
|
||||
visibility: virtualKeyboardVisible ? 'hidden' : 'visible',
|
||||
}}
|
||||
>
|
||||
<ul className={styles.appTabsInner} id="app-tabs" role="tablist">
|
||||
{routes.map(route => {
|
||||
|
||||
@@ -7,7 +7,7 @@ export const appTabsBackground = createVar('appTabsBackground');
|
||||
|
||||
export const appTabs = style({
|
||||
vars: {
|
||||
[appTabsBackground]: cssVarV2('layer/background/secondary'),
|
||||
[appTabsBackground]: cssVarV2('layer/background/mobile/primary'),
|
||||
},
|
||||
backgroundColor: appTabsBackground,
|
||||
borderTop: `1px solid ${cssVarV2('layer/insideBorder/border')}`,
|
||||
|
||||
@@ -2,11 +2,13 @@ import { IconButton } from '@affine/component';
|
||||
import { useCatchEventCallback } from '@affine/core/components/hooks/use-catch-event-hook';
|
||||
import { PagePreview } from '@affine/core/components/page-list/page-content-preview';
|
||||
import { IsFavoriteIcon } from '@affine/core/components/pure/icons';
|
||||
import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta';
|
||||
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/favorite';
|
||||
import {
|
||||
WorkbenchLink,
|
||||
type WorkbenchLinkProps,
|
||||
} from '@affine/core/modules/workbench';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import type { DocMeta } from '@blocksuite/affine/store';
|
||||
import { useLiveData, useService, WorkspaceService } from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
@@ -15,11 +17,9 @@ import { forwardRef, type ReactNode, useMemo } from 'react';
|
||||
import * as styles from './styles.css';
|
||||
import { DocCardTags } from './tag';
|
||||
|
||||
const calcRowsById = (id: string) => {
|
||||
const [MIN, MAX] = [2, 8];
|
||||
|
||||
export const calcRowsById = (id: string, min = 2, max = 8) => {
|
||||
const code = id.charCodeAt(0);
|
||||
return Math.floor((code % (MAX - MIN)) + MIN);
|
||||
return Math.floor((code % (max - min)) + min);
|
||||
};
|
||||
|
||||
export interface DocCardProps extends Omit<WorkbenchLinkProps, 'to'> {
|
||||
@@ -40,8 +40,13 @@ export const DocCard = forwardRef<HTMLAnchorElement, DocCardProps>(
|
||||
{ showTags = true, meta, className, autoHeightById, ...attrs },
|
||||
ref
|
||||
) {
|
||||
const t = useI18n();
|
||||
const favAdapter = useService(CompatibleFavoriteItemsAdapter);
|
||||
const workspace = useService(WorkspaceService).workspace;
|
||||
const docDisplayService = useService(DocDisplayMetaService);
|
||||
const titleInfo = useLiveData(docDisplayService.title$(meta.id));
|
||||
const title =
|
||||
typeof titleInfo === 'string' ? titleInfo : t[titleInfo.i18nKey]();
|
||||
|
||||
const favorited = useLiveData(favAdapter.isFavorite$(meta.id, 'doc'));
|
||||
|
||||
@@ -68,9 +73,7 @@ export const DocCard = forwardRef<HTMLAnchorElement, DocCardProps>(
|
||||
{...attrs}
|
||||
>
|
||||
<header className={styles.head} data-testid="doc-card-header">
|
||||
<h3 className={styles.title}>
|
||||
{meta.title || <span className={styles.untitled}>Untitled</span>}
|
||||
</h3>
|
||||
<h3 className={styles.title}>{title}</h3>
|
||||
<IconButton
|
||||
aria-label="favorite"
|
||||
icon={
|
||||
|
||||
@@ -10,7 +10,7 @@ export const card = style({
|
||||
borderRadius: 12,
|
||||
border: `0.5px solid ${cssVarV2('layer/insideBorder/border')}`,
|
||||
boxShadow: '0px 2px 3px rgba(0,0,0,0.05)',
|
||||
background: cssVarV2('layer/background/primary'),
|
||||
background: cssVarV2('layer/background/mobile/secondary'),
|
||||
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
|
||||
@@ -3,6 +3,5 @@ export * from './doc-card';
|
||||
export * from './rename';
|
||||
export * from './search-input';
|
||||
export * from './search-result';
|
||||
export * from './skeletons';
|
||||
export * from './user-plan-tag';
|
||||
export * from './workspace-selector';
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user