Compare commits

..

10 Commits

Author SHA1 Message Date
DarkSky
b05274300c fix: message attachment merge (#8498) 2024-10-21 10:14:48 +08:00
darkskygit
4b9e2abe9f feat: refresh captcha correctly (#8491)
fix AF-1482
2024-10-16 11:35:58 +08:00
renovate
fa2690064d chore: bump up oxlint version to v0.9.10 (#8354)
This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [oxlint](https://oxc.rs) ([source](https://redirect.github.com/oxc-project/oxc/tree/HEAD/npm/oxlint)) | [`0.9.6` -> `0.9.10`](https://renovatebot.com/diffs/npm/oxlint/0.9.6/0.9.10) | [![age](https://developer.mend.io/api/mc/badges/age/npm/oxlint/0.9.10?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/oxlint/0.9.10?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/oxlint/0.9.6/0.9.10?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/oxlint/0.9.6/0.9.10?slim=true)](https://docs.renovatebot.com/merge-confidence/) |

---

### Release Notes

<details>
<summary>oxc-project/oxc (oxlint)</summary>

### [`v0.9.10`](https://redirect.github.com/oxc-project/oxc/releases/tag/oxlint_v0.9.10): oxlint v0.9.10

[Compare Source](https://redirect.github.com/oxc-project/oxc/compare/oxlint_v0.9.9...oxlint_v0.9.10)

#### \[0.9.10] - 2024-10-08

-   [`95ca01c`](https://redirect.github.com/oxc-project/oxc/commit/95ca01c) cfg: \[**BREAKING**] Make BasicBlock::unreachable private ([#&#8203;6321](https://redirect.github.com/oxc-project/oxc/issues/6321)) (DonIsaac)

-   [`5a73a66`](https://redirect.github.com/oxc-project/oxc/commit/5a73a66) regular_expression: \[**BREAKING**] Simplify public APIs ([#&#8203;6262](https://redirect.github.com/oxc-project/oxc/issues/6262)) (leaysgur)

##### Features

-   [`f272137`](https://redirect.github.com/oxc-project/oxc/commit/f272137) editors/vscode: Clear diagnostics on file deletion ([#&#8203;6326](https://redirect.github.com/oxc-project/oxc/issues/6326)) (dalaoshu)
-   [`1a5f293`](https://redirect.github.com/oxc-project/oxc/commit/1a5f293) editors/vscode: Update VSCode extention to use project's language server ([#&#8203;6132](https://redirect.github.com/oxc-project/oxc/issues/6132)) (dalaoshu)
-   [`376cc09`](https://redirect.github.com/oxc-project/oxc/commit/376cc09) linter: Implement `no-throw-literal` ([#&#8203;6144](https://redirect.github.com/oxc-project/oxc/issues/6144)) (dalaoshu)
-   [`5957214`](https://redirect.github.com/oxc-project/oxc/commit/5957214) linter: Allow fixing in files with source offsets ([#&#8203;6197](https://redirect.github.com/oxc-project/oxc/issues/6197)) (camchenry)
-   [`a089e19`](https://redirect.github.com/oxc-project/oxc/commit/a089e19) linter: Eslint/no-else-return ([#&#8203;4305](https://redirect.github.com/oxc-project/oxc/issues/4305)) (yoho)
-   [`183739f`](https://redirect.github.com/oxc-project/oxc/commit/183739f) linter: Implement prefer-await-to-callbacks ([#&#8203;6153](https://redirect.github.com/oxc-project/oxc/issues/6153)) (dalaoshu)
-   [`ae539af`](https://redirect.github.com/oxc-project/oxc/commit/ae539af) linter: Implement no-return-assign ([#&#8203;6108](https://redirect.github.com/oxc-project/oxc/issues/6108)) (Radu Baston)

##### Bug Fixes

-   [`00df6e5`](https://redirect.github.com/oxc-project/oxc/commit/00df6e5) linter: Friendly diagnostic messages for `no-else-return` ([#&#8203;6349](https://redirect.github.com/oxc-project/oxc/issues/6349)) (DonIsaac)
-   [`71ad5d3`](https://redirect.github.com/oxc-project/oxc/commit/71ad5d3) linter: `no-else-return` fixer fails when `else` has no trailing whitespace ([#&#8203;6348](https://redirect.github.com/oxc-project/oxc/issues/6348)) (DonIsaac)
-   [`9e9808b`](https://redirect.github.com/oxc-project/oxc/commit/9e9808b) linter: Fix regression when parsing ts in vue files ([#&#8203;6336](https://redirect.github.com/oxc-project/oxc/issues/6336)) (Boshen)
-   [`93c6db6`](https://redirect.github.com/oxc-project/oxc/commit/93c6db6) linter: Improve docs and diagnostics message for no-else-return ([#&#8203;6327](https://redirect.github.com/oxc-project/oxc/issues/6327)) (DonIsaac)
-   [`e0a3378`](https://redirect.github.com/oxc-project/oxc/commit/e0a3378) linter: Correct false positive in `unicorn/prefer-string-replace-all` ([#&#8203;6263](https://redirect.github.com/oxc-project/oxc/issues/6263)) (H11)
-   [`ea28ee9`](https://redirect.github.com/oxc-project/oxc/commit/ea28ee9) linter: Improve the fixer of `prefer-namespace-keyword` ([#&#8203;6230](https://redirect.github.com/oxc-project/oxc/issues/6230)) (dalaoshu)
-   [`f6a3450`](https://redirect.github.com/oxc-project/oxc/commit/f6a3450) linter: Get correct source offsets for astro files ([#&#8203;6196](https://redirect.github.com/oxc-project/oxc/issues/6196)) (camchenry)
-   [`be0030c`](https://redirect.github.com/oxc-project/oxc/commit/be0030c) linter: Allow whitespace control characters in `no-control-regex` ([#&#8203;6140](https://redirect.github.com/oxc-project/oxc/issues/6140)) (camchenry)
-   [`e7e8ead`](https://redirect.github.com/oxc-project/oxc/commit/e7e8ead) linter: False positive in `no-return-assign` ([#&#8203;6128](https://redirect.github.com/oxc-project/oxc/issues/6128)) (DonIsaac)

##### Performance

-   [`ac0a82a`](https://redirect.github.com/oxc-project/oxc/commit/ac0a82a) linter: Reuse allocator when there are multiple source texts ([#&#8203;6337](https://redirect.github.com/oxc-project/oxc/issues/6337)) (Boshen)
-   [`50a0029`](https://redirect.github.com/oxc-project/oxc/commit/50a0029) linter: Do not concat vec in `no-useless-length-check` ([#&#8203;6276](https://redirect.github.com/oxc-project/oxc/issues/6276)) (camchenry)

##### Documentation

-   [`7ca70dd`](https://redirect.github.com/oxc-project/oxc/commit/7ca70dd) linter: Add docs for `ContextHost` and `LintContext` ([#&#8203;6272](https://redirect.github.com/oxc-project/oxc/issues/6272)) (camchenry)
-   [`a949ecb`](https://redirect.github.com/oxc-project/oxc/commit/a949ecb) linter: Improve docs for `eslint/getter-return` ([#&#8203;6229](https://redirect.github.com/oxc-project/oxc/issues/6229)) (DonIsaac)
-   [`14ba263`](https://redirect.github.com/oxc-project/oxc/commit/14ba263) linter: Improve docs for `eslint-plugin-import` rules ([#&#8203;6131](https://redirect.github.com/oxc-project/oxc/issues/6131)) (dalaoshu)

##### Refactor

-   [`40932f7`](https://redirect.github.com/oxc-project/oxc/commit/40932f7) cfg: Use IndexVec for storing basic blocks ([#&#8203;6323](https://redirect.github.com/oxc-project/oxc/issues/6323)) (DonIsaac)
-   [`642725c`](https://redirect.github.com/oxc-project/oxc/commit/642725c) linter: Rename vars from `ast_node_id` to `node_id` ([#&#8203;6305](https://redirect.github.com/oxc-project/oxc/issues/6305)) (overlookmotel)
-   [`8413175`](https://redirect.github.com/oxc-project/oxc/commit/8413175) linter: Move shared function from utils to rule ([#&#8203;6127](https://redirect.github.com/oxc-project/oxc/issues/6127)) (dalaoshu)
-   [`ba9c372`](https://redirect.github.com/oxc-project/oxc/commit/ba9c372) linter: Make jest/vitest rule mapping more clear ([#&#8203;6273](https://redirect.github.com/oxc-project/oxc/issues/6273)) (camchenry)
-   [`82b8f21`](https://redirect.github.com/oxc-project/oxc/commit/82b8f21) linter: Add schemars and serde traits to AllowWarnDeny and RuleCategories ([#&#8203;6119](https://redirect.github.com/oxc-project/oxc/issues/6119)) (DonIsaac)
-   [`ea908f7`](https://redirect.github.com/oxc-project/oxc/commit/ea908f7) linter: Consolidate file loading logic ([#&#8203;6130](https://redirect.github.com/oxc-project/oxc/issues/6130)) (DonIsaac)
-   [`db751f0`](https://redirect.github.com/oxc-project/oxc/commit/db751f0) linter: Use regexp AST visitor in `no-control-regex` ([#&#8203;6129](https://redirect.github.com/oxc-project/oxc/issues/6129)) (camchenry)
-   [`3aa7e42`](https://redirect.github.com/oxc-project/oxc/commit/3aa7e42) linter: Use RegExp AST visitor for `no-hex-escape` ([#&#8203;6117](https://redirect.github.com/oxc-project/oxc/issues/6117)) (camchenry)
-   [`9d5b44a`](https://redirect.github.com/oxc-project/oxc/commit/9d5b44a) linter: Use regex visitor in `no-regex-spaces` ([#&#8203;6063](https://redirect.github.com/oxc-project/oxc/issues/6063)) (camchenry)
-   [`0d44cf7`](https://redirect.github.com/oxc-project/oxc/commit/0d44cf7) linter: Use regex visitor in `no-useless-escape` ([#&#8203;6062](https://redirect.github.com/oxc-project/oxc/issues/6062)) (camchenry)
-   [`eeb8873`](https://redirect.github.com/oxc-project/oxc/commit/eeb8873) linter: Use regex visitor in `no-empty-character-class` ([#&#8203;6058](https://redirect.github.com/oxc-project/oxc/issues/6058)) (camchenry)

##### Testing

-   [`d883562`](https://redirect.github.com/oxc-project/oxc/commit/d883562) linter: Invalid `eslint/no-unused-vars` options ([#&#8203;6228](https://redirect.github.com/oxc-project/oxc/issues/6228)) (DonIsaac)

### [`v0.9.9`](https://redirect.github.com/oxc-project/oxc/blob/HEAD/npm/oxlint/CHANGELOG.md#099---2024-09-27)

[Compare Source](https://redirect.github.com/oxc-project/oxc/compare/oxlint_v0.9.8...oxlint_v0.9.9)

##### Bug Fixes

-   [`01b9c4b`](https://redirect.github.com/oxc-project/oxc/commit/01b9c4b) npm/oxlint: Make bin/oxc_language_server an executable ([#&#8203;6066](https://redirect.github.com/oxc-project/oxc/issues/6066)) (Boshen)

### [`v0.9.8`](https://redirect.github.com/oxc-project/oxc/releases/tag/oxlint_v0.9.8): oxlint v0.9.8

[Compare Source](https://redirect.github.com/oxc-project/oxc/compare/oxlint_v0.9.7...oxlint_v0.9.8)

#### \[0.9.8] - 2024-09-24

##### Bug Fixes

-   [`e3c8a12`](https://redirect.github.com/oxc-project/oxc/commit/e3c8a12) linter: Fix panic in sort-keys ([#&#8203;6017](https://redirect.github.com/oxc-project/oxc/issues/6017)) (Boshen)
-   [`4771492`](https://redirect.github.com/oxc-project/oxc/commit/4771492) linter: Fix `import/no_cycle` with `ignoreTypes` ([#&#8203;5995](https://redirect.github.com/oxc-project/oxc/issues/5995)) (Boshen)

##### Performance

-   [`5ae3f36`](https://redirect.github.com/oxc-project/oxc/commit/5ae3f36) linter: `no-fallthrough`: Use string matching instead of Regex for default comment pattern ([#&#8203;6008](https://redirect.github.com/oxc-project/oxc/issues/6008)) (camchenry)
-   [`65d8f9e`](https://redirect.github.com/oxc-project/oxc/commit/65d8f9e) linter, ast-tools, coverage: Use `FxHashSet` instead of `std::collections::HashSet` ([#&#8203;6001](https://redirect.github.com/oxc-project/oxc/issues/6001)) (Cam McHenry)
-   [`2b17003`](https://redirect.github.com/oxc-project/oxc/commit/2b17003) linter, prettier, diagnostics: Use `FxHashMap` instead of `std::collections::HashMap` ([#&#8203;5993](https://redirect.github.com/oxc-project/oxc/issues/5993)) (camchenry)

### [`v0.9.7`](https://redirect.github.com/oxc-project/oxc/blob/HEAD/npm/oxlint/CHANGELOG.md#097---2024-09-23)

[Compare Source](https://redirect.github.com/oxc-project/oxc/compare/oxlint_v0.9.6...oxlint_v0.9.7)

##### Refactor

-   [`ba7b01f`](https://redirect.github.com/oxc-project/oxc/commit/ba7b01f) linter: Add `LinterBuilder` ([#&#8203;5714](https://redirect.github.com/oxc-project/oxc/issues/5714)) (DonIsaac)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/toeverything/AFFiNE).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC44MC4wIiwidXBkYXRlZEluVmVyIjoiMzguOTcuMCIsInRhcmdldEJyYW5jaCI6ImNhbmFyeSIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->
2024-10-16 11:27:27 +08:00
pengx17
7381b5e9f1 fix(mobile): handle touch event correctly (#8496)
related:

radix-ui has a hack for dealing with touch event on mobile devices. we may not want to stop propagation for event that is not being handled, even for links

74b182b401/packages/react/dismissable-layer/src/DismissableLayer.tsx (L243-L261)
2024-10-16 11:24:23 +08:00
CatsJuice
9d953104fa chore: bump theme (#8478) 2024-10-16 11:23:35 +08:00
darkskygit
62d2e220ba feat: separate user content from prompt (#8480) 2024-10-16 11:23:22 +08:00
JimmFly
80c92bed90 fix(core): sidebar can not be collapsed on mobile (#8475)
close AF-1474
2024-10-16 11:23:10 +08:00
renovate
38806e2d39 chore: bump up @blocksuite/icons version to v2.1.68 (#8459)
This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [@blocksuite/icons](https://redirect.github.com/toeverything/icons) | [`2.1.67` -> `2.1.68`](https://renovatebot.com/diffs/npm/@blocksuite%2ficons/2.1.67/2.1.68) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@blocksuite%2ficons/2.1.68?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@blocksuite%2ficons/2.1.68?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@blocksuite%2ficons/2.1.67/2.1.68?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@blocksuite%2ficons/2.1.67/2.1.68?slim=true)](https://docs.renovatebot.com/merge-confidence/) |

---

### Release Notes

<details>
<summary>toeverything/icons (@&#8203;blocksuite/icons)</summary>

### [`v2.1.68`](4bdeb1d0ae...10046ca695)

[Compare Source](4bdeb1d0ae...10046ca695)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about these updates again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/toeverything/AFFiNE).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC45Ny4wIiwidXBkYXRlZEluVmVyIjoiMzguMTE1LjEiLCJ0YXJnZXRCcmFuY2giOiJjYW5hcnkiLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIl19-->
2024-10-16 11:23:03 +08:00
renovate
0dbf73be51 chore: bump up @blocksuite/icons version to v2.1.68 (#8458)
This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [@blocksuite/icons](https://redirect.github.com/toeverything/icons) | [`2.1.67` -> `2.1.68`](https://renovatebot.com/diffs/npm/@blocksuite%2ficons/2.1.67/2.1.68) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@blocksuite%2ficons/2.1.68?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@blocksuite%2ficons/2.1.68?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@blocksuite%2ficons/2.1.67/2.1.68?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@blocksuite%2ficons/2.1.67/2.1.68?slim=true)](https://docs.renovatebot.com/merge-confidence/) |

---

### Release Notes

<details>
<summary>toeverything/icons (@&#8203;blocksuite/icons)</summary>

### [`v2.1.68`](4bdeb1d0ae...10046ca695)

[Compare Source](4bdeb1d0ae...10046ca695)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/toeverything/AFFiNE).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC45Ny4wIiwidXBkYXRlZEluVmVyIjoiMzguOTcuMCIsInRhcmdldEJyYW5jaCI6ImNhbmFyeSIsImxhYmVscyI6WyJkZXBlbmRlbmNpZXMiXX0=-->
2024-10-16 11:22:59 +08:00
CatsJuice
5975a6fb2d feat(core): bump theme, update workspace card color variables, add active status (#8467)
close AF-1468
2024-10-16 11:21:53 +08:00
4886 changed files with 63361 additions and 419002 deletions

View File

@@ -1,8 +1,2 @@
[target.x86_64-pc-windows-msvc]
rustflags = ["-C", "target-feature=+crt-static"]
[target.aarch64-pc-windows-msvc]
rustflags = ["-C", "target-feature=+crt-static"]
[target.'cfg(target_os = "linux")']
rustflags = ["-C", "link-args=-Wl,--warn-unresolved-symbols"]
[target.'cfg(target_os = "macos")']
rustflags = ["-C", "link-args=-all_load"]

View File

@@ -9,7 +9,10 @@ corepack prepare yarn@stable --activate
yarn install
# Build Server Dependencies
yarn affine @affine/server-native build
yarn workspace @affine/server-native build
# Create database
yarn affine @affine/server prisma db push
yarn workspace @affine/server prisma db push
# Create user username: affine, password: affine
echo "INSERT INTO \"users\"(\"id\",\"name\",\"email\",\"email_verified\",\"created_at\",\"password\") VALUES('99f3ad04-7c9b-441e-a6db-79f73aa64db9','affine','affine@affine.pro','2024-02-26 15:54:16.974','2024-02-26 15:54:16.974+00','\$argon2id\$v=19\$m=19456,t=2,p=1\$esDS3QCHRH0Kmeh87YPm5Q\$9S+jf+xzw2Hicj6nkWltvaaaXX3dQIxAFwCfFa9o38A');" | yarn workspace @affine/server prisma db execute --stdin

View File

@@ -21,5 +21,6 @@
}
},
"updateContentCommand": "bash ./.devcontainer/build.sh",
"postCreateCommand": "bash ./.devcontainer/setup-user.sh"
"postCreateCommand": "bash ./.devcontainer/setup-user.sh",
"postStartCommand": ["yarn dev", "yarn workspace @affine/server dev"]
}

View File

@@ -11,7 +11,6 @@ services:
network_mode: service:db
environment:
DATABASE_URL: postgresql://affine:affine@db:5432/affine
REDIS_SERVER_HOST: redis
db:
image: postgres:latest
@@ -22,10 +21,6 @@ services:
POSTGRES_PASSWORD: affine
POSTGRES_USER: affine
POSTGRES_DB: affine
redis:
image: redis
ports:
- 6379:6379
volumes:
postgres-data:

View File

@@ -1,4 +0,0 @@
DATABASE_LOCATION=./postgres
DB_PASSWORD=affine
DB_USERNAME=affine
DB_DATABASE_NAME=affine

View File

@@ -1,3 +0,0 @@
postgres
.env
compose.yml

View File

@@ -1,31 +0,0 @@
name: affine_dev_services
services:
postgres:
env_file:
- .env
image: postgres:16
ports:
- 5432:5432
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_USER: ${DB_USERNAME}
POSTGRES_DB: ${DB_DATABASE_NAME}
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:latest
ports:
- 6379:6379
mailhog:
image: mailhog/mailhog:latest
ports:
- 1025:1025
- 8025:8025
networks:
dev:
volumes:
postgres_data:

View File

@@ -1,23 +0,0 @@
# select a revision to deploy, available values: stable, beta, canary
AFFINE_REVISION=stable
# set the port for the server container it will expose the server on
PORT=3010
# set the host for the server for outgoing links
# AFFINE_SERVER_HTTPS=true
# AFFINE_SERVER_HOST=affine.yourdomain.com
# or
# AFFINE_SERVER_EXTERNAL_URL=https://affine.yourdomain.com
# position of the database data to persist
DB_DATA_LOCATION=~/.affine/self-host/postgres/pgdata
# position of the upload data(images, files, etc.) to persist
UPLOAD_LOCATION=~/.affine/self-host/storage
# position of the configuration files to persist
CONFIG_LOCATION=~/.affine/self-host/config
# database credentials
DB_USERNAME=affine
DB_PASSWORD=
DB_DATABASE=affine

View File

@@ -1,74 +0,0 @@
name: affine
services:
affine:
image: ghcr.io/toeverything/affine-graphql:${AFFINE_REVISION:-stable}
container_name: affine_server
ports:
- '${PORT:-3010}:3010'
depends_on:
redis:
condition: service_healthy
postgres:
condition: service_healthy
affine_migration:
condition: service_completed_successfully
volumes:
# custom configurations
- ${UPLOAD_LOCATION}:/root/.affine/storage
- ${CONFIG_LOCATION}:/root/.affine/config
env_file:
- .env
environment:
- REDIS_SERVER_HOST=redis
- DATABASE_URL=postgresql://${DB_USERNAME}:${DB_PASSWORD}@postgres:5432/${DB_DATABASE:-affine}
restart: unless-stopped
affine_migration:
image: ghcr.io/toeverything/affine-graphql:${AFFINE_REVISION:-stable}
container_name: affine_migration_job
volumes:
# custom configurations
- ${UPLOAD_LOCATION}:/root/.affine/storage
- ${CONFIG_LOCATION}:/root/.affine/config
command: ['sh', '-c', 'node ./scripts/self-host-predeploy.js']
env_file:
- .env
environment:
- REDIS_SERVER_HOST=redis
- DATABASE_URL=postgresql://${DB_USERNAME}:${DB_PASSWORD}@postgres:5432/${DB_DATABASE:-affine}
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
redis:
image: redis
container_name: redis
healthcheck:
test: ['CMD', 'redis-cli', '--raw', 'incr', 'ping']
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
postgres:
image: postgres:16
container_name: postgres
volumes:
- ${DB_DATA_LOCATION}:/var/lib/postgresql/data
environment:
POSTGRES_USER: ${DB_USERNAME}
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: ${DB_DATABASE:-affine}
POSTGRES_INITDB_ARGS: '--data-checksums'
# you better set a password for you database
# or you may add 'POSTGRES_HOST_AUTH_METHOD=trust' to ignore postgres security policy
POSTGRES_HOST_AUTH_METHOD: trust
healthcheck:
test:
['CMD', 'pg_isready', '-U', "${DB_USERNAME}", '-d', "${DB_DATABASE:-affine}"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped

7
.env.template Normal file
View File

@@ -0,0 +1,7 @@
CHANGELOG_URL=
ENABLE_NEW_SETTING_UNSTABLE_API=
ENABLE_CAPTCHA=
CAPTCHA_SITE_KEY=
ENABLE_ENHANCE_SHARE_MODE=
ALLOW_LOCAL_WORKSPACE=
DEBUG_JOTAI=

15
.eslintignore Normal file
View File

@@ -0,0 +1,15 @@
node_modules
dist
.next
out
storybook-static
affine-out
_next
lib
.eslintrc.js
e2e-dist-*
static
web-static
public
packages/frontend/i18n/src/i18n-generated.ts
packages/frontend/templates/*.gen.ts

289
.eslintrc.js Normal file
View File

@@ -0,0 +1,289 @@
const { join } = require('node:path');
const createPattern = packageName => [
{
group: ['**/dist', '**/dist/**'],
message: 'Do not import from dist',
allowTypeImports: false,
},
{
group: ['**/src', '**/src/**'],
message: 'Do not import from src',
allowTypeImports: false,
},
{
group: [`@affine/${packageName}`],
message: 'Do not import package itself',
allowTypeImports: false,
},
{
group: [`@toeverything/${packageName}`],
message: 'Do not import package itself',
allowTypeImports: false,
},
{
group: ['@blocksuite/store'],
message: "Import from '@blocksuite/global/utils'",
importNames: ['assertExists', 'assertEquals'],
},
{
group: ['react-router-dom'],
message: 'Use `useNavigateHelper` instead',
importNames: ['useNavigate'],
},
{
group: ['@affine/env/constant'],
message:
'Do not import from @affine/env/constant. Use `BUILD_CONFIG.isElectron` instead',
importNames: ['isElectron'],
},
];
const allPackages = [
'packages/backend/server',
'packages/frontend/component',
'packages/frontend/core',
'packages/frontend/apps/electron',
'packages/frontend/apps/web',
'packages/frontend/apps/mobile',
'packages/frontend/graphql',
'packages/frontend/i18n',
'packages/frontend/native',
'packages/frontend/templates',
'packages/frontend/track',
'packages/common/debug',
'packages/common/env',
'packages/common/infra',
'packages/common/theme',
'tools/cli',
];
/**
* @type {import('eslint').Linter.Config}
*/
const config = {
root: true,
settings: {
react: {
version: 'detect',
},
next: {
rootDir: 'packages/frontend/core',
},
},
extends: [
'eslint:recommended',
'plugin:react-hooks/recommended',
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'plugin:@typescript-eslint/recommended',
'prettier',
],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaFeatures: {
globalReturn: false,
impliedStrict: true,
jsx: true,
},
ecmaVersion: 'latest',
sourceType: 'module',
project: join(__dirname, 'tsconfig.eslint.json'),
},
plugins: [
'react',
'@typescript-eslint',
'simple-import-sort',
'sonarjs',
'import-x',
'unused-imports',
'unicorn',
'rxjs',
],
rules: {
'array-callback-return': 'error',
'no-undef': 'off',
'no-empty': 'off',
'no-func-assign': 'off',
'no-cond-assign': 'off',
'no-constant-binary-expression': 'error',
'no-constructor-return': 'error',
'no-self-compare': 'error',
eqeqeq: ['error', 'always', { null: 'ignore' }],
'react/prop-types': 'off',
'react/jsx-no-useless-fragment': 'error',
'@typescript-eslint/consistent-type-imports': 'error',
'@typescript-eslint/no-non-null-assertion': 'error',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/await-thenable': 'error',
'@typescript-eslint/require-array-sort-compare': 'error',
'@typescript-eslint/unified-signatures': 'error',
'@typescript-eslint/prefer-for-of': 'error',
'@typescript-eslint/no-unused-vars': [
'error',
{
varsIgnorePattern: '^_',
argsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
},
],
'unused-imports/no-unused-imports': 'error',
'simple-import-sort/imports': 'error',
'simple-import-sort/exports': 'error',
'import-x/no-duplicates': 'error',
'@typescript-eslint/ban-ts-comment': [
'error',
{
'ts-expect-error': 'allow-with-description',
'ts-ignore': true,
'ts-nocheck': true,
'ts-check': false,
},
],
'@typescript-eslint/no-restricted-imports': [
'error',
{
patterns: [
{
group: ['**/dist'],
message: "Don't import from dist",
allowTypeImports: false,
},
{
group: ['**/src'],
message: "Don't import from src",
allowTypeImports: false,
},
{
group: ['@blocksuite/store'],
message: "Import from '@blocksuite/global/utils'",
importNames: ['assertExists', 'assertEquals'],
},
],
},
],
'unicorn/filename-case': [
'error',
{
case: 'kebabCase',
ignore: ['^\\[[a-zA-Z0-9-_]+\\]\\.tsx$'],
},
],
'unicorn/no-unnecessary-await': 'error',
'unicorn/no-useless-fallback-in-spread': 'error',
'unicorn/prefer-dom-node-dataset': 'error',
'unicorn/prefer-dom-node-append': 'error',
'unicorn/prefer-dom-node-remove': 'error',
'unicorn/prefer-array-some': 'error',
'unicorn/prefer-date-now': 'error',
'unicorn/prefer-blob-reading-methods': 'error',
'unicorn/no-typeof-undefined': 'error',
'unicorn/no-useless-promise-resolve-reject': 'error',
'unicorn/no-new-array': 'error',
'unicorn/new-for-builtins': 'error',
'unicorn/prefer-node-protocol': 'error',
'sonarjs/no-all-duplicated-branches': 'error',
'sonarjs/no-element-overwrite': 'error',
'sonarjs/no-empty-collection': 'error',
'sonarjs/no-extra-arguments': 'error',
'sonarjs/no-identical-conditions': 'error',
'sonarjs/no-identical-expressions': 'error',
'sonarjs/no-ignored-return': 'error',
'sonarjs/no-one-iteration-loop': 'error',
'sonarjs/no-use-of-empty-return-value': 'error',
'sonarjs/non-existent-operator': 'error',
'sonarjs/no-collapsible-if': 'error',
'sonarjs/no-same-line-conditional': 'error',
'sonarjs/no-duplicated-branches': 'error',
'sonarjs/no-collection-size-mischeck': 'error',
'sonarjs/no-useless-catch': 'error',
'sonarjs/no-identical-functions': 'error',
'rxjs/finnish': [
'error',
{
functions: false,
methods: false,
strict: true,
types: {
'^LiveData$': true,
// some yjs classes are Observables, but they don't need to be in Finnish notation
'^Doc$': false, // yjs Doc
'^Awareness$': false, // yjs Awareness
'^UndoManager$': false, // yjs UndoManager
},
},
],
},
overrides: [
{
files: 'packages/backend/server/**/*.ts',
rules: {
'@typescript-eslint/consistent-type-imports': 0,
},
},
{
files: '*.cjs',
rules: {
'@typescript-eslint/no-var-requires': 0,
},
},
...allPackages.map(pkg => ({
files: [`${pkg}/src/**/*.ts`, `${pkg}/src/**/*.tsx`, `${pkg}/**/*.mjs`],
rules: {
'@typescript-eslint/no-restricted-imports': [
'error',
{
patterns: createPattern(pkg),
},
],
'@typescript-eslint/no-floating-promises': [
'error',
{
ignoreVoid: false,
ignoreIIFE: false,
},
],
'@typescript-eslint/no-misused-promises': ['error'],
'@typescript-eslint/prefer-readonly': 'error',
'import-x/no-extraneous-dependencies': ['error'],
'react-hooks/exhaustive-deps': [
'warn',
{
additionalHooks:
'(useAsyncCallback|useCatchEventCallback|useDraggable|useDropTarget|useRefEffect)',
},
],
},
})),
{
files: [
'**/__tests__/**/*',
'**/*.stories.tsx',
'**/*.spec.ts',
'**/tests/**/*',
'scripts/**/*',
'**/benchmark/**/*',
'**/__debug__/**/*',
'**/e2e/**/*',
],
rules: {
'@typescript-eslint/no-non-null-assertion': 0,
'@typescript-eslint/ban-ts-comment': [
'error',
{
'ts-expect-error': false,
'ts-ignore': true,
'ts-nocheck': true,
'ts-check': false,
},
],
'@typescript-eslint/no-floating-promises': 0,
'@typescript-eslint/no-misused-promises': 0,
'@typescript-eslint/no-restricted-imports': 0,
},
},
],
};
module.exports = config;

1
.github/CODEOWNERS vendored
View File

@@ -1 +0,0 @@
/blocksuite/ @toeverything/blocksuite-core

View File

@@ -7,6 +7,9 @@ inputs:
package:
description: 'Package to build'
required: true
nx_token:
description: 'Nx Cloud access token'
required: false
runs:
using: 'composite'
@@ -34,13 +37,19 @@ runs:
echo "TARGET_CC=clang" >> "$GITHUB_ENV"
- name: Cache cargo
uses: Swatinem/rust-cache@v2
uses: actions/cache@v4
with:
save-if: ${{ github.ref_name == 'canary' }}
shared-key: ${{ inputs.target }}-inputs.package
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
~/.napi-rs
target/${{ inputs.target }}
key: stable-${{ inputs.target }}-cargo-cache
- name: Build
shell: bash
run: |
yarn workspace ${{ inputs.package }} build --target ${{ inputs.target }} --use-napi-cross
yarn workspace ${{ inputs.package }} nx build ${{ inputs.package }} -- --target ${{ inputs.target }} --use-napi-cross
env:
NX_CLOUD_ACCESS_TOKEN: ${{ inputs.nx_token }}
DEBUG: 'napi:*'

View File

@@ -1,36 +0,0 @@
name: 'Run Copilot E2E Test'
description: 'Run Copilot E2E Test'
inputs:
script:
description: 'Script to run'
default: 'yarn affine @affine-test/affine-cloud-copilot e2e --forbid-only'
required: false
openai-key:
description: 'OpenAI secret key'
required: true
fal-key:
description: 'Fal secret key'
required: true
runs:
using: 'composite'
steps:
- name: Prepare Server Test Environment
uses: ./.github/actions/server-test-env
- name: Server Copilot E2E Test
shell: bash
run: ${{ inputs.script }}
env:
COPILOT: true
DEV_SERVER_URL: http://localhost:8080
COPILOT_OPENAI_API_KEY: ${{ inputs.openai-key }}
COPILOT_FAL_API_KEY: ${{ inputs.fal-key }}
- name: Upload test results
if: ${{ failure() }}
uses: actions/upload-artifact@v4
with:
name: test-results-e2e-server-copilot
path: ./test-results
if-no-files-found: ignore

View File

@@ -40,42 +40,6 @@ const isProduction = buildType === 'stable';
const isBeta = buildType === 'beta';
const isInternal = buildType === 'internal';
const replicaConfig = {
stable: {
web: 3,
graphql: Number(process.env.PRODUCTION_GRAPHQL_REPLICA) || 3,
sync: Number(process.env.PRODUCTION_SYNC_REPLICA) || 3,
renderer: Number(process.env.PRODUCTION_RENDERER_REPLICA) || 3,
},
beta: {
web: 2,
graphql: Number(process.env.BETA_GRAPHQL_REPLICA) || 2,
sync: Number(process.env.BETA_SYNC_REPLICA) || 2,
renderer: Number(process.env.BETA_RENDERER_REPLICA) || 2,
},
canary: {
web: 2,
graphql: 2,
sync: 2,
renderer: 2,
},
};
const cpuConfig = {
beta: {
web: '300m',
graphql: '1',
sync: '1',
renderer: '300m',
},
canary: {
web: '300m',
graphql: '1',
sync: '1',
renderer: '300m',
},
};
const createHelmCommand = ({ isDryRun }) => {
const flag = isDryRun ? '--dry-run' : '--atomic';
const imageTag = `${buildType}-${GIT_SHORT_HASH}`;
@@ -103,18 +67,17 @@ const createHelmCommand = ({ isDryRun }) => {
`--set-json cloud-sql-proxy.nodeSelector=\"{ \\"iam.gke.io/gke-metadata-server-enabled\\": \\"true\\" }\"`,
]
: [];
const cpu = cpuConfig[buildType];
const resources = cpu
? [
`--set web.resources.requests.cpu="${cpu.web}"`,
`--set graphql.resources.requests.cpu="${cpu.graphql}"`,
`--set sync.resources.requests.cpu="${cpu.sync}"`,
]
: [];
const replica = replicaConfig[buildType] || replicaConfig.canary;
const webReplicaCount = isProduction ? 3 : isBeta ? 2 : 2;
const graphqlReplicaCount = isProduction
? Number(process.env.PRODUCTION_GRAPHQL_REPLICA) || 3
: isBeta
? Number(process.env.isBeta_GRAPHQL_REPLICA) || 2
: 2;
const syncReplicaCount = isProduction
? Number(process.env.PRODUCTION_SYNC_REPLICA) || 3
: isBeta
? Number(process.env.BETA_SYNC_REPLICA) || 2
: 2;
const namespace = isProduction
? 'production'
: isBeta
@@ -137,9 +100,9 @@ const createHelmCommand = ({ isDryRun }) => {
`--set-string global.objectStorage.r2.secretAccessKey="${R2_SECRET_ACCESS_KEY}"`,
`--set-string global.version="${APP_VERSION}"`,
...redisAndPostgres,
`--set web.replicaCount=${replica.web}`,
`--set web.replicaCount=${webReplicaCount}`,
`--set-string web.image.tag="${imageTag}"`,
`--set graphql.replicaCount=${replica.graphql}`,
`--set graphql.replicaCount=${graphqlReplicaCount}`,
`--set-string graphql.image.tag="${imageTag}"`,
`--set graphql.app.host=${host}`,
`--set graphql.app.captcha.enabled=true`,
@@ -161,13 +124,11 @@ const createHelmCommand = ({ isDryRun }) => {
`--set graphql.app.experimental.enableJwstCodec=${namespace === 'dev'}`,
`--set graphql.app.features.earlyAccessPreview=false`,
`--set graphql.app.features.syncClientVersionCheck=true`,
`--set sync.replicaCount=${replica.sync}`,
`--set sync.replicaCount=${syncReplicaCount}`,
`--set-string sync.image.tag="${imageTag}"`,
`--set-string renderer.image.tag="${imageTag}"`,
`--set renderer.app.host=${host}`,
`--set renderer.replicaCount=${replica.renderer}`,
...serviceAnnotations,
...resources,
`--timeout 10m`,
flag,
].join(' ');

View File

@@ -1,23 +0,0 @@
name: 'Prepare Server Test Environment'
description: 'Prepare Server Test Environment'
runs:
using: 'composite'
steps:
- name: Initialize database
shell: bash
run: |
psql -h localhost -U postgres -c "CREATE DATABASE affine;"
psql -h localhost -U postgres -c "CREATE USER affine WITH PASSWORD 'affine';"
psql -h localhost -U postgres -c "ALTER USER affine WITH SUPERUSER;"
env:
PGPASSWORD: affine
- name: Run init-db script
shell: bash
env:
NODE_ENV: test
run: |
yarn affine @affine/server prisma generate
yarn affine @affine/server prisma db push
yarn affine @affine/server data-migration run

View File

@@ -17,10 +17,6 @@ inputs:
description: 'Download the Electron binary'
required: false
default: 'true'
corepack-install:
description: 'Install CorePack'
required: false
default: 'false'
hard-link-nm:
description: 'set nmMode to hardlinks-local in .yarnrc.yml'
required: false
@@ -46,14 +42,6 @@ runs:
registry-url: https://npm.pkg.github.com
scope: '@toeverything'
- uses: kenchan0130/actions-system-info@master
id: system-info
- name: Init CorePack
if: ${{ inputs.corepack-install == 'true' }}
shell: bash
run: corepack enable
- name: Set nmMode
if: ${{ inputs.hard-link-nm == 'false' }}
shell: bash
@@ -81,7 +69,7 @@ runs:
path: |
node_modules
${{ steps.yarn-cache.outputs.yarn_global_cache }}
key: node_modules-cache-${{ github.job }}-${{ runner.os }}-${{ runner.arch }}-${{ steps.system-info.outputs.name }}-${{ steps.system-info.outputs.release }}-${{ steps.system-info.outputs.version }}
key: node_modules-cache-${{ github.job }}-${{ runner.os }}
# The network performance on macOS is very poor
# and the decompression performance on Windows is very terrible
@@ -92,7 +80,7 @@ runs:
with:
path: |
${{ steps.yarn-cache.outputs.yarn_global_cache }}
key: node_modules-cache-${{ github.job }}-${{ runner.os }}-${{ runner.arch }}-${{ steps.system-info.outputs.name }}-${{ steps.system-info.outputs.release }}-${{ steps.system-info.outputs.version }}
key: node_modules-cache-${{ github.job }}-${{ runner.os }}
- name: Cache full yarn cache on Linux
uses: actions/cache@v4
@@ -101,7 +89,7 @@ runs:
path: |
node_modules
${{ steps.yarn-cache.outputs.yarn_global_cache }}
key: node_modules-cache-full-${{ runner.os }}-${{ runner.arch }}-${{ steps.system-info.outputs.name }}-${{ steps.system-info.outputs.release }}-${{ steps.system-info.outputs.version }}
key: node_modules-cache-full-${{ runner.os }}
- name: Cache full yarn cache on non-Linux
uses: actions/cache@v4
@@ -109,7 +97,7 @@ runs:
with:
path: |
${{ steps.yarn-cache.outputs.yarn_global_cache }}
key: node_modules-cache-full-${{ runner.os }}-${{ runner.arch }}-${{ steps.system-info.outputs.name }}-${{ steps.system-info.outputs.release }}-${{ steps.system-info.outputs.version }}
key: node_modules-cache-full-${{ runner.os }}
- name: yarn install
if: ${{ inputs.package-install == 'true' }}
@@ -151,7 +139,7 @@ runs:
if: ${{ inputs.playwright-install == 'true' }}
with:
path: ${{ github.workspace }}/node_modules/.cache/ms-playwright
key: '${{ runner.os }}-${{ runner.arch }}-${{ steps.system-info.outputs.name }}-${{ steps.system-info.outputs.release }}-${{ steps.system-info.outputs.version }}-playwright-${{ steps.playwright-version.outputs.version }}'
key: '${{ runner.os }}-playwright-${{ steps.playwright-version.outputs.version }}'
# As a fallback, if the Playwright version has changed, try use the
# most recently cached version. There's a good chance that at least one
# of the browser binary versions haven't been updated, so Playwright can
@@ -161,7 +149,7 @@ runs:
# date cache, but still let Playwright decide if it needs to download
# new binaries or not.
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-${{ steps.system-info.outputs.name }}-${{ steps.system-info.outputs.release }}-${{ steps.system-info.outputs.version }}-playwright-
${{ runner.os }}-playwright-
# If the Playwright browser binaries weren't able to be restored, we tell
# playwright to install everything for us.
@@ -184,9 +172,9 @@ runs:
if: ${{ inputs.electron-install == 'true' }}
with:
path: 'node_modules/.cache/electron'
key: '${{ runner.os }}-${{ runner.arch }}-${{ steps.system-info.outputs.name }}-${{ steps.system-info.outputs.release }}-${{ steps.system-info.outputs.version }}-electron-${{ steps.electron-version.outputs.version }}'
key: '${{ runner.os }}-electron-${{ steps.electron-version.outputs.version }}'
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-${{ steps.system-info.outputs.name }}-${{ steps.system-info.outputs.release }}-${{ steps.system-info.outputs.version }}-electron-
${{ runner.os }}-electron-
- name: Install Electron binary
shell: bash

View File

@@ -1,28 +0,0 @@
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

View File

@@ -1,4 +1,4 @@
FROM openresty/openresty:1.27.1.1-0-buster
FROM openresty/openresty:1.25.3.2-0-buster
WORKDIR /app
COPY ./packages/frontend/apps/web/dist ./dist
COPY ./packages/frontend/admin/dist ./admin

View File

@@ -3,4 +3,4 @@ name: affine
description: AFFiNE cloud chart
type: application
version: 0.0.0
appVersion: "0.19.0"
appVersion: "0.17.0"

View File

@@ -3,7 +3,7 @@ name: graphql
description: AFFiNE GraphQL server
type: application
version: 0.0.0
appVersion: "0.19.0"
appVersion: "0.17.0"
dependencies:
- name: gcloud-sql-proxy
version: 0.0.0

View File

@@ -3,7 +3,7 @@ name: sync
description: AFFiNE Sync Server
type: application
version: 0.0.0
appVersion: "0.19.0"
appVersion: "0.17.0"
dependencies:
- name: gcloud-sql-proxy
version: 0.0.0

20
.github/renovate.json vendored
View File

@@ -7,13 +7,15 @@
"**/bower_components/**",
"**/vendor/**",
"**/examples/**",
"**/__tests__/**"
"**/__tests__/**",
"**/test/**",
"**/__fixtures__/**"
],
"packageRules": [
{
"matchPackagePatterns": ["^eslint", "^@typescript-eslint"],
"rangeStrategy": "replace",
"groupName": "linter",
"matchPackageNames": ["/^eslint/", "/^@typescript-eslint/"]
"groupName": "linter"
},
{
"matchDepNames": ["oxlint"],
@@ -22,15 +24,17 @@
},
{
"groupName": "blocksuite",
"matchPackagePatterns": ["^@blocksuite"],
"excludePackageNames": ["@blocksuite/icons"],
"rangeStrategy": "replace",
"changelogUrl": "https://github.com/toeverything/blocksuite/blob/master/packages/blocks/CHANGELOG.md",
"matchPackageNames": ["/^@blocksuite/", "!@blocksuite/icons"]
"changelogUrl": "https://github.com/toeverything/blocksuite/blob/master/packages/blocks/CHANGELOG.md"
},
{
"groupName": "all non-major dependencies",
"groupSlug": "all-minor-patch",
"matchUpdateTypes": ["minor", "patch"],
"matchPackageNames": ["*", "!/^@blocksuite//", "!/oxlint/"]
"matchPackagePatterns": ["*"],
"excludePackagePatterns": ["^@blocksuite/", "oxlint"],
"matchUpdateTypes": ["minor", "patch"]
},
{
"groupName": "rust toolchain",
@@ -39,7 +43,7 @@
},
{
"groupName": "nestjs",
"matchPackageNames": ["/^@nestjs/"]
"matchPackagePatterns": ["^@nestjs"]
}
],
"commitMessagePrefix": "chore: ",

View File

@@ -7,6 +7,9 @@ on:
type: string
required: true
env:
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
permissions:
contents: 'write'
id-token: 'write'
@@ -47,7 +50,7 @@ jobs:
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Build Core
run: yarn affine @affine/web build
run: yarn nx build @affine/web --skip-nx-cache
env:
R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
@@ -80,7 +83,7 @@ jobs:
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Build Admin
run: yarn affine @affine/admin build
run: yarn nx build @affine/admin --skip-nx-cache
env:
R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
@@ -112,7 +115,7 @@ jobs:
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Build Mobile
run: yarn affine @affine/mobile build
run: yarn nx build @affine/mobile --skip-nx-cache
env:
R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
@@ -132,11 +135,87 @@ jobs:
path: ./packages/frontend/apps/mobile/dist
if-no-files-found: error
build-web-selfhost:
name: Build @affine/web selfhost
runs-on: ubuntu-latest
environment: ${{ github.event.inputs.flavor }}
steps:
- uses: actions/checkout@v4
- name: Setup Version
id: version
uses: ./.github/actions/setup-version
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Build Core
run: yarn nx build @affine/web --skip-nx-cache
env:
BUILD_TYPE: ${{ github.event.inputs.flavor }}
PUBLIC_PATH: '/'
SELF_HOSTED: true
MIXPANEL_TOKEN: ${{ secrets.MIXPANEL_TOKEN }}
- name: Download selfhost fonts
run: node ./scripts/download-blocksuite-fonts.mjs
- name: Upload web artifact
uses: actions/upload-artifact@v4
with:
name: selfhost-web
path: ./packages/frontend/apps/web/dist
if-no-files-found: error
build-mobile-selfhost:
name: Build @affine/mobile selfhost
runs-on: ubuntu-latest
environment: ${{ github.event.inputs.flavor }}
steps:
- uses: actions/checkout@v4
- name: Setup Version
id: version
uses: ./.github/actions/setup-version
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Build Mobile
run: yarn nx build @affine/mobile --skip-nx-cache
env:
BUILD_TYPE: ${{ github.event.inputs.flavor }}
PUBLIC_PATH: '/'
SELF_HOSTED: true
MIXPANEL_TOKEN: ${{ secrets.MIXPANEL_TOKEN }}
- name: Upload mobile artifact
uses: actions/upload-artifact@v4
with:
name: selfhost-mobile
path: ./packages/frontend/apps/mobile/dist
if-no-files-found: error
build-admin-selfhost:
name: Build @affine/admin selfhost
runs-on: ubuntu-latest
environment: ${{ github.event.inputs.flavor }}
steps:
- uses: actions/checkout@v4
- name: Setup Version
id: version
uses: ./.github/actions/setup-version
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Build admin
run: yarn nx build @affine/admin --skip-nx-cache
env:
BUILD_TYPE: ${{ github.event.inputs.flavor }}
PUBLIC_PATH: '/admin/'
SELF_HOSTED: true
MIXPANEL_TOKEN: ${{ secrets.MIXPANEL_TOKEN }}
- name: Upload admin artifact
uses: actions/upload-artifact@v4
with:
name: selfhost-admin
path: ./packages/frontend/admin/dist
if-no-files-found: error
build-server-native:
name: Build Server native - ${{ matrix.targets.name }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
targets:
- name: x86_64-unknown-linux-gnu
@@ -161,6 +240,7 @@ jobs:
with:
target: ${{ matrix.targets.name }}
package: '@affine/server-native'
nx_token: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
- name: Upload ${{ matrix.targets.file }}
uses: actions/upload-artifact@v4
with:
@@ -176,6 +256,9 @@ jobs:
- build-web
- build-mobile
- build-admin
- build-web-selfhost
- build-mobile-selfhost
- build-admin-selfhost
- build-server-native
steps:
- uses: actions/checkout@v4
@@ -251,6 +334,24 @@ jobs:
name: admin
path: ./packages/frontend/admin/dist
- name: Download selfhost web artifact
uses: actions/download-artifact@v4
with:
name: selfhost-web
path: ./packages/frontend/apps/web/dist/selfhost
- name: Download selfhost mobile artifact
uses: actions/download-artifact@v4
with:
name: selfhost-mobile
path: ./packages/frontend/apps/mobile/dist/selfhost
- name: Download selfhost admin artifact
uses: actions/download-artifact@v4
with:
name: selfhost-admin
path: ./packages/frontend/admin/dist/selfhost
- name: Install Node.js dependencies
run: |
yarn config set --json supportedArchitectures.cpu '["x64", "arm64", "arm"]'

View File

@@ -19,6 +19,7 @@ env:
AFFINE_ENV: dev
COVERAGE: true
MACOSX_DEPLOYMENT_TARGET: '10.13'
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/node_modules/.cache/ms-playwright
DEPLOYMENT_TYPE: affine
@@ -27,24 +28,9 @@ concurrency:
cancel-in-progress: true
jobs:
optimize_ci:
name: Optimize CI
runs-on: ubuntu-latest
outputs:
skip: ${{ steps.check_skip.outputs.skip }}
steps:
- uses: actions/checkout@v4
- name: Graphite CI Optimizer
uses: withgraphite/graphite-ci-action@main
id: check_skip
with:
graphite_token: ${{ secrets.GRAPHITE_CI_OPTIMIZER_TOKEN }}
analyze:
name: Analyze
runs-on: ubuntu-latest
needs: optimize_ci
if: needs.optimize_ci.outputs.skip == 'false'
permissions:
actions: read
contents: read
@@ -92,8 +78,6 @@ jobs:
lint:
name: Lint
runs-on: ubuntu-latest
needs: optimize_ci
if: needs.optimize_ci.outputs.skip == 'false'
steps:
- uses: actions/checkout@v4
@@ -106,7 +90,7 @@ jobs:
electron-install: false
full-cache: true
- name: Run i18n codegen
run: yarn affine @affine/i18n build
run: yarn i18n-codegen gen
- name: Run ESLint
run: yarn lint:eslint --max-warnings=0
- name: Run Prettier
@@ -122,8 +106,6 @@ jobs:
check-yarn-binary:
name: Check yarn binary
runs-on: ubuntu-latest
needs: optimize_ci
if: needs.optimize_ci.outputs.skip == 'false'
steps:
- uses: actions/checkout@v4
- name: Run check
@@ -131,43 +113,9 @@ jobs:
yarn set version $(node -e "console.log(require('./package.json').packageManager.split('@')[1])")
git diff --exit-code
e2e-legacy-blocksuite-test:
name: Legacy Blocksuite E2E Test
runs-on: ubuntu-latest
needs: optimize_ci
if: needs.optimize_ci.outputs.skip == 'false'
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
playwright-install: true
electron-install: false
full-cache: true
- name: Run playground build
run: yarn workspace @blocksuite/playground build
- name: Run playwright tests
run: yarn workspace @blocksuite/legacy-e2e test --forbid-only --shard=${{ matrix.shard }}/${{ strategy.job-total }}
- name: Upload test results
if: ${{ failure() }}
uses: actions/upload-artifact@v4
with:
name: test-results-e2e-legacy-bs-${{ matrix.shard }}
path: ./test-results
if-no-files-found: ignore
e2e-test:
name: E2E Test
runs-on: ubuntu-latest
needs: optimize_ci
if: needs.optimize_ci.outputs.skip == 'false'
env:
DISTRIBUTION: web
IN_CI_TEST: true
@@ -185,7 +133,7 @@ jobs:
full-cache: true
- name: Run playwright tests
run: yarn affine @affine-test/affine-local e2e --forbid-only --shard=${{ matrix.shard }}/${{ strategy.job-total }}
run: yarn workspace @affine-test/affine-local e2e --forbid-only --shard=${{ matrix.shard }}/${{ strategy.job-total }}
- name: Upload test results
if: ${{ failure() }}
@@ -198,8 +146,6 @@ jobs:
e2e-mobile-test:
name: E2E Mobile Test
runs-on: ubuntu-latest
needs: optimize_ci
if: needs.optimize_ci.outputs.skip == 'false'
env:
DISTRIBUTION: mobile
IN_CI_TEST: true
@@ -217,7 +163,7 @@ jobs:
full-cache: true
- name: Run playwright tests
run: yarn affine @affine-test/affine-mobile e2e --forbid-only --shard=${{ matrix.shard }}/${{ strategy.job-total }}
run: yarn workspace @affine-test/affine-mobile e2e --forbid-only --shard=${{ matrix.shard }}/${{ strategy.job-total }}
- name: Upload test results
if: ${{ failure() }}
@@ -227,26 +173,44 @@ jobs:
path: ./test-results
if-no-files-found: ignore
e2e-migration-test:
name: E2E Migration Test
runs-on: ubuntu-latest
env:
DISTRIBUTION: web
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
playwright-install: true
electron-install: false
full-cache: true
- name: Run playwright tests
run: yarn workspace @affine-test/affine-migration e2e --forbid-only
- name: Upload test results
if: ${{ failure() }}
uses: actions/upload-artifact@v4
with:
name: test-results-e2e-migration
path: ./test-results
if-no-files-found: ignore
unit-test:
name: Unit Test
runs-on: ubuntu-latest
needs:
- optimize_ci
- build-native
if: needs.optimize_ci.outputs.skip == 'false'
env:
DISTRIBUTION: web
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4, 5]
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
electron-install: true
playwright-install: true
full-cache: true
- name: Download affine.linux-x64-gnu.node
@@ -256,10 +220,10 @@ jobs:
path: ./packages/frontend/native
- name: Unit Test
run: yarn test:coverage --shard=${{ matrix.shard }}/${{ strategy.job-total }}
run: yarn nx test:coverage @affine/monorepo
- name: Upload unit test coverage results
uses: codecov/codecov-action@v5
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./.coverage/store/lcov.info
@@ -270,8 +234,6 @@ jobs:
build-native:
name: Build AFFiNE native (${{ matrix.spec.target }})
runs-on: ${{ matrix.spec.os }}
needs: optimize_ci
if: needs.optimize_ci.outputs.skip == 'false'
env:
CARGO_PROFILE_RELEASE_DEBUG: '1'
strategy:
@@ -301,6 +263,7 @@ jobs:
with:
target: ${{ matrix.spec.target }}
package: '@affine/native'
nx_token: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
- name: Upload ${{ steps.filename.outputs.filename }}
uses: actions/upload-artifact@v4
with:
@@ -311,8 +274,6 @@ jobs:
build-server-native:
name: Build Server native
runs-on: ubuntu-latest
needs: optimize_ci
if: needs.optimize_ci.outputs.skip == 'false'
env:
CARGO_PROFILE_RELEASE_DEBUG: '1'
steps:
@@ -327,6 +288,7 @@ jobs:
with:
target: 'x86_64-unknown-linux-gnu'
package: '@affine/server-native'
nx_token: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
- name: Upload server-native.node
uses: actions/upload-artifact@v4
with:
@@ -337,8 +299,7 @@ jobs:
build-electron-renderer:
name: Build @affine/electron renderer
runs-on: ubuntu-latest
needs: optimize_ci
if: needs.optimize_ci.outputs.skip == 'false'
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
@@ -347,11 +308,12 @@ jobs:
electron-install: false
full-cache: true
- name: Build Electron renderer
run: yarn affine @affine/electron-renderer build
# always skip cache because its fast, and cache configuration is always changing
run: yarn build
env:
DISTRIBUTION: desktop
- name: zip web
run: tar -czf dist.tar.gz --directory=packages/frontend/apps/electron-renderer/dist .
run: tar -czf dist.tar.gz --directory=packages/frontend/apps/electron/renderer/dist .
- name: Upload web artifact
uses: actions/upload-artifact@v4
with:
@@ -362,15 +324,10 @@ jobs:
server-test:
name: Server Test
runs-on: ubuntu-latest
needs:
- optimize_ci
- build-server-native
if: needs.optimize_ci.outputs.skip == 'false'
needs: build-server-native
env:
NODE_ENV: test
DISTRIBUTION: web
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
REDIS_SERVER_HOST: localhost
services:
postgres:
image: postgres
@@ -383,10 +340,6 @@ jobs:
--health-retries 5
ports:
- 5432:5432
redis:
image: redis
ports:
- 6379:6379
mailer:
image: mailhog/mailhog
ports:
@@ -407,17 +360,31 @@ jobs:
name: server-native.node
path: ./packages/backend/server
- name: Prepare Server Test Environment
uses: ./.github/actions/server-test-env
- name: Initialize database
run: |
psql -h localhost -U postgres -c "CREATE DATABASE affine;"
psql -h localhost -U postgres -c "CREATE USER affine WITH PASSWORD 'affine';"
psql -h localhost -U postgres -c "ALTER USER affine WITH SUPERUSER;"
env:
PGPASSWORD: affine
- name: Run init-db script
run: |
yarn workspace @affine/server exec prisma generate
yarn workspace @affine/server exec prisma db push
yarn workspace @affine/server data-migration run
env:
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
- name: Run server tests
run: yarn affine @affine/server test:coverage --forbid-only
run: yarn workspace @affine/server test:coverage
env:
CARGO_TARGET_DIR: '${{ github.workspace }}/target'
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
COPILOT_OPENAI_API_KEY: 'use_fake_openai_api_key'
- name: Upload server test coverage results
uses: codecov/codecov-action@v5
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./packages/backend/server/.coverage/lcov.info
@@ -425,224 +392,30 @@ jobs:
name: affine
fail_ci_if_error: false
rust-test:
name: Run native tests
runs-on: ubuntu-latest
needs: optimize_ci
if: needs.optimize_ci.outputs.skip == 'false'
env:
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 --no-fail-fast
copilot-api-test:
name: Server Copilot Api Test
runs-on: ubuntu-latest
needs:
- optimize_ci
- build-server-native
if: needs.optimize_ci.outputs.skip == 'false'
env:
NODE_ENV: test
DISTRIBUTION: web
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
REDIS_SERVER_HOST: localhost
services:
postgres:
image: postgres
env:
POSTGRES_PASSWORD: affine
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
redis:
image: redis
ports:
- 6379:6379
mailer:
image: mailhog/mailhog
ports:
- 1025:1025
- 8025:8025
steps:
- uses: actions/checkout@v4
- name: Check blocksuite update
id: check-blocksuite-update
env:
BASE_REF: ${{ github.base_ref }}
run: |
if node ./scripts/detect-blocksuite-update.mjs "$BASE_REF"; then
echo "skip=false" >> $GITHUB_OUTPUT
else
echo "skip=true" >> $GITHUB_OUTPUT
fi
- uses: dorny/paths-filter@v3
id: apifilter
with:
filters: |
changed:
- 'packages/backend/server/src/plugins/copilot/**'
- 'packages/backend/server/tests/copilot.*'
- name: Setup Node.js
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.apifilter.outputs.changed == 'true' }}
uses: ./.github/actions/setup-node
with:
electron-install: false
full-cache: true
- name: Download server-native.node
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.apifilter.outputs.changed == 'true' }}
uses: actions/download-artifact@v4
with:
name: server-native.node
path: ./packages/backend/server
- name: Prepare Server Test Environment
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.apifilter.outputs.changed == 'true' }}
uses: ./.github/actions/server-test-env
- name: Run server tests
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.apifilter.outputs.changed == 'true' }}
run: yarn affine @affine/server test:copilot:coverage --forbid-only
env:
CARGO_TARGET_DIR: '${{ github.workspace }}/target'
COPILOT_OPENAI_API_KEY: ${{ secrets.COPILOT_OPENAI_API_KEY }}
COPILOT_FAL_API_KEY: ${{ secrets.COPILOT_FAL_API_KEY }}
- name: Upload server test coverage results
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.apifilter.outputs.changed == 'true' }}
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./packages/backend/server/.coverage/lcov.info
flags: server-test
name: affine
fail_ci_if_error: false
copilot-e2e-test:
name: Server Copilot E2E Test
runs-on: ubuntu-latest
env:
DISTRIBUTION: web
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
IN_CI_TEST: true
REDIS_SERVER_HOST: localhost
DEPLOYMENT_TYPE: affine
strategy:
fail-fast: false
matrix:
shardIndex: [1, 2, 3]
shardTotal: [3]
needs:
- build-server-native
services:
postgres:
image: postgres
env:
POSTGRES_PASSWORD: affine
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
redis:
image: redis
ports:
- 6379:6379
steps:
- uses: actions/checkout@v4
- name: Check blocksuite update
id: check-blocksuite-update
env:
BASE_REF: ${{ github.base_ref }}
run: |
if node ./scripts/detect-blocksuite-update.mjs "$BASE_REF"; then
echo "skip=false" >> $GITHUB_OUTPUT
else
echo "skip=true" >> $GITHUB_OUTPUT
fi
- uses: dorny/paths-filter@v3
id: e2efilter
with:
filters: |
changed:
- 'packages/frontend/core/src/blocksuite/presets/ai/**'
- 'packages/frontend/core/src/components/blocksuite/block-suite-editor/ai/**'
- 'tests/affine-cloud-copilot/**'
- name: Setup Node.js
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.e2efilter.outputs.changed == 'true' }}
uses: ./.github/actions/setup-node
with:
playwright-install: true
electron-install: false
hard-link-nm: false
- name: Download server-native.node
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.e2efilter.outputs.changed == 'true' }}
uses: actions/download-artifact@v4
with:
name: server-native.node
path: ./packages/backend/server
- name: Run Copilot E2E Test ${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.e2efilter.outputs.changed == 'true' }}
uses: ./.github/actions/copilot-test
with:
script: yarn affine @affine-test/affine-cloud-copilot e2e --forbid-only --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
openai-key: ${{ secrets.COPILOT_OPENAI_API_KEY }}
fal-key: ${{ secrets.COPILOT_FAL_API_KEY }}
server-e2e-test:
name: ${{ matrix.tests.name }}
runs-on: ubuntu-latest
needs:
- optimize_ci
- build-server-native
- build-native
if: needs.optimize_ci.outputs.skip == 'false'
env:
DISTRIBUTION: web
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
REDIS_SERVER_HOST: localhost
IN_CI_TEST: true
strategy:
fail-fast: false
matrix:
tests:
- name: 'Server E2E Test 1/3'
script: yarn affine @affine-test/affine-cloud e2e --forbid-only --shard=1/3
script: yarn workspace @affine-test/affine-cloud e2e --forbid-only --shard=1/3
- name: 'Server E2E Test 2/3'
script: yarn affine @affine-test/affine-cloud e2e --forbid-only --shard=2/3
script: yarn workspace @affine-test/affine-cloud e2e --forbid-only --shard=2/3
- name: 'Server E2E Test 3/3'
script: yarn affine @affine-test/affine-cloud e2e --forbid-only --shard=3/3
script: yarn workspace @affine-test/affine-cloud e2e --forbid-only --shard=3/3
- name: 'Server Desktop E2E Test'
script: |
yarn affine @affine/electron build:dev
# Workaround for Electron apps failing to initialize on Ubuntu 24.04 due to AppArmor restrictions
# Disables unprivileged user namespaces restriction to allow Electron apps to run
# Reference: https://github.com/electron/electron/issues/42510
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn affine @affine-test/affine-desktop-cloud e2e
yarn workspace @affine/electron build:dev
xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn workspace @affine-test/affine-desktop-cloud e2e
needs:
- build-server-native
- build-native
services:
postgres:
image: postgres
@@ -655,10 +428,6 @@ jobs:
--health-retries 5
ports:
- 5432:5432
redis:
image: redis
ports:
- 6379:6379
mailer:
image: mailhog/mailhog
ports:
@@ -685,8 +454,19 @@ jobs:
name: affine.linux-x64-gnu.node
path: ./packages/frontend/native
- name: Prepare Server Test Environment
uses: ./.github/actions/server-test-env
- name: Initialize database
run: |
psql -h localhost -U postgres -c "CREATE DATABASE affine;"
psql -h localhost -U postgres -c "CREATE USER affine WITH PASSWORD 'affine';"
psql -h localhost -U postgres -c "ALTER USER affine WITH SUPERUSER;"
env:
PGPASSWORD: affine
- name: Run init-db script
run: |
yarn workspace @affine/server exec prisma generate
yarn workspace @affine/server exec prisma db push
yarn workspace @affine/server data-migration run
- name: ${{ matrix.tests.name }}
run: |
@@ -707,11 +487,6 @@ jobs:
desktop-test:
name: Desktop Test (${{ matrix.spec.os }}, ${{ matrix.spec.platform }}, ${{ matrix.spec.arch }}, ${{ matrix.spec.target }}, ${{ matrix.spec.test }})
runs-on: ${{ matrix.spec.os }}
needs:
- optimize_ci
- build-electron-renderer
- build-native
if: needs.optimize_ci.outputs.skip == 'false'
strategy:
fail-fast: false
matrix:
@@ -744,13 +519,16 @@ jobs:
target: x86_64-pc-windows-msvc,
test: true,
}
needs:
- build-electron-renderer
- build-native
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
timeout-minutes: 10
with:
extra-flags: workspaces focus @affine/electron @affine/monorepo @affine-test/affine-desktop @affine/nbstore @toeverything/infra
extra-flags: workspaces focus @affine/electron @affine/monorepo @affine-test/affine-desktop
playwright-install: true
hard-link-nm: false
enableScripts: false
@@ -771,7 +549,7 @@ jobs:
- name: Run unit tests
if: ${{ matrix.spec.test }}
shell: bash
run: yarn affine @affine/electron vitest
run: yarn workspace @affine/electron vitest
- name: Download web artifact
uses: ./.github/actions/download-web
@@ -779,38 +557,26 @@ jobs:
path: packages/frontend/apps/electron/resources/web-static
- name: Build Desktop Layers
run: yarn affine @affine/electron build
run: yarn workspace @affine/electron build
- name: Run desktop tests
if: ${{ matrix.spec.os == 'ubuntu-latest' }}
run: |
# Workaround for Electron apps failing to initialize on Ubuntu 24.04 due to AppArmor restrictions
# Disables unprivileged user namespaces restriction to allow Electron apps to run
# Reference: https://github.com/electron/electron/issues/42510
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn affine @affine-test/affine-desktop e2e
run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn workspace @affine-test/affine-desktop e2e
- name: Run desktop tests
if: ${{ matrix.spec.test && matrix.spec.os != 'ubuntu-latest' }}
run: yarn affine @affine-test/affine-desktop e2e
run: yarn workspace @affine-test/affine-desktop e2e
- name: Make bundle (macOS)
- name: Make bundle
if: ${{ matrix.spec.target == 'aarch64-apple-darwin' }}
env:
SKIP_BUNDLE: true
SKIP_WEB_BUILD: true
HOIST_NODE_MODULES: 1
run: yarn affine @affine/electron package --platform=darwin --arch=arm64
run: yarn workspace @affine/electron package --platform=darwin --arch=arm64
- name: Make Bundle (Linux)
run: |
sudo add-apt-repository universe
sudo apt install -y libfuse2 elfutils flatpak flatpak-builder
flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
flatpak update
# some flatpak deps need git protocol.file.allow
git config --global protocol.file.allow always
yarn affine @affine/electron make --platform=linux --arch=x64
- name: Make AppImage
run: yarn workspace @affine/electron make --platform=linux --arch=x64
if: ${{ matrix.spec.target == 'x86_64-unknown-linux-gnu' }}
env:
SKIP_WEB_BUILD: 1
@@ -819,7 +585,7 @@ jobs:
- name: Output check
if: ${{ matrix.spec.os == 'macos-14' && matrix.spec.arch == 'arm64' }}
run: |
yarn affine @affine/electron node ./scripts/macos-arm64-output-check.ts
yarn workspace @affine/electron exec node --loader ts-node/esm/transpile-only ./scripts/macos-arm64-output-check.ts
- name: Upload test results
if: ${{ failure() }}
@@ -829,36 +595,20 @@ jobs:
path: ./test-results
if-no-files-found: ignore
test-build-mobile-app:
uses: ./.github/workflows/release-mobile.yml
needs: optimize_ci
if: needs.optimize_ci.outputs.skip == 'false'
with:
build-type: canary
build-target: development
secrets: inherit
permissions:
id-token: 'write'
test-done:
needs:
- analyze
- lint
- check-yarn-binary
- e2e-test
- e2e-legacy-blocksuite-test
- e2e-mobile-test
- e2e-migration-test
- unit-test
- build-native
- build-server-native
- build-electron-renderer
- server-test
- rust-test
- copilot-api-test
- copilot-e2e-test
- server-e2e-test
- desktop-test
- test-build-mobile-app
if: always()
runs-on: ubuntu-latest
name: 3, 2, 1 Launch

View File

@@ -1,29 +0,0 @@
name: Copilot Test Automatically
on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+-canary.[0-9]+'
schedule:
- cron: '0 8 * * *'
workflow_dispatch:
permissions:
actions: write
jobs:
dispatch-test:
runs-on: ubuntu-latest
name: Setup Test
steps:
- name: dispatch test by tag
if: ${{ github.event_name == 'push' || github.event_name == 'workflow_dispatch' }}
uses: benc-uk/workflow-dispatch@v1
with:
workflow: copilot-test.yml
- name: dispatch test by schedule
if: ${{ github.event_name == 'schedule' }}
uses: benc-uk/workflow-dispatch@v1
with:
workflow: copilot-test.yml
ref: canary

View File

@@ -1,198 +0,0 @@
name: Copilot Cron Test
on:
workflow_dispatch:
env:
PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/node_modules/.cache/ms-playwright
jobs:
build-server-native:
name: Build Server native
runs-on: ubuntu-latest
env:
CARGO_PROFILE_RELEASE_DEBUG: '1'
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
extra-flags: workspaces focus @affine/server-native
electron-install: false
- name: Build Rust
uses: ./.github/actions/build-rust
with:
target: 'x86_64-unknown-linux-gnu'
package: '@affine/server-native'
- name: Upload server-native.node
uses: actions/upload-artifact@v4
with:
name: server-native.node
path: ./packages/backend/native/server-native.node
if-no-files-found: error
copilot-api-test:
name: Server Copilot Api Test
runs-on: ubuntu-latest
needs:
- build-server-native
env:
NODE_ENV: test
DISTRIBUTION: web
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
REDIS_SERVER_HOST: localhost
services:
postgres:
image: postgres
env:
POSTGRES_PASSWORD: affine
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
redis:
image: redis
ports:
- 6379:6379
mailer:
image: mailhog/mailhog
ports:
- 1025:1025
- 8025:8025
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
playwright-install: true
electron-install: false
full-cache: true
- name: Download server-native.node
uses: actions/download-artifact@v4
with:
name: server-native.node
path: ./packages/backend/server
- name: Prepare Server Test Environment
uses: ./.github/actions/server-test-env
- name: Run server tests
run: yarn affine @affine/server test:copilot:coverage --forbid-only
env:
CARGO_TARGET_DIR: '${{ github.workspace }}/target'
COPILOT_OPENAI_API_KEY: ${{ secrets.COPILOT_OPENAI_API_KEY }}
COPILOT_FAL_API_KEY: ${{ secrets.COPILOT_FAL_API_KEY }}
- name: Upload server test coverage results
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./packages/backend/server/.coverage/lcov.info
flags: server-test
name: affine
fail_ci_if_error: false
copilot-e2e-test:
name: Server Copilot E2E Test
runs-on: ubuntu-latest
env:
DISTRIBUTION: web
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
REDIS_SERVER_HOST: localhost
IN_CI_TEST: true
strategy:
fail-fast: false
matrix:
shardIndex: [1, 2, 3]
shardTotal: [3]
needs:
- build-server-native
services:
postgres:
image: postgres
env:
POSTGRES_PASSWORD: affine
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
redis:
image: redis
ports:
- 6379:6379
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
playwright-install: true
electron-install: false
hard-link-nm: false
- name: Download server-native.node
uses: actions/download-artifact@v4
with:
name: server-native.node
path: ./packages/backend/server
- name: Run Copilot E2E Test ${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
uses: ./.github/actions/copilot-test
with:
script: yarn affine @affine-test/affine-cloud-copilot e2e --forbid-only --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
openai-key: ${{ secrets.COPILOT_OPENAI_API_KEY }}
fal-key: ${{ secrets.COPILOT_FAL_API_KEY }}
test-done:
needs:
- copilot-api-test
- copilot-e2e-test
if: always()
runs-on: ubuntu-latest
name: Post test result message
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
extra-flags: 'workspaces focus @affine/copilot-result'
electron-install: false
- name: Post Success event to a Slack channel
if: ${{ always() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }}
run: node ./tools/copilot-result/index.js
env:
CHANNEL_ID: ${{ secrets.RELEASE_SLACK_CHNNEL_ID }}
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
BRANCH_SHA: ${{ github.sha }}
BRANCH_NAME: ${{ github.ref }}
COPILOT_RESULT: success
- name: Post Failed event to a Slack channel
id: failed-slack
if: ${{ always() && contains(needs.*.result, 'failure') }}
run: node ./tools/copilot-result/index.js
env:
CHANNEL_ID: ${{ secrets.RELEASE_SLACK_CHNNEL_ID }}
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
BRANCH_SHA: ${{ github.sha }}
BRANCH_NAME: ${{ github.ref }}
COPILOT_RESULT: failed
- name: Post Cancel event to a Slack channel
id: cancel-slack
if: ${{ always() && contains(needs.*.result, 'cancelled') && !contains(needs.*.result, 'failure') }}
run: node ./tools/copilot-result/index.js
env:
CHANNEL_ID: ${{ secrets.RELEASE_SLACK_CHNNEL_ID }}
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
BRANCH_SHA: ${{ github.sha }}
BRANCH_NAME: ${{ github.ref }}
COPILOT_RESULT: canceled

View File

@@ -12,6 +12,8 @@ on:
- beta
- stable
- internal
env:
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
permissions:
contents: 'write'
@@ -171,31 +173,41 @@ jobs:
BLOCKSUITE_REPO_PATH: ${{ github.workspace }}/blocksuite
- name: Post Failed event to a Slack channel
id: failed-slack
uses: slackapi/slack-github-action@v2.0.0
uses: slackapi/slack-github-action@v1.27.0
if: ${{ always() && contains(needs.*.result, 'failure') }}
with:
method: chat.postMessage
token: ${{ secrets.SLACK_BOT_TOKEN }}
channel-id: ${{ secrets.RELEASE_SLACK_CHNNEL_ID }}
payload: |
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 }}`>"
{
"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 }}
- name: Post Cancel event to a Slack channel
id: cancel-slack
uses: slackapi/slack-github-action@v2.0.0
uses: slackapi/slack-github-action@v1.27.0
if: ${{ always() && contains(needs.*.result, 'cancelled') && !contains(needs.*.result, 'failure') }}
with:
token: ${{ secrets.SLACK_BOT_TOKEN }}
method: chat.postMessage
channel-id: ${{ secrets.RELEASE_SLACK_CHNNEL_ID }}
payload: |
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 }}`>"
{
"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 }}

View File

@@ -1,38 +0,0 @@
name: Release Desktop/Mobile Automatically
on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+-canary.[0-9]+'
schedule:
- cron: '0 9 * * *'
permissions:
contents: write
pull-requests: write
actions: write
jobs:
dispatch-release-desktop:
runs-on: ubuntu-latest
name: Setup Release Desktop
steps:
- name: dispatch desktop release by tag
if: ${{ github.event_name == 'push' }}
uses: benc-uk/workflow-dispatch@v1
with:
workflow: release-desktop.yml
inputs: '{ "build-type": "canary", "is-draft": false, "is-pre-release": true }'
- name: dispatch desktop release by schedule
if: ${{ github.event_name == 'schedule' }}
uses: benc-uk/workflow-dispatch@v1
with:
workflow: release-desktop.yml
inputs: '{ "build-type": "canary", "is-draft": false, "is-pre-release": true }'
ref: canary
- name: dispatch desktop release by tag
uses: benc-uk/workflow-dispatch@v1
with:
workflow: release-mobile.yml
inputs: '{ "build-type": "canary", "build-target": "distribution" }'

View File

@@ -0,0 +1,32 @@
name: Release Desktop Automatically
on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+-canary.[0-9]+'
schedule:
- cron: '0 9 * * *'
permissions:
contents: write
pull-requests: write
actions: write
jobs:
dispatch-release-desktop:
runs-on: ubuntu-latest
name: Setup Release Desktop
steps:
- name: dispatch desktop release by tag
if: ${{ github.event_name == 'push' }}
uses: benc-uk/workflow-dispatch@v1
with:
workflow: release-desktop.yml
inputs: '{ "build-type": "canary", "is-draft": false, "is-pre-release": true }'
- name: dispatch desktop release by schedule
if: ${{ github.event_name == 'schedule' }}
uses: benc-uk/workflow-dispatch@v1
with:
workflow: release-desktop.yml
inputs: '{ "build-type": "canary", "is-draft": false, "is-pre-release": true }'
ref: canary

View File

@@ -32,7 +32,7 @@ permissions:
env:
BUILD_TYPE: ${{ github.event.inputs.build-type }}
DEBUG: 'affine:*,napi:*'
DEBUG: napi:*
APP_NAME: affine
MACOSX_DEPLOYMENT_TARGET: '10.13'
@@ -52,7 +52,7 @@ jobs:
- name: Setup @sentry/cli
uses: ./.github/actions/setup-sentry
- name: generate-assets
run: yarn affine @affine/electron generate-assets
run: yarn workspace @affine/electron generate-assets
env:
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: 'affine'
@@ -60,6 +60,7 @@ jobs:
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SENTRY_RELEASE: ${{ steps.version.outputs.APP_VERSION }}
RELEASE_VERSION: ${{ steps.version.outputs.APP_VERSION }}
SKIP_NX_CACHE: 'true'
MIXPANEL_TOKEN: ${{ secrets.MIXPANEL_TOKEN }}
- name: Upload web artifact
@@ -70,7 +71,6 @@ jobs:
make-distribution:
strategy:
fail-fast: false
matrix:
spec:
- runner: macos-14
@@ -87,7 +87,6 @@ jobs:
target: x86_64-unknown-linux-gnu
runs-on: ${{ matrix.spec.runner }}
needs: before-make
environment: ${{ github.event.inputs.build-type }}
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
@@ -97,7 +96,6 @@ 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
@@ -108,7 +106,7 @@ jobs:
timeout-minutes: 10
uses: ./.github/actions/setup-node
with:
extra-flags: workspaces focus @affine/electron @affine/monorepo @affine/nbstore @toeverything/infra
extra-flags: workspaces focus @affine/electron @affine/monorepo
hard-link-nm: false
nmHoistingLimits: workspaces
enableScripts: false
@@ -117,13 +115,14 @@ jobs:
with:
target: ${{ matrix.spec.target }}
package: '@affine/native'
nx_token: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
- uses: actions/download-artifact@v4
with:
name: web
path: packages/frontend/apps/electron/resources/web-static
- name: Build Desktop Layers
run: yarn affine @affine/electron build
run: yarn workspace @affine/electron build
- name: Signing By Apple Developer ID
if: ${{ matrix.spec.platform == 'darwin' }}
@@ -132,18 +131,14 @@ jobs:
p12-file-base64: ${{ secrets.CERTIFICATES_P12 }}
p12-password: ${{ secrets.CERTIFICATES_P12_PASSWORD }}
- name: Install additional dependencies on Linux
- name: Install fuse on Linux (for patching AppImage)
if: ${{ matrix.spec.platform == 'linux' }}
run: |
sudo add-apt-repository universe
sudo apt install -y libfuse2 elfutils flatpak flatpak-builder
flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
flatpak update
# some flatpak deps need git protocol.file.allow
git config --global protocol.file.allow always
sudo apt install libfuse2 -y
- name: make
run: yarn affine @affine/electron make --platform=${{ matrix.spec.platform }} --arch=${{ matrix.spec.arch }}
run: yarn workspace @affine/electron make --platform=${{ matrix.spec.platform }} --arch=${{ matrix.spec.arch }}
env:
SKIP_WEB_BUILD: 1
HOIST_NODE_MODULES: 1
@@ -163,25 +158,23 @@ jobs:
if: ${{ matrix.spec.platform == 'linux' }}
run: |
mkdir -p builds
mv packages/frontend/apps/electron/out/*/make/zip/linux/${{ matrix.spec.arch }}/*.zip ./builds/affine-${{ needs.before-make.outputs.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-${{ matrix.spec.arch }}.zip
mv packages/frontend/apps/electron/out/*/make/*.AppImage ./builds/affine-${{ needs.before-make.outputs.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-${{ matrix.spec.arch }}.appimage
mv packages/frontend/apps/electron/out/*/make/deb/${{ matrix.spec.arch }}/*.deb ./builds/affine-${{ needs.before-make.outputs.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-${{ matrix.spec.arch }}.deb
mv packages/frontend/apps/electron/out/*/make/flatpak/*/*.flatpak ./builds/affine-${{ needs.before-make.outputs.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-${{ matrix.spec.arch }}.flatpak
mv packages/frontend/apps/electron/out/*/make/zip/linux/x64/*.zip ./builds/affine-${{ needs.before-make.outputs.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-x64.zip
mv packages/frontend/apps/electron/out/*/make/*.AppImage ./builds/affine-${{ needs.before-make.outputs.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-x64.appimage
- uses: actions/attest-build-provenance@v2
- uses: actions/attest-build-provenance@v1
if: ${{ matrix.spec.platform == 'darwin' }}
with:
subject-path: |
./builds/affine-${{ needs.before-make.outputs.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-macos-${{ matrix.spec.arch }}.zip
./builds/affine-${{ needs.before-make.outputs.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-macos-${{ matrix.spec.arch }}.dmg
- uses: actions/attest-build-provenance@v2
- uses: actions/attest-build-provenance@v1
if: ${{ matrix.spec.platform == 'linux' }}
with:
subject-path: |
./builds/affine-${{ needs.before-make.outputs.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-x64.zip
./builds/affine-${{ needs.before-make.outputs.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-x64.appimage
./builds/affine-${{ needs.before-make.outputs.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-linux-x64.deb
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
@@ -189,31 +182,23 @@ jobs:
path: builds
package-distribution-windows:
environment: ${{ github.event.inputs.build-type }}
strategy:
fail-fast: false
matrix:
spec:
- runner: windows-latest
platform: win32
arch: x64
target: x86_64-pc-windows-msvc
- runner: windows-latest
platform: win32
arch: arm64
target: aarch64-pc-windows-msvc
runs-on: ${{ matrix.spec.runner }}
needs: before-make
outputs:
FILES_TO_BE_SIGNED_x64: ${{ steps.get_files_to_be_signed.outputs.FILES_TO_BE_SIGNED_x64 }}
FILES_TO_BE_SIGNED_arm64: ${{ steps.get_files_to_be_signed.outputs.FILES_TO_BE_SIGNED_arm64 }}
FILES_TO_BE_SIGNED: ${{ steps.get_files_to_be_signed.outputs.FILES_TO_BE_SIGNED }}
env:
SKIP_GENERATE_ASSETS: 1
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
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
@@ -224,7 +209,7 @@ jobs:
timeout-minutes: 10
uses: ./.github/actions/setup-node
with:
extra-flags: workspaces focus @affine/electron @affine/monorepo @affine/nbstore @toeverything/infra
extra-flags: workspaces focus @affine/electron @affine/monorepo
hard-link-nm: false
nmHoistingLimits: workspaces
- name: Build AFFiNE native
@@ -232,16 +217,17 @@ jobs:
with:
target: ${{ matrix.spec.target }}
package: '@affine/native'
nx_token: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
- uses: actions/download-artifact@v4
with:
name: web
path: packages/frontend/apps/electron/resources/web-static
- name: Build Desktop Layers
run: yarn affine @affine/electron build
run: yarn workspace @affine/electron build
- name: package
run: yarn affine @affine/electron package --platform=${{ matrix.spec.platform }} --arch=${{ matrix.spec.arch }}
run: yarn workspace @affine/electron package --platform=${{ matrix.spec.platform }} --arch=${{ matrix.spec.arch }}
env:
SKIP_WEB_BUILD: 1
HOIST_NODE_MODULES: 1
@@ -250,7 +236,7 @@ jobs:
id: get_files_to_be_signed
run: |
Set-Variable -Name FILES_TO_BE_SIGNED -Value ((Get-ChildItem -Path packages/frontend/apps/electron/out -Recurse -File | Where-Object { $_.Extension -in @(".exe", ".node", ".dll", ".msi") } | ForEach-Object { '"' + $_.FullName.Replace((Get-Location).Path + '\packages\frontend\apps\electron\out\', '') + '"' }) -join ' ')
"FILES_TO_BE_SIGNED_${{ matrix.spec.arch }}=$FILES_TO_BE_SIGNED" >> $env:GITHUB_OUTPUT
"FILES_TO_BE_SIGNED=$FILES_TO_BE_SIGNED" >> $env:GITHUB_OUTPUT
echo $FILES_TO_BE_SIGNED
- name: Zip artifacts for faster upload
@@ -264,36 +250,25 @@ jobs:
archive.zip
!**/*.map
sign-packaged-artifacts-windows_x64:
sign-packaged-artifacts-windows:
needs: package-distribution-windows
uses: ./.github/workflows/windows-signer.yml
with:
files: ${{ needs.package-distribution-windows.outputs.FILES_TO_BE_SIGNED_x64 }}
files: ${{ needs.package-distribution-windows.outputs.FILES_TO_BE_SIGNED }}
artifact-name: packaged-win32-x64
sign-packaged-artifacts-windows_arm64:
needs: package-distribution-windows
uses: ./.github/workflows/windows-signer.yml
with:
files: ${{ needs.package-distribution-windows.outputs.FILES_TO_BE_SIGNED_arm64 }}
artifact-name: packaged-win32-arm64
make-windows-installer:
needs:
- sign-packaged-artifacts-windows_x64
- sign-packaged-artifacts-windows_arm64
needs: sign-packaged-artifacts-windows
strategy:
fail-fast: false
matrix:
spec:
- platform: win32
- runner: windows-latest
platform: win32
arch: x64
- platform: win32
arch: arm64
runs-on: windows-latest
target: x86_64-pc-windows-msvc
runs-on: ${{ matrix.spec.runner }}
outputs:
FILES_TO_BE_SIGNED_x64: ${{ steps.get_files_to_be_signed.outputs.FILES_TO_BE_SIGNED_x64 }}
FILES_TO_BE_SIGNED_arm64: ${{ steps.get_files_to_be_signed.outputs.FILES_TO_BE_SIGNED_arm64 }}
FILES_TO_BE_SIGNED: ${{ steps.get_files_to_be_signed.outputs.FILES_TO_BE_SIGNED }}
steps:
- uses: actions/checkout@v4
- name: Setup Version
@@ -306,8 +281,6 @@ jobs:
extra-flags: workspaces focus @affine/electron @affine/monorepo
hard-link-nm: false
nmHoistingLimits: workspaces
env:
npm_config_arch: ${{ matrix.spec.arch }}
- name: Download and overwrite packaged artifacts
uses: actions/download-artifact@v4
with:
@@ -317,10 +290,10 @@ jobs:
run: Expand-Archive -Path signed.zip -DestinationPath packages/frontend/apps/electron/out
- name: Make squirrel.windows installer
run: yarn affine @affine/electron make-squirrel --platform=${{ matrix.spec.platform }} --arch=${{ matrix.spec.arch }}
run: yarn workspace @affine/electron make-squirrel --platform=${{ matrix.spec.platform }} --arch=${{ matrix.spec.arch }}
- name: Make nsis.windows installer
run: yarn affine @affine/electron make-nsis --platform=${{ matrix.spec.platform }} --arch=${{ matrix.spec.arch }}
run: yarn workspace @affine/electron make-nsis --platform=${{ matrix.spec.platform }} --arch=${{ matrix.spec.arch }}
- name: Zip artifacts for faster upload
run: Compress-Archive -CompressionLevel Fastest -Path packages/frontend/apps/electron/out/${{ env.BUILD_TYPE }}/make/* -DestinationPath archive.zip
@@ -329,7 +302,7 @@ jobs:
id: get_files_to_be_signed
run: |
Set-Variable -Name FILES_TO_BE_SIGNED -Value ((Get-ChildItem -Path packages/frontend/apps/electron/out/${{ env.BUILD_TYPE }}/make -Recurse -File | Where-Object { $_.Extension -in @(".exe", ".node", ".dll", ".msi") } | ForEach-Object { '"' + $_.FullName.Replace((Get-Location).Path + '\packages\frontend\apps\electron\out\${{ env.BUILD_TYPE }}\make\', '') + '"' }) -join ' ')
"FILES_TO_BE_SIGNED_${{ matrix.spec.arch }}=$FILES_TO_BE_SIGNED" >> $env:GITHUB_OUTPUT
"FILES_TO_BE_SIGNED=$FILES_TO_BE_SIGNED" >> $env:GITHUB_OUTPUT
echo $FILES_TO_BE_SIGNED
- name: Save installer for signing
@@ -338,37 +311,22 @@ jobs:
name: installer-${{ matrix.spec.platform }}-${{ matrix.spec.arch }}
path: archive.zip
sign-installer-artifacts-windows-x64:
sign-installer-artifacts-windows:
needs: make-windows-installer
uses: ./.github/workflows/windows-signer.yml
with:
files: ${{ needs.make-windows-installer.outputs.FILES_TO_BE_SIGNED_x64 }}
files: ${{ needs.make-windows-installer.outputs.FILES_TO_BE_SIGNED }}
artifact-name: installer-win32-x64
sign-installer-artifacts-windows-arm64:
needs: make-windows-installer
uses: ./.github/workflows/windows-signer.yml
with:
files: ${{ needs.make-windows-installer.outputs.FILES_TO_BE_SIGNED_arm64 }}
artifact-name: installer-win32-arm64
finalize-installer-windows:
needs:
[
sign-installer-artifacts-windows-x64,
sign-installer-artifacts-windows-arm64,
before-make,
]
needs: [sign-installer-artifacts-windows, before-make]
strategy:
fail-fast: false
matrix:
spec:
- runner: windows-latest
platform: win32
arch: x64
- runner: windows-latest
platform: win32
arch: arm64
target: x86_64-pc-windows-msvc
runs-on: ${{ matrix.spec.runner }}
steps:
- name: Download and overwrite installer artifacts
@@ -382,16 +340,16 @@ jobs:
- name: Save artifacts
run: |
mkdir -p builds
mv packages/frontend/apps/electron/out/*/make/zip/win32/${{ matrix.spec.arch }}/AFFiNE*-win32-${{ matrix.spec.arch }}-*.zip ./builds/affine-${{ needs.before-make.outputs.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-windows-${{ matrix.spec.arch }}.zip
mv packages/frontend/apps/electron/out/*/make/squirrel.windows/${{ matrix.spec.arch }}/*.exe ./builds/affine-${{ needs.before-make.outputs.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-windows-${{ matrix.spec.arch }}.exe
mv packages/frontend/apps/electron/out/*/make/nsis.windows/${{ matrix.spec.arch }}/*.exe ./builds/affine-${{ needs.before-make.outputs.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-windows-${{ matrix.spec.arch }}.nsis.exe
mv packages/frontend/apps/electron/out/*/make/zip/win32/x64/AFFiNE*-win32-x64-*.zip ./builds/affine-${{ needs.before-make.outputs.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-windows-x64.zip
mv packages/frontend/apps/electron/out/*/make/squirrel.windows/x64/*.exe ./builds/affine-${{ needs.before-make.outputs.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-windows-x64.exe
mv packages/frontend/apps/electron/out/*/make/nsis.windows/x64/*.exe ./builds/affine-${{ needs.before-make.outputs.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-windows-x64.nsis.exe
- uses: actions/attest-build-provenance@v2
- uses: actions/attest-build-provenance@v1
with:
subject-path: |
./builds/affine-${{ needs.before-make.outputs.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-windows-${{ matrix.spec.arch }}.zip
./builds/affine-${{ needs.before-make.outputs.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-windows-${{ matrix.spec.arch }}.exe
./builds/affine-${{ needs.before-make.outputs.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-windows-${{ matrix.spec.arch }}.nsis.exe
./builds/affine-${{ needs.before-make.outputs.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-windows-x64.zip
./builds/affine-${{ needs.before-make.outputs.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-windows-x64.exe
./builds/affine-${{ needs.before-make.outputs.RELEASE_VERSION }}-${{ env.BUILD_TYPE }}-windows-x64.nsis.exe
- name: Upload Artifact
uses: actions/upload-artifact@v4
@@ -415,37 +373,28 @@ jobs:
uses: actions/download-artifact@v4
with:
name: affine-darwin-x64-builds
path: ./release
path: ./
- name: Download Artifacts (macos-arm64)
uses: actions/download-artifact@v4
with:
name: affine-darwin-arm64-builds
path: ./release
path: ./
- name: Download Artifacts (windows-x64)
uses: actions/download-artifact@v4
with:
name: affine-win32-x64-builds
path: ./release
- name: Download Artifacts (windows-arm64)
uses: actions/download-artifact@v4
with:
name: affine-win32-arm64-builds
path: ./release
path: ./
- name: Download Artifacts (linux-x64)
uses: actions/download-artifact@v4
with:
name: affine-linux-x64-builds
path: ./release
path: ./
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Copy Selfhost Release Files
run: |
cp ./.docker/selfhost/compose.yml ./release/docker-compose.yml
cp ./.docker/selfhost/.env.example ./release/.env.example
- name: Generate Release yml
run: |
node ./scripts/generate-release-yml.mjs
node ./packages/frontend/apps/electron/scripts/generate-yml.js
env:
RELEASE_VERSION: ${{ needs.before-make.outputs.RELEASE_VERSION }}
- name: Create Release Draft
@@ -457,8 +406,13 @@ jobs:
draft: ${{ github.event.inputs.is-draft }}
prerelease: ${{ github.event.inputs.is-pre-release }}
files: |
./release/*
./release/.env.example
./VERSION
./*.zip
./*.dmg
./*.exe
./*.appimage
./*.apk
./*.yml
- name: Create Nightly Release Draft
if: ${{ github.ref_type == 'branch' }}
uses: softprops/action-gh-release@v2
@@ -474,5 +428,10 @@ jobs:
draft: false
prerelease: true
files: |
./release/*
./release/.env.example
./VERSION
./*.zip
./*.dmg
./*.exe
./*.appimage
./*.apk
./*.yml

View File

@@ -1,245 +0,0 @@
name: Release Mobile App
on:
workflow_call:
inputs:
build-target:
description: 'Build Target'
type: string
required: true
build-type:
description: 'Build Type'
type: string
required: true
workflow_dispatch:
inputs:
build-target:
description: 'Build Target'
type: choice
required: true
default: distribution
options:
- development
- distribution
build-type:
description: 'Build Type'
type: choice
required: true
default: canary
options:
- canary
- beta
- stable
env:
BUILD_TYPE: ${{ inputs.build-type || github.event.inputs.build-type }}
BUILD_TARGET: ${{ inputs.build-target || github.event.inputs.build-target }}
DEBUG: napi:*
KEYCHAIN_NAME: ${{ github.workspace }}/signing_temp
jobs:
output-env:
runs-on: ubuntu-latest
outputs:
ENVIRONMENT: ${{ steps.env.outputs.ENVIRONMENT }}
steps:
- name: Output Environment
id: env
run: |
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
echo "ENVIRONMENT=${{ github.event.inputs.build-type }}" >> $GITHUB_OUTPUT
else
echo "ENVIRONMENT=" >> $GITHUB_OUTPUT
fi
build-ios-web:
needs:
- output-env
runs-on: ubuntu-latest
environment: ${{ needs.output-env.outputs.ENVIRONMENT }}
outputs:
RELEASE_VERSION: ${{ steps.version.outputs.APP_VERSION }}
steps:
- uses: actions/checkout@v4
- name: Setup Version
id: version
uses: ./.github/actions/setup-version
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Setup @sentry/cli
uses: ./.github/actions/setup-sentry
- name: Build Mobile
run: yarn affine @affine/ios build
env:
PUBLIC_PATH: '/'
MIXPANEL_TOKEN: ${{ secrets.MIXPANEL_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: 'affine'
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SENTRY_RELEASE: ${{ steps.version.outputs.APP_VERSION }}
RELEASE_VERSION: ${{ steps.version.outputs.APP_VERSION }}
- name: Upload ios artifact
uses: actions/upload-artifact@v4
with:
name: ios
path: packages/frontend/apps/ios/dist
build-android-web:
runs-on: ubuntu-latest
needs:
- output-env
environment: ${{ needs.output-env.outputs.ENVIRONMENT }}
outputs:
RELEASE_VERSION: ${{ steps.version.outputs.APP_VERSION }}
steps:
- uses: actions/checkout@v4
- name: Setup Version
id: version
uses: ./.github/actions/setup-version
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Setup @sentry/cli
uses: ./.github/actions/setup-sentry
- name: Build Mobile
run: yarn affine @affine/android build
env:
PUBLIC_PATH: '/'
MIXPANEL_TOKEN: ${{ secrets.MIXPANEL_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: 'affine'
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SENTRY_RELEASE: ${{ steps.version.outputs.APP_VERSION }}
RELEASE_VERSION: ${{ steps.version.outputs.APP_VERSION }}
- name: Upload android artifact
uses: actions/upload-artifact@v4
with:
name: android
path: packages/frontend/apps/android/dist
ios:
runs-on: macos-latest
needs:
- build-ios-web
steps:
- uses: actions/checkout@v4
- name: Download mobile artifact
uses: actions/download-artifact@v4
with:
name: ios
path: packages/frontend/apps/ios/dist
- name: Setup Node.js
uses: ./.github/actions/setup-node
timeout-minutes: 10
with:
extra-flags: workspaces focus @affine/ios
playwright-install: false
electron-install: false
hard-link-nm: false
enableScripts: false
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest-stable
- name: Cap sync
run: yarn workspace @affine/ios cap sync
- name: Signing By Apple Developer ID
uses: apple-actions/import-codesign-certs@v3
id: import-codesign-certs
with:
p12-file-base64: ${{ secrets.CERTIFICATES_P12_MOBILE }}
p12-password: ${{ secrets.CERTIFICATES_P12_PASSWORD_MOBILE }}
- name: Setup Rust
uses: ./.github/actions/setup-rust
with:
targets: 'aarch64-apple-ios'
- name: Build Rust
run: |
brew install swiftformat
cargo build -p affine_mobile_native --lib --release --target aarch64-apple-ios
cargo run -p affine_mobile_native --bin uniffi-bindgen generate --library target/aarch64-apple-ios/release/libaffine_mobile_native.a --language swift --out-dir packages/frontend/apps/ios/App/App/uniffi
- name: Testflight
if: ${{ env.BUILD_TYPE != 'stable' }}
working-directory: packages/frontend/apps/ios/App
run: |
echo -n "${{ env.BUILD_PROVISION_PROFILE }}" | base64 --decode -o $PP_PATH
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles
fastlane beta
env:
BUILD_PROVISION_PROFILE: ${{ secrets.BUILD_PROVISION_PROFILE }}
PP_PATH: ${{ runner.temp }}/build_pp.mobileprovision
APPLE_STORE_CONNECT_API_KEY_ID: ${{ secrets.APPLE_STORE_CONNECT_API_KEY_ID }}
APPLE_STORE_CONNECT_API_ISSUER_ID: ${{ secrets.APPLE_STORE_CONNECT_API_ISSUER_ID }}
APPLE_STORE_CONNECT_API_KEY: ${{ secrets.APPLE_STORE_CONNECT_API_KEY }}
android:
runs-on: ubuntu-latest
permissions:
id-token: 'write'
needs:
- build-android-web
steps:
- uses: actions/checkout@v4
- name: Download mobile artifact
uses: actions/download-artifact@v4
with:
name: android
path: packages/frontend/apps/android/dist
- name: Setup Node.js
uses: ./.github/actions/setup-node
timeout-minutes: 10
with:
extra-flags: workspaces focus @affine/android @affine/playstore-auto-bump
playwright-install: false
electron-install: false
hard-link-nm: false
enableScripts: false
- name: Setup Rust
uses: ./.github/actions/setup-rust
with:
targets: 'aarch64-linux-android'
- name: Cap sync
run: yarn workspace @affine/android cap sync
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Auth gcloud
id: auth
uses: google-github-actions/auth@v2
if: ${{ env.BUILD_TARGET == 'distribution' }}
with:
workload_identity_provider: 'projects/${{ secrets.GCP_PROJECT_NUMBER }}/locations/global/workloadIdentityPools/github-actions/providers/github-actions-helm-deploy'
service_account: '${{ secrets.GCP_HELM_DEPLOY_SERVICE_ACCOUNT }}'
token_format: 'access_token'
project_id: '${{ secrets.GCP_PROJECT_ID }}'
access_token_scopes: 'https://www.googleapis.com/auth/androidpublisher'
- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
cache: 'gradle'
- name: Auto increment version code
id: bump
if: ${{ env.BUILD_TARGET == 'distribution' }}
run: yarn affine @affine/playstore-auto-bump bump
env:
GOOGLE_APPLICATION_CREDENTIALS: ${{ steps.auth.outputs.credentials_file_path }}
- name: Build
run: |
echo -n "${{ env.AFFINE_ANDROID_SIGN_KEYSTORE }}" | base64 --decode > packages/frontend/apps/android/affine.keystore
yarn workspace @affine/android cap build android
env:
AFFINE_ANDROID_KEYSTORE_PASSWORD: ${{ secrets.AFFINE_ANDROID_KEYSTORE_PASSWORD }}
AFFINE_ANDROID_KEYSTORE_ALIAS_PASSWORD: ${{ secrets.AFFINE_ANDROID_KEYSTORE_ALIAS_PASSWORD }}
AFFINE_ANDROID_SIGN_KEYSTORE: ${{ secrets.AFFINE_ANDROID_SIGN_KEYSTORE }}
- name: Upload to Google Play
uses: r0adkll/upload-google-play@v1
if: ${{ env.BUILD_TARGET == 'distribution' }}
with:
serviceAccountJson: ${{ steps.auth.outputs.credentials_file_path }}
packageName: app.affine.pro
releaseFiles: packages/frontend/apps/android/App/app/build/outputs/bundle/release/app-release-signed.aab
track: internal
status: draft
existingEditId: ${{ steps.bump.outputs.EDIT_ID }}

View File

@@ -32,7 +32,7 @@ jobs:
skip_untranslated_strings: true
localization_branch_name: l10n_crowdin_translations
create_pull_request: true
pull_request_title: 'chore(i18n): sync translations'
pull_request_title: 'New Crowdin Translations'
pull_request_body: 'New Crowdin translations by [Crowdin GH Action](https://github.com/crowdin/github-action)'
pull_request_base_branch_name: 'canary'
config: packages/frontend/i18n/crowdin.yml

View File

@@ -15,7 +15,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Publish
uses: cloudflare/wrangler-action@v3.13.0
uses: cloudflare/wrangler-action@v3.7.0
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: ${{ secrets.CF_ACCOUNT_ID }}

6
.gitignore vendored
View File

@@ -29,6 +29,7 @@ node_modules
# IDE - VSCode
.vscode/*
!.vscode/tasks.json
!.vscode/settings.template.json
!.vscode/launch.template.json
!.vscode/extensions.json
@@ -58,6 +59,7 @@ Thumbs.db
.vercel
out/
storybook-static
i18n-generated.ts
test-results
playwright-report
@@ -80,7 +82,3 @@ apps/web/next-routes.conf
packages/frontend/templates/edgeless
packages/frontend/core/public/static/templates
# script
af
af.cmd

View File

@@ -3,8 +3,8 @@
"version": 1,
"list": [
{
"input": "./src/resources/en.json",
"output": "./src/i18n.gen",
"input": "./packages/frontend/i18n/src/resources/en.json",
"output": "./packages/frontend/i18n/src/i18n-generated",
"parser": {
"type": "i18next",
"contextSeparator": "$",

2
.nvmrc
View File

@@ -1 +1 @@
20.18.1
20.17.0

View File

@@ -1,36 +1,27 @@
# we will make this file shared by prettier|eslint|oxlint
**/node_modules
.yarn
.github
.vscode
.yarnrc.yml
.docker
**/.storybook
# compiled output
.coverage
.nx/**
yarn.lock
target
lib
test-results
**/dist
**/lib
**/storybook-static
**/web-static
**/public
**/e2e-dist-*
**/static
.next
out
dist
.yarn
.github/helm
_next
storybook-static
web-static
public
packages/backend/server/src/schema.gql
packages/backend/server/src/fundamentals/error/errors.gen.ts
packages/frontend/i18n/src/i18n-generated.ts
packages/frontend/graphql/src/graphql/index.ts
tests/affine-legacy/**/static
.yarnrc.yml
packages/frontend/templates/*.gen.ts
packages/frontend/templates/onboarding
# generated files
**/*.gen.ts
**/*.gql
**/*.d.ts
# per files
tools/cli/src/webpack/error-handler.js
# auto-generated by NAPI-RS
# fixme(@joooye34): need script to check and generate ignore list here
packages/backend/native/index.d.ts
packages/frontend/native/index.d.ts
packages/frontend/native/index.js
packages/frontend/graphql/src/graphql/index.ts
packages/frontend/graphql/src/schema.ts
packages/frontend/apps/android/App/app/build/**
blocksuite/tests-legacy/snapshots

View File

@@ -1,21 +1,25 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Run Dev",
"type": "node-terminal",
"request": "launch",
"command": "yarn run dev"
},
{
"name": "Run Dev Locally",
"type": "node-terminal",
"request": "launch",
"command": "yarn run dev:local"
},
{
"name": "Launch AFFiNE Cloud",
"type": "node",
"request": "launch",
"runtimeExecutable": "yarn",
"cwd": "${workspaceFolder}",
"runtimeArgs": ["affine", "@affine/server", "dev"]
},
{
"name": "Lanuch AFFiNE Web",
"type": "node",
"request": "launch",
"runtimeExecutable": "yarn",
"cwd": "${workspaceFolder}",
"runtimeArgs": ["affine", "@affine/web", "dev"]
"runtimeArgs": ["workspace", "@affine/server", "dev"]
}
]
}

View File

@@ -1,19 +0,0 @@
diff --git a/dist/esm/adapter/external-adapter.js b/dist/esm/adapter/external-adapter.js
index ef7a963d91f08c9e70c8ed9c6b41972bec349319..e682841ec10a4a8a9ce7a79642e58de5c9e664d5 100644
--- a/dist/esm/adapter/external-adapter.js
+++ b/dist/esm/adapter/external-adapter.js
@@ -54,9 +54,11 @@ var adapter = makeAdapter({
type: 'dragenter',
listener: function listener(event) {
// drag operation was started within the document, it won't be an "external" drag
- if (didDragStartLocally) {
- return;
- }
+
+ // we will handle all events actually
+ // if (didDragStartLocally) {
+ // return;
+ // }
// Note: not checking if event was cancelled (`event.defaultPrevented`) as
// cancelling a "dragenter" accepts the drag operation (not prevent it)

View File

@@ -1,13 +0,0 @@
diff --git a/package.json b/package.json
index 5fef2811aa86f3f1f8228daef7d867863e71db72..b795fbd2a0e1cba0b6389ff051220f4e3c52fc13 100644
--- a/package.json
+++ b/package.json
@@ -34,7 +34,7 @@
"deno": "./index.js",
"react-native": "./index.js",
"worker": "./index.js",
- "browser": "./index.dom.js",
+ "browser": "./index.js",
"default": "./index.js"
}
},

View File

@@ -0,0 +1,39 @@
diff --git a/dist/yjs.cjs b/dist/yjs.cjs
index d2dc06ae11a6eb44f8c8445d4298c0e89c3e4da2..a30ab04fa9f3b77666939caa88335c68c40f194c 100644
--- a/dist/yjs.cjs
+++ b/dist/yjs.cjs
@@ -414,7 +414,7 @@ const equalDeleteSets = (ds1, ds2) => {
*/
-const generateNewClientId = random__namespace.uint32;
+const generateNewClientId = random__namespace.uint53;
/**
* @typedef {Object} DocOpts
diff --git a/dist/yjs.mjs b/dist/yjs.mjs
index 20c9e58c32bcb6bc714200a2561fd1f542c49523..14267e5e36d9781ca3810d5b70ff8c051dac779e 100644
--- a/dist/yjs.mjs
+++ b/dist/yjs.mjs
@@ -378,7 +378,7 @@ const equalDeleteSets = (ds1, ds2) => {
*/
-const generateNewClientId = random.uint32;
+const generateNewClientId = random.uint53;
/**
* @typedef {Object} DocOpts
diff --git a/src/utils/Doc.js b/src/utils/Doc.js
index 62643617c86e57c64dd9babdb792fa8888357ec0..4df5048ab12af1ae0f1154da67f06dce1fda7b49 100644
--- a/src/utils/Doc.js
+++ b/src/utils/Doc.js
@@ -20,7 +20,7 @@ import * as map from 'lib0/map'
import * as array from 'lib0/array'
import * as promise from 'lib0/promise'
-export const generateNewClientId = random.uint32
+export const generateNewClientId = random.uint53
/**
* @typedef {Object} DocOpts

View File

@@ -1,39 +0,0 @@
diff --git a/dist/yjs.cjs b/dist/yjs.cjs
index 8a343ca9d0a153e95b27ad337e0553a8cc80d5ca..7199cf6e05d9c2c3491e56c4d4bda109e1755563 100644
--- a/dist/yjs.cjs
+++ b/dist/yjs.cjs
@@ -416,7 +416,7 @@ const equalDeleteSets = (ds1, ds2) => {
*/
-const generateNewClientId = random__namespace.uint32;
+const generateNewClientId = random__namespace.uint53;
/**
* @typedef {Object} DocOpts
diff --git a/dist/yjs.mjs b/dist/yjs.mjs
index 1c29ce7fe8f146b78911d0af9a53d1b516e86494..220fa0faacf4dc2a787e18f7cc79100e7c516e3a 100644
--- a/dist/yjs.mjs
+++ b/dist/yjs.mjs
@@ -379,7 +379,7 @@ const equalDeleteSets = (ds1, ds2) => {
*/
-const generateNewClientId = random.uint32;
+const generateNewClientId = random.uint53;
/**
* @typedef {Object} DocOpts
diff --git a/src/utils/Doc.js b/src/utils/Doc.js
index d5165426f2314fc3c2388e64841e7cd6498a92a9..4bb2e5a8b79bb59f08a011af77e69af862312292 100644
--- a/src/utils/Doc.js
+++ b/src/utils/Doc.js
@@ -20,7 +20,7 @@ import * as map from 'lib0/map'
import * as array from 'lib0/array'
import * as promise from 'lib0/promise'
-export const generateNewClientId = random.uint32
+export const generateNewClientId = random.uint53
/**
* @typedef {Object} DocOpts

925
.yarn/releases/yarn-4.5.0.cjs vendored Executable file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -12,4 +12,4 @@ npmPublishAccess: public
npmPublishRegistry: "https://registry.npmjs.org"
yarnPath: .yarn/releases/yarn-4.6.0.cjs
yarnPath: .yarn/releases/yarn-4.5.0.cjs

1651
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,48 +1,29 @@
[workspace]
members = [
"./packages/backend/native",
"./packages/common/native",
"./packages/frontend/mobile-native",
"./packages/frontend/native",
"./packages/frontend/native/nbstore",
"./packages/frontend/native/schema",
"./packages/frontend/native/sqlite_v1",
]
members = ["./packages/backend/native", "./packages/frontend/native", "./packages/frontend/native/schema"]
resolver = "2"
[workspace.dependencies]
affine_common = { path = "./packages/common/native" }
affine_nbstore = { path = "./packages/frontend/native/nbstore" }
anyhow = "1"
base64-simd = "0.8"
chrono = "0.4"
criterion2 = { version = "2", default-features = false }
dashmap = "6"
dotenvy = "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"] }
objc2 = "0.5.2"
objc2-foundation = "0.2.2"
once_cell = "1"
parking_lot = "0.12"
homedir = "0.3"
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"] }
thiserror = "2"
tiktoken-rs = "0.6"
tokio = "1.37"
uniffi = "0.28"
uuid = "1.8"
v_htmlescape = "0.15"
y-octo = { git = "https://github.com/y-crdt/y-octo.git", branch = "main" }
anyhow = "1"
chrono = "0.4"
dotenv = "0.15"
file-format = { version = "0.25", 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 = "6", 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.5"
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

View File

@@ -1,7 +1,7 @@
<div align="center">
<h1 style="border-bottom: none">
<b><a href="https://affine.pro">AFFiNE.Pro</a></b><br />
<b><a href="https://affine.pro">AFFiNE.PRO</a></b><br />
Write, Draw and Plan All at Once
<br>
</h1>
@@ -33,6 +33,7 @@
[![Releases](https://img.shields.io/github/downloads/toeverything/AFFiNE/total)](https://github.com/toeverything/AFFiNE/releases/latest)
[![All Contributors][all-contributors-badge]](#contributors)
[![TypeScript-version-icon]](https://www.typescriptlang.org/)
[![Rust-version-icon]](https://www.rust-lang.org/)
</div>
@@ -54,7 +55,7 @@ Star us, and you will receive all release notifications from GitHub without any
## What is AFFiNE
[AFFiNE](https://affine.pro) is an open-source, all-in-one workspace and an operating system for all the building blocks that assemble your knowledge base and much more -- wiki, knowledge management, presentation and digital assets. It's a better alternative to Notion and Miro.
AFFiNE is an open-source, all-in-one workspace and an operating system for all the building blocks that assemble your knowledge base and much more -- wiki, knowledge management, presentation and digital assets. It's a better alternative to Notion and Miro.
## Features
@@ -64,7 +65,7 @@ Star us, and you will receive all release notifications from GitHub without any
**Multimodal AI partner ready to kick in any work**
- Write up professional work report? Turn an outline into expressive and presentable slides? Summary an article into a well-structured mindmap? Sorting your job plan and backlog for tasks? Or... draw and code prototype apps and web pages directly all with one prompt? With you, [AFFiNE AI](https://affine.pro/ai) pushes your creativity to the edge of your imagination,just like [Canvas AI](https://affine.pro/blog/best-canvas-ai) to generate mind map for brainstorming.
- Write up professional work report? Turn an outline into expressive and presentable slides? Summary an article into a well-structured mindmap? Sorting your job plan and backlog for tasks? Or... draw and code prototype apps and web pages directly all with one prompt? With you, AFFiNE AI pushes your creativity to the edge of your imagination.
**Local-first & Real-time collaborative**
@@ -107,37 +108,6 @@ Looking for **other ways to contribute** and wondering where to start? Check out
If you have questions, you are welcome to contact us. One of the best places to get more info and learn more is in the [AFFiNE Community](https://community.affine.pro) where you can engage with other like-minded individuals.
## Templates
AFFiNE now provides pre-built [templates](https://affine.pro/templates) from our team. Following are the Top 10 most popular templates among AFFiNE users,if you want to contribute, you can contribute your own template so other people can use it too.
- [vision board template](https://affine.pro/templates/category-vision-board-template)
- [one pager template](https://affine.pro/templates/category-one-pager-template-free)
- [sample lesson plan math template](https://affine.pro/templates/sample-lesson-plan-math-template)
- [grr lesson plan template free](https://affine.pro/templates/grr-lesson-plan-template-free)
- [free editable lesson plan template for pre k](https://affine.pro/templates/free-editable-lesson-plan-template-for-pre-k)
- [high note collection planners](https://affine.pro/templates/high-note-collection-planners)
- [digital planner](https://affine.pro/templates/category-digital-planner)
- [ADHD Planner](https://affine.pro/templates/adhd-planner)
- [Reading Log](https://affine.pro/templates/reading-log)
- [Cornell Notes Template](https://affine.pro/templates/category-cornell-notes-template)
## Blog
Welcome to the AFFiNE blog section! Here, youll find the latest insights, tips, and guides on how to maximize your experience with AFFiNE and AFFiNE AI, the leading Canvas AI tool for flexible note-taking and creative organization.
- [vision board template](https://affine.pro/blog/8-free-printable-vision-board-templates-examples-2023)
- [itinerary template](https://affine.pro/blog/free-customized-travel-itinerary-planner-templates)
- [one pager template](https://affine.pro/blog/top-12-one-pager-examples-how-to-create-your-own)
- [cornell notes template](https://affine.pro/blog/the-cornell-notes-template-and-system-learning-tips)
- [swot chart template](https://affine.pro/blog/top-10-free-editable-swot-analysis-template-examples)
- [apps like luna task](https://affine.pro/blog/apps-like-luna-task)
- [note taking ai from rough notes to mind map](https://affine.pro/blog/dynamic-AI-notes)
- [canvas ai](https://affine.pro/blog/best-canvas-ai)
- [one pager](https://affine.pro/blog/top-12-one-pager-examples-how-to-create-your-own)
- [SOP Template](https://affine.pro/blog/how-to-write-sop-step-by-step-guide-5-best-free-tools-templates)
- [Chore Chart](https://affine.pro/blog/10-best-free-chore-chart-templates-kids-adults)
## Ecosystem
| Name | | |
@@ -221,6 +191,7 @@ See [LICENSE] for details.
[jobs available]: ./docs/jobs.md
[latest packages]: https://github.com/toeverything/AFFiNE/pkgs/container/affine-self-hosted
[contributor license agreement]: https://github.com/toeverything/affine/edit/canary/.github/CLA.md
[rust-version-icon]: https://img.shields.io/badge/Rust-1.79.0-dea584
[stars-icon]: https://img.shields.io/github/stars/toeverything/AFFiNE.svg?style=flat&logo=github&colorB=red&label=stars
[codecov]: https://codecov.io/gh/toeverything/affine/branch/canary/graphs/badge.svg?branch=canary
[node-version-icon]: https://img.shields.io/badge/node-%3E=18.16.1-success

View File

@@ -6,8 +6,8 @@ We recommend users to always use the latest major version. Security updates will
| Version | Supported |
| --------------- | ------------------ |
| 0.17.x (stable) | :white_check_mark: |
| < 0.17.x | :x: |
| 0.15.x (stable) | :white_check_mark: |
| < 0.15.x | :x: |
## Reporting a Vulnerability

View File

@@ -1,101 +0,0 @@
{
"name": "@blocksuite/affine",
"description": "BlockSuite for Affine",
"type": "module",
"scripts": {
"build": "tsc --build --verbose",
"test:unit": "nx vite:test --run --passWithNoTests",
"test:unit:coverage": "nx vite:test --run --coverage",
"test:e2e": "playwright test"
},
"sideEffects": false,
"keywords": [],
"author": "toeverything",
"license": "MIT",
"dependencies": {
"@blocksuite/block-std": "workspace:*",
"@blocksuite/blocks": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/inline": "workspace:*",
"@blocksuite/presets": "workspace:*",
"@blocksuite/store": "workspace:*"
},
"exports": {
".": "./src/index.ts",
"./effects": "./src/effects.ts",
"./block-std": "./src/block-std/index.ts",
"./block-std/gfx": "./src/block-std/gfx.ts",
"./global": "./src/global/index.ts",
"./global/utils": "./src/global/utils.ts",
"./global/env": "./src/global/env.ts",
"./global/exceptions": "./src/global/exceptions.ts",
"./global/di": "./src/global/di.ts",
"./global/types": "./src/global/types.ts",
"./store": "./src/store/index.ts",
"./inline": "./src/inline/index.ts",
"./inline/consts": "./src/inline/consts.ts",
"./inline/types": "./src/inline/types.ts",
"./presets": "./src/presets/index.ts",
"./blocks": "./src/blocks/index.ts",
"./blocks/schemas": "./src/blocks/schemas.ts"
},
"typesVersions": {
"*": {
"effects": [
"dist/effects.d.ts"
],
"block-std": [
"dist/block-std/index.d.ts"
],
"block-std/gfx": [
"dist/block-std/gfx.d.ts"
],
"global": [
"dist/global/index.d.ts"
],
"global/utils": [
"dist/global/utils.d.ts"
],
"global/env": [
"dist/global/env.d.ts"
],
"global/exceptions": [
"dist/global/exceptions.d.ts"
],
"global/di": [
"dist/global/di.d.ts"
],
"global/types": [
"dist/global/types.d.ts"
],
"store": [
"dist/store/index.d.ts"
],
"inline": [
"dist/inline/index.d.ts"
],
"inline/consts": [
"dist/inline/consts.d.ts"
],
"inline/types": [
"dist/inline/types.d.ts"
],
"presets": [
"dist/presets/index.d.ts"
],
"blocks": [
"dist/blocks/index.d.ts"
],
"blocks/schemas": [
"dist/blocks/schemas.d.ts"
]
}
},
"files": [
"src",
"dist",
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.19.0"
}

View File

@@ -1 +0,0 @@
export * from '@blocksuite/block-std/gfx';

View File

@@ -1 +0,0 @@
export * from '@blocksuite/block-std';

View File

@@ -1 +0,0 @@
export * from '@blocksuite/blocks';

View File

@@ -1 +0,0 @@
export * from '@blocksuite/blocks/schemas';

View File

@@ -1,7 +0,0 @@
import { effects as blocksEffects } from '@blocksuite/blocks/effects';
import { effects as presetsEffects } from '@blocksuite/presets/effects';
export function effects() {
blocksEffects();
presetsEffects();
}

View File

@@ -1 +0,0 @@
export * from '@blocksuite/global/di';

View File

@@ -1 +0,0 @@
export * from '@blocksuite/global/env';

View File

@@ -1 +0,0 @@
export * from '@blocksuite/global/exceptions';

View File

@@ -1 +0,0 @@
export * from '@blocksuite/global';

View File

@@ -1 +0,0 @@
export * from '@blocksuite/global/types';

View File

@@ -1 +0,0 @@
export * from '@blocksuite/global/utils';

View File

@@ -1 +0,0 @@
export {};

View File

@@ -1 +0,0 @@
export * from '@blocksuite/inline/consts';

View File

@@ -1 +0,0 @@
export * from '@blocksuite/inline';

View File

@@ -1 +0,0 @@
export * from '@blocksuite/inline/types';

View File

@@ -1 +0,0 @@
export * from '@blocksuite/presets';

View File

@@ -1,5 +0,0 @@
/* eslint-disable @typescript-eslint/no-restricted-imports */
// oxlint-disable-next-line
// @ts-ignore FIXME: typecheck error
export * from '@blocksuite/store';

View File

@@ -1,29 +0,0 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src/",
"outDir": "./dist/",
"noEmit": false
},
"include": ["./src"],
"references": [
{
"path": "../../framework/global"
},
{
"path": "../../framework/store"
},
{
"path": "../../framework/block-std"
},
{
"path": "../../framework/inline"
},
{
"path": "../../blocks"
},
{
"path": "../../presets"
}
]
}

View File

@@ -1,30 +0,0 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
esbuild: {
target: 'es2018',
},
test: {
globalSetup: '../../../scripts/vitest-global.js',
include: ['src/__tests__/**/*.unit.spec.ts'],
testTimeout: 1000,
coverage: {
provider: 'istanbul', // or 'c8'
reporter: ['lcov'],
reportsDirectory: '../../../.coverage/affine',
},
/**
* Custom handler for console.log in tests.
*
* Return `false` to ignore the log.
*/
onConsoleLog(log, type) {
if (log.includes('https://lit.dev/msg/dev-mode')) {
return false;
}
console.warn(`Unexpected ${type} log`, log);
throw new Error(log);
},
environment: 'happy-dom',
},
});

View File

@@ -1,46 +0,0 @@
{
"name": "@blocksuite/affine-block-attachment",
"description": "Attachment block for BlockSuite.",
"type": "module",
"scripts": {
"build": "tsc",
"test:unit": "nx vite:test --run --passWithNoTests",
"test:unit:coverage": "nx vite:test --run --coverage",
"test:e2e": "playwright test"
},
"sideEffects": false,
"keywords": [],
"author": "toeverything",
"license": "MIT",
"dependencies": {
"@blocksuite/affine-block-embed": "workspace:*",
"@blocksuite/affine-block-surface": "workspace:*",
"@blocksuite/affine-components": "workspace:*",
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/block-std": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.1.75",
"@blocksuite/inline": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.10",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.3",
"file-type": "^19.5.0",
"lit": "^3.2.0",
"minimatch": "^10.0.1",
"zod": "^3.23.8"
},
"exports": {
".": "./src/index.ts",
"./effects": "./src/effects.ts"
},
"files": [
"src",
"dist",
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.19.0"
}

View File

@@ -1,115 +0,0 @@
import { AttachmentBlockSchema } from '@blocksuite/affine-model';
import {
BlockNotionHtmlAdapterExtension,
type BlockNotionHtmlAdapterMatcher,
FetchUtils,
HastUtils,
} from '@blocksuite/affine-shared/adapters';
import { getFilenameFromContentDisposition } from '@blocksuite/affine-shared/utils';
import { sha } from '@blocksuite/global/utils';
import { getAssetName, nanoid } from '@blocksuite/store';
export const attachmentBlockNotionHtmlAdapterMatcher: BlockNotionHtmlAdapterMatcher =
{
flavour: AttachmentBlockSchema.model.flavour,
toMatch: o => {
return (
HastUtils.isElement(o.node) &&
o.node.tagName === 'figure' &&
!!HastUtils.querySelector(o.node, '.source')
);
},
fromMatch: () => false,
toBlockSnapshot: {
enter: async (o, context) => {
if (!HastUtils.isElement(o.node)) {
return;
}
const { assets, walkerContext } = context;
if (!assets) {
return;
}
const embededFigureWrapper = HastUtils.querySelector(o.node, '.source');
let embededURL = '';
if (embededFigureWrapper) {
const embedA = HastUtils.querySelector(embededFigureWrapper, 'a');
embededURL =
typeof embedA?.properties.href === 'string'
? embedA.properties.href
: '';
}
if (embededURL) {
let blobId = '';
let name = '';
let type = '';
let size = 0;
if (!FetchUtils.fetchable(embededURL)) {
const embededURLSplit = embededURL.split('/');
while (embededURLSplit.length > 0) {
const key = assets
.getPathBlobIdMap()
.get(decodeURIComponent(embededURLSplit.join('/')));
if (key) {
blobId = key;
break;
}
embededURLSplit.shift();
}
const value = assets.getAssets().get(blobId);
if (value) {
name = getAssetName(assets.getAssets(), blobId);
size = value.size;
type = value.type;
}
} else {
const res = await fetch(embededURL).catch(error => {
console.warn('Error fetching embed:', error);
return null;
});
if (!res) {
return;
}
const resCloned = res.clone();
name =
getFilenameFromContentDisposition(
res.headers.get('Content-Disposition') ?? ''
) ??
(embededURL.split('/').at(-1) ?? 'file') +
'.' +
(res.headers.get('Content-Type')?.split('/').at(-1) ?? 'blob');
const file = new File([await res.blob()], name, {
type: res.headers.get('Content-Type') ?? '',
});
size = file.size;
type = file.type;
blobId = await sha(await resCloned.arrayBuffer());
assets?.getAssets().set(blobId, file);
await assets?.writeToBlob(blobId);
}
walkerContext
.openNode(
{
type: 'block',
id: nanoid(),
flavour: AttachmentBlockSchema.model.flavour,
props: {
name,
size,
type,
sourceId: blobId,
},
children: [],
},
'children'
)
.closeNode();
walkerContext.skipAllChildren();
}
},
},
fromBlockSnapshot: {},
};
export const AttachmentBlockNotionHtmlAdapterExtension =
BlockNotionHtmlAdapterExtension(attachmentBlockNotionHtmlAdapterMatcher);

View File

@@ -1,299 +0,0 @@
import { getEmbedCardIcons } from '@blocksuite/affine-block-embed';
import { CaptionedBlockComponent } from '@blocksuite/affine-components/caption';
import { HoverController } from '@blocksuite/affine-components/hover';
import {
AttachmentIcon16,
getAttachmentFileIcons,
} from '@blocksuite/affine-components/icons';
import { Peekable } from '@blocksuite/affine-components/peek';
import { toast } from '@blocksuite/affine-components/toast';
import {
type AttachmentBlockModel,
AttachmentBlockStyles,
} from '@blocksuite/affine-model';
import { ThemeProvider } from '@blocksuite/affine-shared/services';
import { humanFileSize } from '@blocksuite/affine-shared/utils';
import { Slice } from '@blocksuite/store';
import { flip, offset } from '@floating-ui/dom';
import { html, nothing } from 'lit';
import { property, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { ref } from 'lit/directives/ref.js';
import { styleMap } from 'lit/directives/style-map.js';
import type { AttachmentBlockService } from './attachment-service.js';
import { AttachmentOptionsTemplate } from './components/options.js';
import { AttachmentEmbedProvider } from './embed.js';
import { styles } from './styles.js';
import { checkAttachmentBlob, downloadAttachmentBlob } from './utils.js';
@Peekable()
export class AttachmentBlockComponent extends CaptionedBlockComponent<
AttachmentBlockModel,
AttachmentBlockService
> {
static override styles = styles;
protected _isDragging = false;
protected _isResizing = false;
protected _isSelected = false;
protected _whenHover: HoverController | null = new HoverController(
this,
({ abortController }) => {
const selection = this.host.selection;
const textSelection = selection.find('text');
if (
!!textSelection &&
(!!textSelection.to || !!textSelection.from.length)
) {
return null;
}
const blockSelections = selection.filter('block');
if (
blockSelections.length > 1 ||
(blockSelections.length === 1 &&
blockSelections[0].blockId !== this.blockId)
) {
return null;
}
return {
template: AttachmentOptionsTemplate({
block: this,
model: this.model,
abortController,
}),
computePosition: {
referenceElement: this,
placement: 'top-start',
middleware: [flip(), offset(4)],
autoUpdate: true,
},
};
}
);
blockDraggable = true;
protected containerStyleMap = styleMap({
position: 'relative',
width: '100%',
margin: '18px 0px',
});
convertTo = () => {
return this.std
.get(AttachmentEmbedProvider)
.convertTo(this.model, this.service.maxFileSize);
};
copy = () => {
const slice = Slice.fromModels(this.doc, [this.model]);
this.std.clipboard.copySlice(slice).catch(console.error);
toast(this.host, 'Copied to clipboard');
};
download = () => {
downloadAttachmentBlob(this);
};
embedded = () => {
return this.std
.get(AttachmentEmbedProvider)
.embedded(this.model, this.service.maxFileSize);
};
open = () => {
if (!this.blobUrl) {
return;
}
window.open(this.blobUrl, '_blank');
};
refreshData = () => {
checkAttachmentBlob(this).catch(console.error);
};
protected get embedView() {
return this.std
.get(AttachmentEmbedProvider)
.render(this.model, this.blobUrl, this.service.maxFileSize);
}
private _selectBlock() {
const selectionManager = this.host.selection;
const blockSelection = selectionManager.create('block', {
blockId: this.blockId,
});
selectionManager.setGroup('note', [blockSelection]);
}
override connectedCallback() {
super.connectedCallback();
this.refreshData();
this.contentEditable = 'false';
if (!this.model.style) {
this.doc.withoutTransact(() => {
this.doc.updateBlock(this.model, {
style: AttachmentBlockStyles[1],
});
});
}
this.model.propsUpdated.on(({ key }) => {
if (key === 'sourceId') {
// Reset the blob url when the sourceId is changed
if (this.blobUrl) {
URL.revokeObjectURL(this.blobUrl);
this.blobUrl = undefined;
}
this.refreshData();
}
});
// Workaround for https://github.com/toeverything/blocksuite/issues/4724
this.disposables.add(
this.std.get(ThemeProvider).theme$.subscribe(() => this.requestUpdate())
);
// this is required to prevent iframe from capturing pointer events
this.disposables.add(
this.std.selection.slots.changed.on(() => {
this._isSelected =
!!this.selected?.is('block') || !!this.selected?.is('surface');
this._showOverlay =
this._isResizing || this._isDragging || !this._isSelected;
})
);
// this is required to prevent iframe from capturing pointer events
this.handleEvent('dragStart', () => {
this._isDragging = true;
this._showOverlay =
this._isResizing || this._isDragging || !this._isSelected;
});
this.handleEvent('dragEnd', () => {
this._isDragging = false;
this._showOverlay =
this._isResizing || this._isDragging || !this._isSelected;
});
}
override disconnectedCallback() {
if (this.blobUrl) {
URL.revokeObjectURL(this.blobUrl);
}
super.disconnectedCallback();
}
override firstUpdated() {
// lazy bindings
this.disposables.addFromEvent(this, 'click', this.onClick);
}
protected onClick(event: MouseEvent) {
// the peek view need handle shift + click
if (event.defaultPrevented) return;
event.stopPropagation();
this._selectBlock();
}
override renderBlock() {
const { name, size, style } = this.model;
const cardStyle = style ?? AttachmentBlockStyles[1];
const theme = this.std.get(ThemeProvider).theme;
const { LoadingIcon } = getEmbedCardIcons(theme);
const titleIcon = this.loading ? LoadingIcon : AttachmentIcon16;
const titleText = this.loading ? 'Loading...' : name;
const infoText = this.error ? 'File loading failed.' : humanFileSize(size);
const fileType = name.split('.').pop() ?? '';
const FileTypeIcon = getAttachmentFileIcons(fileType);
const embedView = this.embedView;
return html`
<div
${this._whenHover ? ref(this._whenHover.setReference) : nothing}
class="affine-attachment-container"
draggable="${this.blockDraggable ? 'true' : 'false'}"
style=${this.containerStyleMap}
>
${embedView
? html`<div class="affine-attachment-embed-container">
${embedView}
<div
class=${classMap({
'affine-attachment-iframe-overlay': true,
hide: !this._showOverlay,
})}
></div>
</div>`
: html`<div
class=${classMap({
'affine-attachment-card': true,
[cardStyle]: true,
loading: this.loading,
error: this.error,
unsynced: false,
})}
>
<div class="affine-attachment-content">
<div class="affine-attachment-content-title">
<div class="affine-attachment-content-title-icon">
${titleIcon}
</div>
<div class="affine-attachment-content-title-text">
${titleText}
</div>
</div>
<div class="affine-attachment-content-info">${infoText}</div>
</div>
<div class="affine-attachment-banner">${FileTypeIcon}</div>
</div>`}
</div>
`;
}
@state()
protected accessor _showOverlay = true;
@property({ attribute: false })
accessor allowEmbed = false;
@property({ attribute: false })
accessor blobUrl: string | undefined = undefined;
@property({ attribute: false })
accessor downloading = false;
@property({ attribute: false })
accessor error = false;
@property({ attribute: false })
accessor loading = false;
override accessor useCaptionEditor = true;
}
declare global {
interface HTMLElementTagNameMap {
'affine-attachment': AttachmentBlockComponent;
}
}

View File

@@ -1,72 +0,0 @@
import { EdgelessLegacySlotIdentifier } from '@blocksuite/affine-block-surface';
import type { HoverController } from '@blocksuite/affine-components/hover';
import { AttachmentBlockStyles } from '@blocksuite/affine-model';
import {
EMBED_CARD_HEIGHT,
EMBED_CARD_WIDTH,
} from '@blocksuite/affine-shared/consts';
import { toGfxBlockComponent } from '@blocksuite/block-std';
import { styleMap } from 'lit/directives/style-map.js';
import { AttachmentBlockComponent } from './attachment-block.js';
export class AttachmentEdgelessBlockComponent extends toGfxBlockComponent(
AttachmentBlockComponent
) {
protected override _whenHover: HoverController | null = null;
override blockDraggable = false;
get slots() {
return this.std.get(EdgelessLegacySlotIdentifier);
}
override connectedCallback(): void {
super.connectedCallback();
this._disposables.add(
this.slots.elementResizeStart.on(() => {
this._isResizing = true;
this._showOverlay = true;
})
);
this._disposables.add(
this.slots.elementResizeEnd.on(() => {
this._isResizing = false;
this._showOverlay =
this._isResizing || this._isDragging || !this._isSelected;
})
);
}
override onClick(_: MouseEvent) {
return;
}
override renderGfxBlock() {
const { style$ } = this.model;
const cardStyle = style$.value ?? AttachmentBlockStyles[1];
const width = EMBED_CARD_WIDTH[cardStyle];
const height = EMBED_CARD_HEIGHT[cardStyle];
const bound = this.model.elementBound;
const scaleX = bound.w / width;
const scaleY = bound.h / height;
this.containerStyleMap = styleMap({
width: `${width}px`,
height: `${height}px`,
transform: `scale(${scaleX}, ${scaleY})`,
transformOrigin: '0 0',
overflow: 'hidden',
});
return this.renderPageContent();
}
}
declare global {
interface HTMLElementTagNameMap {
'affine-edgeless-attachment': AttachmentEdgelessBlockComponent;
}
}

View File

@@ -1,66 +0,0 @@
import { FileDropConfigExtension } from '@blocksuite/affine-components/drag-indicator';
import { AttachmentBlockSchema } from '@blocksuite/affine-model';
import { TelemetryProvider } from '@blocksuite/affine-shared/services';
import {
isInsideEdgelessEditor,
matchFlavours,
} from '@blocksuite/affine-shared/utils';
import { BlockService } from '@blocksuite/block-std';
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
import { addAttachments, addSiblingAttachmentBlocks } from './utils.js';
// bytes.parse('2GB')
const maxFileSize = 2147483648;
export class AttachmentBlockService extends BlockService {
static override readonly flavour = AttachmentBlockSchema.model.flavour;
maxFileSize = maxFileSize;
}
export const AttachmentDropOption = FileDropConfigExtension({
flavour: AttachmentBlockSchema.model.flavour,
onDrop: ({ files, targetModel, place, point, std }) => {
// generic attachment block for all files except images
const attachmentFiles = files.filter(
file => !file.type.startsWith('image/')
);
if (!attachmentFiles.length) return false;
if (
targetModel &&
!matchFlavours(targetModel, ['affine:surface' as BlockSuite.Flavour])
) {
addSiblingAttachmentBlocks(
std.host,
attachmentFiles,
// TODO: use max file size from service
maxFileSize,
targetModel,
place
).catch(console.error);
return true;
}
if (isInsideEdgelessEditor(std.host)) {
const gfx = std.get(GfxControllerIdentifier);
point = gfx.viewport.toViewCoordFromClientCoord(point);
addAttachments(std, attachmentFiles, point).catch(console.error);
std.getOptional(TelemetryProvider)?.track('CanvasElementAdded', {
control: 'canvas:drop',
page: 'whiteboard editor',
module: 'toolbar',
segment: 'toolbar',
type: 'attachment',
});
return true;
}
return false;
},
});

View File

@@ -1,30 +0,0 @@
import {
BlockViewExtension,
type ExtensionType,
FlavourExtension,
} from '@blocksuite/block-std';
import { literal } from 'lit/static-html.js';
import { AttachmentBlockNotionHtmlAdapterExtension } from './adapters/notion-html.js';
import {
AttachmentBlockService,
AttachmentDropOption,
} from './attachment-service.js';
import {
AttachmentEmbedConfigExtension,
AttachmentEmbedService,
} from './embed.js';
export const AttachmentBlockSpec: ExtensionType[] = [
FlavourExtension('affine:attachment'),
AttachmentBlockService,
BlockViewExtension('affine:attachment', model => {
return model.parent?.flavour === 'affine:surface'
? literal`affine-edgeless-attachment`
: literal`affine-attachment`;
}),
AttachmentDropOption,
AttachmentEmbedConfigExtension(),
AttachmentEmbedService,
AttachmentBlockNotionHtmlAdapterExtension,
];

View File

@@ -1,77 +0,0 @@
import {
CopyIcon,
DeleteIcon,
DownloadIcon,
DuplicateIcon,
RefreshIcon,
} from '@blocksuite/affine-components/icons';
import type { MenuItemGroup } from '@blocksuite/affine-components/toolbar';
import { cloneAttachmentProperties } from '../utils.js';
import type { AttachmentToolbarMoreMenuContext } from './context.js';
export const BUILT_IN_GROUPS: MenuItemGroup<AttachmentToolbarMoreMenuContext>[] =
[
{
type: 'clipboard',
items: [
{
type: 'copy',
label: 'Copy',
icon: CopyIcon,
disabled: ({ doc }) => doc.readonly,
action: ctx => ctx.blockComponent.copy(),
},
{
type: 'duplicate',
label: 'Duplicate',
icon: DuplicateIcon,
disabled: ({ doc }) => doc.readonly,
action: ({ doc, blockComponent, close }) => {
const model = blockComponent.model;
const prop: { flavour: 'affine:attachment' } = {
flavour: 'affine:attachment',
...cloneAttachmentProperties(model),
};
doc.addSiblingBlocks(model, [prop]);
close();
},
},
{
type: 'reload',
label: 'Reload',
icon: RefreshIcon,
disabled: ({ doc }) => doc.readonly,
action: ({ blockComponent, close }) => {
blockComponent.refreshData();
close();
},
},
{
type: 'download',
label: 'Download',
icon: DownloadIcon,
disabled: ({ doc }) => doc.readonly,
action: ({ blockComponent, close }) => {
blockComponent.download();
close();
},
},
],
},
{
type: 'delete',
items: [
{
type: 'delete',
label: 'Delete',
icon: DeleteIcon,
disabled: ({ doc }) => doc.readonly,
action: ({ doc, blockComponent, close }) => {
doc.deleteBlock(blockComponent.model);
close();
},
},
],
},
];

View File

@@ -1,45 +0,0 @@
import { MenuContext } from '@blocksuite/affine-components/toolbar';
import type { AttachmentBlockComponent } from '../attachment-block.js';
export class AttachmentToolbarMoreMenuContext extends MenuContext {
override close = () => {
this.abortController.abort();
};
get doc() {
return this.blockComponent.doc;
}
get host() {
return this.blockComponent.host;
}
get selectedBlockModels() {
if (this.blockComponent.model) return [this.blockComponent.model];
return [];
}
get std() {
return this.blockComponent.std;
}
constructor(
public blockComponent: AttachmentBlockComponent,
public abortController: AbortController
) {
super();
}
isEmpty() {
return false;
}
isMultiple() {
return false;
}
isSingle() {
return true;
}
}

View File

@@ -1,226 +0,0 @@
import {
CaptionIcon,
DownloadIcon,
EditIcon,
MoreVerticalIcon,
SmallArrowDownIcon,
} from '@blocksuite/affine-components/icons';
import { createLitPortal } from '@blocksuite/affine-components/portal';
import {
cloneGroups,
getMoreMenuConfig,
renderGroups,
renderToolbarSeparator,
} from '@blocksuite/affine-components/toolbar';
import {
type AttachmentBlockModel,
defaultAttachmentProps,
} from '@blocksuite/affine-model';
import {
EMBED_CARD_HEIGHT,
EMBED_CARD_WIDTH,
} from '@blocksuite/affine-shared/consts';
import { Bound } from '@blocksuite/global/utils';
import { flip, offset } from '@floating-ui/dom';
import { html, nothing } from 'lit';
import { join } from 'lit/directives/join.js';
import { repeat } from 'lit/directives/repeat.js';
import type { AttachmentBlockComponent } from '../attachment-block.js';
import { BUILT_IN_GROUPS } from './config.js';
import { AttachmentToolbarMoreMenuContext } from './context.js';
import { RenameModal } from './rename-model.js';
import { styles } from './styles.js';
export function attachmentViewToggleMenu({
block,
callback,
}: {
block: AttachmentBlockComponent;
callback?: () => void;
}) {
const model = block.model;
const readonly = model.doc.readonly;
const embedded = model.embed;
const viewType = embedded ? 'embed' : 'card';
const viewActions = [
{
type: 'card',
label: 'Card view',
disabled: readonly || !embedded,
action: () => {
const style = defaultAttachmentProps.style!;
const width = EMBED_CARD_WIDTH[style];
const height = EMBED_CARD_HEIGHT[style];
const bound = Bound.deserialize(model.xywh);
bound.w = width;
bound.h = height;
model.doc.updateBlock(model, {
style,
embed: false,
xywh: bound.serialize(),
});
callback?.();
},
},
{
type: 'embed',
label: 'Embed view',
disabled: readonly || embedded || !block.embedded(),
action: () => {
block.convertTo();
callback?.();
},
},
];
return html`
<editor-menu-button
.contentPadding=${'8px'}
.button=${html`
<editor-icon-button
aria-label="Switch view"
.justify=${'space-between'}
.labelHeight=${'20px'}
.iconContainerWidth=${'110px'}
>
<div class="label">
<span style="text-transform: capitalize">${viewType}</span>
view
</div>
${SmallArrowDownIcon}
</editor-icon-button>
`}
>
<div data-size="small" data-orientation="vertical">
${repeat(
viewActions,
button => button.type,
({ type, label, action, disabled }) => html`
<editor-menu-action
aria-label=${label}
data-testid=${`link-to-${type}`}
?data-selected=${type === viewType}
?disabled=${disabled}
@click=${action}
>
${label}
</editor-menu-action>
`
)}
</div>
</editor-menu-button>
`;
}
export function AttachmentOptionsTemplate({
block,
model,
abortController,
}: {
block: AttachmentBlockComponent;
model: AttachmentBlockModel;
abortController: AbortController;
}) {
const std = block.std;
const editorHost = block.host;
const readonly = model.doc.readonly;
const context = new AttachmentToolbarMoreMenuContext(block, abortController);
const groups = getMoreMenuConfig(std).configure(cloneGroups(BUILT_IN_GROUPS));
const moreMenuActions = renderGroups(groups, context);
const buttons = [
// preview
// html`
// <editor-icon-button aria-label="Preview" .tooltip=${'Preview'}>
// ${ViewIcon}
// </editor-icon-button>
// `,
readonly
? nothing
: html`
<editor-icon-button
aria-label="Rename"
.tooltip=${'Rename'}
@click=${() => {
abortController.abort();
const renameAbortController = new AbortController();
createLitPortal({
template: RenameModal({
model,
editorHost,
abortController: renameAbortController,
}),
computePosition: {
referenceElement: block,
placement: 'top-start',
middleware: [flip(), offset(4)],
// It has a overlay mask, so we don't need to update the position.
// autoUpdate: true,
},
abortController: renameAbortController,
});
}}
>
${EditIcon}
</editor-icon-button>
`,
attachmentViewToggleMenu({
block,
callback: () => abortController.abort(),
}),
readonly
? nothing
: html`
<editor-icon-button
aria-label="Download"
.tooltip=${'Download'}
@click=${() => block.download()}
>
${DownloadIcon}
</editor-icon-button>
`,
readonly
? nothing
: html`
<editor-icon-button
aria-label="Caption"
.tooltip=${'Caption'}
@click=${() => block.captionEditor?.show()}
>
${CaptionIcon}
</editor-icon-button>
`,
html`
<editor-menu-button
.contentPadding=${'8px'}
.button=${html`
<editor-icon-button aria-label="More" .tooltip=${'More'}>
${MoreVerticalIcon}
</editor-icon-button>
`}
>
<div data-size="large" data-orientation="vertical">
${moreMenuActions}
</div>
</editor-menu-button>
`,
];
return html`
<style>
${styles}
</style>
<editor-toolbar class="affine-attachment-toolbar">
${join(
buttons.filter(button => button !== nothing),
renderToolbarSeparator
)}
</editor-toolbar>
`;
}

View File

@@ -1,92 +0,0 @@
import { ConfirmIcon } from '@blocksuite/affine-components/icons';
import { toast } from '@blocksuite/affine-components/toast';
import type { AttachmentBlockModel } from '@blocksuite/affine-model';
import type { EditorHost } from '@blocksuite/block-std';
import { html } from 'lit';
import { createRef, ref } from 'lit/directives/ref.js';
import { renameStyles } from './styles.js';
export const RenameModal = ({
editorHost,
model,
abortController,
}: {
editorHost: EditorHost;
model: AttachmentBlockModel;
abortController: AbortController;
}) => {
const inputRef = createRef<HTMLInputElement>();
// Fix auto focus
setTimeout(() => inputRef.value?.focus());
const originalName = model.name;
const nameWithoutExtension = originalName.slice(
0,
originalName.lastIndexOf('.')
);
const originalExtension = originalName.slice(originalName.lastIndexOf('.'));
const includeExtension =
originalExtension.includes('.') &&
originalExtension.length <= 7 &&
// including the dot
originalName.length > originalExtension.length;
let fileName = includeExtension ? nameWithoutExtension : originalName;
const extension = includeExtension ? originalExtension : '';
const onConfirm = () => {
const newFileName = fileName + extension;
if (!newFileName) {
toast(editorHost, 'File name cannot be empty');
return;
}
model.doc.updateBlock(model, {
name: newFileName,
});
abortController.abort();
};
const onInput = (e: InputEvent) => {
fileName = (e.target as HTMLInputElement).value;
};
const onKeydown = (e: KeyboardEvent) => {
e.stopPropagation();
if (e.key === 'Escape' && !e.isComposing) {
abortController.abort();
return;
}
if (e.key === 'Enter' && !e.isComposing) {
onConfirm();
return;
}
};
return html`
<style>
${renameStyles}
</style>
<div
class="affine-attachment-rename-overlay-mask"
@click="${() => abortController.abort()}"
></div>
<div class="affine-attachment-rename-container">
<div class="affine-attachment-rename-input-wrapper">
<input
${ref(inputRef)}
type="text"
.value=${fileName}
@input=${onInput}
@keydown=${onKeydown}
/>
<span class="affine-attachment-rename-extension">${extension}</span>
</div>
<editor-icon-button
class="affine-confirm-button"
.iconSize=${'24px'}
@click=${onConfirm}
>
${ConfirmIcon}
</editor-icon-button>
</div>
`;
};

View File

@@ -1,101 +0,0 @@
import { FONT_XS, PANEL_BASE } from '@blocksuite/affine-shared/styles';
import { css } from 'lit';
export const renameStyles = css`
.affine-attachment-rename-container {
${PANEL_BASE};
position: relative;
display: flex;
align-items: center;
width: 320px;
gap: 12px;
padding: 12px;
z-index: var(--affine-z-index-popover);
}
.affine-attachment-rename-input-wrapper {
display: flex;
min-width: 280px;
height: 30px;
box-sizing: border-box;
padding: 4px 10px;
background: var(--affine-white-10);
border-radius: 4px;
border: 1px solid var(--affine-border-color);
}
.affine-attachment-rename-input-wrapper:focus-within {
border-color: var(--affine-blue-700);
box-shadow: var(--affine-active-shadow);
}
.affine-attachment-rename-input-wrapper input {
flex: 1;
border: none;
outline: none;
background: transparent;
color: var(--affine-text-primary-color);
${FONT_XS};
}
.affine-attachment-rename-input-wrapper input::placeholder {
color: var(--affine-placeholder-color);
}
.affine-attachment-rename-extension {
font-size: var(--affine-font-xs);
color: var(--affine-text-secondary-color);
}
.affine-attachment-rename-overlay-mask {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: var(--affine-z-index-popover);
}
`;
export const moreMenuStyles = css`
.affine-attachment-options-more {
box-sizing: border-box;
padding-bottom: 4px;
}
.affine-attachment-options-more-container {
display: flex;
flex-direction: column;
align-items: center;
color: var(--affine-text-primary-color);
border-radius: 8px;
padding: 8px;
background: var(--affine-background-overlay-panel-color);
box-shadow: var(--affine-shadow-2);
}
.affine-attachment-options-more-container > icon-button {
display: flex;
align-items: center;
padding: 8px;
gap: 8px;
}
.affine-attachment-options-more-container > icon-button[hidden] {
display: none;
}
.affine-attachment-options-more-container > icon-button:hover.danger {
background: var(--affine-background-error-color);
color: var(--affine-error-color);
}
.affine-attachment-options-more-container > icon-button:hover.danger > svg {
color: var(--affine-error-color);
}
`;
export const styles = css`
:host {
z-index: 1;
}
`;

View File

@@ -1,19 +0,0 @@
import { AttachmentBlockComponent } from './attachment-block';
import { AttachmentEdgelessBlockComponent } from './attachment-edgeless-block';
import type { AttachmentBlockService } from './attachment-service';
export function effects() {
customElements.define(
'affine-edgeless-attachment',
AttachmentEdgelessBlockComponent
);
customElements.define('affine-attachment', AttachmentBlockComponent);
}
declare global {
namespace BlockSuite {
interface BlockServices {
'affine:attachment': AttachmentBlockService;
}
}
}

View File

@@ -1,192 +0,0 @@
import type {
AttachmentBlockModel,
ImageBlockProps,
} from '@blocksuite/affine-model';
import {
transformModel,
withTempBlobData,
} from '@blocksuite/affine-shared/utils';
import type { ExtensionType } from '@blocksuite/block-std';
import { Extension } from '@blocksuite/block-std';
import type { Container } from '@blocksuite/global/di';
import { createIdentifier } from '@blocksuite/global/di';
import type { TemplateResult } from 'lit';
import { html } from 'lit';
export type AttachmentEmbedConfig = {
name: string;
/**
* Check if the attachment can be turned into embed view.
*/
check: (model: AttachmentBlockModel, maxFileSize: number) => boolean;
/**
* The action will be executed when the 「Turn into embed view」 button is clicked.
*/
action?: (model: AttachmentBlockModel) => Promise<void> | void;
/**
* The template will be used to render the embed view.
*/
template?: (model: AttachmentBlockModel, blobUrl: string) => TemplateResult;
};
// Single embed config.
export const AttachmentEmbedConfigIdentifier =
createIdentifier<AttachmentEmbedConfig>(
'AffineAttachmentEmbedConfigIdentifier'
);
export function AttachmentEmbedConfigExtension(
configs: AttachmentEmbedConfig[] = embedConfig
): ExtensionType {
return {
setup: di => {
configs.forEach(option => {
di.addImpl(AttachmentEmbedConfigIdentifier(option.name), () => option);
});
},
};
}
// A embed config map.
export const AttachmentEmbedConfigMapIdentifier = createIdentifier<
Map<string, AttachmentEmbedConfig>
>('AffineAttachmentEmbedConfigMapIdentifier');
export const AttachmentEmbedProvider = createIdentifier<AttachmentEmbedService>(
'AffineAttachmentEmbedProvider'
);
export class AttachmentEmbedService extends Extension {
// 10MB
static MAX_EMBED_SIZE = 10 * 1024 * 1024;
get keys() {
return this.configs.keys();
}
get values() {
return this.configs.values();
}
constructor(private readonly configs: Map<string, AttachmentEmbedConfig>) {
super();
}
static override setup(di: Container) {
di.addImpl(AttachmentEmbedConfigMapIdentifier, provider =>
provider.getAll(AttachmentEmbedConfigIdentifier)
);
di.addImpl(AttachmentEmbedProvider, AttachmentEmbedService, [
AttachmentEmbedConfigMapIdentifier,
]);
}
// Converts to embed view.
convertTo(
model: AttachmentBlockModel,
maxFileSize = AttachmentEmbedService.MAX_EMBED_SIZE
) {
const config = this.values.find(config => config.check(model, maxFileSize));
if (!config || !config.action) {
model.doc.updateBlock(model, { embed: true });
return;
}
config.action(model)?.catch(console.error);
}
embedded(
model: AttachmentBlockModel,
maxFileSize = AttachmentEmbedService.MAX_EMBED_SIZE
) {
return this.values.some(config => config.check(model, maxFileSize));
}
render(
model: AttachmentBlockModel,
blobUrl?: string,
maxFileSize = AttachmentEmbedService.MAX_EMBED_SIZE
) {
if (!model.embed || !blobUrl) return;
const config = this.values.find(config => config.check(model, maxFileSize));
if (!config || !config.template) {
console.error('No embed view template found!', model, model.type);
return;
}
return config.template(model, blobUrl);
}
}
const embedConfig: AttachmentEmbedConfig[] = [
{
name: 'image',
check: model =>
model.doc.schema.flavourSchemaMap.has('affine:image') &&
model.type.startsWith('image/'),
action: model => turnIntoImageBlock(model),
},
{
name: 'pdf',
check: (model, maxFileSize) =>
model.type === 'application/pdf' && model.size <= maxFileSize,
template: (_, blobUrl) => {
// More options: https://tinytip.co/tips/html-pdf-params/
// https://chromium.googlesource.com/chromium/src/+/refs/tags/121.0.6153.1/chrome/browser/resources/pdf/open_pdf_params_parser.ts
const parameters = '#toolbar=0';
return html`<iframe
style="width: 100%; color-scheme: auto;"
height="480"
src=${blobUrl + parameters}
loading="lazy"
scrolling="no"
frameborder="no"
allowTransparency
allowfullscreen
type="application/pdf"
></iframe>`;
},
},
{
name: 'video',
check: (model, maxFileSize) =>
model.type.startsWith('video/') && model.size <= maxFileSize,
template: (_, blobUrl) =>
html`<video width="100%;" height="480" controls src=${blobUrl}></video>`,
},
{
name: 'audio',
check: (model, maxFileSize) =>
model.type.startsWith('audio/') && model.size <= maxFileSize,
template: (_, blobUrl) =>
html`<audio controls src=${blobUrl} style="margin: 4px;"></audio>`,
},
];
/**
* Turn the attachment block into an image block.
*/
export function turnIntoImageBlock(model: AttachmentBlockModel) {
if (!model.doc.schema.flavourSchemaMap.has('affine:image')) {
console.error('The image flavour is not supported!');
return;
}
const sourceId = model.sourceId;
if (!sourceId) return;
const { saveAttachmentData, getImageData } = withTempBlobData();
saveAttachmentData(sourceId, { name: model.name });
const imageConvertData = model.sourceId
? getImageData(model.sourceId)
: undefined;
const imageProp: Partial<ImageBlockProps> = {
sourceId,
caption: model.caption,
size: model.size,
...imageConvertData,
};
transformModel(model, 'affine:image', imageProp);
}

View File

@@ -1,11 +0,0 @@
export * from './adapters/notion-html';
export * from './attachment-block';
export * from './attachment-service';
export * from './attachment-spec';
export { attachmentViewToggleMenu } from './components/options';
export {
type AttachmentEmbedConfig,
AttachmentEmbedConfigIdentifier,
AttachmentEmbedProvider,
} from './embed';
export { addAttachments, addSiblingAttachmentBlocks } from './utils';

View File

@@ -1,158 +0,0 @@
import {
EMBED_CARD_HEIGHT,
EMBED_CARD_WIDTH,
} from '@blocksuite/affine-shared/consts';
import { css } from 'lit';
export const styles = css`
.affine-attachment-card {
margin: 0 auto;
box-sizing: border-box;
display: flex;
gap: 12px;
width: 100%;
height: ${EMBED_CARD_HEIGHT.horizontalThin}px;
padding: 12px;
border-radius: 8px;
border: 1px solid var(--affine-background-tertiary-color);
opacity: var(--add, 1);
background: var(--affine-background-primary-color);
user-select: none;
}
.affine-attachment-content {
height: 100%;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 12px;
flex: 1 0 0;
border-radius: var(--1, 0px);
opacity: var(--add, 1);
}
.affine-attachment-content-title {
display: flex;
flex-direction: row;
gap: 8px;
align-items: center;
align-self: stretch;
padding: var(--1, 0px);
border-radius: var(--1, 0px);
opacity: var(--add, 1);
}
.affine-attachment-content-title-icon {
display: flex;
width: 16px;
height: 16px;
align-items: center;
justify-content: center;
}
.affine-attachment-content-title-icon svg {
width: 16px;
height: 16px;
fill: var(--affine-background-primary-color);
}
.affine-attachment-content-title-text {
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
word-break: break-all;
overflow: hidden;
text-overflow: ellipsis;
color: var(--affine-text-primary-color);
font-family: var(--affine-font-family);
font-size: var(--affine-font-sm);
font-style: normal;
font-weight: 600;
line-height: 22px;
}
.affine-attachment-content-info {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
flex: 1 0 0;
word-break: break-all;
overflow: hidden;
color: var(--affine-text-secondary-color);
text-overflow: ellipsis;
font-family: var(--affine-font-family);
font-size: var(--affine-font-xs);
font-style: normal;
font-weight: 400;
line-height: 20px;
}
.affine-attachment-banner {
display: flex;
align-items: center;
justify-content: center;
}
.affine-attachment-banner svg {
width: 40px;
height: 40px;
}
.affine-attachment-card.loading {
background: var(--affine-background-secondary-color);
.affine-attachment-content-title-text {
color: var(--affine-placeholder-color);
}
}
.affine-attachment-card.error,
.affine-attachment-card.unsynced {
background: var(--affine-background-secondary-color);
}
.affine-attachment-card.cubeThick {
width: ${EMBED_CARD_WIDTH.cubeThick}px;
height: ${EMBED_CARD_HEIGHT.cubeThick}px;
flex-direction: column-reverse;
.affine-attachment-content {
width: 100%;
flex-direction: column;
align-items: flex-start;
justify-content: space-between;
}
.affine-attachment-banner {
justify-content: flex-start;
}
}
.affine-attachment-embed-container {
position: relative;
width: 100%;
height: 100%;
}
.affine-attachment-iframe-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.affine-attachment-iframe-overlay.hide {
display: none;
}
`;

View File

@@ -1,336 +0,0 @@
import { toast } from '@blocksuite/affine-components/toast';
import type {
AttachmentBlockModel,
AttachmentBlockProps,
} from '@blocksuite/affine-model';
import { defaultAttachmentProps } from '@blocksuite/affine-model';
import {
EMBED_CARD_HEIGHT,
EMBED_CARD_WIDTH,
} from '@blocksuite/affine-shared/consts';
import { TelemetryProvider } from '@blocksuite/affine-shared/services';
import { humanFileSize } from '@blocksuite/affine-shared/utils';
import type { BlockStdScope, EditorHost } from '@blocksuite/block-std';
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
import { Bound, type IVec, Point, Vec } from '@blocksuite/global/utils';
import type { BlockModel } from '@blocksuite/store';
import type { AttachmentBlockComponent } from './attachment-block.js';
export function cloneAttachmentProperties(model: AttachmentBlockModel) {
const clonedProps = {} as AttachmentBlockProps;
for (const cur in defaultAttachmentProps) {
const key = cur as keyof AttachmentBlockProps;
// @ts-expect-error it's safe because we just cloned the props simply
clonedProps[key] = model[
key
] as AttachmentBlockProps[keyof AttachmentBlockProps];
}
return clonedProps;
}
const attachmentUploads = new Set<string>();
export function setAttachmentUploading(blockId: string) {
attachmentUploads.add(blockId);
}
export function setAttachmentUploaded(blockId: string) {
attachmentUploads.delete(blockId);
}
function isAttachmentUploading(blockId: string) {
return attachmentUploads.has(blockId);
}
/**
* This function will not verify the size of the file.
*/
export async function uploadAttachmentBlob(
editorHost: EditorHost,
blockId: string,
blob: Blob,
filetype: string,
isEdgeless?: boolean
): Promise<void> {
if (isAttachmentUploading(blockId)) {
return;
}
const doc = editorHost.doc;
let sourceId: string | undefined;
try {
setAttachmentUploading(blockId);
sourceId = await doc.blobSync.set(blob);
} catch (error) {
console.error(error);
if (error instanceof Error) {
toast(
editorHost,
`Failed to upload attachment! ${error.message || error.toString()}`
);
}
} finally {
setAttachmentUploaded(blockId);
const block = doc.getBlock(blockId);
doc.withoutTransact(() => {
if (!block) return;
doc.updateBlock(block.model, {
sourceId,
} satisfies Partial<AttachmentBlockProps>);
});
editorHost.std
.getOptional(TelemetryProvider)
?.track('AttachmentUploadedEvent', {
page: `${isEdgeless ? 'whiteboard' : 'doc'} editor`,
module: 'attachment',
segment: 'attachment',
control: 'uploader',
type: filetype,
category: block && sourceId ? 'success' : 'failure',
});
}
}
async function getAttachmentBlob(model: AttachmentBlockModel) {
const sourceId = model.sourceId;
if (!sourceId) {
return null;
}
const doc = model.doc;
let blob = await doc.blobSync.get(sourceId);
if (blob) {
blob = new Blob([blob], { type: model.type });
}
return blob;
}
export async function checkAttachmentBlob(block: AttachmentBlockComponent) {
const model = block.model;
const { id, sourceId } = model;
if (isAttachmentUploading(id)) {
block.loading = true;
block.error = false;
block.allowEmbed = false;
if (block.blobUrl) {
URL.revokeObjectURL(block.blobUrl);
block.blobUrl = undefined;
}
return;
}
try {
if (!sourceId) {
return;
}
const blob = await getAttachmentBlob(model);
if (!blob) {
return;
}
block.loading = false;
block.error = false;
block.allowEmbed = block.embedded();
if (block.blobUrl) {
URL.revokeObjectURL(block.blobUrl);
}
block.blobUrl = URL.createObjectURL(blob);
} catch (error) {
console.warn(error, model, sourceId);
block.loading = false;
block.error = true;
block.allowEmbed = false;
if (block.blobUrl) {
URL.revokeObjectURL(block.blobUrl);
block.blobUrl = undefined;
}
}
}
/**
* Since the size of the attachment may be very large,
* the download process may take a long time!
*/
export function downloadAttachmentBlob(block: AttachmentBlockComponent) {
const { host, model, loading, error, downloading, blobUrl } = block;
if (downloading) {
toast(host, 'Download in progress...');
return;
}
if (loading) {
toast(host, 'Please wait, file is loading...');
return;
}
const name = model.name;
const shortName = name.length < 20 ? name : name.slice(0, 20) + '...';
if (error || !blobUrl) {
toast(host, `Failed to download ${shortName}!`);
return;
}
block.downloading = true;
toast(host, `Downloading ${shortName}`);
const tmpLink = document.createElement('a');
const event = new MouseEvent('click');
tmpLink.download = name;
tmpLink.href = blobUrl;
tmpLink.dispatchEvent(event);
tmpLink.remove();
block.downloading = false;
}
export async function getFileType(file: File) {
if (file.type) {
return file.type;
}
// If the file type is not available, try to get it from the buffer.
const buffer = await file.arrayBuffer();
const FileType = await import('file-type');
const fileType = await FileType.fileTypeFromBuffer(buffer);
return fileType ? fileType.mime : '';
}
/**
* Add a new attachment block before / after the specified block.
*/
export async function addSiblingAttachmentBlocks(
editorHost: EditorHost,
files: File[],
maxFileSize: number,
targetModel: BlockModel,
place: 'before' | 'after' = 'after'
) {
if (!files.length) {
return;
}
const isSizeExceeded = files.some(file => file.size > maxFileSize);
if (isSizeExceeded) {
toast(
editorHost,
`You can only upload files less than ${humanFileSize(
maxFileSize,
true,
0
)}`
);
return;
}
const doc = targetModel.doc;
// Get the types of all files
const types = await Promise.all(files.map(file => getFileType(file)));
const attachmentBlockProps: (Partial<AttachmentBlockProps> & {
flavour: 'affine:attachment';
})[] = files.map((file, index) => ({
flavour: 'affine:attachment',
name: file.name,
size: file.size,
type: types[index],
}));
const blockIds = doc.addSiblingBlocks(
targetModel,
attachmentBlockProps,
place
);
blockIds.forEach(
(blockId, index) =>
void uploadAttachmentBlob(editorHost, blockId, files[index], types[index])
);
return blockIds;
}
export async function addAttachments(
std: BlockStdScope,
files: File[],
point?: IVec
): Promise<string[]> {
if (!files.length) return [];
const attachmentService = std.getService('affine:attachment');
const gfx = std.get(GfxControllerIdentifier);
if (!attachmentService) {
console.error('Attachment service not found');
return [];
}
const maxFileSize = attachmentService.maxFileSize;
const isSizeExceeded = files.some(file => file.size > maxFileSize);
if (isSizeExceeded) {
toast(
std.host,
`You can only upload files less than ${humanFileSize(
maxFileSize,
true,
0
)}`
);
return [];
}
let { x, y } = gfx.viewport.center;
if (point) [x, y] = gfx.viewport.toModelCoord(...point);
const CARD_STACK_GAP = 32;
const dropInfos: { blockId: string; file: File }[] = files.map(
(file, index) => {
const point = new Point(
x + index * CARD_STACK_GAP,
y + index * CARD_STACK_GAP
);
const center = Vec.toVec(point);
const bound = Bound.fromCenter(
center,
EMBED_CARD_WIDTH.cubeThick,
EMBED_CARD_HEIGHT.cubeThick
);
const blockId = std.doc.addBlock(
'affine:attachment',
{
name: file.name,
size: file.size,
type: file.type,
style: 'cubeThick',
xywh: bound.serialize(),
} satisfies Partial<AttachmentBlockProps>,
gfx.surface
);
return { blockId, file };
}
);
// upload file and update the attachment model
const uploadPromises = dropInfos.map(async ({ blockId, file }) => {
const filetype = await getFileType(file);
await uploadAttachmentBlob(std.host, blockId, file, filetype, true);
return blockId;
});
const blockIds = await Promise.all(uploadPromises);
gfx.selection.set({
elements: blockIds,
editing: false,
});
return blockIds;
}

View File

@@ -1,38 +0,0 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src/",
"outDir": "./dist/",
"noEmit": false
},
"include": ["./src"],
"references": [
{
"path": "../../framework/global"
},
{
"path": "../../framework/store"
},
{
"path": "../../framework/block-std"
},
{
"path": "../../framework/inline"
},
{
"path": "../model"
},
{
"path": "../components"
},
{
"path": "../shared"
},
{
"path": "../block-embed"
},
{
"path": "../block-surface"
}
]
}

View File

@@ -1,44 +0,0 @@
{
"name": "@blocksuite/affine-block-bookmark",
"description": "Bookmark block for BlockSuite.",
"type": "module",
"scripts": {
"build": "tsc",
"test:unit": "nx vite:test --run --passWithNoTests",
"test:unit:coverage": "nx vite:test --run --coverage",
"test:e2e": "playwright test"
},
"sideEffects": false,
"keywords": [],
"author": "toeverything",
"license": "MIT",
"dependencies": {
"@blocksuite/affine-block-embed": "workspace:*",
"@blocksuite/affine-components": "workspace:*",
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/block-std": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.1.75",
"@blocksuite/inline": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.10",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.3",
"lit": "^3.2.0",
"minimatch": "^10.0.1",
"zod": "^3.23.8"
},
"exports": {
".": "./src/index.ts",
"./effects": "./src/effects.ts"
},
"files": [
"src",
"dist",
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.19.0"
}

View File

@@ -1,13 +0,0 @@
import type { ExtensionType } from '@blocksuite/block-std';
import { BookmarkBlockHtmlAdapterExtension } from './html.js';
import { BookmarkBlockMarkdownAdapterExtension } from './markdown.js';
import { BookmarkBlockNotionHtmlAdapterExtension } from './notion-html.js';
import { BookmarkBlockPlainTextAdapterExtension } from './plain-text.js';
export const BookmarkBlockAdapterExtensions: ExtensionType[] = [
BookmarkBlockHtmlAdapterExtension,
BookmarkBlockMarkdownAdapterExtension,
BookmarkBlockNotionHtmlAdapterExtension,
BookmarkBlockPlainTextAdapterExtension,
];

View File

@@ -1,10 +0,0 @@
import { createEmbedBlockHtmlAdapterMatcher } from '@blocksuite/affine-block-embed';
import { BookmarkBlockSchema } from '@blocksuite/affine-model';
import { BlockHtmlAdapterExtension } from '@blocksuite/affine-shared/adapters';
export const bookmarkBlockHtmlAdapterMatcher =
createEmbedBlockHtmlAdapterMatcher(BookmarkBlockSchema.model.flavour);
export const BookmarkBlockHtmlAdapterExtension = BlockHtmlAdapterExtension(
bookmarkBlockHtmlAdapterMatcher
);

View File

@@ -1,4 +0,0 @@
export * from './html.js';
export * from './markdown.js';
export * from './notion-html.js';
export * from './plain-text.js';

View File

@@ -1,9 +0,0 @@
import { createEmbedBlockMarkdownAdapterMatcher } from '@blocksuite/affine-block-embed';
import { BookmarkBlockSchema } from '@blocksuite/affine-model';
import { BlockMarkdownAdapterExtension } from '@blocksuite/affine-shared/adapters';
export const bookmarkBlockMarkdownAdapterMatcher =
createEmbedBlockMarkdownAdapterMatcher(BookmarkBlockSchema.model.flavour);
export const BookmarkBlockMarkdownAdapterExtension =
BlockMarkdownAdapterExtension(bookmarkBlockMarkdownAdapterMatcher);

View File

@@ -1,71 +0,0 @@
import { BookmarkBlockSchema } from '@blocksuite/affine-model';
import {
BlockNotionHtmlAdapterExtension,
type BlockNotionHtmlAdapterMatcher,
HastUtils,
} from '@blocksuite/affine-shared/adapters';
import { nanoid } from '@blocksuite/store';
export const bookmarkBlockNotionHtmlAdapterMatcher: BlockNotionHtmlAdapterMatcher =
{
flavour: BookmarkBlockSchema.model.flavour,
toMatch: o => {
return (
HastUtils.isElement(o.node) &&
o.node.tagName === 'figure' &&
!!HastUtils.querySelector(o.node, '.bookmark')
);
},
fromMatch: () => false,
toBlockSnapshot: {
enter: (o, context) => {
if (!HastUtils.isElement(o.node)) {
return;
}
const bookmark = HastUtils.querySelector(o.node, '.bookmark');
if (!bookmark) {
return;
}
const { walkerContext } = context;
const bookmarkURL = bookmark.properties?.href;
const bookmarkTitle = HastUtils.getTextContent(
HastUtils.querySelector(bookmark, '.bookmark-title')
);
const bookmarkDescription = HastUtils.getTextContent(
HastUtils.querySelector(bookmark, '.bookmark-description')
);
const bookmarkIcon = HastUtils.querySelector(
bookmark,
'.bookmark-icon'
);
const bookmarkIconURL =
typeof bookmarkIcon?.properties?.src === 'string'
? bookmarkIcon.properties.src
: '';
walkerContext
.openNode(
{
type: 'block',
id: nanoid(),
flavour: BookmarkBlockSchema.model.flavour,
props: {
type: 'card',
url: bookmarkURL ?? '',
title: bookmarkTitle,
description: bookmarkDescription,
icon: bookmarkIconURL,
},
children: [],
},
'children'
)
.closeNode();
walkerContext.skipAllChildren();
},
},
fromBlockSnapshot: {},
};
export const BookmarkBlockNotionHtmlAdapterExtension =
BlockNotionHtmlAdapterExtension(bookmarkBlockNotionHtmlAdapterMatcher);

View File

@@ -1,9 +0,0 @@
import { createEmbedBlockPlainTextAdapterMatcher } from '@blocksuite/affine-block-embed';
import { BookmarkBlockSchema } from '@blocksuite/affine-model';
import { BlockPlainTextAdapterExtension } from '@blocksuite/affine-shared/adapters';
export const bookmarkBlockPlainTextAdapterMatcher =
createEmbedBlockPlainTextAdapterMatcher(BookmarkBlockSchema.model.flavour);
export const BookmarkBlockPlainTextAdapterExtension =
BlockPlainTextAdapterExtension(bookmarkBlockPlainTextAdapterMatcher);

View File

@@ -1,119 +0,0 @@
import {
CaptionedBlockComponent,
SelectedStyle,
} from '@blocksuite/affine-components/caption';
import type { BookmarkBlockModel } from '@blocksuite/affine-model';
import { DocModeProvider } from '@blocksuite/affine-shared/services';
import { html } from 'lit';
import { property, query } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { type StyleInfo, styleMap } from 'lit/directives/style-map.js';
import type { BookmarkBlockService } from './bookmark-service.js';
import { refreshBookmarkUrlData } from './utils.js';
export const BOOKMARK_MIN_WIDTH = 450;
export class BookmarkBlockComponent extends CaptionedBlockComponent<
BookmarkBlockModel,
BookmarkBlockService
> {
private _fetchAbortController?: AbortController;
blockDraggable = true;
protected containerStyleMap!: ReturnType<typeof styleMap>;
open = () => {
let link = this.model.url;
if (!link.match(/^[a-zA-Z]+:\/\//)) {
link = 'https://' + link;
}
window.open(link, '_blank');
};
refreshData = () => {
refreshBookmarkUrlData(this, this._fetchAbortController?.signal).catch(
console.error
);
};
override connectedCallback() {
super.connectedCallback();
const mode = this.std.get(DocModeProvider).getEditorMode();
const miniWidth = `${BOOKMARK_MIN_WIDTH}px`;
this.containerStyleMap = styleMap({
position: 'relative',
width: '100%',
...(mode === 'edgeless' ? { miniWidth } : {}),
});
this._fetchAbortController = new AbortController();
this.contentEditable = 'false';
if (!this.model.description && !this.model.title) {
this.refreshData();
}
this.disposables.add(
this.model.propsUpdated.on(({ key }) => {
if (key === 'url') {
this.refreshData();
}
})
);
}
override disconnectedCallback(): void {
super.disconnectedCallback();
this._fetchAbortController?.abort();
}
override renderBlock() {
const selected = !!this.selected?.is('block');
return html`
<div
draggable="${this.blockDraggable ? 'true' : 'false'}"
class=${classMap({
'affine-bookmark-container': true,
'selected-style': selected,
})}
style=${this.containerStyleMap}
>
<bookmark-card
.bookmark=${this}
.loading=${this.loading}
.error=${this.error}
></bookmark-card>
</div>
`;
}
protected override accessor blockContainerStyles: StyleInfo = {
margin: '18px 0',
};
@query('bookmark-card')
accessor bookmarkCard!: HTMLElement;
@property({ attribute: false })
accessor error = false;
@property({ attribute: false })
accessor loading = false;
override accessor selectedStyle = SelectedStyle.Border;
override accessor useCaptionEditor = true;
override accessor useZeroWidth = true;
}
declare global {
interface HTMLElementTagNameMap {
'affine-bookmark': BookmarkBlockComponent;
}
}

View File

@@ -1,53 +0,0 @@
import {
EMBED_CARD_HEIGHT,
EMBED_CARD_WIDTH,
} from '@blocksuite/affine-shared/consts';
import { toGfxBlockComponent } from '@blocksuite/block-std';
import { styleMap } from 'lit/directives/style-map.js';
import { BookmarkBlockComponent } from './bookmark-block.js';
export class BookmarkEdgelessBlockComponent extends toGfxBlockComponent(
BookmarkBlockComponent
) {
override blockDraggable = false;
override getRenderingRect() {
const elementBound = this.model.elementBound;
const style = this.model.style$.value;
return {
x: elementBound.x,
y: elementBound.y,
w: EMBED_CARD_WIDTH[style],
h: EMBED_CARD_HEIGHT[style],
zIndex: this.toZIndex(),
};
}
override renderGfxBlock() {
const style = this.model.style$.value;
const width = EMBED_CARD_WIDTH[style];
const height = EMBED_CARD_HEIGHT[style];
const bound = this.model.elementBound;
const scaleX = bound.w / width;
const scaleY = bound.h / height;
this.containerStyleMap = styleMap({
width: `100%`,
height: `100%`,
transform: `scale(${scaleX}, ${scaleY})`,
transformOrigin: '0 0',
});
return this.renderPageContent();
}
protected override accessor blockContainerStyles = {};
}
declare global {
interface HTMLElementTagNameMap {
'affine-edgeless-bookmark': BookmarkEdgelessBlockComponent;
}
}

View File

@@ -1,16 +0,0 @@
import { LinkPreviewer } from '@blocksuite/affine-block-embed';
import { BookmarkBlockSchema } from '@blocksuite/affine-model';
import { BlockService } from '@blocksuite/block-std';
export class BookmarkBlockService extends BlockService {
static override readonly flavour = BookmarkBlockSchema.model.flavour;
private static readonly linkPreviewer = new LinkPreviewer();
static setLinkPreviewEndpoint =
BookmarkBlockService.linkPreviewer.setEndpoint;
queryUrlData = (url: string, signal?: AbortSignal) => {
return BookmarkBlockService.linkPreviewer.query(url, signal);
};
}

View File

@@ -1,23 +0,0 @@
import {
BlockViewExtension,
CommandExtension,
type ExtensionType,
FlavourExtension,
} from '@blocksuite/block-std';
import { literal } from 'lit/static-html.js';
import { BookmarkBlockAdapterExtensions } from './adapters/extension.js';
import { BookmarkBlockService } from './bookmark-service.js';
import { commands } from './commands/index.js';
export const BookmarkBlockSpec: ExtensionType[] = [
FlavourExtension('affine:bookmark'),
BookmarkBlockService,
CommandExtension(commands),
BlockViewExtension('affine:bookmark', model => {
return model.parent?.flavour === 'affine:surface'
? literal`affine-edgeless-bookmark`
: literal`affine-bookmark`;
}),
BookmarkBlockAdapterExtensions,
].flat();

View File

@@ -1,9 +0,0 @@
import type { BlockCommands } from '@blocksuite/block-std';
import { insertBookmarkCommand } from './insert-bookmark.js';
import { insertLinkByQuickSearchCommand } from './insert-link-by-quick-search.js';
export const commands: BlockCommands = {
insertBookmark: insertBookmarkCommand,
insertLinkByQuickSearch: insertLinkByQuickSearchCommand,
};

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