mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-05 17:13:43 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b05274300c | ||
|
|
4b9e2abe9f | ||
|
|
fa2690064d | ||
|
|
7381b5e9f1 | ||
|
|
9d953104fa | ||
|
|
62d2e220ba | ||
|
|
80c92bed90 | ||
|
|
38806e2d39 | ||
|
|
0dbf73be51 | ||
|
|
5975a6fb2d |
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
DATABASE_LOCATION=./postgres
|
||||
DB_PASSWORD=affine
|
||||
DB_USERNAME=affine
|
||||
DB_DATABASE_NAME=affine
|
||||
3
.docker/dev/.gitignore
vendored
3
.docker/dev/.gitignore
vendored
@@ -1,3 +0,0 @@
|
||||
postgres
|
||||
.env
|
||||
compose.yml
|
||||
@@ -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:
|
||||
@@ -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
|
||||
@@ -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
7
.env.template
Normal 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
15
.eslintignore
Normal 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
289
.eslintrc.js
Normal 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
1
.github/CODEOWNERS
vendored
@@ -1 +0,0 @@
|
||||
/blocksuite/ @toeverything/blocksuite-core
|
||||
|
||||
17
.github/actions/build-rust/action.yml
vendored
17
.github/actions/build-rust/action.yml
vendored
@@ -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:*'
|
||||
|
||||
36
.github/actions/copilot-test/action.yml
vendored
36
.github/actions/copilot-test/action.yml
vendored
@@ -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
|
||||
67
.github/actions/deploy/deploy.mjs
vendored
67
.github/actions/deploy/deploy.mjs
vendored
@@ -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(' ');
|
||||
|
||||
23
.github/actions/server-test-env/action.yml
vendored
23
.github/actions/server-test-env/action.yml
vendored
@@ -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
|
||||
28
.github/actions/setup-node/action.yml
vendored
28
.github/actions/setup-node/action.yml
vendored
@@ -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
|
||||
|
||||
28
.github/actions/setup-rust/action.yml
vendored
28
.github/actions/setup-rust/action.yml
vendored
@@ -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
|
||||
2
.github/deployment/front/Dockerfile
vendored
2
.github/deployment/front/Dockerfile
vendored
@@ -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
|
||||
|
||||
2
.github/helm/affine/Chart.yaml
vendored
2
.github/helm/affine/Chart.yaml
vendored
@@ -3,4 +3,4 @@ name: affine
|
||||
description: AFFiNE cloud chart
|
||||
type: application
|
||||
version: 0.0.0
|
||||
appVersion: "0.19.0"
|
||||
appVersion: "0.17.0"
|
||||
|
||||
@@ -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
|
||||
|
||||
2
.github/helm/affine/charts/sync/Chart.yaml
vendored
2
.github/helm/affine/charts/sync/Chart.yaml
vendored
@@ -3,7 +3,7 @@ name: sync
|
||||
description: AFFiNE Sync Server
|
||||
type: application
|
||||
version: 0.0.0
|
||||
appVersion: "0.19.0"
|
||||
appVersion: "0.17.0"
|
||||
dependencies:
|
||||
- name: gcloud-sql-proxy
|
||||
version: 0.0.0
|
||||
|
||||
20
.github/renovate.json
vendored
20
.github/renovate.json
vendored
@@ -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: ",
|
||||
|
||||
109
.github/workflows/build-images.yml
vendored
109
.github/workflows/build-images.yml
vendored
@@ -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"]'
|
||||
|
||||
432
.github/workflows/build-test.yml
vendored
432
.github/workflows/build-test.yml
vendored
@@ -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
|
||||
|
||||
29
.github/workflows/copilot-test-automatically.yml
vendored
29
.github/workflows/copilot-test-automatically.yml
vendored
@@ -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
|
||||
198
.github/workflows/copilot-test.yml
vendored
198
.github/workflows/copilot-test.yml
vendored
@@ -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
|
||||
52
.github/workflows/deploy.yml
vendored
52
.github/workflows/deploy.yml
vendored
@@ -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 }}
|
||||
|
||||
38
.github/workflows/release-automatically.yml
vendored
38
.github/workflows/release-automatically.yml
vendored
@@ -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" }'
|
||||
32
.github/workflows/release-desktop-automatically.yml
vendored
Normal file
32
.github/workflows/release-desktop-automatically.yml
vendored
Normal 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
|
||||
163
.github/workflows/release-desktop.yml
vendored
163
.github/workflows/release-desktop.yml
vendored
@@ -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
|
||||
|
||||
245
.github/workflows/release-mobile.yml
vendored
245
.github/workflows/release-mobile.yml
vendored
@@ -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 }}
|
||||
2
.github/workflows/sync-i18n.yml
vendored
2
.github/workflows/sync-i18n.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/workers.yml
vendored
2
.github/workflows/workers.yml
vendored
@@ -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
6
.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -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": "$",
|
||||
@@ -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
|
||||
|
||||
22
.vscode/launch.template.json
vendored
22
.vscode/launch.template.json
vendored
@@ -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"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -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"
|
||||
}
|
||||
},
|
||||
39
.yarn/patches/yjs-npm-13.6.18-ad0d5f7c43.patch
Normal file
39
.yarn/patches/yjs-npm-13.6.18-ad0d5f7c43.patch
Normal 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
|
||||
@@ -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
925
.yarn/releases/yarn-4.5.0.cjs
vendored
Executable file
File diff suppressed because one or more lines are too long
934
.yarn/releases/yarn-4.6.0.cjs
vendored
934
.yarn/releases/yarn-4.6.0.cjs
vendored
File diff suppressed because one or more lines are too long
@@ -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
1651
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
63
Cargo.toml
63
Cargo.toml
@@ -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
|
||||
|
||||
39
README.md
39
README.md
@@ -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 @@
|
||||
[](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, you’ll 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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from '@blocksuite/block-std/gfx';
|
||||
@@ -1 +0,0 @@
|
||||
export * from '@blocksuite/block-std';
|
||||
@@ -1 +0,0 @@
|
||||
export * from '@blocksuite/blocks';
|
||||
@@ -1 +0,0 @@
|
||||
export * from '@blocksuite/blocks/schemas';
|
||||
@@ -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();
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from '@blocksuite/global/di';
|
||||
@@ -1 +0,0 @@
|
||||
export * from '@blocksuite/global/env';
|
||||
@@ -1 +0,0 @@
|
||||
export * from '@blocksuite/global/exceptions';
|
||||
@@ -1 +0,0 @@
|
||||
export * from '@blocksuite/global';
|
||||
@@ -1 +0,0 @@
|
||||
export * from '@blocksuite/global/types';
|
||||
@@ -1 +0,0 @@
|
||||
export * from '@blocksuite/global/utils';
|
||||
@@ -1 +0,0 @@
|
||||
export {};
|
||||
@@ -1 +0,0 @@
|
||||
export * from '@blocksuite/inline/consts';
|
||||
@@ -1 +0,0 @@
|
||||
export * from '@blocksuite/inline';
|
||||
@@ -1 +0,0 @@
|
||||
export * from '@blocksuite/inline/types';
|
||||
@@ -1 +0,0 @@
|
||||
export * from '@blocksuite/presets';
|
||||
@@ -1,5 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-restricted-imports */
|
||||
|
||||
// oxlint-disable-next-line
|
||||
// @ts-ignore FIXME: typecheck error
|
||||
export * from '@blocksuite/store';
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
@@ -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,
|
||||
];
|
||||
@@ -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();
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
@@ -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>
|
||||
`;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
`;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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;
|
||||
}
|
||||
`;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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,
|
||||
];
|
||||
@@ -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
|
||||
);
|
||||
@@ -1,4 +0,0 @@
|
||||
export * from './html.js';
|
||||
export * from './markdown.js';
|
||||
export * from './notion-html.js';
|
||||
export * from './plain-text.js';
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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
Reference in New Issue
Block a user