Compare commits

..

6 Commits

Author SHA1 Message Date
CatsJuice
1623f5d82f fix(core): cannot view pricing page if not logged in (#8907)
Due to lifetime plan card show `Upgrade` button that require accout, should show `Login` instead
2024-11-27 15:01:31 +08:00
JimmFly
2abf40b465 fix(core): image block size limits were not enforced as expected (#8908) 2024-11-27 15:00:06 +08:00
EYHN
dea0574a89 fix(core): improve doc meta performance (#8913) 2024-11-27 14:59:53 +08:00
liuyi
a73c08ff24 fix(core): wrong app scheme fallback (#8914) 2024-11-25 16:50:09 +08:00
liuyi
c4410751e4 fix(server): ignore invalid subscription variant for subscriptinos query as well (#8894) 2024-11-22 13:51:03 +08:00
liuyi
47899a7eaf fix(server): ignore invalid subscription variant (#8892) 2024-11-22 12:37:15 +08:00
4028 changed files with 36700 additions and 378067 deletions

View File

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

View File

@@ -9,7 +9,7 @@ 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

7
.env.template Normal file
View File

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

16
.eslintignore Normal file
View File

@@ -0,0 +1,16 @@
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/i18n/src/i18n-completenesses.json
packages/frontend/templates/*.gen.ts

289
.eslintrc.js Normal file
View File

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

1
.github/CODEOWNERS vendored
View File

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

View File

@@ -7,10 +7,9 @@ inputs:
package:
description: 'Package to build'
required: true
no-build:
description: 'Whether to skip building'
nx_token:
description: 'Nx Cloud access token'
required: false
default: 'false'
runs:
using: 'composite'
@@ -18,72 +17,39 @@ runs:
- name: Print rustup toolchain version
shell: bash
id: rustup-version
working-directory: ${{ env.DEV_DRIVE_WORKSPACE || github.workspace }}
run: |
export RUST_TOOLCHAIN_VERSION="$(grep 'channel' rust-toolchain.toml | head -1 | awk -F '"' '{print $2}')"
echo "Rust toolchain version: $RUST_TOOLCHAIN_VERSION"
echo "RUST_TOOLCHAIN_VERSION=$RUST_TOOLCHAIN_VERSION" >> "$GITHUB_OUTPUT"
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
if: ${{ runner.os != 'Windows' }}
with:
toolchain: '${{ steps.rustup-version.outputs.RUST_TOOLCHAIN_VERSION }}'
targets: ${{ inputs.target }}
env:
CARGO_INCREMENTAL: '1'
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
if: ${{ runner.os == 'Windows' }}
with:
toolchain: '${{ steps.rustup-version.outputs.RUST_TOOLCHAIN_VERSION }}'
targets: ${{ inputs.target }}
env:
CARGO_INCREMENTAL: '1'
CARGO_HOME: ${{ env.DEV_DRIVE }}/.cargo
RUSTUP_HOME: ${{ env.DEV_DRIVE }}/.rustup
- name: Set CC
if: ${{ contains(inputs.target, 'linux') && inputs.package != '@affine/native' && inputs.no-build != 'true' }}
working-directory: ${{ env.DEV_DRIVE_WORKSPACE || github.workspace }}
if: ${{ contains(inputs.target, 'linux') && inputs.package != '@affine/native' }}
shell: bash
run: |
echo "CC=clang" >> "$GITHUB_ENV"
echo "TARGET_CC=clang" >> "$GITHUB_ENV"
- name: Cache cargo
uses: Swatinem/rust-cache@v2
if: ${{ runner.os == 'Windows' }}
uses: actions/cache@v4
with:
workspaces: ${{ env.DEV_DRIVE_WORKSPACE }}
save-if: ${{ github.ref_name == 'canary' }}
shared-key: ${{ inputs.target }}-${{ inputs.package }}
env:
CARGO_HOME: ${{ env.DEV_DRIVE }}/.cargo
RUSTUP_HOME: ${{ env.DEV_DRIVE }}/.rustup
- name: Cache cargo
uses: Swatinem/rust-cache@v2
if: ${{ runner.os != 'Windows' }}
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
if: ${{ runner.os != 'Windows' && inputs.no-build != 'true' }}
run: |
yarn workspace ${{ inputs.package }} build --target ${{ inputs.target }} --use-napi-cross
env:
NX_CLOUD_ACCESS_TOKEN: ${{ inputs.nx_token }}
DEBUG: 'napi:*'
- name: Build
working-directory: ${{ env.DEV_DRIVE_WORKSPACE || github.workspace }}
shell: bash
if: ${{ runner.os == 'Windows' && inputs.no-build != 'true' }}
run: |
yarn workspace ${{ inputs.package }} build --target ${{ inputs.target }} --use-napi-cross
env:
DEBUG: 'napi:*'
CARGO_HOME: ${{ env.DEV_DRIVE }}/.cargo
RUSTUP_HOME: ${{ env.DEV_DRIVE }}/.rustup

View File

@@ -3,7 +3,7 @@ description: 'Run Copilot E2E Test'
inputs:
script:
description: 'Script to run'
default: 'yarn affine @affine-test/affine-cloud-copilot e2e --forbid-only'
default: 'yarn workspace @affine-test/affine-cloud-copilot e2e --forbid-only'
required: false
openai-key:
description: 'OpenAI secret key'

View File

@@ -15,9 +15,7 @@ runs:
- 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
yarn workspace @affine/server exec prisma generate
yarn workspace @affine/server exec prisma db push
yarn workspace @affine/server data-migration run

View File

@@ -35,19 +35,10 @@ inputs:
full-cache:
description: 'Full installation cache'
required: false
runs:
using: 'composite'
steps:
- name: Output workspace path
id: workspace-path
shell: bash
run: |
if [ -n "${{ env.DEV_DRIVE_WORKSPACE }}" ]; then
echo "workspace_path=${{ env.DEV_DRIVE_WORKSPACE }}" >> $GITHUB_OUTPUT
else
echo "workspace_path=${{ github.workspace }}" >> $GITHUB_OUTPUT
fi
- name: Setup Node.js
uses: actions/setup-node@v4
with:
@@ -55,37 +46,29 @@ 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
working-directory: ${{ steps.workspace-path.outputs.workspace_path }}
run: corepack enable
- name: Set nmMode
if: ${{ inputs.hard-link-nm == 'false' }}
shell: bash
working-directory: ${{ steps.workspace-path.outputs.workspace_path }}
run: yarn config set nmMode classic
- name: Set nmHoistingLimits
if: ${{ inputs.nmHoistingLimits }}
shell: bash
working-directory: ${{ steps.workspace-path.outputs.workspace_path }}
run: yarn config set nmHoistingLimits ${{ inputs.nmHoistingLimits }}
- name: Set enableScripts
if: ${{ inputs.enableScripts == 'false' }}
shell: bash
working-directory: ${{ steps.workspace-path.outputs.workspace_path }}
run: yarn config set enableScripts false
- name: Set yarn global cache path
shell: bash
id: yarn-cache
working-directory: ${{ steps.workspace-path.outputs.workspace_path }}
run: node -e "const p = $(yarn config cacheFolder --json).effective; console.log('yarn_global_cache=' + p)" >> $GITHUB_OUTPUT
- name: Cache non-full yarn cache on Linux
@@ -93,9 +76,9 @@ runs:
if: ${{ inputs.full-cache != 'true' && runner.os == 'Linux' }}
with:
path: |
${{ steps.workspace-path.outputs.workspace_path }}/node_modules
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
@@ -106,7 +89,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
@@ -115,7 +98,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
@@ -123,13 +106,23 @@ 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' }}
continue-on-error: true
shell: bash
working-directory: ${{ steps.workspace-path.outputs.workspace_path }}
run: yarn ${{ inputs.extra-flags }}
env:
HUSKY: '0'
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1'
ELECTRON_SKIP_BINARY_DOWNLOAD: '1'
SENTRYCLI_SKIP_DOWNLOAD: '1'
DEBUG: '*'
- name: yarn install (try again)
if: ${{ steps.install.outcome == 'failure' }}
shell: bash
run: yarn ${{ inputs.extra-flags }}
env:
HUSKY: '0'
@@ -142,7 +135,6 @@ runs:
id: playwright-version
if: ${{ inputs.playwright-install == 'true' }}
shell: bash
working-directory: ${{ steps.workspace-path.outputs.workspace_path }}
run: echo "version=$(yarn why --json @playwright/test | grep -h 'workspace:.' | jq --raw-output '.children[].locator' | sed -e 's/@playwright\/test@.*://' | head -n 1)" >> $GITHUB_OUTPUT
# Attempt to restore the correct Playwright browser binaries based on the
@@ -155,8 +147,8 @@ runs:
id: playwright-cache
if: ${{ inputs.playwright-install == 'true' }}
with:
path: ${{ steps.workspace-path.outputs.workspace_path }}/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 }}'
path: ${{ github.workspace }}/node_modules/.cache/ms-playwright
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
@@ -166,7 +158,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.
@@ -174,14 +166,12 @@ runs:
shell: bash
if: inputs.playwright-install == 'true'
run: yarn playwright install --with-deps chromium webkit
working-directory: ${{ steps.workspace-path.outputs.workspace_path }}
env:
PLAYWRIGHT_BROWSERS_PATH: ${{ steps.workspace-path.outputs.workspace_path }}/node_modules/.cache/ms-playwright
PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/node_modules/.cache/ms-playwright
- name: Get installed Electron version
id: electron-version
if: ${{ inputs.electron-install == 'true' }}
working-directory: ${{ steps.workspace-path.outputs.workspace_path }}
shell: bash
run: |
echo "version=$(yarn why --json electron | grep -h 'workspace:.' | jq --raw-output '.children[].locator' | sed -e 's/@playwright\/test@.*://' | head -n 1)" >> $GITHUB_OUTPUT
@@ -190,20 +180,14 @@ runs:
id: electron-cache
if: ${{ inputs.electron-install == 'true' }}
with:
path: ${{ steps.workspace-path.outputs.workspace_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 }}'
path: 'node_modules/.cache/electron'
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
if: inputs.electron-install == 'true'
run: node ./node_modules/electron/install.js
working-directory: ${{ steps.workspace-path.outputs.workspace_path }}
env:
electron_config_cache: ${{ steps.workspace-path.outputs.workspace_path }}/node_modules/.cache/electron
- name: Write PLAYWRIGHT_BROWSERS_PATH env
shell: bash
run: |
echo "PLAYWRIGHT_BROWSERS_PATH=${{ steps.workspace-path.outputs.workspace_path }}/node_modules/.cache/ms-playwright" >> $GITHUB_ENV
electron_config_cache: ./node_modules/.cache/electron

28
.github/actions/setup-rust/action.yml vendored Normal file
View File

@@ -0,0 +1,28 @@
name: 'Rust setup'
description: 'Rust setup, including cache configuration'
inputs:
components:
description: 'Cargo components'
required: false
targets:
description: 'Cargo target'
required: false
toolchain:
description: 'Rustup toolchain'
required: false
default: 'stable'
runs:
using: 'composite'
steps:
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ inputs.toolchain }}
targets: ${{ inputs.targets }}
components: ${{ inputs.components }}
- name: Add Targets
if: ${{ inputs.targets }}
run: rustup target add ${{ inputs.targets }}
shell: bash
- uses: Swatinem/rust-cache@v2

View File

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

View File

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

View File

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

20
.github/renovate.json vendored
View File

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

View File

@@ -7,6 +7,9 @@ on:
type: string
required: true
env:
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
permissions:
contents: 'write'
id-token: 'write'
@@ -47,7 +50,7 @@ jobs:
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Build Core
run: yarn affine @affine/web build
run: yarn nx build @affine/web --skip-nx-cache
env:
R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
@@ -80,7 +83,7 @@ jobs:
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Build Admin
run: yarn affine @affine/admin build
run: yarn nx build @affine/admin --skip-nx-cache
env:
R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
@@ -112,7 +115,7 @@ jobs:
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Build Mobile
run: yarn affine @affine/mobile build
run: yarn nx build @affine/mobile --skip-nx-cache
env:
R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
@@ -136,7 +139,6 @@ jobs:
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 +163,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:

View File

@@ -19,6 +19,8 @@ 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
concurrency:
@@ -26,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
@@ -91,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
@@ -105,7 +90,7 @@ jobs:
electron-install: false
full-cache: true
- name: Run i18n codegen
run: yarn affine @affine/i18n build
run: yarn workspace @affine/i18n build
- name: Run ESLint
run: yarn lint:eslint --max-warnings=0
- name: Run Prettier
@@ -121,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
@@ -130,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
@@ -184,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() }}
@@ -197,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
@@ -216,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() }}
@@ -226,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
@@ -255,7 +220,7 @@ 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
@@ -269,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:
@@ -279,16 +242,11 @@ jobs:
spec:
- { os: ubuntu-latest, target: x86_64-unknown-linux-gnu }
- { os: windows-latest, target: x86_64-pc-windows-msvc }
- { os: macos-latest, target: x86_64-apple-darwin }
- { os: macos-latest, target: aarch64-apple-darwin }
- { os: macos-14, target: x86_64-apple-darwin }
- { os: macos-14, target: aarch64-apple-darwin }
steps:
- uses: actions/checkout@v4
- uses: samypr100/setup-dev-drive@v3
if: ${{ matrix.spec.os == 'windows-latest' }}
with:
workspace-copy: true
drive-size: 8GB
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
@@ -296,7 +254,6 @@ jobs:
electron-install: false
- name: Setup filename
id: filename
working-directory: ${{ env.DEV_DRIVE_WORKSPACE || github.workspace }}
shell: bash
run: |
export PLATFORM_ARCH_ABI=$(node -e "console.log(require('@napi-rs/cli').parseTriple('${{ matrix.spec.target }}').platformArchABI)")
@@ -306,18 +263,17 @@ 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:
name: ${{ steps.filename.outputs.filename }}
path: ${{ env.DEV_DRIVE_WORKSPACE || github.workspace }}/packages/frontend/native/${{ steps.filename.outputs.filename }}
path: ./packages/frontend/native/${{ steps.filename.outputs.filename }}
if-no-files-found: error
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:
@@ -332,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:
@@ -342,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
@@ -352,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:
@@ -367,15 +324,11 @@ 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
@@ -388,10 +341,6 @@ jobs:
--health-retries 5
ports:
- 5432:5432
redis:
image: redis
ports:
- 6379:6379
mailer:
image: mailhog/mailhog
ports:
@@ -416,7 +365,7 @@ jobs:
uses: ./.github/actions/server-test-env
- 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'
COPILOT_OPENAI_API_KEY: 'use_fake_openai_api_key'
@@ -430,39 +379,32 @@ jobs:
name: affine
fail_ci_if_error: false
rust-test:
name: Run native tests
server-native-test:
name: Run server native tests
runs-on: ubuntu-latest
needs: optimize_ci
if: needs.optimize_ci.outputs.skip == 'false'
env:
RUSTFLAGS: -D warnings
CARGO_TERM_COLOR: always
steps:
- uses: actions/checkout@v4
- name: Setup Rust
uses: ./.github/actions/build-rust
with:
package: 'affine'
no-build: 'true'
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
run: cargo nextest run --release
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
@@ -475,10 +417,6 @@ jobs:
--health-retries 5
ports:
- 5432:5432
redis:
image: redis
ports:
- 6379:6379
mailer:
image: mailhog/mailhog
ports:
@@ -499,41 +437,40 @@ jobs:
fi
- uses: dorny/paths-filter@v3
id: apifilter
id: filter
with:
filters: |
changed:
- 'packages/backend/server/src/plugins/copilot/**'
- 'packages/backend/server/tests/copilot.*'
backend:
- 'packages/backend/server/src/**'
- name: Setup Node.js
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.apifilter.outputs.changed == 'true' }}
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.filter.outputs.backend == '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' }}
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.filter.outputs.backend == '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' }}
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.filter.outputs.backend == '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
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.filter.outputs.backend == 'true' }}
run: yarn workspace @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' }}
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.filter.outputs.backend == 'true' }}
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
@@ -549,8 +486,6 @@ jobs:
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:
@@ -570,10 +505,6 @@ jobs:
--health-retries 5
ports:
- 5432:5432
redis:
image: redis
ports:
- 6379:6379
steps:
- uses: actions/checkout@v4
@@ -588,17 +519,8 @@ jobs:
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' }}
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' }}
uses: ./.github/actions/setup-node
with:
playwright-install: true
@@ -606,51 +528,48 @@ jobs:
hard-link-nm: false
- name: Download server-native.node
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.e2efilter.outputs.changed == 'true' }}
if: ${{ steps.check-blocksuite-update.outputs.skip != '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' }}
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' }}
uses: ./.github/actions/copilot-test
with:
script: yarn affine @affine-test/affine-cloud-copilot e2e --forbid-only --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
script: yarn workspace @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
yarn workspace @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
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
@@ -663,10 +582,6 @@ jobs:
--health-retries 5
ports:
- 5432:5432
redis:
image: redis
ports:
- 6379:6379
mailer:
image: mailhog/mailhog
ports:
@@ -715,24 +630,19 @@ 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:
spec:
- {
os: macos-latest,
os: macos-14,
platform: macos,
arch: x64,
target: x86_64-apple-darwin,
test: false,
}
- {
os: macos-latest,
os: macos-14,
platform: macos,
arch: arm64,
target: aarch64-apple-darwin,
@@ -752,13 +662,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
@@ -779,7 +692,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
@@ -787,7 +700,7 @@ 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' }}
@@ -796,11 +709,11 @@ jobs:
# 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
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)
if: ${{ matrix.spec.target == 'aarch64-apple-darwin' }}
@@ -808,7 +721,7 @@ jobs:
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: |
@@ -818,7 +731,7 @@ jobs:
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
yarn workspace @affine/electron make --platform=linux --arch=x64
if: ${{ matrix.spec.target == 'x86_64-unknown-linux-gnu' }}
env:
SKIP_WEB_BUILD: 1
@@ -827,7 +740,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() }}
@@ -839,8 +752,6 @@ jobs:
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
@@ -854,14 +765,14 @@ jobs:
- 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
- server-native-test
- copilot-api-test
- copilot-e2e-test
- server-e2e-test

View File

@@ -3,6 +3,10 @@ name: Copilot Cron Test
on:
workflow_dispatch:
env:
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/node_modules/.cache/ms-playwright
jobs:
build-server-native:
name: Build Server native
@@ -21,6 +25,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:
@@ -37,7 +42,6 @@ jobs:
NODE_ENV: test
DISTRIBUTION: web
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
REDIS_SERVER_HOST: localhost
services:
postgres:
image: postgres
@@ -50,10 +54,6 @@ jobs:
--health-retries 5
ports:
- 5432:5432
redis:
image: redis
ports:
- 6379:6379
mailer:
image: mailhog/mailhog
ports:
@@ -79,7 +79,7 @@ jobs:
uses: ./.github/actions/server-test-env
- name: Run server tests
run: yarn affine @affine/server test:copilot:coverage --forbid-only
run: yarn workspace @affine/server test:copilot:coverage --forbid-only
env:
CARGO_TARGET_DIR: '${{ github.workspace }}/target'
COPILOT_OPENAI_API_KEY: ${{ secrets.COPILOT_OPENAI_API_KEY }}
@@ -100,7 +100,6 @@ jobs:
env:
DISTRIBUTION: web
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
REDIS_SERVER_HOST: localhost
IN_CI_TEST: true
strategy:
fail-fast: false
@@ -121,10 +120,6 @@ jobs:
--health-retries 5
ports:
- 5432:5432
redis:
image: redis
ports:
- 6379:6379
steps:
- uses: actions/checkout@v4
@@ -144,7 +139,7 @@ jobs:
- 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 }}
script: yarn workspace @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 }}

View File

@@ -12,6 +12,8 @@ on:
- beta
- stable
- internal
env:
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
permissions:
contents: 'write'

View File

@@ -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
@@ -108,7 +108,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 +117,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' }}
@@ -143,10 +144,11 @@ jobs:
git config --global protocol.file.allow always
- 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
DEBUG: '*'
- name: signing DMG
if: ${{ matrix.spec.platform == 'darwin' }}
@@ -168,14 +170,14 @@ jobs:
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
- 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: |
@@ -191,7 +193,6 @@ jobs:
package-distribution-windows:
environment: ${{ github.event.inputs.build-type }}
strategy:
fail-fast: false
matrix:
spec:
- runner: windows-latest
@@ -224,7 +225,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 +233,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
@@ -283,7 +285,6 @@ jobs:
- sign-packaged-artifacts-windows_x64
- sign-packaged-artifacts-windows_arm64
strategy:
fail-fast: false
matrix:
spec:
- platform: win32
@@ -317,10 +318,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
@@ -360,7 +361,6 @@ jobs:
before-make,
]
strategy:
fail-fast: false
matrix:
spec:
- runner: windows-latest
@@ -386,7 +386,7 @@ jobs:
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
- 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
@@ -415,37 +415,33 @@ 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
path: ./
- 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 +453,15 @@ jobs:
draft: ${{ github.event.inputs.is-draft }}
prerelease: ${{ github.event.inputs.is-pre-release }}
files: |
./release/*
./release/.env.example
./VERSION
./*.zip
./*.dmg
./*.exe
./*.appimage
./*.deb
./*.flatpak
./*.apk
./*.yml
- name: Create Nightly Release Draft
if: ${{ github.ref_type == 'branch' }}
uses: softprops/action-gh-release@v2
@@ -474,5 +477,12 @@ jobs:
draft: false
prerelease: true
files: |
./release/*
./release/.env.example
./VERSION
./*.zip
./*.dmg
./*.exe
./*.appimage
./*.deb
./*.apk
./*.flatpak
./*.yml

View File

@@ -37,25 +37,9 @@ env:
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 }}
environment: ${{ inputs.build-type || github.event.inputs.build-type }}
outputs:
RELEASE_VERSION: ${{ steps.version.outputs.APP_VERSION }}
steps:
@@ -68,7 +52,7 @@ jobs:
- name: Setup @sentry/cli
uses: ./.github/actions/setup-sentry
- name: Build Mobile
run: yarn affine @affine/ios build
run: yarn nx build @affine/ios --skip-nx-cache
env:
PUBLIC_PATH: '/'
MIXPANEL_TOKEN: ${{ secrets.MIXPANEL_TOKEN }}
@@ -78,6 +62,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'
- name: Upload ios artifact
uses: actions/upload-artifact@v4
with:
@@ -86,9 +71,7 @@ jobs:
build-android-web:
runs-on: ubuntu-latest
needs:
- output-env
environment: ${{ needs.output-env.outputs.ENVIRONMENT }}
environment: ${{ github.event.inputs.build-type || inputs.build-type }}
outputs:
RELEASE_VERSION: ${{ steps.version.outputs.APP_VERSION }}
steps:
@@ -101,7 +84,7 @@ jobs:
- name: Setup @sentry/cli
uses: ./.github/actions/setup-sentry
- name: Build Mobile
run: yarn affine @affine/android build
run: yarn nx build @affine/android --skip-nx-cache
env:
PUBLIC_PATH: '/'
MIXPANEL_TOKEN: ${{ secrets.MIXPANEL_TOKEN }}
@@ -111,6 +94,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'
- name: Upload android artifact
uses: actions/upload-artifact@v4
with:
@@ -137,9 +121,6 @@ jobs:
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
@@ -148,17 +129,9 @@ jobs:
with:
p12-file-base64: ${{ secrets.CERTIFICATES_P12_MOBILE }}
p12-password: ${{ secrets.CERTIFICATES_P12_PASSWORD_MOBILE }}
- name: Setup Rust
uses: ./.github/actions/build-rust
- uses: maxim-lobanov/setup-xcode@v1
with:
target: 'aarch64-apple-ios'
package: 'affine_mobile_native'
no-build: 'true'
- 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
xcode-version: latest-stable
- name: Testflight
if: ${{ env.BUILD_TYPE != 'stable' }}
working-directory: packages/frontend/apps/ios/App
@@ -196,17 +169,8 @@ jobs:
electron-install: false
hard-link-nm: false
enableScripts: false
- name: Setup Rust
uses: ./.github/actions/build-rust
with:
target: 'aarch64-linux-android'
package: 'affine_mobile_native'
no-build: 'true'
- 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
@@ -221,11 +185,10 @@ jobs:
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
run: yarn workspace @affine/playstore-auto-bump bump
env:
GOOGLE_APPLICATION_CREDENTIALS: ${{ steps.auth.outputs.credentials_file_path }}
- name: Build
@@ -236,7 +199,6 @@ jobs:
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' }}

View File

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

5
.gitignore vendored
View File

@@ -29,6 +29,7 @@ node_modules
# IDE - VSCode
.vscode/*
!.vscode/tasks.json
!.vscode/settings.template.json
!.vscode/launch.template.json
!.vscode/extensions.json
@@ -80,7 +81,3 @@ apps/web/next-routes.conf
packages/frontend/templates/edgeless
packages/frontend/core/public/static/templates
# script
af
af.cmd

2
.nvmrc
View File

@@ -1 +1 @@
20.18.1
20.18.0

View File

@@ -1,36 +1,28 @@
# 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/i18n/src/i18n-completenesses.json
packages/frontend/graphql/src/graphql/index.ts
tests/affine-legacy/**/static
.yarnrc.yml
packages/frontend/templates/*.gen.ts
packages/frontend/templates/onboarding
# generated files
**/*.gen.ts
**/*.gql
**/*.d.ts
# per files
tools/cli/src/webpack/error-handler.js
# auto-generated by NAPI-RS
# fixme(@joooye34): need script to check and generate ignore list here
packages/backend/native/index.d.ts
packages/frontend/native/index.d.ts
packages/frontend/native/index.js
packages/frontend/graphql/src/graphql/index.ts
packages/frontend/graphql/src/schema.ts
packages/frontend/apps/android/App/app/build/**
blocksuite/tests-legacy/snapshots

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,8 @@
diff --git a/dist/yjs.cjs b/dist/yjs.cjs
index 8a343ca9d0a153e95b27ad337e0553a8cc80d5ca..7199cf6e05d9c2c3491e56c4d4bda109e1755563 100644
index d2dc06ae11a6eb44f8c8445d4298c0e89c3e4da2..a30ab04fa9f3b77666939caa88335c68c40f194c 100644
--- a/dist/yjs.cjs
+++ b/dist/yjs.cjs
@@ -416,7 +416,7 @@ const equalDeleteSets = (ds1, ds2) => {
@@ -414,7 +414,7 @@ const equalDeleteSets = (ds1, ds2) => {
*/
@@ -12,10 +12,10 @@ index 8a343ca9d0a153e95b27ad337e0553a8cc80d5ca..7199cf6e05d9c2c3491e56c4d4bda109
/**
* @typedef {Object} DocOpts
diff --git a/dist/yjs.mjs b/dist/yjs.mjs
index 1c29ce7fe8f146b78911d0af9a53d1b516e86494..220fa0faacf4dc2a787e18f7cc79100e7c516e3a 100644
index 20c9e58c32bcb6bc714200a2561fd1f542c49523..14267e5e36d9781ca3810d5b70ff8c051dac779e 100644
--- a/dist/yjs.mjs
+++ b/dist/yjs.mjs
@@ -379,7 +379,7 @@ const equalDeleteSets = (ds1, ds2) => {
@@ -378,7 +378,7 @@ const equalDeleteSets = (ds1, ds2) => {
*/
@@ -25,7 +25,7 @@ index 1c29ce7fe8f146b78911d0af9a53d1b516e86494..220fa0faacf4dc2a787e18f7cc79100e
/**
* @typedef {Object} DocOpts
diff --git a/src/utils/Doc.js b/src/utils/Doc.js
index d5165426f2314fc3c2388e64841e7cd6498a92a9..4bb2e5a8b79bb59f08a011af77e69af862312292 100644
index 62643617c86e57c64dd9babdb792fa8888357ec0..4df5048ab12af1ae0f1154da67f06dce1fda7b49 100644
--- a/src/utils/Doc.js
+++ b/src/utils/Doc.js
@@ -20,7 +20,7 @@ import * as map from 'lib0/map'

File diff suppressed because one or more lines are too long

View File

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

1256
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,48 +1,36 @@
[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/common/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" }
affine_common = { path = "./packages/common/native" }
anyhow = "1"
chrono = "0.4"
dotenv = "0.15"
file-format = { version = "0.26", features = ["reader"] }
mimalloc = "0.1"
napi = { version = "3.0.0-alpha.12", features = ["async", "chrono_date", "error_anyhow", "napi9", "serde"] }
napi-build = { version = "2" }
napi-derive = { version = "3.0.0-alpha.12" }
notify = { version = "7", features = ["serde"] }
once_cell = "1"
parking_lot = "0.12"
rand = "0.8"
rayon = "1.10"
serde = "1"
serde_json = "1"
sha3 = "0.10"
sqlx = { version = "0.8", default-features = false, features = ["chrono", "macros", "migrate", "runtime-tokio", "sqlite", "tls-rustls"] }
tiktoken-rs = "0.6"
tokio = "1.37"
uuid = "1.8"
v_htmlescape = "0.15"
y-octo = { git = "https://github.com/y-crdt/y-octo.git", branch = "main" }
[profile.dev.package.sqlx-macros]
opt-level = 3

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,63 +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'])) {
addSiblingAttachmentBlocks(
std.host,
attachmentFiles,
// TODO: use max file size from service
maxFileSize,
targetModel,
place
).catch(console.error);
return true;
}
if (isInsideEdgelessEditor(std.host)) {
const gfx = std.get(GfxControllerIdentifier);
point = gfx.viewport.toViewCoordFromClientCoord(point);
addAttachments(std, attachmentFiles, point).catch(console.error);
std.getOptional(TelemetryProvider)?.track('CanvasElementAdded', {
control: 'canvas:drop',
page: 'whiteboard editor',
module: 'toolbar',
segment: 'toolbar',
type: 'attachment',
});
return true;
}
return false;
},
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,15 +0,0 @@
import type * as SurfaceEffects from '@blocksuite/affine-block-surface/effects';
declare type _GLOBAL_ = typeof SurfaceEffects;
export * from './adapters/notion-html';
export * from './attachment-block';
export * from './attachment-service';
export * from './attachment-spec';
export { attachmentViewToggleMenu } from './components/options';
export {
type AttachmentEmbedConfig,
AttachmentEmbedConfigIdentifier,
AttachmentEmbedProvider,
} from './embed';
export { addAttachments, addSiblingAttachmentBlocks } from './utils';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,25 +0,0 @@
import '@blocksuite/affine-block-embed/effects';
import { insertEmbedCard } from '@blocksuite/affine-block-embed';
import type { EmbedCardStyle } from '@blocksuite/affine-model';
import { EmbedOptionProvider } from '@blocksuite/affine-shared/services';
import type { Command } from '@blocksuite/block-std';
export const insertBookmarkCommand: Command<
never,
'insertedLinkType',
{ url: string }
> = (ctx, next) => {
const { url, std } = ctx;
const embedOptions = std.get(EmbedOptionProvider).getEmbedBlockOptions(url);
let flavour = 'affine:bookmark';
let targetStyle: EmbedCardStyle = 'vertical';
const props: Record<string, unknown> = { url };
if (embedOptions) {
flavour = embedOptions.flavour;
targetStyle = embedOptions.styles[0];
}
insertEmbedCard(std, { flavour, targetStyle, props });
next();
};

View File

@@ -1,44 +0,0 @@
import type { InsertedLinkType } from '@blocksuite/affine-block-embed';
import { QuickSearchProvider } from '@blocksuite/affine-shared/services';
import type { Command } from '@blocksuite/block-std';
export const insertLinkByQuickSearchCommand: Command<
never,
'insertedLinkType'
> = (ctx, next) => {
const { std } = ctx;
const quickSearchService = std.getOptional(QuickSearchProvider);
if (!quickSearchService) {
next();
return;
}
const insertedLinkType: Promise<InsertedLinkType> = quickSearchService
.openQuickSearch()
.then(result => {
if (!result) return null;
// add linked doc
if ('docId' in result) {
std.command.exec('insertEmbedLinkedDoc', {
docId: result.docId,
params: result.params,
});
return {
flavour: 'affine:embed-linked-doc',
};
}
// add normal link;
if ('externalUrl' in result) {
std.command.exec('insertBookmark', { url: result.externalUrl });
return {
flavour: 'affine:bookmark',
};
}
return null;
});
next({ insertedLinkType });
};

View File

@@ -1,164 +0,0 @@
import { getEmbedCardIcons } from '@blocksuite/affine-block-embed';
import { WebIcon16 } from '@blocksuite/affine-components/icons';
import { ThemeProvider } from '@blocksuite/affine-shared/services';
import { getHostName } from '@blocksuite/affine-shared/utils';
import { ShadowlessElement } from '@blocksuite/block-std';
import { WithDisposable } from '@blocksuite/global/utils';
import { OpenInNewIcon } from '@blocksuite/icons/lit';
import { html } from 'lit';
import { property, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import type { BookmarkBlockComponent } from '../bookmark-block.js';
import { styles } from '../styles.js';
export class BookmarkCard extends WithDisposable(ShadowlessElement) {
static override styles = styles;
private _handleClick(event: MouseEvent) {
event.stopPropagation();
const model = this.bookmark.model;
if (model.parent?.flavour !== 'affine:surface') {
this._selectBlock();
}
}
private _handleDoubleClick(event: MouseEvent) {
event.stopPropagation();
this.bookmark.open();
}
private _selectBlock() {
const selectionManager = this.bookmark.host.selection;
const blockSelection = selectionManager.create('block', {
blockId: this.bookmark.blockId,
});
selectionManager.setGroup('note', [blockSelection]);
}
override connectedCallback(): void {
super.connectedCallback();
this.disposables.add(
this.bookmark.model.propsUpdated.on(() => {
this.requestUpdate();
})
);
this.disposables.add(
this.bookmark.std
.get(ThemeProvider)
.theme$.subscribe(() => this.requestUpdate())
);
this.disposables.add(
this.bookmark.selection.slots.changed.on(() => {
this._isSelected =
!!this.bookmark.selected?.is('block') ||
!!this.bookmark.selected?.is('surface');
})
);
}
override render() {
const { icon, title, url, description, image, style } = this.bookmark.model;
const cardClassMap = classMap({
loading: this.loading,
error: this.error,
[style]: true,
selected: this._isSelected,
});
const domainName = url.match(
/^(?:https?:\/\/)?(?:[^@\n]+@)?(?:www\.)?([^:/\n]+)/im
)?.[1];
const titleText = this.loading
? 'Loading...'
: !title
? this.error
? (domainName ?? 'Link card')
: ''
: title;
const theme = this.bookmark.std.get(ThemeProvider).theme;
const { LoadingIcon, EmbedCardBannerIcon } = getEmbedCardIcons(theme);
const titleIconType =
!icon?.split('.').pop() || icon?.split('.').pop() === 'svg'
? 'svg+xml'
: icon?.split('.').pop();
const titleIcon = this.loading
? LoadingIcon
: icon
? html`<object
type="image/${titleIconType}"
data=${icon}
draggable="false"
>
${WebIcon16}
</object>`
: WebIcon16;
const descriptionText = this.loading
? ''
: !description
? this.error
? 'Failed to retrieve link information.'
: url
: (description ?? '');
const bannerImage =
!this.loading && image
? html`<object type="image/webp" data=${image} draggable="false">
${EmbedCardBannerIcon}
</object>`
: EmbedCardBannerIcon;
return html`
<div
class="affine-bookmark-card ${cardClassMap}"
@click=${this._handleClick}
@dblclick=${this._handleDoubleClick}
>
<div class="affine-bookmark-content">
<div class="affine-bookmark-content-title">
<div class="affine-bookmark-content-title-icon">${titleIcon}</div>
<div class="affine-bookmark-content-title-text">${titleText}</div>
</div>
<div class="affine-bookmark-content-description">
${descriptionText}
</div>
<div class="affine-bookmark-content-url" @click=${this.bookmark.open}>
<span>${getHostName(url)}</span>
<div class="affine-bookmark-content-url-icon">
${OpenInNewIcon({ width: '12', height: '12' })}
</div>
</div>
</div>
<div class="affine-bookmark-banner">${bannerImage}</div>
</div>
`;
}
@state()
private accessor _isSelected = false;
@property({ attribute: false })
accessor bookmark!: BookmarkBlockComponent;
@property({ attribute: false })
accessor error!: boolean;
@property({ attribute: false })
accessor loading!: boolean;
}
declare global {
interface HTMLElementTagNameMap {
'bookmark-card': BookmarkCard;
}
}

View File

@@ -1,101 +0,0 @@
import { type BlockComponent, ShadowlessElement } from '@blocksuite/block-std';
import { WithDisposable } from '@blocksuite/global/utils';
import type { BlockModel } from '@blocksuite/store';
import { html } from 'lit';
import { property, query } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { embedCardModalStyles } from './styles.js';
export class EmbedCardEditCaptionEditModal extends WithDisposable(
ShadowlessElement
) {
static override styles = embedCardModalStyles;
private get _doc() {
return this.block.doc;
}
private get _model() {
return this.block.model as BlockModel<{ caption: string }>;
}
private _onKeydown(e: KeyboardEvent) {
e.stopPropagation();
if (e.key === 'Enter' && !e.isComposing) {
this._onSave();
}
if (e.key === 'Escape') {
this.remove();
}
}
private _onSave() {
const caption = this.captionInput.value;
this._doc.updateBlock(this._model, {
caption,
});
this.remove();
}
override connectedCallback() {
super.connectedCallback();
this.updateComplete
.then(() => {
this.captionInput.focus();
})
.catch(console.error);
this.disposables.addFromEvent(this, 'keydown', this._onKeydown);
}
override render() {
return html`
<div class="embed-card-modal">
<div class="embed-card-modal-mask" @click=${() => this.remove()}></div>
<div class="embed-card-modal-wrapper">
<div class="embed-card-modal-row">
<label for="card-title">Caption</label>
<textarea
class="embed-card-modal-input caption"
placeholder="Write a caption..."
.value=${this._model.caption ?? ''}
></textarea>
</div>
<div class="embed-card-modal-row">
<button
class=${classMap({
'embed-card-modal-button': true,
save: true,
})}
@click=${() => this._onSave()}
>
Save
</button>
</div>
</div>
</div>
`;
}
@property({ attribute: false })
accessor block!: BlockComponent;
@query('.embed-card-modal-input.caption')
accessor captionInput!: HTMLTextAreaElement;
}
export function toggleEmbedCardCaptionEditModal(block: BlockComponent) {
const host = block.host;
host.selection.clear();
const embedCardEditCaptionEditModal = new EmbedCardEditCaptionEditModal();
embedCardEditCaptionEditModal.block = block;
document.body.append(embedCardEditCaptionEditModal);
}
declare global {
interface HTMLElementTagNameMap {
'embed-card-caption-edit-modal': EmbedCardEditCaptionEditModal;
}
}

View File

@@ -1,231 +0,0 @@
import { EdgelessCRUDIdentifier } from '@blocksuite/affine-block-surface';
import { toast } from '@blocksuite/affine-components/toast';
import type { EmbedCardStyle } from '@blocksuite/affine-model';
import {
EMBED_CARD_HEIGHT,
EMBED_CARD_WIDTH,
} from '@blocksuite/affine-shared/consts';
import { EmbedOptionProvider } from '@blocksuite/affine-shared/services';
import { isValidUrl } from '@blocksuite/affine-shared/utils';
import type { EditorHost } from '@blocksuite/block-std';
import { ShadowlessElement } from '@blocksuite/block-std';
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
import { Bound, Vec, WithDisposable } from '@blocksuite/global/utils';
import type { BlockModel } from '@blocksuite/store';
import { html } from 'lit';
import { property, query, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { embedCardModalStyles } from './styles.js';
export class EmbedCardCreateModal extends WithDisposable(ShadowlessElement) {
static override styles = embedCardModalStyles;
private readonly _onCancel = () => {
this.remove();
};
private readonly _onConfirm = () => {
const url = this.input.value;
if (!isValidUrl(url)) {
toast(this.host, 'Invalid link');
return;
}
const embedOptions = this.host.std
.get(EmbedOptionProvider)
.getEmbedBlockOptions(url);
const { mode } = this.createOptions;
if (mode === 'page') {
const { parentModel, index } = this.createOptions;
let flavour = 'affine:bookmark';
if (embedOptions) {
flavour = embedOptions.flavour;
}
this.host.doc.addBlock(
flavour as never,
{
url,
},
parentModel,
index
);
} else if (mode === 'edgeless') {
let flavour = 'affine:bookmark',
targetStyle: EmbedCardStyle = 'vertical';
if (embedOptions) {
flavour = embedOptions.flavour;
targetStyle = embedOptions.styles[0];
}
const gfx = this.host.std.get(GfxControllerIdentifier);
const crud = this.host.std.get(EdgelessCRUDIdentifier);
const viewport = gfx.viewport;
const surfaceModel = gfx.surface;
if (!surfaceModel) {
return;
}
const center = Vec.toVec(viewport.center);
crud.addBlock(
flavour,
{
url,
xywh: Bound.fromCenter(
center,
EMBED_CARD_WIDTH[targetStyle],
EMBED_CARD_HEIGHT[targetStyle]
).serialize(),
style: targetStyle,
},
surfaceModel
);
gfx.tool.setTool(
// @ts-expect-error FIXME: resolve after gfx tool refactor
'default'
);
}
this.onConfirm();
this.remove();
};
private readonly _onDocumentKeydown = (e: KeyboardEvent) => {
e.stopPropagation();
if (e.key === 'Enter' && !e.isComposing) {
this._onConfirm();
}
if (e.key === 'Escape') {
this.remove();
}
};
private _handleInput(e: InputEvent) {
const target = e.target as HTMLInputElement;
this._linkInputValue = target.value;
}
override connectedCallback() {
super.connectedCallback();
this.updateComplete
.then(() => {
requestAnimationFrame(() => {
this.input.focus();
});
})
.catch(console.error);
this.disposables.addFromEvent(this, 'keydown', this._onDocumentKeydown);
}
override render() {
return html`<div class="embed-card-modal">
<div class="embed-card-modal-mask" @click=${this._onCancel}></div>
<div class="embed-card-modal-wrapper">
<div class="embed-card-modal-row">
<div class="embed-card-modal-title">${this.titleText}</div>
</div>
<div class="embed-card-modal-row">
<div class="embed-card-modal-description">
${this.descriptionText}
</div>
</div>
<div class="embed-card-modal-row">
<input
class="embed-card-modal-input link"
id="card-description"
type="text"
placeholder="Input in https://..."
value=${this._linkInputValue}
@input=${this._handleInput}
/>
</div>
<div class="embed-card-modal-row">
<button
class=${classMap({
'embed-card-modal-button': true,
save: true,
})}
?disabled=${!isValidUrl(this._linkInputValue)}
@click=${this._onConfirm}
>
Confirm
</button>
</div>
</div>
</div>`;
}
@state()
private accessor _linkInputValue = '';
@property({ attribute: false })
accessor createOptions!:
| {
mode: 'page';
parentModel: BlockModel | string;
index?: number;
}
| {
mode: 'edgeless';
};
@property({ attribute: false })
accessor descriptionText!: string;
@property({ attribute: false })
accessor host!: EditorHost;
@query('input')
accessor input!: HTMLInputElement;
@property({ attribute: false })
accessor onConfirm!: () => void;
@property({ attribute: false })
accessor titleText!: string;
}
export async function toggleEmbedCardCreateModal(
host: EditorHost,
titleText: string,
descriptionText: string,
createOptions:
| {
mode: 'page';
parentModel: BlockModel | string;
index?: number;
}
| {
mode: 'edgeless';
}
): Promise<void> {
host.selection.clear();
const embedCardCreateModal = new EmbedCardCreateModal();
embedCardCreateModal.host = host;
embedCardCreateModal.titleText = titleText;
embedCardCreateModal.descriptionText = descriptionText;
embedCardCreateModal.createOptions = createOptions;
document.body.append(embedCardCreateModal);
return new Promise(resolve => {
embedCardCreateModal.onConfirm = () => resolve();
});
}
declare global {
interface HTMLElementTagNameMap {
'embed-card-create-modal': EmbedCardCreateModal;
}
}

View File

@@ -1,450 +0,0 @@
import {
EmbedLinkedDocBlockComponent,
EmbedSyncedDocBlockComponent,
} from '@blocksuite/affine-block-embed';
import {
notifyLinkedDocClearedAliases,
notifyLinkedDocSwitchedToCard,
} from '@blocksuite/affine-components/notification';
import { toast } from '@blocksuite/affine-components/toast';
import type { AliasInfo } from '@blocksuite/affine-model';
import {
EmbedLinkedDocModel,
EmbedSyncedDocModel,
} from '@blocksuite/affine-model';
import {
type LinkEventType,
type TelemetryEvent,
TelemetryProvider,
} from '@blocksuite/affine-shared/services';
import { FONT_SM, FONT_XS } from '@blocksuite/affine-shared/styles';
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import {
listenClickAway,
stopPropagation,
} from '@blocksuite/affine-shared/utils';
import type {
BlockComponent,
BlockStdScope,
EditorHost,
} from '@blocksuite/block-std';
import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils';
import { autoUpdate, computePosition, flip, offset } from '@floating-ui/dom';
import { computed, signal } from '@preact/signals-core';
import { css, html, LitElement } from 'lit';
import { property, query } from 'lit/decorators.js';
import { choose } from 'lit/directives/choose.js';
import { classMap } from 'lit/directives/class-map.js';
import { live } from 'lit/directives/live.js';
import type { LinkableEmbedModel } from './type.js';
import { isInternalEmbedModel } from './type.js';
export class EmbedCardEditModal extends SignalWatcher(
WithDisposable(LitElement)
) {
static override styles = css`
:host {
position: absolute;
top: 0;
left: 0;
z-index: var(--affine-z-index-popover);
animation: affine-popover-fade-in 0.2s ease;
}
@keyframes affine-popover-fade-in {
from {
opacity: 0;
transform: translateY(-3px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.embed-card-modal-wrapper {
display: flex;
padding: 12px;
flex-direction: column;
justify-content: flex-end;
align-items: flex-start;
gap: 12px;
width: 421px;
color: var(--affine-icon-color);
box-shadow: var(--affine-overlay-shadow);
background: ${unsafeCSSVarV2('layer/background/overlayPanel')};
border-radius: 4px;
border: 0.5px solid ${unsafeCSSVarV2('layer/insideBorder/border')};
}
.row {
width: 100%;
display: flex;
align-items: center;
gap: 12px;
}
.row .input {
display: flex;
padding: 4px 10px;
width: 100%;
min-width: 100%;
box-sizing: border-box;
border-radius: 4px;
user-select: none;
background: transparent;
border: 1px solid ${unsafeCSSVarV2('input/border/default')};
color: var(--affine-text-primary-color);
${FONT_SM};
}
.input::placeholder {
color: var(--affine-placeholder-color);
}
.input:focus {
border-color: ${unsafeCSSVarV2('input/border/active')};
outline: none;
}
textarea.input {
min-height: 80px;
resize: none;
}
.row.actions {
justify-content: flex-end;
}
.row.actions .button {
display: flex;
padding: 4px 12px;
align-items: center;
gap: 4px;
border-radius: 4px;
border: 1px solid ${unsafeCSSVarV2('button/innerBlackBorder')};
background: ${unsafeCSSVarV2('button/secondary')};
${FONT_XS};
color: ${unsafeCSSVarV2('text/primary')};
}
.row.actions .button[disabled],
.row.actions .button:disabled {
pointer-events: none;
color: ${unsafeCSSVarV2('text/disable')};
}
.row.actions .button.save {
color: ${unsafeCSSVarV2('button/pureWhiteText')};
background: ${unsafeCSSVarV2('button/primary')};
}
.row.actions .button[disabled].save,
.row.actions .button:disabled.save {
opacity: 0.5;
}
`;
private _blockComponent: BlockComponent | null = null;
private readonly _hide = () => {
this.remove();
};
private readonly _onKeydown = (e: KeyboardEvent) => {
e.stopPropagation();
if (e.key === 'Enter' && !(e.isComposing || e.shiftKey)) {
this._onSave();
}
if (e.key === 'Escape') {
e.preventDefault();
this.remove();
}
};
private readonly _onReset = () => {
const blockComponent = this._blockComponent;
if (!blockComponent) {
this.remove();
return;
}
const std = blockComponent.std;
this.model.doc.updateBlock(this.model, { title: null, description: null });
if (
this.isEmbedLinkedDocModel &&
blockComponent instanceof EmbedLinkedDocBlockComponent
) {
blockComponent.refreshData();
notifyLinkedDocClearedAliases(std);
}
blockComponent.requestUpdate();
track(std, this.model, this.viewType, 'ResetedAlias', { control: 'reset' });
this.remove();
};
private readonly _onSave = () => {
const blockComponent = this._blockComponent;
if (!blockComponent) {
this.remove();
return;
}
const title = this.title$.value.trim();
if (title.length === 0) {
toast(this.host, 'Title can not be empty');
return;
}
const std = blockComponent.std;
const description = this.description$.value.trim();
const props: AliasInfo = { title };
if (description) props.description = description;
if (
this.isEmbedSyncedDocModel &&
blockComponent instanceof EmbedSyncedDocBlockComponent
) {
blockComponent.convertToCard(props);
notifyLinkedDocSwitchedToCard(std);
} else {
this.model.doc.updateBlock(this.model, props);
blockComponent.requestUpdate();
}
track(std, this.model, this.viewType, 'SavedAlias', { control: 'save' });
this.remove();
};
private readonly _updateDescription = (e: InputEvent) => {
const target = e.target as HTMLTextAreaElement;
this.description$.value = target.value;
};
private readonly _updateTitle = (e: InputEvent) => {
const target = e.target as HTMLInputElement;
this.title$.value = target.value;
};
get isEmbedLinkedDocModel() {
return this.model instanceof EmbedLinkedDocModel;
}
get isEmbedSyncedDocModel() {
return this.model instanceof EmbedSyncedDocModel;
}
get isInternalEmbedModel() {
return isInternalEmbedModel(this.model);
}
get modelType(): 'linked' | 'synced' | null {
if (this.isEmbedLinkedDocModel) return 'linked';
if (this.isEmbedSyncedDocModel) return 'synced';
return null;
}
get placeholders() {
if (this.isInternalEmbedModel) {
return {
title: 'Add title alias',
description:
'Add description alias (empty to inherit document content)',
};
}
return {
title: 'Write a title',
description: 'Write a description...',
};
}
private _updateInfo() {
const title = this.model.title || this.originalDocInfo?.title || '';
const description =
this.model.description || this.originalDocInfo?.description || '';
this.title$.value = title;
this.description$.value = description;
}
override connectedCallback() {
super.connectedCallback();
this._updateInfo();
}
override firstUpdated() {
const blockComponent = this.host.std.view.getBlock(this.model.id);
if (!blockComponent) return;
this._blockComponent = blockComponent;
this.disposables.add(
autoUpdate(blockComponent, this, () => {
computePosition(blockComponent, this, {
placement: 'top-start',
middleware: [flip(), offset(8)],
})
.then(({ x, y }) => {
this.style.left = `${x}px`;
this.style.top = `${y}px`;
})
.catch(console.error);
})
);
this.disposables.add(listenClickAway(this, this._hide));
this.disposables.addFromEvent(this, 'keydown', this._onKeydown);
this.disposables.addFromEvent(this, 'pointerdown', stopPropagation);
this.titleInput.focus();
this.titleInput.select();
}
override render() {
return html`
<div class="embed-card-modal-wrapper">
<div class="row">
<input
class="input title"
type="text"
placeholder=${this.placeholders.title}
.value=${live(this.title$.value)}
@input=${this._updateTitle}
/>
</div>
<div class="row">
<textarea
class="input description"
maxlength="500"
placeholder=${this.placeholders.description}
.value=${live(this.description$.value)}
@input=${this._updateDescription}
></textarea>
</div>
<div class="row actions">
${choose(this.modelType, [
[
'linked',
() => html`
<button
class=${classMap({
button: true,
reset: true,
})}
.disabled=${this.resetButtonDisabled$.value}
@click=${this._onReset}
>
Reset
</button>
`,
],
[
'synced',
() => html`
<button
class=${classMap({
button: true,
cancel: true,
})}
@click=${this._hide}
>
Cancel
</button>
`,
],
])}
<button
class=${classMap({
button: true,
save: true,
})}
.disabled=${this.saveButtonDisabled$.value}
@click=${this._onSave}
>
Save
</button>
</div>
</div>
`;
}
accessor description$ = signal<string>('');
@property({ attribute: false })
accessor host!: EditorHost;
@property({ attribute: false })
accessor model!: LinkableEmbedModel;
@property({ attribute: false })
accessor originalDocInfo: AliasInfo | undefined = undefined;
accessor resetButtonDisabled$ = computed<boolean>(
() =>
!(
Boolean(this.model.title$.value?.length) ||
Boolean(this.model.description$.value?.length)
)
);
accessor saveButtonDisabled$ = computed<boolean>(
() => this.title$.value.trim().length === 0
);
accessor title$ = signal<string>('');
@query('.input.title')
accessor titleInput!: HTMLInputElement;
@property({ attribute: false })
accessor viewType!: string;
}
export function toggleEmbedCardEditModal(
host: EditorHost,
embedCardModel: LinkableEmbedModel,
viewType: string,
originalDocInfo?: AliasInfo
) {
document.body.querySelector('embed-card-edit-modal')?.remove();
const embedCardEditModal = new EmbedCardEditModal();
embedCardEditModal.model = embedCardModel;
embedCardEditModal.host = host;
embedCardEditModal.viewType = viewType;
embedCardEditModal.originalDocInfo = originalDocInfo;
document.body.append(embedCardEditModal);
}
declare global {
interface HTMLElementTagNameMap {
'embed-card-edit-modal': EmbedCardEditModal;
}
}
function track(
std: BlockStdScope,
model: LinkableEmbedModel,
viewType: string,
event: LinkEventType,
props: Partial<TelemetryEvent>
) {
std.getOptional(TelemetryProvider)?.track(event, {
segment: 'toolbar',
page: 'doc editor',
module: 'embed card edit popup',
type: `${viewType} view`,
category: isInternalEmbedModel(model) ? 'linked doc' : 'link',
...props,
});
}

View File

@@ -1,4 +0,0 @@
export * from './embed-card-caption-edit-modal';
export * from './embed-card-create-modal';
export * from './embed-card-edit-modal';
export * from './type';

View File

@@ -1,120 +0,0 @@
import { FONT_XS, PANEL_BASE } from '@blocksuite/affine-shared/styles';
import { css } from 'lit';
export const embedCardModalStyles = css`
.embed-card-modal-mask {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
margin: auto;
z-index: 1;
}
.embed-card-modal-wrapper {
${PANEL_BASE};
flex-direction: column;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
margin: auto;
z-index: 2;
width: 305px;
height: max-content;
padding: 12px;
gap: 12px;
border-radius: 8px;
font-size: var(--affine-font-xs);
line-height: 20px;
}
.embed-card-modal-row {
display: flex;
flex-direction: column;
align-self: stretch;
}
.embed-card-modal-row label {
padding: 0px 2px;
color: var(--affine-text-secondary-color);
font-weight: 600;
}
.embed-card-modal-input {
display: flex;
padding-left: 10px;
padding-right: 10px;
border-radius: 8px;
border: 1px solid var(--affine-border-color);
background: var(--affine-white-10);
color: var(--affine-text-primary-color);
${FONT_XS};
}
input.embed-card-modal-input {
padding-top: 4px;
padding-bottom: 4px;
}
textarea.embed-card-modal-input {
padding-top: 6px;
padding-bottom: 6px;
min-width: 100%;
max-width: 100%;
}
.embed-card-modal-input:focus {
border-color: var(--affine-blue-700);
box-shadow: var(--affine-active-shadow);
outline: none;
}
.embed-card-modal-input::placeholder {
color: var(--affine-placeholder-color);
}
.embed-card-modal-row:has(.embed-card-modal-button) {
flex-direction: row;
gap: 4px;
justify-content: flex-end;
}
.embed-card-modal-row:has(.embed-card-modal-button.reset) {
justify-content: space-between;
}
.embed-card-modal-button {
padding: 4px 18px;
border-radius: 8px;
box-sizing: border-box;
}
.embed-card-modal-button.save {
border: 1px solid var(--affine-black-10);
background: var(--affine-primary-color);
color: var(--affine-pure-white);
}
.embed-card-modal-button[disabled] {
pointer-events: none;
cursor: not-allowed;
color: var(--affine-text-disable-color);
background: transparent;
}
.embed-card-modal-button.reset {
padding: 4px 0;
border: none;
background: transparent;
text-decoration: underline;
color: var(--affine-secondary-color);
user-select: none;
}
.embed-card-modal-title {
font-size: 18px;
font-weight: 600;
line-height: 26px;
user-select: none;
}
.embed-card-modal-description {
font-size: 15px;
font-weight: 500;
line-height: 24px;
user-select: none;
}
`;

View File

@@ -1,79 +0,0 @@
import {
EmbedFigmaBlockComponent,
EmbedGithubBlockComponent,
EmbedHtmlBlockComponent,
EmbedLinkedDocBlockComponent,
EmbedLoomBlockComponent,
EmbedSyncedDocBlockComponent,
EmbedYoutubeBlockComponent,
} from '@blocksuite/affine-block-embed';
import type {
BookmarkBlockModel,
EmbedFigmaModel,
EmbedGithubModel,
EmbedHtmlModel,
EmbedLoomModel,
EmbedYoutubeModel,
} from '@blocksuite/affine-model';
import {
EmbedLinkedDocModel,
EmbedSyncedDocModel,
} from '@blocksuite/affine-model';
import type { BlockComponent } from '@blocksuite/block-std';
import { BookmarkBlockComponent } from '../../bookmark-block';
export type ExternalEmbedBlockComponent =
| BookmarkBlockComponent
| EmbedFigmaBlockComponent
| EmbedGithubBlockComponent
| EmbedLoomBlockComponent
| EmbedYoutubeBlockComponent;
export type InternalEmbedBlockComponent =
| EmbedLinkedDocBlockComponent
| EmbedSyncedDocBlockComponent;
export type LinkableEmbedBlockComponent =
| ExternalEmbedBlockComponent
| InternalEmbedBlockComponent;
export type BuiltInEmbedBlockComponent =
| LinkableEmbedBlockComponent
| EmbedHtmlBlockComponent;
export type ExternalEmbedModel =
| BookmarkBlockModel
| EmbedFigmaModel
| EmbedGithubModel
| EmbedLoomModel
| EmbedYoutubeModel;
export type InternalEmbedModel = EmbedLinkedDocModel | EmbedSyncedDocModel;
export type LinkableEmbedModel = ExternalEmbedModel | InternalEmbedModel;
export type BuiltInEmbedModel = LinkableEmbedModel | EmbedHtmlModel;
export function isEmbedCardBlockComponent(
block: BlockComponent
): block is BuiltInEmbedBlockComponent {
return (
block instanceof BookmarkBlockComponent ||
block instanceof EmbedFigmaBlockComponent ||
block instanceof EmbedGithubBlockComponent ||
block instanceof EmbedHtmlBlockComponent ||
block instanceof EmbedLoomBlockComponent ||
block instanceof EmbedYoutubeBlockComponent ||
block instanceof EmbedLinkedDocBlockComponent ||
block instanceof EmbedSyncedDocBlockComponent
);
}
export function isInternalEmbedModel(
model: BuiltInEmbedModel
): model is InternalEmbedModel {
return (
model instanceof EmbedLinkedDocModel || model instanceof EmbedSyncedDocModel
);
}

View File

@@ -1,2 +0,0 @@
export * from './bookmark-card';
export * from './embed-card-modal';

View File

@@ -1,39 +0,0 @@
import { BookmarkBlockComponent } from './bookmark-block';
import { BookmarkEdgelessBlockComponent } from './bookmark-edgeless-block';
import type { BookmarkBlockService } from './bookmark-service';
import type { insertBookmarkCommand } from './commands/insert-bookmark';
import type { insertLinkByQuickSearchCommand } from './commands/insert-link-by-quick-search';
import { BookmarkCard } from './components/bookmark-card';
import {
EmbedCardCreateModal,
EmbedCardEditCaptionEditModal,
EmbedCardEditModal,
} from './components/embed-card-modal';
export function effects() {
customElements.define(
'affine-edgeless-bookmark',
BookmarkEdgelessBlockComponent
);
customElements.define('affine-bookmark', BookmarkBlockComponent);
customElements.define('bookmark-card', BookmarkCard);
customElements.define('embed-card-create-modal', EmbedCardCreateModal);
customElements.define('embed-card-edit-modal', EmbedCardEditModal);
customElements.define(
'embed-card-caption-edit-modal',
EmbedCardEditCaptionEditModal
);
}
declare global {
namespace BlockSuite {
interface Commands {
insertBookmark: typeof insertBookmarkCommand;
insertLinkByQuickSearch: typeof insertLinkByQuickSearchCommand;
}
interface BlockServices {
'affine:bookmark': BookmarkBlockService;
}
}
}

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