Compare commits

..

5 Commits

Author SHA1 Message Date
LongYinan
2bfc7e33f1 Fix 2025-05-05 15:51:28 +08:00
LongYinan
e320240f24 Update run.ts 2025-05-05 15:51:28 +08:00
LongYinan
93d93abd8a Fix cycle require yarn.js 2025-05-05 15:51:28 +08:00
LongYinan
7fbe5173c3 reduce server tests sharding 2025-05-05 15:51:28 +08:00
LongYinan
359ed9698b chore: switch to oxnode 2025-05-05 15:51:27 +08:00
2181 changed files with 35489 additions and 91941 deletions

View File

@@ -6,7 +6,6 @@ yarn install
# Build Server Dependencies # Build Server Dependencies
yarn affine @affine/server-native build yarn affine @affine/server-native build
yarn affine @affine/reader build
# Create database # Create database
yarn affine @affine/server prisma migrate reset -f yarn affine @affine/server prisma migrate reset -f

View File

@@ -10,7 +10,6 @@ services:
environment: environment:
DATABASE_URL: postgresql://affine:affine@db:5432/affine DATABASE_URL: postgresql://affine:affine@db:5432/affine
REDIS_SERVER_HOST: redis REDIS_SERVER_HOST: redis
AFFINE_INDEXER_SEARCH_ENDPOINT: http://indexer:9308
db: db:
image: pgvector/pgvector:pg16 image: pgvector/pgvector:pg16
@@ -24,19 +23,5 @@ services:
redis: redis:
image: redis image: redis
indexer:
image: manticoresearch/manticore:${MANTICORE_VERSION:-9.3.2}
ulimits:
nproc: 65535
nofile:
soft: 65535
hard: 65535
memlock:
soft: -1
hard: -1
volumes:
- manticoresearch_data:/var/lib/manticore
volumes: volumes:
postgres-data: postgres-data:
manticoresearch_data:

View File

@@ -3,13 +3,4 @@ DB_VERSION=16
# database credentials # database credentials
DB_PASSWORD=affine DB_PASSWORD=affine
DB_USERNAME=affine DB_USERNAME=affine
DB_DATABASE_NAME=affine DB_DATABASE_NAME=affine
# elasticsearch env
# ELASTIC_VERSION=9.0.1
# enable for arm64, e.g.: macOS M1+
# ELASTIC_VERSION_ARM64=-arm64
# ELASTIC_PLATFORM=linux/arm64
# manticoresearch
MANTICORE_VERSION=9.3.2

View File

@@ -1,6 +1,3 @@
postgres postgres
.env .env
compose.yml compose.yml
certs/*
!certs/.gitkeep
nginx/conf.d/*

View File

@@ -1,27 +0,0 @@
# Dev containers
## Develop with domain
> MacOs only, OrbStack only
### 1. Generate and install Root CA
```bash
# the root ca file will be located at `./.docker/dev/certs/ca`
yarn affine cert --install
```
### 2. Generate domain certs
```bash
# certificates will be located at `./.docker/dev/certs/${domain}`
yarn affine cert --domain dev.affine.fail
```
### 3. Enable dns and nginx service in compose.yml
### 4. Add custom dns server
```bash
echo "nameserver 127.0.0.1" | sudo tee /etc/resolver/dev.affine.fail
```

View File

@@ -24,78 +24,8 @@ services:
- 1025:1025 - 1025:1025
- 8025:8025 - 8025:8025
# https://manual.manticoresearch.com/Starting_the_server/Docker
manticoresearch:
image: manticoresearch/manticore:${MANTICORE_VERSION:-9.3.2}
ports:
- 9308:9308
ulimits:
nproc: 65535
nofile:
soft: 65535
hard: 65535
memlock:
soft: -1
hard: -1
volumes:
- manticoresearch_data:/var/lib/manticore
# elasticsearch:
# image: docker.elastic.co/elasticsearch/elasticsearch:${ELASTIC_VERSION:-9.0.1}${ELASTIC_VERSION_ARM64}
# platform: ${ELASTIC_PLATFORM}
# labels:
# co.elastic.logs/module: elasticsearch
# volumes:
# - elasticsearch_data:/usr/share/elasticsearch/data
# ports:
# - ${ES_PORT:-9200}:9200
# environment:
# - node.name=es01
# - cluster.name=affine-dev
# - discovery.type=single-node
# - bootstrap.memory_lock=true
# - xpack.security.enabled=false
# - xpack.security.http.ssl.enabled=false
# - xpack.security.transport.ssl.enabled=false
# - xpack.license.self_generated.type=basic
# mem_limit: ${ES_MEM_LIMIT:-1073741824}
# ulimits:
# memlock:
# soft: -1
# hard: -1
# healthcheck:
# test:
# [
# "CMD-SHELL",
# "curl -s http://localhost:9200 | grep -q 'affine-dev'",
# ]
# interval: 10s
# timeout: 10s
# retries: 120
# dns:
# image: strm/dnsmasq
# volumes:
# - ./dnsmasq.conf:/etc/dnsmasq.d/local.conf
# ports:
# - "53:53/udp"
# cap_add:
# - NET_ADMIN
# depends_on:
# - nginx
# nginx:
# image: nginx:alpine
# volumes:
# - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
# - ./nginx/conf.d:/etc/nginx/conf.d:ro
# - ./certs:/etc/nginx/certs:ro
# network_mode: host
networks: networks:
dev: dev:
volumes: volumes:
postgres_data: postgres_data:
manticoresearch_data:
elasticsearch_data:

View File

@@ -1,2 +0,0 @@
log-queries
address=/dev.affine.fail/127.0.0.1

View File

@@ -1,28 +0,0 @@
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
keepalive_timeout 65;
types_hash_max_size 2048;
client_max_body_size 512M;
server_names_hash_bucket_size 128;
ssi on;
gzip on;
include "/etc/nginx/conf.d/*";
}

View File

@@ -1,27 +0,0 @@
server {
listen 80;
server_name DEV_DOMAIN;
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
http2 on;
ssl_certificate /etc/nginx/certs/$host/crt;
ssl_certificate_key /etc/nginx/certs/$host/key;
server_name DEV_DOMAIN;
location / {
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
resolver 127.0.0.1;
}
}

View File

@@ -1,25 +0,0 @@
[req]
distinguished_name = req_distinguished_name
req_extensions = v3_req
[req_distinguished_name]
countryName = Country Name (2 letter code)
countryName_default = US
stateOrProvinceName = State or Province Name (full name)
stateOrProvinceName_default = MN
localityName = Locality Name (eg, city)
localityName_default = Minneapolis
organizationalUnitName = Organizational Unit Name (eg, section)
organizationalUnitName_default = Domain Control Validated
commonName = Internet Widgits Ltd
commonName_max = 64
[ v3_req ]
# Extensions to add to a certificate request
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
subjectAltName = @alt_names
[alt_names]
DNS.1 = DEV_DOMAIN
DNS.2 = *.DEV_DOMAIN

View File

@@ -20,4 +20,4 @@ CONFIG_LOCATION=~/.affine/self-host/config
# database credentials # database credentials
DB_USERNAME=affine DB_USERNAME=affine
DB_PASSWORD= DB_PASSWORD=
DB_DATABASE=affine DB_DATABASE=affine

View File

@@ -1 +0,0 @@
.env

View File

@@ -21,7 +21,6 @@ services:
environment: environment:
- REDIS_SERVER_HOST=redis - REDIS_SERVER_HOST=redis
- DATABASE_URL=postgresql://${DB_USERNAME}:${DB_PASSWORD}@postgres:5432/${DB_DATABASE:-affine} - DATABASE_URL=postgresql://${DB_USERNAME}:${DB_PASSWORD}@postgres:5432/${DB_DATABASE:-affine}
- AFFINE_INDEXER_ENABLED=false
restart: unless-stopped restart: unless-stopped
affine_migration: affine_migration:
@@ -37,7 +36,6 @@ services:
environment: environment:
- REDIS_SERVER_HOST=redis - REDIS_SERVER_HOST=redis
- DATABASE_URL=postgresql://${DB_USERNAME}:${DB_PASSWORD}@postgres:5432/${DB_DATABASE:-affine} - DATABASE_URL=postgresql://${DB_USERNAME}:${DB_PASSWORD}@postgres:5432/${DB_DATABASE:-affine}
- AFFINE_INDEXER_ENABLED=false
depends_on: depends_on:
postgres: postgres:
condition: service_healthy condition: service_healthy
@@ -55,7 +53,7 @@ services:
restart: unless-stopped restart: unless-stopped
postgres: postgres:
image: pgvector/pgvector:pg16 image: postgres:16
container_name: affine_postgres container_name: affine_postgres
volumes: volumes:
- ${DB_DATA_LOCATION}:/var/lib/postgresql/data - ${DB_DATA_LOCATION}:/var/lib/postgresql/data

View File

@@ -31,13 +31,9 @@
"properties": { "properties": {
"queue": { "queue": {
"type": "object", "type": "object",
"description": "The config for job queues\n@default {\"attempts\":5,\"backoff\":{\"type\":\"exponential\",\"delay\":1000},\"removeOnComplete\":true,\"removeOnFail\":{\"age\":86400,\"count\":500}}\n@link https://api.docs.bullmq.io/interfaces/v5.QueueOptions.html", "description": "The config for job queues\n@default {\"attempts\":5,\"removeOnComplete\":true,\"removeOnFail\":{\"age\":86400,\"count\":500}}\n@link https://api.docs.bullmq.io/interfaces/v5.QueueOptions.html",
"default": { "default": {
"attempts": 5, "attempts": 5,
"backoff": {
"type": "exponential",
"delay": 1000
},
"removeOnComplete": true, "removeOnComplete": true,
"removeOnFail": { "removeOnFail": {
"age": 86400, "age": 86400,
@@ -52,19 +48,7 @@
}, },
"queues.copilot": { "queues.copilot": {
"type": "object", "type": "object",
"description": "The config for copilot job queue\n@default {\"concurrency\":10}", "description": "The config for copilot job queue\n@default {\"concurrency\":1}",
"properties": {
"concurrency": {
"type": "number"
}
},
"default": {
"concurrency": 10
}
},
"queues.doc": {
"type": "object",
"description": "The config for doc job queue\n@default {\"concurrency\":1}",
"properties": { "properties": {
"concurrency": { "concurrency": {
"type": "number" "type": "number"
@@ -74,9 +58,9 @@
"concurrency": 1 "concurrency": 1
} }
}, },
"queues.indexer": { "queues.doc": {
"type": "object", "type": "object",
"description": "The config for indexer job queue\n@default {\"concurrency\":1}", "description": "The config for doc job queue\n@default {\"concurrency\":1}",
"properties": { "properties": {
"concurrency": { "concurrency": {
"type": "number" "type": "number"
@@ -139,6 +123,32 @@
} }
} }
}, },
"websocket": {
"type": "object",
"description": "Configuration for websocket module",
"properties": {
"transports": {
"type": "array",
"description": "The enabled transports for accepting websocket traffics.\n@default [\"websocket\",\"polling\"]\n@link https://docs.nestjs.com/websockets/gateways#transports",
"items": {
"type": "string",
"enum": [
"websocket",
"polling"
]
},
"default": [
"websocket",
"polling"
]
},
"maxHttpBufferSize": {
"type": "number",
"description": "How many bytes or characters a message can be, before closing the session (to avoid DoS).\n@default 100000000",
"default": 100000000
}
}
},
"auth": { "auth": {
"type": "object", "type": "object",
"description": "Configuration for auth module", "description": "Configuration for auth module",
@@ -491,32 +501,6 @@
} }
} }
}, },
"websocket": {
"type": "object",
"description": "Configuration for websocket module",
"properties": {
"transports": {
"type": "array",
"description": "The enabled transports for accepting websocket traffics.\n@default [\"websocket\",\"polling\"]\n@link https://docs.nestjs.com/websockets/gateways#transports",
"items": {
"type": "string",
"enum": [
"websocket",
"polling"
]
},
"default": [
"websocket",
"polling"
]
},
"maxHttpBufferSize": {
"type": "number",
"description": "How many bytes or characters a message can be, before closing the session (to avoid DoS).\n@default 100000000",
"default": 100000000
}
}
},
"server": { "server": {
"type": "object", "type": "object",
"description": "Configuration for server module", "description": "Configuration for server module",
@@ -643,41 +627,6 @@
"apiKey": "" "apiKey": ""
} }
}, },
"providers.geminiVertex": {
"type": "object",
"description": "The config for the google vertex provider.\n@default {}",
"properties": {
"location": {
"type": "string",
"description": "The location of the google vertex provider."
},
"project": {
"type": "string",
"description": "The project name of the google vertex provider."
},
"googleAuthOptions": {
"type": "object",
"description": "The google auth options for the google vertex provider.",
"properties": {
"credentials": {
"type": "object",
"description": "The credentials for the google vertex provider.",
"properties": {
"client_email": {
"type": "string",
"description": "The client email for the google vertex provider."
},
"private_key": {
"type": "string",
"description": "The private key for the google vertex provider."
}
}
}
}
}
},
"default": {}
},
"providers.perplexity": { "providers.perplexity": {
"type": "object", "type": "object",
"description": "The config for the perplexity provider.\n@default {\"apiKey\":\"\"}", "description": "The config for the perplexity provider.\n@default {\"apiKey\":\"\"}",
@@ -685,48 +634,6 @@
"apiKey": "" "apiKey": ""
} }
}, },
"providers.anthropic": {
"type": "object",
"description": "The config for the anthropic provider.\n@default {\"apiKey\":\"\"}",
"default": {
"apiKey": ""
}
},
"providers.anthropicVertex": {
"type": "object",
"description": "The config for the google vertex provider.\n@default {}",
"properties": {
"location": {
"type": "string",
"description": "The location of the google vertex provider."
},
"project": {
"type": "string",
"description": "The project name of the google vertex provider."
},
"googleAuthOptions": {
"type": "object",
"description": "The google auth options for the google vertex provider.",
"properties": {
"credentials": {
"type": "object",
"description": "The credentials for the google vertex provider.",
"properties": {
"client_email": {
"type": "string",
"description": "The client email for the google vertex provider."
},
"private_key": {
"type": "string",
"description": "The private key for the google vertex provider."
}
}
}
}
}
},
"default": {}
},
"unsplash": { "unsplash": {
"type": "object", "type": "object",
"description": "The config for the unsplash key.\n@default {\"key\":\"\"}", "description": "The config for the unsplash key.\n@default {\"key\":\"\"}",
@@ -734,13 +641,6 @@
"key": "" "key": ""
} }
}, },
"exa": {
"type": "object",
"description": "The config for the exa web search key.\n@default {\"key\":\"\"}",
"default": {
"key": ""
}
},
"storage": { "storage": {
"type": "object", "type": "object",
"description": "The config for the storage provider.\n@default {\"provider\":\"fs\",\"bucket\":\"copilot\",\"config\":{\"path\":\"~/.affine/storage\"}}", "description": "The config for the storage provider.\n@default {\"provider\":\"fs\",\"bucket\":\"copilot\",\"config\":{\"path\":\"~/.affine/storage\"}}",
@@ -880,47 +780,6 @@
} }
} }
}, },
"indexer": {
"type": "object",
"description": "Configuration for indexer module",
"properties": {
"enabled": {
"type": "boolean",
"description": "Enable indexer plugin\n@default false\n@environment `AFFINE_INDEXER_ENABLED`",
"default": false
},
"provider.type": {
"type": "string",
"description": "Indexer search service provider name\n@default \"manticoresearch\"\n@environment `AFFINE_INDEXER_SEARCH_PROVIDER`",
"default": "manticoresearch"
},
"provider.endpoint": {
"type": "string",
"description": "Indexer search service endpoint\n@default \"http://localhost:9308\"\n@environment `AFFINE_INDEXER_SEARCH_ENDPOINT`",
"default": "http://localhost:9308"
},
"provider.apiKey": {
"type": "string",
"description": "Indexer search service api key. Optional for elasticsearch\n@default \"\"\n@environment `AFFINE_INDEXER_SEARCH_API_KEY`\n@link https://www.elastic.co/guide/server/current/api-key.html",
"default": ""
},
"provider.username": {
"type": "string",
"description": "Indexer search service auth username, if not set, basic auth will be disabled. Optional for elasticsearch\n@default \"\"\n@environment `AFFINE_INDEXER_SEARCH_USERNAME`\n@link https://www.elastic.co/guide/en/elasticsearch/reference/current/http-clients.html",
"default": ""
},
"provider.password": {
"type": "string",
"description": "Indexer search service auth password, if not set, basic auth will be disabled. Optional for elasticsearch\n@default \"\"\n@environment `AFFINE_INDEXER_SEARCH_PASSWORD`",
"default": ""
},
"autoIndex.batchSize": {
"type": "number",
"description": "Number of workspaces automatically indexed per batch\n@default 10",
"default": 10
}
}
},
"oauth": { "oauth": {
"type": "object", "type": "object",
"description": "Configuration for oauth module", "description": "Configuration for oauth module",
@@ -965,43 +824,13 @@
}, },
"providers.oidc": { "providers.oidc": {
"type": "object", "type": "object",
"description": "OIDC OAuth provider config\n@default {\"clientId\":\"\",\"clientSecret\":\"\",\"issuer\":\"\",\"args\":{}}\n@link https://openid.net/specs/openid-connect-core-1_0.html", "description": "OIDC OAuth provider config\n@default {\"clientId\":\"\",\"clientSecret\":\"\",\"issuer\":\"\",\"args\":{}}",
"properties": {
"clientId": {
"type": "string"
},
"clientSecret": {
"type": "string"
},
"args": {
"type": "object"
}
},
"default": { "default": {
"clientId": "", "clientId": "",
"clientSecret": "", "clientSecret": "",
"issuer": "", "issuer": "",
"args": {} "args": {}
} }
},
"providers.apple": {
"type": "object",
"description": "Apple OAuth provider config\n@default {\"clientId\":\"\",\"clientSecret\":\"\"}\n@link https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js/implementing_sign_in_with_apple_in_your_app",
"properties": {
"clientId": {
"type": "string"
},
"clientSecret": {
"type": "string"
},
"args": {
"type": "object"
}
},
"default": {
"clientId": "",
"clientSecret": ""
}
} }
} }
}, },

View File

@@ -16,9 +16,6 @@ const {
REDIS_SERVER_HOST, REDIS_SERVER_HOST,
REDIS_SERVER_PASSWORD, REDIS_SERVER_PASSWORD,
STATIC_IP_NAME, STATIC_IP_NAME,
AFFINE_INDEXER_SEARCH_PROVIDER,
AFFINE_INDEXER_SEARCH_ENDPOINT,
AFFINE_INDEXER_SEARCH_API_KEY,
} = process.env; } = process.env;
const buildType = BUILD_TYPE || 'canary'; const buildType = BUILD_TYPE || 'canary';
@@ -84,11 +81,6 @@ const createHelmCommand = ({ isDryRun }) => {
`--set-string global.redis.password="${REDIS_SERVER_PASSWORD}"`, `--set-string global.redis.password="${REDIS_SERVER_PASSWORD}"`,
] ]
: []; : [];
const indexerOptions = [
`--set-string global.indexer.provider="${AFFINE_INDEXER_SEARCH_PROVIDER}"`,
`--set-string global.indexer.endpoint="${AFFINE_INDEXER_SEARCH_ENDPOINT}"`,
`--set-string global.indexer.apiKey="${AFFINE_INDEXER_SEARCH_API_KEY}"`,
];
const serviceAnnotations = [ const serviceAnnotations = [
`--set-json web.serviceAccount.annotations="{ \\"iam.gke.io/gcp-service-account\\": \\"${APP_IAM_ACCOUNT}\\" }"`, `--set-json web.serviceAccount.annotations="{ \\"iam.gke.io/gcp-service-account\\": \\"${APP_IAM_ACCOUNT}\\" }"`,
`--set-json graphql.serviceAccount.annotations="{ \\"iam.gke.io/gcp-service-account\\": \\"${APP_IAM_ACCOUNT}\\" }"`, `--set-json graphql.serviceAccount.annotations="{ \\"iam.gke.io/gcp-service-account\\": \\"${APP_IAM_ACCOUNT}\\" }"`,
@@ -138,7 +130,6 @@ const createHelmCommand = ({ isDryRun }) => {
`--set-string global.ingress.host="${host}"`, `--set-string global.ingress.host="${host}"`,
`--set-string global.version="${APP_VERSION}"`, `--set-string global.version="${APP_VERSION}"`,
...redisAndPostgres, ...redisAndPostgres,
...indexerOptions,
`--set web.replicaCount=${replica.web}`, `--set web.replicaCount=${replica.web}`,
`--set-string web.image.tag="${imageTag}"`, `--set-string web.image.tag="${imageTag}"`,
`--set graphql.replicaCount=${replica.graphql}`, `--set graphql.replicaCount=${replica.graphql}`,

View File

@@ -4,11 +4,6 @@ description: 'Prepare Server Test Environment'
runs: runs:
using: 'composite' using: 'composite'
steps: steps:
- name: Bundle @affine/reader
shell: bash
run: |
yarn affine @affine/reader build
- name: Initialize database - name: Initialize database
shell: bash shell: bash
run: | run: |
@@ -26,10 +21,13 @@ runs:
yarn affine @affine/server prisma generate yarn affine @affine/server prisma generate
yarn affine @affine/server prisma migrate deploy yarn affine @affine/server prisma migrate deploy
yarn affine @affine/server data-migration run yarn affine @affine/server data-migration run
- name: Import config - name: Import config
shell: bash shell: bash
env:
DEFAULT_CONFIG: '{}'
run: | run: |
printf '%s\n' "${SERVER_CONFIG:-$DEFAULT_CONFIG}" > ./packages/backend/server/config.json printf '{"copilot":{"enabled":true,"providers.fal":{"apiKey":"%s"},"providers.gemini":{"apiKey":"%s"},"providers.openai":{"apiKey":"%s"},"providers.perplexity":{"apiKey":"%s"},"providers.anthropic":{"apiKey":"%s"},"exa":{"key":"%s"}}}' \
"$COPILOT_FAL_API_KEY" \
"$COPILOT_GOOGLE_API_KEY" \
"$COPILOT_OPENAI_API_KEY" \
"$COPILOT_PERPLEXITY_API_KEY" \
"$COPILOT_ANTHROPIC_API_KEY" \
"$COPILOT_EXA_API_KEY" > ./packages/backend/server/config.json

View File

@@ -69,15 +69,6 @@ spec:
key: redis-password key: redis-password
- name: REDIS_SERVER_DATABASE - name: REDIS_SERVER_DATABASE
value: "{{ .Values.global.redis.database }}" value: "{{ .Values.global.redis.database }}"
- name: AFFINE_INDEXER_SEARCH_PROVIDER
value: "{{ .Values.global.indexer.provider }}"
- name: AFFINE_INDEXER_SEARCH_ENDPOINT
value: "{{ .Values.global.indexer.endpoint }}"
- name: AFFINE_INDEXER_SEARCH_API_KEY
valueFrom:
secretKeyRef:
name: indexer
key: indexer-apiKey
- name: AFFINE_SERVER_PORT - name: AFFINE_SERVER_PORT
value: "{{ .Values.global.docService.port }}" value: "{{ .Values.global.docService.port }}"
- name: AFFINE_SERVER_SUB_PATH - name: AFFINE_SERVER_SUB_PATH

View File

@@ -67,15 +67,6 @@ spec:
key: redis-password key: redis-password
- name: REDIS_SERVER_DATABASE - name: REDIS_SERVER_DATABASE
value: "{{ .Values.global.redis.database }}" value: "{{ .Values.global.redis.database }}"
- name: AFFINE_INDEXER_SEARCH_PROVIDER
value: "{{ .Values.global.indexer.provider }}"
- name: AFFINE_INDEXER_SEARCH_ENDPOINT
value: "{{ .Values.global.indexer.endpoint }}"
- name: AFFINE_INDEXER_SEARCH_API_KEY
valueFrom:
secretKeyRef:
name: indexer
key: indexer-apiKey
- name: AFFINE_SERVER_PORT - name: AFFINE_SERVER_PORT
value: "{{ .Values.service.port }}" value: "{{ .Values.service.port }}"
- name: AFFINE_SERVER_SUB_PATH - name: AFFINE_SERVER_SUB_PATH

View File

@@ -44,15 +44,6 @@ spec:
secretKeyRef: secretKeyRef:
name: redis name: redis
key: redis-password key: redis-password
- name: AFFINE_INDEXER_SEARCH_PROVIDER
value: "{{ .Values.global.indexer.provider }}"
- name: AFFINE_INDEXER_SEARCH_ENDPOINT
value: "{{ .Values.global.indexer.endpoint }}"
- name: AFFINE_INDEXER_SEARCH_API_KEY
valueFrom:
secretKeyRef:
name: indexer
key: indexer-apiKey
resources: resources:
requests: requests:
cpu: '100m' cpu: '100m'

View File

@@ -69,15 +69,6 @@ spec:
key: redis-password key: redis-password
- name: REDIS_SERVER_DATABASE - name: REDIS_SERVER_DATABASE
value: "{{ .Values.global.redis.database }}" value: "{{ .Values.global.redis.database }}"
- name: AFFINE_INDEXER_SEARCH_PROVIDER
value: "{{ .Values.global.indexer.provider }}"
- name: AFFINE_INDEXER_SEARCH_ENDPOINT
value: "{{ .Values.global.indexer.endpoint }}"
- name: AFFINE_INDEXER_SEARCH_API_KEY
valueFrom:
secretKeyRef:
name: indexer
key: indexer-apiKey
- name: AFFINE_SERVER_PORT - name: AFFINE_SERVER_PORT
value: "{{ .Values.service.port }}" value: "{{ .Values.service.port }}"
- name: AFFINE_SERVER_SUB_PATH - name: AFFINE_SERVER_SUB_PATH

View File

@@ -69,15 +69,6 @@ spec:
key: redis-password key: redis-password
- name: REDIS_SERVER_DATABASE - name: REDIS_SERVER_DATABASE
value: "{{ .Values.global.redis.database }}" value: "{{ .Values.global.redis.database }}"
- name: AFFINE_INDEXER_SEARCH_PROVIDER
value: "{{ .Values.global.indexer.provider }}"
- name: AFFINE_INDEXER_SEARCH_ENDPOINT
value: "{{ .Values.global.indexer.endpoint }}"
- name: AFFINE_INDEXER_SEARCH_API_KEY
valueFrom:
secretKeyRef:
name: indexer
key: indexer-apiKey
- name: AFFINE_SERVER_PORT - name: AFFINE_SERVER_PORT
value: "{{ .Values.service.port }}" value: "{{ .Values.service.port }}"
- name: AFFINE_SERVER_HOST - name: AFFINE_SERVER_HOST

View File

@@ -1,13 +0,0 @@
{{- if .Values.global.indexer.apiKey -}}
apiVersion: v1
kind: Secret
metadata:
name: indexer
annotations:
"helm.sh/hook": pre-install,pre-upgrade
"helm.sh/hook-weight": "-2"
"helm.sh/hook-delete-policy": before-hook-creation
type: Opaque
data:
indexer-apiKey: {{ .Values.global.indexer.apiKey | b64enc }}
{{- end }}

View File

@@ -1,13 +0,0 @@
{{- if eq .Values.global.deployment.platform "gcp" -}}
apiVersion: monitoring.googleapis.com/v1
kind: PodMonitoring
metadata:
name: "{{ .Release.Name }}-monitoring"
spec:
selector:
matchLabels:
app.kubernetes.io/instance: {{ .Release.Name }}
endpoints:
- port: 9464
interval: 30s
{{- end }}

View File

@@ -21,11 +21,6 @@ global:
username: '' username: ''
password: '' password: ''
database: 0 database: 0
indexer:
provider: ''
endpoint: ''
username: ''
password: ''
docService: docService:
name: 'affine-doc' name: 'affine-doc'
port: 3020 port: 3020

View File

@@ -113,7 +113,6 @@ jobs:
build-server-native: build-server-native:
name: Build Server native - ${{ matrix.targets.name }} name: Build Server native - ${{ matrix.targets.name }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
environment: ${{ github.event.inputs.flavor }}
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@@ -137,9 +136,6 @@ jobs:
extra-flags: workspaces focus @affine/server-native extra-flags: workspaces focus @affine/server-native
- name: Build Rust - name: Build Rust
uses: ./.github/actions/build-rust uses: ./.github/actions/build-rust
env:
AFFINE_PRO_PUBLIC_KEY: ${{ secrets.AFFINE_PRO_PUBLIC_KEY }}
AFFINE_PRO_LICENSE_AES_KEY: ${{ secrets.AFFINE_PRO_LICENSE_AES_KEY }}
with: with:
target: ${{ matrix.targets.name }} target: ${{ matrix.targets.name }}
package: '@affine/server-native' package: '@affine/server-native'
@@ -176,8 +172,6 @@ jobs:
path: ./packages/backend/native path: ./packages/backend/native
- name: List server-native files - name: List server-native files
run: ls -alh ./packages/backend/native run: ls -alh ./packages/backend/native
- name: Build @affine/reader
run: yarn workspace @affine/reader build
- name: Build Server - name: Build Server
run: yarn workspace @affine/server build run: yarn workspace @affine/server build
- name: Upload server dist - name: Upload server dist
@@ -259,9 +253,6 @@ jobs:
- name: Generate Prisma client - name: Generate Prisma client
run: yarn workspace @affine/server prisma generate run: yarn workspace @affine/server prisma generate
- name: Mv node_modules
run: mv ./node_modules ./packages/backend/server
- name: Setup Version - name: Setup Version
id: version id: version
uses: ./.github/actions/setup-version uses: ./.github/actions/setup-version

View File

@@ -20,7 +20,6 @@ env:
COVERAGE: true COVERAGE: true
MACOSX_DEPLOYMENT_TARGET: '10.13' MACOSX_DEPLOYMENT_TARGET: '10.13'
DEPLOYMENT_TYPE: affine DEPLOYMENT_TYPE: affine
AFFINE_INDEXER_ENABLED: true
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
@@ -126,7 +125,6 @@ jobs:
- name: Run BS Docs Build - name: Run BS Docs Build
run: | run: |
yarn affine bs-docs build yarn affine bs-docs build
git checkout packages/frontend/i18n/src/i18n-completenesses.json
git status --porcelain | grep . && { git status --porcelain | grep . && {
echo "Run 'yarn typecheck && yarn affine bs-docs build' and make sure all changes are submitted" echo "Run 'yarn typecheck && yarn affine bs-docs build' and make sure all changes are submitted"
exit 1 exit 1
@@ -152,15 +150,13 @@ jobs:
- name: Clippy - name: Clippy
run: | run: |
rustup component add clippy rustup component add clippy
cargo clippy --workspace --exclude affine_server_native --all-targets --all-features -- -D warnings cargo clippy --all-targets --all-features -- -D warnings
cargo clippy -p affine_server_native --all-targets --all-features -- -D warnings
check-git-status: check-git-status:
name: Check Git Status name: Check Git Status
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: needs:
- optimize_ci - optimize_ci
- build-server-native
if: needs.optimize_ci.outputs.skip == 'false' if: needs.optimize_ci.outputs.skip == 'false'
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -169,26 +165,13 @@ jobs:
with: with:
full-cache: true full-cache: true
- name: Download server-native.node
uses: actions/download-artifact@v4
with:
name: server-native.node
path: ./packages/backend/native
- name: Bundle @affine/reader
shell: bash
run: |
yarn workspace @affine/reader build
- name: Run Check - name: Run Check
run: | run: |
yarn affine init yarn affine init
yarn affine gql build yarn affine gql build
yarn affine i18n build yarn affine i18n build
yarn affine server genconfig
git checkout packages/frontend/i18n/src/i18n-completenesses.json
git status --porcelain | grep . && { git status --porcelain | grep . && {
echo "Run 'yarn affine init && yarn affine gql build && yarn affine i18n build && yarn affine server genconfig' and make sure all changes are submitted" echo "Run 'yarn affine init && yarn affine gql build && yarn affine i18n build' and make sure all changes are submitted"
exit 1 exit 1
} || { } || {
echo "All changes are submitted" echo "All changes are submitted"
@@ -559,86 +542,12 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
node_index: [0, 1, 2, 3, 4, 5, 6, 7] node_index: [0, 1, 2, 3]
total_nodes: [8] total_nodes: [4]
env: env:
NODE_ENV: test NODE_ENV: test
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
REDIS_SERVER_HOST: localhost REDIS_SERVER_HOST: localhost
services:
postgres:
image: pgvector/pgvector:pg16
env:
POSTGRES_PASSWORD: affine
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
redis:
image: redis
ports:
- 6379:6379
mailer:
image: mailhog/mailhog
ports:
- 1025:1025
- 8025:8025
indexer:
image: manticoresearch/manticore:9.3.2
ports:
- 9308:9308
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: ./.github/actions/setup-node
with:
electron-install: false
full-cache: true
- name: Download server-native.node
uses: actions/download-artifact@v4
with:
name: server-native.node
path: ./packages/backend/native
- name: Prepare Server Test Environment
uses: ./.github/actions/server-test-env
- name: Run server tests
run: yarn affine @affine/server test:coverage --forbid-only
env:
CARGO_TARGET_DIR: '${{ github.workspace }}/target'
CI_NODE_INDEX: ${{ matrix.node_index }}
CI_NODE_TOTAL: ${{ matrix.total_nodes }}
- name: Upload server test coverage results
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./packages/backend/server/.coverage/lcov.info
flags: server-test
name: affine
fail_ci_if_error: false
server-test-elasticsearch:
name: Server Test with Elasticsearch
runs-on: ubuntu-latest
needs:
- optimize_ci
- build-server-native
if: needs.optimize_ci.outputs.skip == 'false'
strategy:
fail-fast: false
env:
NODE_ENV: test
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
REDIS_SERVER_HOST: localhost
AFFINE_INDEXER_SEARCH_PROVIDER: elasticsearch
AFFINE_INDEXER_SEARCH_ENDPOINT: http://localhost:9200
services: services:
postgres: postgres:
image: pgvector/pgvector:pg16 image: pgvector/pgvector:pg16
@@ -661,20 +570,6 @@ jobs:
- 1025:1025 - 1025:1025
- 8025:8025 - 8025:8025
steps: steps:
# https://github.com/elastic/elastic-github-actions/blob/master/elasticsearch/README.md
- name: Configure sysctl limits for Elasticsearch
run: |
sudo swapoff -a
sudo sysctl -w vm.swappiness=1
sudo sysctl -w fs.file-max=262144
sudo sysctl -w vm.max_map_count=262144
- name: Runs Elasticsearch
uses: elastic/elastic-github-actions/elasticsearch@master
with:
stack-version: 9.0.1
security-enabled: false
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Setup Node.js - name: Setup Node.js
@@ -692,8 +587,8 @@ jobs:
- name: Prepare Server Test Environment - name: Prepare Server Test Environment
uses: ./.github/actions/server-test-env uses: ./.github/actions/server-test-env
- name: Run server tests with elasticsearch only - name: Run server tests
run: yarn affine @affine/server test:coverage "**/*/*elasticsearch.spec.ts" --forbid-only run: yarn affine @affine/server test:coverage --forbid-only
env: env:
CARGO_TARGET_DIR: '${{ github.workspace }}/target' CARGO_TARGET_DIR: '${{ github.workspace }}/target'
CI_NODE_INDEX: ${{ matrix.node_index }} CI_NODE_INDEX: ${{ matrix.node_index }}
@@ -736,10 +631,6 @@ jobs:
image: redis image: redis
ports: ports:
- 6379:6379 - 6379:6379
indexer:
image: manticoresearch/manticore:9.3.2
ports:
- 9308:9308
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -925,7 +816,7 @@ jobs:
uses: taiki-e/install-action@nextest uses: taiki-e/install-action@nextest
- name: Run tests - name: Run tests
run: cargo nextest run --workspace --exclude affine_server_native --features use-as-lib --release --no-fail-fast run: cargo nextest run --release --no-fail-fast
copilot-api-test: copilot-api-test:
name: Server Copilot Api Test name: Server Copilot Api Test
@@ -960,10 +851,6 @@ jobs:
ports: ports:
- 1025:1025 - 1025:1025
- 8025:8025 - 8025:8025
indexer:
image: manticoresearch/manticore:9.3.2
ports:
- 9308:9308
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -1003,7 +890,12 @@ jobs:
- name: Prepare Server Test Environment - 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.apifilter.outputs.changed == 'true' }}
env: env:
SERVER_CONFIG: ${{ secrets.TEST_SERVER_CONFIG }} COPILOT_OPENAI_API_KEY: ${{ secrets.COPILOT_OPENAI_API_KEY }}
COPILOT_GOOGLE_API_KEY: ${{ secrets.COPILOT_GOOGLE_API_KEY }}
COPILOT_FAL_API_KEY: ${{ secrets.COPILOT_FAL_API_KEY }}
COPILOT_PERPLEXITY_API_KEY: ${{ secrets.COPILOT_PERPLEXITY_API_KEY }}
COPILOT_ANTHROPIC_API_KEY: ${{ secrets.COPILOT_ANTHROPIC_API_KEY }}
COPILOT_EXA_API_KEY: ${{ secrets.COPILOT_EXA_API_KEY }}
uses: ./.github/actions/server-test-env uses: ./.github/actions/server-test-env
- name: Run server tests - name: Run server tests
@@ -1054,10 +946,6 @@ jobs:
image: redis image: redis
ports: ports:
- 6379:6379 - 6379:6379
indexer:
image: manticoresearch/manticore:9.3.2
ports:
- 9308:9308
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -1080,7 +968,6 @@ jobs:
- 'packages/backend/server/src/plugins/copilot/**' - 'packages/backend/server/src/plugins/copilot/**'
- 'packages/backend/server/tests/copilot.*' - 'packages/backend/server/tests/copilot.*'
- 'packages/frontend/core/src/blocksuite/ai/**' - 'packages/frontend/core/src/blocksuite/ai/**'
- 'packages/frontend/core/src/modules/workspace-indexer-embedding/**'
- 'tests/affine-cloud-copilot/**' - 'tests/affine-cloud-copilot/**'
- name: Setup Node.js - name: Setup Node.js
@@ -1102,7 +989,12 @@ jobs:
- name: Prepare Server Test Environment - name: Prepare Server Test Environment
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.e2efilter.outputs.changed == 'true' }} if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.e2efilter.outputs.changed == 'true' }}
env: env:
SERVER_CONFIG: ${{ secrets.TEST_SERVER_CONFIG }} COPILOT_OPENAI_API_KEY: ${{ secrets.COPILOT_OPENAI_API_KEY }}
COPILOT_GOOGLE_API_KEY: ${{ secrets.COPILOT_GOOGLE_API_KEY }}
COPILOT_FAL_API_KEY: ${{ secrets.COPILOT_FAL_API_KEY }}
COPILOT_PERPLEXITY_API_KEY: ${{ secrets.COPILOT_PERPLEXITY_API_KEY }}
COPILOT_ANTHROPIC_API_KEY: ${{ secrets.COPILOT_ANTHROPIC_API_KEY }}
COPILOT_EXA_API_KEY: ${{ secrets.COPILOT_EXA_API_KEY }}
uses: ./.github/actions/server-test-env uses: ./.github/actions/server-test-env
- name: Run Copilot E2E Test ${{ matrix.shardIndex }}/${{ matrix.shardTotal }} - name: Run Copilot E2E Test ${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
@@ -1176,10 +1068,6 @@ jobs:
ports: ports:
- 1025:1025 - 1025:1025
- 8025:8025 - 8025:8025
indexer:
image: manticoresearch/manticore:9.3.2
ports:
- 9308:9308
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -1349,13 +1237,6 @@ jobs:
target: x86_64-unknown-linux-gnu, target: x86_64-unknown-linux-gnu,
test: true, test: true,
} }
- {
os: windows-latest,
platform: windows,
arch: x64,
target: x86_64-pc-windows-msvc,
test: true,
}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Setup Node.js - name: Setup Node.js
@@ -1396,18 +1277,6 @@ jobs:
HOIST_NODE_MODULES: 1 HOIST_NODE_MODULES: 1
run: yarn affine @affine/electron package --platform=darwin --arch=arm64 run: yarn affine @affine/electron package --platform=darwin --arch=arm64
- name: Make Bundle (Windows)
if: ${{ matrix.spec.target == 'x86_64-pc-windows-msvc' }}
shell: bash
env:
SKIP_BUNDLE: true
SKIP_WEB_BUILD: true
HOIST_NODE_MODULES: 1
run: |
rm -rf packages/frontend/apps/electron/node_modules/@affine/nbstore/node_modules/@blocksuite/affine/node_modules
rm -rf packages/frontend/apps/electron/node_modules/@affine/native/node_modules
yarn affine @affine/electron package --platform=win32 --arch=x64
- name: Make Bundle (Linux) - name: Make Bundle (Linux)
run: | run: |
sudo add-apt-repository universe sudo add-apt-repository universe

View File

@@ -59,10 +59,6 @@ jobs:
ports: ports:
- 1025:1025 - 1025:1025
- 8025:8025 - 8025:8025
indexer:
image: manticoresearch/manticore:9.3.2
ports:
- 9308:9308
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -81,7 +77,12 @@ jobs:
- name: Prepare Server Test Environment - name: Prepare Server Test Environment
env: env:
SERVER_CONFIG: ${{ secrets.TEST_SERVER_CONFIG }} COPILOT_OPENAI_API_KEY: ${{ secrets.COPILOT_OPENAI_API_KEY }}
COPILOT_FAL_API_KEY: ${{ secrets.COPILOT_FAL_API_KEY }}
COPILOT_GOOGLE_API_KEY: ${{ secrets.COPILOT_GOOGLE_API_KEY }}
COPILOT_PERPLEXITY_API_KEY: ${{ secrets.COPILOT_PERPLEXITY_API_KEY }}
COPILOT_ANTHROPIC_API_KEY: ${{ secrets.COPILOT_ANTHROPIC_API_KEY }}
COPILOT_EXA_API_KEY: ${{ secrets.COPILOT_EXA_API_KEY }}
uses: ./.github/actions/server-test-env uses: ./.github/actions/server-test-env
- name: Run server tests - name: Run server tests
@@ -129,10 +130,6 @@ jobs:
image: redis image: redis
ports: ports:
- 6379:6379 - 6379:6379
indexer:
image: manticoresearch/manticore:9.3.2
ports:
- 9308:9308
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -151,7 +148,12 @@ jobs:
- name: Prepare Server Test Environment - name: Prepare Server Test Environment
env: env:
SERVER_CONFIG: ${{ secrets.TEST_SERVER_CONFIG }} COPILOT_OPENAI_API_KEY: ${{ secrets.COPILOT_OPENAI_API_KEY }}
COPILOT_FAL_API_KEY: ${{ secrets.COPILOT_FAL_API_KEY }}
COPILOT_GOOGLE_API_KEY: ${{ secrets.COPILOT_GOOGLE_API_KEY }}
COPILOT_PERPLEXITY_API_KEY: ${{ secrets.COPILOT_PERPLEXITY_API_KEY }}
COPILOT_ANTHROPIC_API_KEY: ${{ secrets.COPILOT_ANTHROPIC_API_KEY }}
COPILOT_EXA_API_KEY: ${{ secrets.COPILOT_EXA_API_KEY }}
uses: ./.github/actions/server-test-env uses: ./.github/actions/server-test-env
- name: Run Copilot E2E Test ${{ matrix.shardIndex }}/${{ matrix.shardTotal }} - name: Run Copilot E2E Test ${{ matrix.shardIndex }}/${{ matrix.shardTotal }}

View File

@@ -103,9 +103,6 @@ jobs:
CLOUD_SQL_IAM_ACCOUNT: ${{ secrets.CLOUD_SQL_IAM_ACCOUNT }} CLOUD_SQL_IAM_ACCOUNT: ${{ secrets.CLOUD_SQL_IAM_ACCOUNT }}
APP_IAM_ACCOUNT: ${{ secrets.APP_IAM_ACCOUNT }} APP_IAM_ACCOUNT: ${{ secrets.APP_IAM_ACCOUNT }}
STATIC_IP_NAME: ${{ secrets.STATIC_IP_NAME }} STATIC_IP_NAME: ${{ secrets.STATIC_IP_NAME }}
AFFINE_INDEXER_SEARCH_PROVIDER: ${{ secrets.AFFINE_INDEXER_SEARCH_PROVIDER }}
AFFINE_INDEXER_SEARCH_ENDPOINT: ${{ secrets.AFFINE_INDEXER_SEARCH_ENDPOINT }}
AFFINE_INDEXER_SEARCH_API_KEY: ${{ secrets.AFFINE_INDEXER_SEARCH_API_KEY }}
deploy-done: deploy-done:
needs: needs:
@@ -159,7 +156,7 @@ jobs:
BLOCKSUITE_REPO_PATH: ${{ github.workspace }}/blocksuite BLOCKSUITE_REPO_PATH: ${{ github.workspace }}/blocksuite
- name: Post Failed event to a Slack channel - name: Post Failed event to a Slack channel
id: failed-slack id: failed-slack
uses: slackapi/slack-github-action@v2.1.0 uses: slackapi/slack-github-action@v2.0.0
if: ${{ always() && contains(needs.*.result, 'failure') }} if: ${{ always() && contains(needs.*.result, 'failure') }}
with: with:
method: chat.postMessage method: chat.postMessage
@@ -174,7 +171,7 @@ jobs:
text: "<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|Backend deploy failed `${{ github.event.inputs.flavor }}`>" text: "<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|Backend deploy failed `${{ github.event.inputs.flavor }}`>"
- name: Post Cancel event to a Slack channel - name: Post Cancel event to a Slack channel
id: cancel-slack id: cancel-slack
uses: slackapi/slack-github-action@v2.1.0 uses: slackapi/slack-github-action@v2.0.0
if: ${{ always() && contains(needs.*.result, 'cancelled') && !contains(needs.*.result, 'failure') }} if: ${{ always() && contains(needs.*.result, 'cancelled') && !contains(needs.*.result, 'failure') }}
with: with:
token: ${{ secrets.SLACK_BOT_TOKEN }} token: ${{ secrets.SLACK_BOT_TOKEN }}

View File

@@ -252,7 +252,7 @@ jobs:
shell: bash shell: bash
# node_modules of nbstore is not needed for building, and it will make the build process out of memory # node_modules of nbstore is not needed for building, and it will make the build process out of memory
run: | run: |
rm -rf packages/frontend/apps/electron/node_modules/@affine/nbstore/node_modules/@blocksuite/affine/node_modules rm -rf packages/frontend/apps/electron/node_modules/@affine/nbstore/node_modules/@blocksuite
rm -rf packages/frontend/apps/electron/node_modules/@affine/native/node_modules rm -rf packages/frontend/apps/electron/node_modules/@affine/native/node_modules
- name: package - name: package

View File

@@ -117,10 +117,31 @@ jobs:
name: android name: android
path: packages/frontend/apps/android/dist path: packages/frontend/apps/android/dist
ios: determine-ios-runner:
runs-on: ${{ github.ref_name == 'canary' && 'macos-latest' || 'blaze/macos-14' }} runs-on: ubuntu-latest
needs: needs:
- build-ios-web - build-ios-web
outputs:
RUNNER: ${{ steps.runner.outputs.RUNNER }}
steps:
- name: Determine Runner
id: runner
# Randomly pick runner with 80% chance for blaze/macos-14 and 20% chance for namespace-profile-macos
# blaze/macos-14 is free but has limited concurrency
run: |
RANDOM_NUMBER=$(( $RANDOM % 100 + 1 ))
if [ $RANDOM_NUMBER -le 20 ]; then
echo "Selected namespace-profile-macos (20% probability)"
echo "RUNNER=namespace-profile-macos" >> $GITHUB_OUTPUT
else
echo "Selected blaze/macos-14 (80% probability)"
echo "RUNNER=blaze/macos-14" >> $GITHUB_OUTPUT
fi
ios:
runs-on: ${{ github.ref_name == 'canary' && 'macos-latest' || needs.determine-ios-runner.outputs.RUNNER }}
needs:
- determine-ios-runner
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Download mobile artifact - name: Download mobile artifact

2
.nvmrc
View File

@@ -1 +1 @@
22.16.0 22.15.0

View File

@@ -38,5 +38,3 @@ packages/frontend/apps/ios/App/**
tests/blocksuite/snapshots tests/blocksuite/snapshots
blocksuite/docs/api/** blocksuite/docs/api/**
packages/frontend/admin/src/config.json packages/frontend/admin/src/config.json
**/test-docs.json
**/test-blocks.json

966
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -47,9 +47,9 @@ log = "0.4"
loom = { version = "0.7", features = ["checkpoint"] } loom = { version = "0.7", features = ["checkpoint"] }
mimalloc = "0.1" mimalloc = "0.1"
nanoid = "0.4" nanoid = "0.4"
napi = { version = "3.0.0-beta.3", features = ["async", "chrono_date", "error_anyhow", "napi9", "serde"] } napi = { version = "3.0.0-alpha.31", features = ["async", "chrono_date", "error_anyhow", "napi9", "serde"] }
napi-build = { version = "2" } napi-build = { version = "2" }
napi-derive = { version = "3.0.0-beta.3" } napi-derive = { version = "3.0.0-alpha.28" }
nom = "8" nom = "8"
notify = { version = "8", features = ["serde"] } notify = { version = "8", features = ["serde"] }
objc2 = "0.6" objc2 = "0.6"
@@ -57,7 +57,7 @@ objc2-foundation = "0.3"
once_cell = "1" once_cell = "1"
ordered-float = "5" ordered-float = "5"
parking_lot = "0.12" parking_lot = "0.12"
path-ext = "0.1.2" path-ext = "0.1.1"
pdf-extract = { git = "https://github.com/toeverything/pdf-extract", branch = "darksky/improve-font-decoding" } pdf-extract = { git = "https://github.com/toeverything/pdf-extract", branch = "darksky/improve-font-decoding" }
phf = { version = "0.11", features = ["macros"] } phf = { version = "0.11", features = ["macros"] }
proptest = "1.3" proptest = "1.3"
@@ -77,12 +77,12 @@ smol_str = "0.3"
sqlx = { version = "0.8", default-features = false, features = ["chrono", "macros", "migrate", "runtime-tokio", "sqlite", "tls-rustls"] } sqlx = { version = "0.8", default-features = false, features = ["chrono", "macros", "migrate", "runtime-tokio", "sqlite", "tls-rustls"] }
strum_macros = "0.27.0" strum_macros = "0.27.0"
symphonia = { version = "0.5", features = ["all", "opt-simd"] } symphonia = { version = "0.5", features = ["all", "opt-simd"] }
text-splitter = "0.27" text-splitter = "0.25"
thiserror = "2" thiserror = "2"
tiktoken-rs = "0.7" tiktoken-rs = "0.6"
tokio = "1.45" tokio = "1.37"
tree-sitter = { version = "0.25" } tree-sitter = { version = "0.25" }
tree-sitter-c = { version = "0.24" } tree-sitter-c = { version = "0.23" }
tree-sitter-c-sharp = { version = "0.23" } tree-sitter-c-sharp = { version = "0.23" }
tree-sitter-cpp = { version = "0.23" } tree-sitter-cpp = { version = "0.23" }
tree-sitter-go = { version = "0.23" } tree-sitter-go = { version = "0.23" }

View File

@@ -43,7 +43,7 @@ _Special thanks to [Blaze](https://runblaze.dev) for their support of this proje
<a href="https://affine.pro/redirect/discord">Discord</a> | <a href="https://affine.pro/redirect/discord">Discord</a> |
<a href="https://app.affine.pro">Live Demo</a> | <a href="https://app.affine.pro">Live Demo</a> |
<a href="https://affine.pro/blog/">Blog</a> | <a href="https://affine.pro/blog/">Blog</a> |
<a href="https://docs.affine.pro/">Documentation</a> <a href="https://docs.affine.pro/docs/">Documentation</a>
</div> </div>
<br/> <br/>
@@ -89,7 +89,7 @@ Star us, and you will receive all release notifications from GitHub without any
**Self-host & Shape your own AFFiNE** **Self-host & Shape your own AFFiNE**
- You have the freedom to manage, self-host, fork and build your own AFFiNE. Plugin community and third-party blocks are coming soon. More tractions on [Blocksuite](https://blocksuite.io). Check there to learn how to [self-host AFFiNE](https://docs.affine.pro/self-host-affine). - You have the freedom to manage, self-host, fork and build your own AFFiNE. Plugin community and third-party blocks are coming soon. More tractions on [Blocksuite](https://blocksuite.io). Check there to learn how to [self-host AFFiNE](https://docs.affine.pro/docs/self-host-affine).
## Acknowledgement ## Acknowledgement
@@ -191,9 +191,7 @@ We would like to express our gratitude to all the individuals who have already c
## Self-Host ## Self-Host
Begin with Docker to deploy your own feature-rich, unrestricted version of AFFiNE. Our team is diligently updating to the latest version. For more information on how to self-host AFFiNE, please refer to our [documentation](https://docs.affine.pro/self-host-affine). Begin with Docker to deploy your own feature-rich, unrestricted version of AFFiNE. Our team is diligently updating to the latest version. For more information on how to self-host AFFiNE, please refer to our [documentation](https://docs.affine.pro/docs/self-host-affine).
[![Run on ClawCloud](https://raw.githubusercontent.com/ClawCloud/Run-Template/refs/heads/main/Run-on-ClawCloud.svg)](https://template.run.claw.cloud/?openapp=system-fastdeploy%3FtemplateName%3Daffine)
## Hiring ## Hiring

View File

@@ -33,7 +33,6 @@
"@blocksuite/affine-components": "workspace:*", "@blocksuite/affine-components": "workspace:*",
"@blocksuite/affine-ext-loader": "workspace:*", "@blocksuite/affine-ext-loader": "workspace:*",
"@blocksuite/affine-foundation": "workspace:*", "@blocksuite/affine-foundation": "workspace:*",
"@blocksuite/affine-fragment-adapter-panel": "workspace:*",
"@blocksuite/affine-fragment-doc-title": "workspace:*", "@blocksuite/affine-fragment-doc-title": "workspace:*",
"@blocksuite/affine-fragment-frame-panel": "workspace:*", "@blocksuite/affine-fragment-frame-panel": "workspace:*",
"@blocksuite/affine-fragment-outline": "workspace:*", "@blocksuite/affine-fragment-outline": "workspace:*",
@@ -59,14 +58,11 @@
"@blocksuite/affine-shared": "workspace:*", "@blocksuite/affine-shared": "workspace:*",
"@blocksuite/affine-widget-drag-handle": "workspace:*", "@blocksuite/affine-widget-drag-handle": "workspace:*",
"@blocksuite/affine-widget-edgeless-auto-connect": "workspace:*", "@blocksuite/affine-widget-edgeless-auto-connect": "workspace:*",
"@blocksuite/affine-widget-edgeless-dragging-area": "workspace:*",
"@blocksuite/affine-widget-edgeless-selected-rect": "workspace:*",
"@blocksuite/affine-widget-edgeless-toolbar": "workspace:*", "@blocksuite/affine-widget-edgeless-toolbar": "workspace:*",
"@blocksuite/affine-widget-edgeless-zoom-toolbar": "workspace:*", "@blocksuite/affine-widget-edgeless-zoom-toolbar": "workspace:*",
"@blocksuite/affine-widget-frame-title": "workspace:*", "@blocksuite/affine-widget-frame-title": "workspace:*",
"@blocksuite/affine-widget-keyboard-toolbar": "workspace:*", "@blocksuite/affine-widget-keyboard-toolbar": "workspace:*",
"@blocksuite/affine-widget-linked-doc": "workspace:*", "@blocksuite/affine-widget-linked-doc": "workspace:*",
"@blocksuite/affine-widget-note-slicer": "workspace:*",
"@blocksuite/affine-widget-page-dragging-area": "workspace:*", "@blocksuite/affine-widget-page-dragging-area": "workspace:*",
"@blocksuite/affine-widget-remote-selection": "workspace:*", "@blocksuite/affine-widget-remote-selection": "workspace:*",
"@blocksuite/affine-widget-scroll-anchoring": "workspace:*", "@blocksuite/affine-widget-scroll-anchoring": "workspace:*",
@@ -182,8 +178,6 @@
"./widgets/drag-handle/view": "./src/widgets/drag-handle/view.ts", "./widgets/drag-handle/view": "./src/widgets/drag-handle/view.ts",
"./widgets/edgeless-auto-connect": "./src/widgets/edgeless-auto-connect/index.ts", "./widgets/edgeless-auto-connect": "./src/widgets/edgeless-auto-connect/index.ts",
"./widgets/edgeless-auto-connect/view": "./src/widgets/edgeless-auto-connect/view.ts", "./widgets/edgeless-auto-connect/view": "./src/widgets/edgeless-auto-connect/view.ts",
"./widgets/edgeless-dragging-area": "./src/widgets/edgeless-dragging-area/index.ts",
"./widgets/edgeless-dragging-area/view": "./src/widgets/edgeless-dragging-area/view.ts",
"./widgets/edgeless-toolbar": "./src/widgets/edgeless-toolbar/index.ts", "./widgets/edgeless-toolbar": "./src/widgets/edgeless-toolbar/index.ts",
"./widgets/edgeless-toolbar/view": "./src/widgets/edgeless-toolbar/view.ts", "./widgets/edgeless-toolbar/view": "./src/widgets/edgeless-toolbar/view.ts",
"./widgets/frame-title": "./src/widgets/frame-title/index.ts", "./widgets/frame-title": "./src/widgets/frame-title/index.ts",
@@ -210,8 +204,6 @@
"./fragments/frame-panel/view": "./src/fragments/frame-panel/view.ts", "./fragments/frame-panel/view": "./src/fragments/frame-panel/view.ts",
"./fragments/outline": "./src/fragments/outline/index.ts", "./fragments/outline": "./src/fragments/outline/index.ts",
"./fragments/outline/view": "./src/fragments/outline/view.ts", "./fragments/outline/view": "./src/fragments/outline/view.ts",
"./fragments/adapter-panel": "./src/fragments/adapter-panel/index.ts",
"./fragments/adapter-panel/view": "./src/fragments/adapter-panel/view.ts",
"./gfx/text": "./src/gfx/text/index.ts", "./gfx/text": "./src/gfx/text/index.ts",
"./gfx/text/store": "./src/gfx/text/store.ts", "./gfx/text/store": "./src/gfx/text/store.ts",
"./gfx/text/view": "./src/gfx/text/view.ts", "./gfx/text/view": "./src/gfx/text/view.ts",
@@ -266,7 +258,6 @@
"./components/toolbar": "./src/components/toolbar.ts", "./components/toolbar": "./src/components/toolbar.ts",
"./components/view-dropdown-menu": "./src/components/view-dropdown-menu.ts", "./components/view-dropdown-menu": "./src/components/view-dropdown-menu.ts",
"./components/tooltip-content-with-shortcut": "./src/components/tooltip-content-with-shortcut.ts", "./components/tooltip-content-with-shortcut": "./src/components/tooltip-content-with-shortcut.ts",
"./components/resource": "./src/components/resource.ts",
"./rich-text": "./src/rich-text/index.ts", "./rich-text": "./src/rich-text/index.ts",
"./rich-text/effects": "./src/rich-text/effects.ts", "./rich-text/effects": "./src/rich-text/effects.ts",
"./shared/adapters": "./src/shared/adapters.ts", "./shared/adapters": "./src/shared/adapters.ts",
@@ -282,9 +273,7 @@
"./model": "./src/model/index.ts", "./model": "./src/model/index.ts",
"./sync": "./src/sync/index.ts", "./sync": "./src/sync/index.ts",
"./extensions/store": "./src/extensions/store.ts", "./extensions/store": "./src/extensions/store.ts",
"./extensions/view": "./src/extensions/view.ts", "./extensions/view": "./src/extensions/view.ts"
"./foundation/store": "./src/foundation/store.ts",
"./foundation/view": "./src/foundation/view.ts"
}, },
"files": [ "files": [
"src", "src",
@@ -295,7 +284,6 @@
"version": "0.21.0", "version": "0.21.0",
"devDependencies": { "devDependencies": {
"@vanilla-extract/vite-plugin": "^5.0.0", "@vanilla-extract/vite-plugin": "^5.0.0",
"msw": "^2.8.4", "vitest": "3.1.2"
"vitest": "3.1.3"
} }
} }

View File

@@ -2360,65 +2360,6 @@ describe('html to snapshot', () => {
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot); expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
}); });
test('should preserve space in p', async () => {
const html = template(
`<p>A <b>bold text</b> followed by a <i>italic text</i></p>`
);
const blockSnapshot: BlockSnapshot = {
type: 'block',
id: 'matchesReplaceMap[0]',
flavour: 'affine:note',
props: {
xywh: '[0,0,800,95]',
background: DefaultTheme.noteBackgrounColor,
index: 'a0',
hidden: false,
displayMode: NoteDisplayMode.DocAndEdgeless,
},
children: [
{
type: 'block',
id: 'matchesReplaceMap[1]',
flavour: 'affine:paragraph',
props: {
type: 'text',
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: 'A ',
},
{
insert: 'bold text',
attributes: {
bold: true,
},
},
{
insert: ' followed by a ',
},
{
insert: 'italic text',
attributes: {
italic: true,
},
},
],
},
},
children: [],
},
],
};
const htmlAdapter = new HtmlAdapter(createJob(), provider);
const rawBlockSnapshot = await htmlAdapter.toBlockSnapshot({
file: html,
});
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
});
test('span nested in p', async () => { test('span nested in p', async () => {
const html = template( const html = template(
`<p><span>aaa</span><span>bbb</span><span>ccc</span></p>` `<p><span>aaa</span><span>bbb</span><span>ccc</span></p>`
@@ -2697,335 +2638,4 @@ describe('html to snapshot', () => {
}); });
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot); expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
}); });
test('block level element in b should not be treated as inline', async () => {
const html = template(`<b><p><span>aaa</span></p></b>`);
const blockSnapshot: BlockSnapshot = {
type: 'block',
id: 'matchesReplaceMap[0]',
flavour: 'affine:note',
props: {
xywh: '[0,0,800,95]',
background: DefaultTheme.noteBackgrounColor,
index: 'a0',
hidden: false,
displayMode: NoteDisplayMode.DocAndEdgeless,
},
children: [
{
type: 'block',
id: 'matchesReplaceMap[1]',
flavour: 'affine:paragraph',
props: {
type: 'text',
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: 'aaa',
},
],
},
},
children: [],
},
],
};
const htmlAdapter = new HtmlAdapter(createJob(), provider);
const rawBlockSnapshot = await htmlAdapter.toBlockSnapshot({
file: html,
});
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
});
describe('strong element', () => {
test('should not be bold when font-weight is normal', async () => {
const html = template(`<span style="font-weight: normal;">aaa</span>`);
const blockSnapshot: BlockSnapshot = {
type: 'block',
id: 'matchesReplaceMap[0]',
flavour: 'affine:note',
props: {
xywh: '[0,0,800,95]',
background: DefaultTheme.noteBackgrounColor,
index: 'a0',
hidden: false,
displayMode: NoteDisplayMode.DocAndEdgeless,
},
children: [
{
type: 'block',
id: 'matchesReplaceMap[1]',
flavour: 'affine:paragraph',
props: {
type: 'text',
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: 'aaa',
},
],
},
},
children: [],
},
],
};
const htmlAdapter = new HtmlAdapter(createJob(), provider);
const rawBlockSnapshot = await htmlAdapter.toBlockSnapshot({
file: html,
});
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
});
test('should be bold when font-weight is bold or 500-900 ', async () => {
const html = template(
`<p><span style="font-weight: bold;">aaa</span><span style="font-weight: 100;">aaa</span><span style="font-weight: 500;">bbb</span><span style="font-weight: 200;">bbb</span><span style="font-weight: 600;">ccc</span><span style="font-weight: 300;">ccc</span><span style="font-weight: 700;">ddd</span></p>`
);
const blockSnapshot: BlockSnapshot = {
type: 'block',
id: 'matchesReplaceMap[0]',
flavour: 'affine:note',
props: {
xywh: '[0,0,800,95]',
background: DefaultTheme.noteBackgrounColor,
index: 'a0',
hidden: false,
displayMode: NoteDisplayMode.DocAndEdgeless,
},
children: [
{
type: 'block',
id: 'matchesReplaceMap[1]',
flavour: 'affine:paragraph',
props: {
type: 'text',
text: {
'$blocksuite:internal:text$': true,
delta: [
{
attributes: {
bold: true,
},
insert: 'aaa',
},
{
insert: 'aaa',
},
{
attributes: {
bold: true,
},
insert: 'bbb',
},
{
insert: 'bbb',
},
{
attributes: {
bold: true,
},
insert: 'ccc',
},
{
insert: 'ccc',
},
{
attributes: {
bold: true,
},
insert: 'ddd',
},
],
},
},
children: [],
},
],
};
const htmlAdapter = new HtmlAdapter(createJob(), provider);
const rawBlockSnapshot = await htmlAdapter.toBlockSnapshot({
file: html,
});
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
});
});
test('should be italic when tag is i or em or span with style font-style: italic', async () => {
const html = template(
`<p><i>aaa</i><span>aaa</span><em>bbb</em><span>bbb</span><span style="font-style: italic;">ccc</span></p>`
);
const blockSnapshot: BlockSnapshot = {
type: 'block',
id: 'matchesReplaceMap[0]',
flavour: 'affine:note',
props: {
xywh: '[0,0,800,95]',
background: DefaultTheme.noteBackgrounColor,
index: 'a0',
hidden: false,
displayMode: NoteDisplayMode.DocAndEdgeless,
},
children: [
{
type: 'block',
id: 'matchesReplaceMap[1]',
flavour: 'affine:paragraph',
props: {
type: 'text',
text: {
'$blocksuite:internal:text$': true,
delta: [
{
attributes: {
italic: true,
},
insert: 'aaa',
},
{
insert: 'aaa',
},
{
attributes: {
italic: true,
},
insert: 'bbb',
},
{
insert: 'bbb',
},
{
attributes: {
italic: true,
},
insert: 'ccc',
},
],
},
},
children: [],
},
],
};
const htmlAdapter = new HtmlAdapter(createJob(), provider);
const rawBlockSnapshot = await htmlAdapter.toBlockSnapshot({
file: html,
});
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
});
test('should be underline when tag is u or span with style text-decoration: underline', async () => {
const html = template(
`<p><u>aaa</u><span>aaa</span><span style="text-decoration: underline;">bbb</span></p>`
);
const blockSnapshot: BlockSnapshot = {
type: 'block',
id: 'matchesReplaceMap[0]',
flavour: 'affine:note',
props: {
xywh: '[0,0,800,95]',
background: DefaultTheme.noteBackgrounColor,
index: 'a0',
hidden: false,
displayMode: NoteDisplayMode.DocAndEdgeless,
},
children: [
{
type: 'block',
id: 'matchesReplaceMap[1]',
flavour: 'affine:paragraph',
props: {
type: 'text',
text: {
'$blocksuite:internal:text$': true,
delta: [
{
attributes: {
underline: true,
},
insert: 'aaa',
},
{
insert: 'aaa',
},
{
attributes: {
underline: true,
},
insert: 'bbb',
},
],
},
},
children: [],
},
],
};
const htmlAdapter = new HtmlAdapter(createJob(), provider);
const rawBlockSnapshot = await htmlAdapter.toBlockSnapshot({
file: html,
});
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
});
test('should be strike when tag is del or span with style text-decoration: line-through', async () => {
const html = template(
`<p><del>aaa</del><span>aaa</span><span style="text-decoration: line-through;">bbb</span></p>`
);
const blockSnapshot: BlockSnapshot = {
type: 'block',
id: 'matchesReplaceMap[0]',
flavour: 'affine:note',
props: {
xywh: '[0,0,800,95]',
background: DefaultTheme.noteBackgrounColor,
index: 'a0',
hidden: false,
displayMode: NoteDisplayMode.DocAndEdgeless,
},
children: [
{
type: 'block',
id: 'matchesReplaceMap[1]',
flavour: 'affine:paragraph',
props: {
type: 'text',
text: {
'$blocksuite:internal:text$': true,
delta: [
{
attributes: {
strike: true,
},
insert: 'aaa',
},
{
insert: 'aaa',
},
{
attributes: {
strike: true,
},
insert: 'bbb',
},
],
},
},
children: [],
},
],
};
const htmlAdapter = new HtmlAdapter(createJob(), provider);
const rawBlockSnapshot = await htmlAdapter.toBlockSnapshot({
file: html,
});
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
});
}); });

View File

@@ -4,9 +4,6 @@ import {
TableModelFlavour, TableModelFlavour,
} from '@blocksuite/affine-model'; } from '@blocksuite/affine-model';
import { import {
CalloutAdmonitionType,
CalloutExportStyle,
calloutMarkdownExportMiddleware,
embedSyncedDocMiddleware, embedSyncedDocMiddleware,
MarkdownAdapter, MarkdownAdapter,
} from '@blocksuite/affine-shared/adapters'; } from '@blocksuite/affine-shared/adapters';
@@ -2452,288 +2449,119 @@ World!
expect(target.file).toBe(markdown); expect(target.file).toBe(markdown);
}); });
describe('callout', () => { test('callout', async () => {
test('without export middleware', async () => { const blockSnapshot: BlockSnapshot = {
const blockSnapshot: BlockSnapshot = { type: 'block',
type: 'block', id: 'block:vu6SK6WJpW',
id: 'block:vu6SK6WJpW', flavour: 'affine:page',
flavour: 'affine:page', props: {
props: { title: {
title: { '$blocksuite:internal:text$': true,
'$blocksuite:internal:text$': true, delta: [],
delta: [],
},
}, },
children: [ },
{ children: [
type: 'block', {
id: 'block:Tk4gSPocAt', type: 'block',
flavour: 'affine:surface', id: 'block:Tk4gSPocAt',
props: { flavour: 'affine:surface',
elements: {}, props: {
}, elements: {},
children: [],
}, },
{ children: [],
type: 'block', },
id: 'block:WfnS5ZDCJT', {
flavour: 'affine:note', type: 'block',
props: { id: 'block:WfnS5ZDCJT',
xywh: '[0,0,800,95]', flavour: 'affine:note',
background: DefaultTheme.noteBackgrounColor, props: {
index: 'a0', xywh: '[0,0,800,95]',
hidden: false, background: DefaultTheme.noteBackgrounColor,
displayMode: NoteDisplayMode.DocAndEdgeless, index: 'a0',
}, hidden: false,
children: [ displayMode: NoteDisplayMode.DocAndEdgeless,
{
type: 'block',
id: 'block:8hOLxad5Fv',
flavour: 'affine:callout',
props: {
emoji: '💡',
},
children: [
{
type: 'block',
id: 'block:8hOLxad5Fv',
flavour: 'affine:paragraph',
props: {
type: 'text',
text: {
'$blocksuite:internal:text$': true,
delta: [{ insert: 'First callout' }],
},
},
children: [],
},
],
},
{
type: 'block',
id: 'block:8hOLxadvdv',
flavour: 'affine:callout',
props: {
emoji: '',
},
children: [
{
type: 'block',
id: 'block:8hOLxad5Fv',
flavour: 'affine:paragraph',
props: {
type: 'text',
text: {
'$blocksuite:internal:text$': true,
delta: [
{ insert: 'Warning second callout without emoji' },
],
},
},
children: [],
},
{
type: 'block',
id: 'block:8hOLxad5Fv',
flavour: 'affine:paragraph',
props: {
type: 'text',
text: {
'$blocksuite:internal:text$': true,
delta: [{ insert: 'Text in second callout' }],
},
},
children: [],
},
],
},
],
}, },
], children: [
}; {
type: 'block',
id: 'block:8hOLxad5Fv',
flavour: 'affine:callout',
props: {
emoji: '💡',
},
children: [
{
type: 'block',
id: 'block:8hOLxad5Fv',
flavour: 'affine:paragraph',
props: {
type: 'text',
text: {
'$blocksuite:internal:text$': true,
delta: [{ insert: 'First callout' }],
},
},
children: [],
},
],
},
{
type: 'block',
id: 'block:8hOLxadvdv',
flavour: 'affine:callout',
props: {
emoji: '',
},
children: [
{
type: 'block',
id: 'block:8hOLxad5Fv',
flavour: 'affine:paragraph',
props: {
type: 'text',
text: {
'$blocksuite:internal:text$': true,
delta: [{ insert: 'Second callout without emoji' }],
},
},
children: [],
},
],
},
{
type: 'block',
id: 'block:8hOLxbfdb',
flavour: 'affine:paragraph',
props: {
type: 'quote',
text: {
'$blocksuite:internal:text$': true,
delta: [{ insert: 'This is a regular blockquote' }],
},
},
children: [],
},
],
},
],
};
const markdown = `> \\[!💡] const markdown = `> \\[!💡]
> >
> First callout > First callout
> \\[!] > \\[!]
> >
> Warning second callout without emoji > Second callout without emoji
>
> Text in second callout > This is a regular blockquote
`; `;
const mdAdapter = new MarkdownAdapter(createJob(), provider); const mdAdapter = new MarkdownAdapter(createJob(), provider);
const target = await mdAdapter.fromBlockSnapshot({ const target = await mdAdapter.fromBlockSnapshot({
snapshot: blockSnapshot, snapshot: blockSnapshot,
});
expect(target.file).toBe(markdown);
});
test('with export middleware', async () => {
const blockSnapshot: BlockSnapshot = {
type: 'block',
id: 'block:vu6SK6WJpW',
flavour: 'affine:page',
props: {
title: {
'$blocksuite:internal:text$': true,
delta: [],
},
},
children: [
{
type: 'block',
id: 'block:Tk4gSPocAt',
flavour: 'affine:surface',
props: {
elements: {},
},
children: [],
},
{
type: 'block',
id: 'block:WfnS5ZDCJT',
flavour: 'affine:note',
props: {
xywh: '[0,0,800,95]',
background: DefaultTheme.noteBackgrounColor,
index: 'a0',
hidden: false,
displayMode: NoteDisplayMode.DocAndEdgeless,
},
children: [
{
type: 'block',
id: 'block:8hOLxad5Fv',
flavour: 'affine:callout',
props: {
emoji: '💡',
},
children: [
{
type: 'block',
id: 'block:8hOLxad5Fv',
flavour: 'affine:paragraph',
props: {
type: 'text',
text: {
'$blocksuite:internal:text$': true,
delta: [
{ insert: 'Callout that does not have a title' },
],
},
},
children: [],
},
],
},
{
type: 'block',
id: 'block:8hOLxadvdv',
flavour: 'affine:callout',
props: {
emoji: '',
},
children: [
{
type: 'block',
id: 'block:8hOLxad5Fv',
flavour: 'affine:paragraph',
props: {
type: 'text',
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert:
'Warning callout with custom title and multiple paragraphs',
},
],
},
},
children: [],
},
{
type: 'block',
id: 'block:8hOLxad5Fv',
flavour: 'affine:paragraph',
props: {
type: 'text',
text: {
'$blocksuite:internal:text$': true,
delta: [{ insert: 'Text in second callout' }],
},
},
children: [],
},
],
},
{
type: 'block',
id: 'block:8hOLxad5Fv',
flavour: 'affine:callout',
props: {
emoji: '💡',
},
children: [
{
type: 'block',
id: 'block:8hOLxad5Fv',
flavour: 'affine:paragraph',
props: {
type: 'text',
text: {
'$blocksuite:internal:text$': true,
delta: [
{ insert: 'details' },
{ insert: ' ' },
{ insert: '\nText in details callout with new line' },
],
},
},
children: [],
},
],
},
],
},
],
};
const markdown = `::: info
Callout that does not have a title
:::
::: warning callout with custom title and multiple paragraphs
Text in second callout
:::
::: details
Text in details callout with new line
:::
`;
const mdAdapter = new MarkdownAdapter(
createJob([
calloutMarkdownExportMiddleware({
style: CalloutExportStyle.Admonitions,
admonitionType: CalloutAdmonitionType.Info,
}),
]),
provider
);
const target = await mdAdapter.fromBlockSnapshot({
snapshot: blockSnapshot,
});
expect(target.file).toBe(markdown);
}); });
expect(target.file).toBe(markdown);
}); });
}); });
@@ -4393,61 +4221,6 @@ hhh
}, },
children: [], children: [],
}, },
{
type: 'block',
id: 'matchesReplaceMap[2]',
flavour: 'affine:paragraph',
props: {
type: 'h6',
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: 'Sources',
},
],
},
collapsed: true,
},
children: [],
},
{
type: 'block',
id: 'matchesReplaceMap[3]',
flavour: 'affine:bookmark',
props: {
style: 'citation',
url,
title,
description,
icon: favicon,
footnoteIdentifier: '1',
},
children: [],
},
{
type: 'block',
id: 'matchesReplaceMap[4]',
flavour: 'affine:embed-linked-doc',
props: {
style: 'citation',
pageId: 'deadbeef',
footnoteIdentifier: '2',
},
children: [],
},
{
type: 'block',
id: 'matchesReplaceMap[5]',
flavour: 'affine:attachment',
props: {
name: 'test.txt',
sourceId: 'abcdefg',
footnoteIdentifier: '3',
style: 'citation',
},
children: [],
},
], ],
}; };
@@ -4472,101 +4245,6 @@ hhh
}); });
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot); expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
}); });
test('should handle footnote reference with url prefix', async () => {
const blockSnapshot = {
type: 'block',
id: 'matchesReplaceMap[0]',
flavour: 'affine:note',
props: {
xywh: '[0,0,800,95]',
background: DefaultTheme.noteBackgrounColor,
index: 'a0',
hidden: false,
displayMode: NoteDisplayMode.DocAndEdgeless,
},
children: [
{
type: 'block',
id: 'matchesReplaceMap[1]',
flavour: 'affine:paragraph',
props: {
type: 'text',
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: 'https://example.com',
attributes: {
link: 'https://example.com',
},
},
{
insert: ' ',
},
{
insert: ' ',
attributes: {
footnote: {
label: '1',
reference: {
type: 'url',
url,
favicon,
title,
description,
},
},
},
},
],
},
},
children: [],
},
{
type: 'block',
id: 'matchesReplaceMap[2]',
flavour: 'affine:paragraph',
props: {
type: 'h6',
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: 'Sources',
},
],
},
collapsed: true,
},
children: [],
},
{
type: 'block',
id: 'matchesReplaceMap[3]',
flavour: 'affine:bookmark',
props: {
style: 'citation',
url,
title,
description,
icon: favicon,
footnoteIdentifier: '1',
},
children: [],
},
],
};
const markdown = `https://example.com[^1]\n\n[^1]: {"type":"url","url":"${url}","favicon":"${favicon}","title":"${title}","description":"${description}"}\n`;
const mdAdapter = new MarkdownAdapter(createJob(), provider);
const rawBlockSnapshot = await mdAdapter.toBlockSnapshot({
file: markdown,
});
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
});
}); });
test('should not wrap url with angle brackets if it is not a url', async () => { test('should not wrap url with angle brackets if it is not a url', async () => {

View File

@@ -1,14 +1,11 @@
import { DefaultTheme, NoteDisplayMode } from '@blocksuite/affine-model'; import { DefaultTheme, NoteDisplayMode } from '@blocksuite/affine-model';
import { NotionHtmlAdapter } from '@blocksuite/affine-shared/adapters'; import { NotionHtmlAdapter } from '@blocksuite/affine-shared/adapters';
import { DEFAULT_IMAGE_PROXY_ENDPOINT } from '@blocksuite/affine-shared/consts';
import { import {
AssetsManager, AssetsManager,
type BlockSnapshot, type BlockSnapshot,
MemoryBlobCRUD, MemoryBlobCRUD,
} from '@blocksuite/store'; } from '@blocksuite/store';
import { http, HttpResponse } from 'msw'; import { describe, expect, test } from 'vitest';
import { setupServer } from 'msw/node';
import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vitest';
import { createJob } from '../utils/create-job.js'; import { createJob } from '../utils/create-job.js';
import { getProvider } from '../utils/get-provider.js'; import { getProvider } from '../utils/get-provider.js';
@@ -1198,71 +1195,43 @@ describe('notion html to snapshot', () => {
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot); expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
}); });
describe('image', () => { test('image', async () => {
const originalUrl = const html = `<div class="page-body">
'https://raw.githubusercontent.com/toeverything/blocksuite/master/assets/logo.svg'; <figure id="ed3d2ae9-62f5-433a-9049-9ddbd1c81ac5" class="image"><a
href="https://raw.githubusercontent.com/toeverything/blocksuite/master/assets/logo.svg"><img src="https://raw.githubusercontent.com/toeverything/blocksuite/master/assets/logo.svg" /></a>
</figure>
</div>`;
const imageProxy = DEFAULT_IMAGE_PROXY_ENDPOINT; const blockSnapshot: BlockSnapshot = {
const imageUrl = `${imageProxy}?url=${encodeURIComponent(originalUrl)}`; type: 'block',
id: 'matchesReplaceMap[0]',
// Mock the image request flavour: 'affine:note',
const imageRequestHandlers = [ props: {
http.get(imageUrl.toString(), async () => { xywh: '[0,0,800,95]',
// Return a mock image blob background: DefaultTheme.noteBackgrounColor,
const mockImageBlob = new Blob(['mock image data'], { index: 'a0',
type: 'image/svg+xml', hidden: false,
}); displayMode: NoteDisplayMode.DocAndEdgeless,
return new HttpResponse(mockImageBlob, { },
headers: { children: [
'Content-Type': 'image/svg+xml', {
type: 'block',
id: 'matchesReplaceMap[1]',
flavour: 'affine:image',
props: {
sourceId: 'matchesReplaceMap[2]',
}, },
}); children: [],
}),
];
const server = setupServer(...imageRequestHandlers);
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterAll(() => server.close());
afterEach(() => server.resetHandlers());
test('network image resource', async () => {
const html = `<div class="page-body">
<figure id="ed3d2ae9-62f5-433a-9049-9ddbd1c81ac5" class="image"><a
href="${originalUrl}"><img src="${originalUrl}" /></a>
</figure>
</div>`;
const blockSnapshot: BlockSnapshot = {
type: 'block',
id: 'matchesReplaceMap[0]',
flavour: 'affine:note',
props: {
xywh: '[0,0,800,95]',
background: DefaultTheme.noteBackgrounColor,
index: 'a0',
hidden: false,
displayMode: NoteDisplayMode.DocAndEdgeless,
}, },
children: [ ],
{ };
type: 'block',
id: 'matchesReplaceMap[1]',
flavour: 'affine:image',
props: {
sourceId: 'matchesReplaceMap[2]',
},
children: [],
},
],
};
const adapter = new NotionHtmlAdapter(createJob(), provider); const adapter = new NotionHtmlAdapter(createJob(), provider);
const rawBlockSnapshot = await adapter.toBlockSnapshot({ const rawBlockSnapshot = await adapter.toBlockSnapshot({
file: html, file: html,
assets: new AssetsManager({ blob: new MemoryBlobCRUD() }), assets: new AssetsManager({ blob: new MemoryBlobCRUD() }),
});
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
}); });
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
}); });
test('bookmark', async () => { test('bookmark', async () => {

View File

@@ -106,65 +106,4 @@ describe('notion-text to snapshot', () => {
}); });
expect(nanoidReplacement(target!)).toEqual(sliceSnapshot); expect(nanoidReplacement(target!)).toEqual(sliceSnapshot);
}); });
test('notion text with empty styles array', () => {
const notionText =
'{"blockType":"text","editing":[["a "],["bold text",[["b"]]],[" hello world"]],"selection":{"startIndex":0,"endIndex":23},"action":"copy"}';
const sliceSnapshot: SliceSnapshot = {
type: 'slice',
content: [
{
type: 'block',
id: 'matchesReplaceMap[0]',
flavour: 'affine:note',
props: {
xywh: '[0,0,800,95]',
background: DefaultTheme.noteBackgrounColor,
index: 'a0',
hidden: false,
displayMode: 'both',
},
children: [
{
type: 'block',
id: 'matchesReplaceMap[1]',
flavour: 'affine:paragraph',
props: {
type: 'text',
text: {
'$blocksuite:internal:text$': true,
delta: [
{
insert: 'a ',
},
{
insert: 'bold text',
attributes: {
bold: true,
},
},
{
insert: ' hello world',
},
],
},
},
children: [],
},
],
},
],
workspaceId: '',
pageId: '',
};
const ntAdapter = new NotionTextAdapter(createJob(), provider);
const target = ntAdapter.toSliceSnapshot({
file: notionText,
workspaceId: '',
pageId: '',
});
expect(nanoidReplacement(target!)).toEqual(sliceSnapshot);
});
}); });

View File

@@ -1 +0,0 @@
export * from '@blocksuite/affine-components/resource';

View File

@@ -19,7 +19,6 @@ import { SurfaceViewExtension } from '@blocksuite/affine-block-surface/view';
import { SurfaceRefViewExtension } from '@blocksuite/affine-block-surface-ref/view'; import { SurfaceRefViewExtension } from '@blocksuite/affine-block-surface-ref/view';
import { TableViewExtension } from '@blocksuite/affine-block-table/view'; import { TableViewExtension } from '@blocksuite/affine-block-table/view';
import { FoundationViewExtension } from '@blocksuite/affine-foundation/view'; import { FoundationViewExtension } from '@blocksuite/affine-foundation/view';
import { AdapterPanelViewExtension } from '@blocksuite/affine-fragment-adapter-panel/view';
import { DocTitleViewExtension } from '@blocksuite/affine-fragment-doc-title/view'; import { DocTitleViewExtension } from '@blocksuite/affine-fragment-doc-title/view';
import { FramePanelViewExtension } from '@blocksuite/affine-fragment-frame-panel/view'; import { FramePanelViewExtension } from '@blocksuite/affine-fragment-frame-panel/view';
import { OutlineViewExtension } from '@blocksuite/affine-fragment-outline/view'; import { OutlineViewExtension } from '@blocksuite/affine-fragment-outline/view';
@@ -41,14 +40,11 @@ import { InlinePresetViewExtension } from '@blocksuite/affine-inline-preset/view
import { ReferenceViewExtension } from '@blocksuite/affine-inline-reference/view'; import { ReferenceViewExtension } from '@blocksuite/affine-inline-reference/view';
import { DragHandleViewExtension } from '@blocksuite/affine-widget-drag-handle/view'; import { DragHandleViewExtension } from '@blocksuite/affine-widget-drag-handle/view';
import { EdgelessAutoConnectViewExtension } from '@blocksuite/affine-widget-edgeless-auto-connect/view'; import { EdgelessAutoConnectViewExtension } from '@blocksuite/affine-widget-edgeless-auto-connect/view';
import { EdgelessDraggingAreaViewExtension } from '@blocksuite/affine-widget-edgeless-dragging-area/view';
import { EdgelessSelectedRectViewExtension } from '@blocksuite/affine-widget-edgeless-selected-rect/view';
import { EdgelessToolbarViewExtension } from '@blocksuite/affine-widget-edgeless-toolbar/view'; import { EdgelessToolbarViewExtension } from '@blocksuite/affine-widget-edgeless-toolbar/view';
import { EdgelessZoomToolbarViewExtension } from '@blocksuite/affine-widget-edgeless-zoom-toolbar/view'; import { EdgelessZoomToolbarViewExtension } from '@blocksuite/affine-widget-edgeless-zoom-toolbar/view';
import { FrameTitleViewExtension } from '@blocksuite/affine-widget-frame-title/view'; import { FrameTitleViewExtension } from '@blocksuite/affine-widget-frame-title/view';
import { KeyboardToolbarViewExtension } from '@blocksuite/affine-widget-keyboard-toolbar/view'; import { KeyboardToolbarViewExtension } from '@blocksuite/affine-widget-keyboard-toolbar/view';
import { LinkedDocViewExtension } from '@blocksuite/affine-widget-linked-doc/view'; import { LinkedDocViewExtension } from '@blocksuite/affine-widget-linked-doc/view';
import { NoteSlicerViewExtension } from '@blocksuite/affine-widget-note-slicer/view';
import { PageDraggingAreaViewExtension } from '@blocksuite/affine-widget-page-dragging-area/view'; import { PageDraggingAreaViewExtension } from '@blocksuite/affine-widget-page-dragging-area/view';
import { RemoteSelectionViewExtension } from '@blocksuite/affine-widget-remote-selection/view'; import { RemoteSelectionViewExtension } from '@blocksuite/affine-widget-remote-selection/view';
import { ScrollAnchoringViewExtension } from '@blocksuite/affine-widget-scroll-anchoring/view'; import { ScrollAnchoringViewExtension } from '@blocksuite/affine-widget-scroll-anchoring/view';
@@ -103,9 +99,9 @@ export function getInternalViewExtensions() {
InlinePresetViewExtension, InlinePresetViewExtension,
// Widget // Widget
// order will affect the z-index of the widget
DragHandleViewExtension, DragHandleViewExtension,
EdgelessAutoConnectViewExtension, EdgelessAutoConnectViewExtension,
EdgelessToolbarViewExtension,
FrameTitleViewExtension, FrameTitleViewExtension,
KeyboardToolbarViewExtension, KeyboardToolbarViewExtension,
LinkedDocViewExtension, LinkedDocViewExtension,
@@ -116,15 +112,10 @@ export function getInternalViewExtensions() {
ViewportOverlayViewExtension, ViewportOverlayViewExtension,
EdgelessZoomToolbarViewExtension, EdgelessZoomToolbarViewExtension,
PageDraggingAreaViewExtension, PageDraggingAreaViewExtension,
EdgelessSelectedRectViewExtension,
EdgelessDraggingAreaViewExtension,
NoteSlicerViewExtension,
EdgelessToolbarViewExtension,
// Fragment // Fragment
DocTitleViewExtension, DocTitleViewExtension,
FramePanelViewExtension, FramePanelViewExtension,
OutlineViewExtension, OutlineViewExtension,
AdapterPanelViewExtension,
]; ];
} }

View File

@@ -1 +0,0 @@
export * from '@blocksuite/affine-foundation/store';

View File

@@ -1 +0,0 @@
export * from '@blocksuite/affine-foundation/view';

View File

@@ -1 +0,0 @@
export * from '@blocksuite/affine-fragment-adapter-panel';

View File

@@ -1 +0,0 @@
export * from '@blocksuite/affine-fragment-adapter-panel/view';

View File

@@ -1 +0,0 @@
export * from '@blocksuite/affine-widget-edgeless-dragging-area';

View File

@@ -1 +0,0 @@
export * from '@blocksuite/affine-widget-edgeless-dragging-area/view';

View File

@@ -1 +0,0 @@
export * from '@blocksuite/affine-widget-edgeless-selected-rect';

View File

@@ -1 +0,0 @@
export * from '@blocksuite/affine-widget-edgeless-selected-rect/view';

View File

@@ -1 +0,0 @@
export * from '@blocksuite/affine-widget-note-slicer';

View File

@@ -1 +0,0 @@
export * from '@blocksuite/affine-widget-note-slicer/view';

View File

@@ -30,7 +30,6 @@
{ "path": "../components" }, { "path": "../components" },
{ "path": "../ext-loader" }, { "path": "../ext-loader" },
{ "path": "../foundation" }, { "path": "../foundation" },
{ "path": "../fragments/adapter-panel" },
{ "path": "../fragments/doc-title" }, { "path": "../fragments/doc-title" },
{ "path": "../fragments/frame-panel" }, { "path": "../fragments/frame-panel" },
{ "path": "../fragments/outline" }, { "path": "../fragments/outline" },
@@ -56,14 +55,11 @@
{ "path": "../shared" }, { "path": "../shared" },
{ "path": "../widgets/drag-handle" }, { "path": "../widgets/drag-handle" },
{ "path": "../widgets/edgeless-auto-connect" }, { "path": "../widgets/edgeless-auto-connect" },
{ "path": "../widgets/edgeless-dragging-area" },
{ "path": "../widgets/edgeless-selected-rect" },
{ "path": "../widgets/edgeless-toolbar" }, { "path": "../widgets/edgeless-toolbar" },
{ "path": "../widgets/edgeless-zoom-toolbar" }, { "path": "../widgets/edgeless-zoom-toolbar" },
{ "path": "../widgets/frame-title" }, { "path": "../widgets/frame-title" },
{ "path": "../widgets/keyboard-toolbar" }, { "path": "../widgets/keyboard-toolbar" },
{ "path": "../widgets/linked-doc" }, { "path": "../widgets/linked-doc" },
{ "path": "../widgets/note-slicer" },
{ "path": "../widgets/page-dragging-area" }, { "path": "../widgets/page-dragging-area" },
{ "path": "../widgets/remote-selection" }, { "path": "../widgets/remote-selection" },
{ "path": "../widgets/scroll-anchoring" }, { "path": "../widgets/scroll-anchoring" },

View File

@@ -20,11 +20,12 @@
"@blocksuite/icons": "^2.2.12", "@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*", "@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*", "@blocksuite/store": "workspace:*",
"@blocksuite/sync": "workspace:*",
"@floating-ui/dom": "^1.6.13", "@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2", "@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0", "@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15", "@toeverything/theme": "^1.1.14",
"file-type": "^21.0.0", "file-type": "^20.0.0",
"lit": "^3.2.0", "lit": "^3.2.0",
"minimatch": "^10.0.1", "minimatch": "^10.0.1",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
@@ -32,6 +33,7 @@
}, },
"exports": { "exports": {
".": "./src/index.ts", ".": "./src/index.ts",
"./effects": "./src/effects.ts",
"./store": "./src/store.ts", "./store": "./src/store.ts",
"./view": "./src/view.ts" "./view": "./src/view.ts"
}, },

View File

@@ -10,6 +10,7 @@ import {
isFootnoteDefinitionNode, isFootnoteDefinitionNode,
type MarkdownAST, type MarkdownAST,
} from '@blocksuite/affine-shared/adapters'; } from '@blocksuite/affine-shared/adapters';
import { FeatureFlagService } from '@blocksuite/affine-shared/services';
import { nanoid } from '@blocksuite/store'; import { nanoid } from '@blocksuite/store';
const isAttachmentFootnoteDefinitionNode = (node: MarkdownAST) => { const isAttachmentFootnoteDefinitionNode = (node: MarkdownAST) => {
@@ -35,7 +36,15 @@ export const attachmentBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher
fromMatch: o => o.node.flavour === AttachmentBlockSchema.model.flavour, fromMatch: o => o.node.flavour === AttachmentBlockSchema.model.flavour,
toBlockSnapshot: { toBlockSnapshot: {
enter: (o, context) => { enter: (o, context) => {
if (!isFootnoteDefinitionNode(o.node)) { const { provider } = context;
let enableCitation = false;
try {
const featureFlagService = provider?.get(FeatureFlagService);
enableCitation = !!featureFlagService?.getFlag('enable_citation');
} catch {
enableCitation = false;
}
if (!isFootnoteDefinitionNode(o.node) || !enableCitation) {
return; return;
} }
@@ -64,7 +73,6 @@ export const attachmentBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher
name: fileName, name: fileName,
sourceId: blobId, sourceId: blobId,
footnoteIdentifier, footnoteIdentifier,
style: 'citation',
}, },
children: [], children: [],
}, },

View File

@@ -4,25 +4,19 @@ import {
} from '@blocksuite/affine-components/caption'; } from '@blocksuite/affine-components/caption';
import { import {
getAttachmentFileIcon, getAttachmentFileIcon,
LoadingIcon, getLoadingIconWith,
} from '@blocksuite/affine-components/icons'; } from '@blocksuite/affine-components/icons';
import { Peekable } from '@blocksuite/affine-components/peek'; import { Peekable } from '@blocksuite/affine-components/peek';
import {
type ResolvedStateInfo,
ResourceController,
} from '@blocksuite/affine-components/resource';
import { toast } from '@blocksuite/affine-components/toast'; import { toast } from '@blocksuite/affine-components/toast';
import { import {
type AttachmentBlockModel, type AttachmentBlockModel,
AttachmentBlockStyles, AttachmentBlockStyles,
} from '@blocksuite/affine-model'; } from '@blocksuite/affine-model';
import { import {
CitationProvider,
DocModeProvider,
FileSizeLimitProvider, FileSizeLimitProvider,
TelemetryProvider, ThemeProvider,
} from '@blocksuite/affine-shared/services'; } from '@blocksuite/affine-shared/services';
import { formatSize } from '@blocksuite/affine-shared/utils'; import { humanFileSize } from '@blocksuite/affine-shared/utils';
import { import {
AttachmentIcon, AttachmentIcon,
ResetIcon, ResetIcon,
@@ -30,27 +24,25 @@ import {
WarningIcon, WarningIcon,
} from '@blocksuite/icons/lit'; } from '@blocksuite/icons/lit';
import { BlockSelection } from '@blocksuite/std'; import { BlockSelection } from '@blocksuite/std';
import { nanoid, Slice } from '@blocksuite/store'; import { Slice } from '@blocksuite/store';
import { computed, signal } from '@preact/signals-core'; import { type BlobState } from '@blocksuite/sync';
import { effect, signal } from '@preact/signals-core';
import { html, type TemplateResult } from 'lit'; import { html, type TemplateResult } from 'lit';
import { property } from 'lit/decorators.js';
import { choose } from 'lit/directives/choose.js'; import { choose } from 'lit/directives/choose.js';
import { type ClassInfo, classMap } from 'lit/directives/class-map.js'; import { type ClassInfo, classMap } from 'lit/directives/class-map.js';
import { guard } from 'lit/directives/guard.js';
import { styleMap } from 'lit/directives/style-map.js'; import { styleMap } from 'lit/directives/style-map.js';
import { when } from 'lit/directives/when.js'; import { when } from 'lit/directives/when.js';
import { filter } from 'rxjs/operators';
import { AttachmentEmbedProvider } from './embed'; import { AttachmentEmbedProvider } from './embed';
import { styles } from './styles'; import { styles } from './styles';
import { downloadAttachmentBlob, refreshData } from './utils'; import { downloadAttachmentBlob, refreshData } from './utils';
type AttachmentResolvedStateInfo = ResolvedStateInfo & { type State = 'loading' | 'uploading' | 'warning' | 'oversize' | 'none';
kind?: TemplateResult;
};
@Peekable({ @Peekable({
enableOn: ({ model }: AttachmentBlockComponent) => { enableOn: ({ model }: AttachmentBlockComponent) => {
return !model.store.readonly && model.props.type.endsWith('pdf'); return !model.doc.readonly && model.props.type.endsWith('pdf');
}, },
}) })
export class AttachmentBlockComponent extends CaptionedBlockComponent<AttachmentBlockModel> { export class AttachmentBlockComponent extends CaptionedBlockComponent<AttachmentBlockModel> {
@@ -58,18 +50,7 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
blockDraggable = true; blockDraggable = true;
resourceController = new ResourceController( blobState$ = signal<Partial<BlobState>>({});
computed(() => this.model.props.sourceId$.value)
);
get blobUrl() {
return this.resourceController.blobUrl$.value;
}
get filetype() {
const name = this.model.props.name$.value;
return name.split('.').pop() ?? '';
}
protected containerStyleMap = styleMap({ protected containerStyleMap = styleMap({
position: 'relative', position: 'relative',
@@ -81,12 +62,8 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
return this.std.get(FileSizeLimitProvider).maxFileSize; return this.std.get(FileSizeLimitProvider).maxFileSize;
} }
get citationService() {
return this.std.get(CitationProvider);
}
get isCitation() { get isCitation() {
return this.citationService.isCitationModel(this.model); return !!this.model.props.footnoteIdentifier;
} }
convertTo = () => { convertTo = () => {
@@ -96,7 +73,7 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
}; };
copy = () => { copy = () => {
const slice = Slice.fromModels(this.store, [this.model]); const slice = Slice.fromModels(this.doc, [this.model]);
this.std.clipboard.copySlice(slice).catch(console.error); this.std.clipboard.copySlice(slice).catch(console.error);
toast(this.host, 'Copied to clipboard'); toast(this.host, 'Copied to clipboard');
}; };
@@ -120,23 +97,33 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
window.open(blobUrl, '_blank'); window.open(blobUrl, '_blank');
}; };
// Refreshes data.
refreshData = () => { refreshData = () => {
refreshData(this).catch(console.error); refreshData(this.std, this).catch(console.error);
}; };
private readonly _refreshKey$ = signal<string | null>(null); updateBlobState(state: Partial<BlobState>) {
this.blobState$.value = { ...this.blobState$.value, ...state };
}
// Refreshes the embed component. determineState = (
reload = () => { downloading: boolean,
if (this.model.props.embed) { uploading: boolean,
this._refreshKey$.value = nanoid(); overSize: boolean,
return; error: boolean
} ): State => {
if (overSize) return 'oversize';
this.refreshData(); if (error) return 'warning';
if (uploading) return 'uploading';
if (downloading) return 'loading';
return 'none';
}; };
protected get embedView() {
return this.std
.get(AttachmentEmbedProvider)
.render(this.model, this.blobUrl ?? undefined, this._maxFileSize);
}
private _selectBlock() { private _selectBlock() {
const selectionManager = this.host.selection; const selectionManager = this.host.selection;
const blockSelection = selectionManager.create(BlockSelection, { const blockSelection = selectionManager.create(BlockSelection, {
@@ -145,59 +132,49 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
selectionManager.setGroup('note', [blockSelection]); selectionManager.setGroup('note', [blockSelection]);
} }
private readonly _trackCitationDeleteEvent = () => {
// Check citation delete event
this._disposables.add(
this.std.store.slots.blockUpdated
.pipe(
filter(payload => {
if (!payload.isLocal) return false;
const { flavour, id, type } = payload;
if (
type !== 'delete' ||
flavour !== this.model.flavour ||
id !== this.model.id
)
return false;
const { model } = payload;
if (!this.citationService.isCitationModel(model)) return false;
return true;
})
)
.subscribe(() => {
this.citationService.trackEvent('Delete');
})
);
};
override connectedCallback() { override connectedCallback() {
super.connectedCallback(); super.connectedCallback();
this.contentEditable = 'false'; this.contentEditable = 'false';
this.resourceController.setEngine(this.std.store.blobSync); this.refreshData();
this.disposables.add(this.resourceController.subscribe());
this.disposables.add(this.resourceController);
this.disposables.add( this.disposables.add(
this.model.props.sourceId$.subscribe(() => { effect(() => {
this.refreshData(); const blobId = this.model.props.sourceId$.value;
if (!blobId) return;
const blobState$ = this.std.store.blobSync.blobState$(blobId);
if (!blobState$) return;
const subscription = blobState$.subscribe(state => {
if (state.overSize || state.errorMessage) {
state.uploading = false;
state.downloading = false;
}
this.updateBlobState(state);
});
return () => subscription.unsubscribe();
}) })
); );
if (!this.model.props.style && !this.store.readonly) { if (!this.model.props.style && !this.doc.readonly) {
this.store.withoutTransact(() => { this.doc.withoutTransact(() => {
this.store.updateBlock(this.model, { this.doc.updateBlock(this.model, {
style: AttachmentBlockStyles[1], style: AttachmentBlockStyles[1],
}); });
}); });
} }
}
this._trackCitationDeleteEvent(); override disconnectedCallback() {
const blobUrl = this.blobUrl;
if (blobUrl) {
URL.revokeObjectURL(blobUrl);
}
super.disconnectedCallback();
} }
override firstUpdated() { override firstUpdated() {
@@ -229,22 +206,6 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
@click=${(event: MouseEvent) => { @click=${(event: MouseEvent) => {
event.stopPropagation(); event.stopPropagation();
onOverFileSize?.(); onOverFileSize?.();
{
const mode =
this.std.get(DocModeProvider).getEditorMode() ?? 'page';
const segment = mode === 'page' ? 'doc' : 'whiteboard';
this.std
.getOptional(TelemetryProvider)
?.track('AttachmentUpgradedEvent', {
segment,
page: `${segment} editor`,
module: 'attachment',
control: 'upgrade',
category: 'card',
type: this.model.props.name.split('.').pop() ?? '',
});
}
}} }}
> >
${UpgradeIcon()} Upgrade ${UpgradeIcon()} Upgrade
@@ -253,199 +214,150 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
); );
}; };
protected renderNormalButton = (needUpload: boolean) => { protected renderReloadButton = () => {
const label = needUpload ? 'retry' : 'reload';
const run = async () => {
if (needUpload) {
await this.resourceController.upload();
return;
}
this.refreshData();
};
return html` return html`
<button <button
class="affine-attachment-content-button" class="affine-attachment-content-button"
@click=${(event: MouseEvent) => { @click=${(event: MouseEvent) => {
event.stopPropagation(); event.stopPropagation();
run().catch(console.error); this.refreshData();
{
const mode =
this.std.get(DocModeProvider).getEditorMode() ?? 'page';
const segment = mode === 'page' ? 'doc' : 'whiteboard';
this.std
.getOptional(TelemetryProvider)
?.track('AttachmentReloadedEvent', {
segment,
page: `${segment} editor`,
module: 'attachment',
control: label,
category: 'card',
type: this.filetype,
});
}
}} }}
> >
${ResetIcon()} ${label} ${ResetIcon()} Reload
</button> </button>
`; `;
}; };
protected renderWithHorizontal( protected renderWithHorizontal(
classInfo: ClassInfo, classInfo: ClassInfo,
{ icon: TemplateResult,
icon, title: string,
title, description: string,
description, kind: TemplateResult,
kind, state: State
state,
needUpload,
}: AttachmentResolvedStateInfo
) { ) {
return html` return html`<div class=${classMap(classInfo)}>
<div class=${classMap(classInfo)}> <div class="affine-attachment-content">
<div class="affine-attachment-content"> <div class="affine-attachment-content-title">
<div class="affine-attachment-content-title"> <div class="affine-attachment-content-title-icon">${icon}</div>
<div class="affine-attachment-content-title-icon">${icon}</div>
<div class="affine-attachment-content-title-text truncate">
${title}
</div>
</div>
<div class="affine-attachment-content-description"> <div class="affine-attachment-content-title-text truncate">
<div class="affine-attachment-content-info truncate"> ${title}
${description}
</div>
${choose(state, [
['error', () => this.renderNormalButton(needUpload)],
['error:oversize', this.renderUpgradeButton],
])}
</div> </div>
</div> </div>
<div class="affine-attachment-banner">${kind}</div> <div class="affine-attachment-content-description">
<div class="affine-attachment-content-info truncate">
${description}
</div>
${choose(state, [
['oversize', this.renderUpgradeButton],
['warning', this.renderReloadButton],
])}
</div>
</div> </div>
`;
<div class="affine-attachment-banner">${kind}</div>
</div>`;
} }
protected renderWithVertical( protected renderWithVertical(
classInfo: ClassInfo, classInfo: ClassInfo,
{ icon: TemplateResult,
icon, title: string,
title, description: string,
description, kind: TemplateResult,
kind, state?: State
state,
needUpload,
}: AttachmentResolvedStateInfo
) { ) {
return html` return html`<div class=${classMap(classInfo)}>
<div class=${classMap(classInfo)}> <div class="affine-attachment-content">
<div class="affine-attachment-content"> <div class="affine-attachment-content-title">
<div class="affine-attachment-content-title"> <div class="affine-attachment-content-title-icon">${icon}</div>
<div class="affine-attachment-content-title-icon">${icon}</div>
<div class="affine-attachment-content-title-text truncate">
${title}
</div>
</div>
<div class="affine-attachment-content-info truncate"> <div class="affine-attachment-content-title-text truncate">
${description} ${title}
</div> </div>
</div> </div>
<div class="affine-attachment-banner"> <div class="affine-attachment-content-info truncate">
${kind} ${description}
${choose(state, [
['error', () => this.renderNormalButton(needUpload)],
['error:oversize', this.renderUpgradeButton],
])}
</div> </div>
</div> </div>
`;
<div class="affine-attachment-banner">
${kind}
${choose(state, [
['oversize', this.renderUpgradeButton],
['warning', this.renderReloadButton],
])}
</div>
</div>`;
} }
protected resolvedState$ = computed<AttachmentResolvedStateInfo>(() => { protected renderCard = () => {
const size = this.model.props.size; const { name, size, style } = this.model.props;
const name = this.model.props.name$.value; const cardStyle = style ?? AttachmentBlockStyles[1];
const kind = getAttachmentFileIcon(this.filetype);
const resolvedState = this.resourceController.resolveStateWith({ const theme = this.std.get(ThemeProvider).theme$.value;
loadingIcon: LoadingIcon(), const loadingIcon = getLoadingIconWith(theme);
errorIcon: WarningIcon(),
icon: AttachmentIcon(),
title: name,
description: formatSize(size),
});
return { ...resolvedState, kind }; const blobState = this.blobState$.value;
}); const {
uploading = false,
protected renderCardView = () => { downloading = false,
const resolvedState = this.resolvedState$.value; overSize = false,
const cardStyle = this.model.props.style$.value ?? AttachmentBlockStyles[1]; errorMessage,
} = blobState;
const warning = !overSize && Boolean(errorMessage);
const error = overSize || warning;
const state = this.determineState(downloading, uploading, overSize, error);
const loading = state === 'loading' || state === 'uploading';
const classInfo = { const classInfo = {
'affine-attachment-card': true, 'affine-attachment-card': true,
[cardStyle]: true, [cardStyle]: true,
loading: resolvedState.loading, error,
error: resolvedState.error, loading,
}; };
const icon = loading
? loadingIcon
: error
? WarningIcon()
: AttachmentIcon();
const title = uploading ? 'Uploading...' : loading ? 'Loading...' : name;
const description = errorMessage || humanFileSize(size);
const kind = getAttachmentFileIcon(name.split('.').pop() ?? '');
return when( return when(
cardStyle === 'cubeThick', cardStyle === 'cubeThick',
() => this.renderWithVertical(classInfo, resolvedState), () =>
() => this.renderWithHorizontal(classInfo, resolvedState) this.renderWithVertical(
classInfo,
icon,
title,
description,
kind,
state
),
() =>
this.renderWithHorizontal(
classInfo,
icon,
title,
description,
kind,
state
)
); );
}; };
protected renderEmbedView = () => {
const { model, blobUrl } = this;
if (!model.props.embed || !blobUrl) return null;
const { std, _maxFileSize } = this;
const provider = std.get(AttachmentEmbedProvider);
const render = provider.getRender(model, _maxFileSize);
if (!render) return null;
const enabled = provider.shouldShowStatus(model);
return html`
<div class="affine-attachment-embed-container">
${guard([this._refreshKey$.value], () => render(model, blobUrl))}
</div>
${when(enabled, () => {
const resolvedState = this.resolvedState$.value;
if (resolvedState.state !== 'error') return null;
// It should be an error messge.
const message = resolvedState.description;
if (!message) return null;
const needUpload = resolvedState.needUpload;
const action = () =>
needUpload ? this.resourceController.upload() : this.reload();
return html`
<affine-resource-status
class="affine-attachment-embed-status"
.message=${message}
.needUpload=${needUpload}
.action=${action}
></affine-resource-status>
`;
})}
`;
};
private readonly _renderCitation = () => { private readonly _renderCitation = () => {
const { name, footnoteIdentifier } = this.model.props; const { name, footnoteIdentifier } = this.model.props;
const icon = getAttachmentFileIcon(this.filetype); const fileType = name.split('.').pop() ?? '';
const fileTypeIcon = getAttachmentFileIcon(fileType);
return html`<affine-citation-card return html`<affine-citation-card
.icon=${icon} .icon=${fileTypeIcon}
.citationTitle=${name} .citationTitle=${name}
.citationIdentifier=${footnoteIdentifier} .citationIdentifier=${footnoteIdentifier}
.active=${this.selected$.value} .active=${this.selected$.value}
@@ -464,12 +376,23 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
${when( ${when(
this.isCitation, this.isCitation,
() => this._renderCitation(), () => this._renderCitation(),
() => this.renderEmbedView() ?? this.renderCardView() () =>
when(
this.embedView,
() =>
html`<div class="affine-attachment-embed-container">
${this.embedView}
</div>`,
this.renderCard
)
)} )}
</div> </div>
`; `;
} }
@property({ attribute: false })
accessor blobUrl: string | null = null;
override accessor selectedStyle = SelectedStyle.Border; override accessor selectedStyle = SelectedStyle.Border;
override accessor useCaptionEditor = true; override accessor useCaptionEditor = true;

View File

@@ -1,14 +1,10 @@
import { EdgelessLegacySlotIdentifier } from '@blocksuite/affine-block-surface'; import { EdgelessLegacySlotIdentifier } from '@blocksuite/affine-block-surface';
import { import { AttachmentBlockStyles } from '@blocksuite/affine-model';
AttachmentBlockSchema,
AttachmentBlockStyles,
} from '@blocksuite/affine-model';
import { import {
EMBED_CARD_HEIGHT, EMBED_CARD_HEIGHT,
EMBED_CARD_WIDTH, EMBED_CARD_WIDTH,
} from '@blocksuite/affine-shared/consts'; } from '@blocksuite/affine-shared/consts';
import { toGfxBlockComponent } from '@blocksuite/std'; import { toGfxBlockComponent } from '@blocksuite/std';
import { GfxViewInteractionExtension } from '@blocksuite/std/gfx';
import { styleMap } from 'lit/directives/style-map.js'; import { styleMap } from 'lit/directives/style-map.js';
import { AttachmentBlockComponent } from './attachment-block.js'; import { AttachmentBlockComponent } from './attachment-block.js';
@@ -52,21 +48,3 @@ declare global {
'affine-edgeless-attachment': AttachmentEdgelessBlockComponent; 'affine-edgeless-attachment': AttachmentEdgelessBlockComponent;
} }
} }
export const AttachmentBlockInteraction = GfxViewInteractionExtension(
AttachmentBlockSchema.model.flavour,
{
resizeConstraint: {
lockRatio: true,
},
handleRotate: () => {
return {
beforeRotate: context => {
context.set({
rotatable: false,
});
},
};
},
}
);

View File

@@ -0,0 +1,31 @@
import { AttachmentBlockSchema } from '@blocksuite/affine-model';
import { SlashMenuConfigExtension } from '@blocksuite/affine-widget-slash-menu';
import { BlockViewExtension, FlavourExtension } from '@blocksuite/std';
import type { ExtensionType } from '@blocksuite/store';
import { literal } from 'lit/static-html.js';
import { AttachmentBlockAdapterExtensions } from './adapters/extension.js';
import { AttachmentDropOption } from './attachment-service.js';
import { attachmentSlashMenuConfig } from './configs/slash-menu.js';
import { createBuiltinToolbarConfigExtension } from './configs/toolbar';
import {
AttachmentEmbedConfigExtension,
AttachmentEmbedService,
} from './embed';
const flavour = AttachmentBlockSchema.model.flavour;
export const AttachmentBlockSpec: ExtensionType[] = [
FlavourExtension(flavour),
BlockViewExtension(flavour, model => {
return model.parent?.flavour === 'affine:surface'
? literal`affine-edgeless-attachment`
: literal`affine-attachment`;
}),
AttachmentDropOption,
AttachmentEmbedConfigExtension(),
AttachmentEmbedService,
AttachmentBlockAdapterExtensions,
createBuiltinToolbarConfigExtension(flavour),
SlashMenuConfigExtension(flavour, attachmentSlashMenuConfig),
].flat();

View File

@@ -1,7 +1,6 @@
import { ConfirmIcon } from '@blocksuite/affine-components/icons'; import { ConfirmIcon } from '@blocksuite/affine-components/icons';
import { toast } from '@blocksuite/affine-components/toast'; import { toast } from '@blocksuite/affine-components/toast';
import type { AttachmentBlockModel } from '@blocksuite/affine-model'; import type { AttachmentBlockModel } from '@blocksuite/affine-model';
import { CitationProvider } from '@blocksuite/affine-shared/services';
import type { EditorHost } from '@blocksuite/std'; import type { EditorHost } from '@blocksuite/std';
import { html } from 'lit'; import { html } from 'lit';
import { createRef, ref } from 'lit/directives/ref.js'; import { createRef, ref } from 'lit/directives/ref.js';
@@ -34,7 +33,6 @@ export const RenameModal = ({
let fileName = includeExtension ? nameWithoutExtension : originalName; let fileName = includeExtension ? nameWithoutExtension : originalName;
const extension = includeExtension ? originalExtension : ''; const extension = includeExtension ? originalExtension : '';
const citationService = editorHost.std.get(CitationProvider);
const abort = () => abortController.abort(); const abort = () => abortController.abort();
const onConfirm = () => { const onConfirm = () => {
@@ -43,12 +41,9 @@ export const RenameModal = ({
toast(editorHost, 'File name cannot be empty'); toast(editorHost, 'File name cannot be empty');
return; return;
} }
model.store.updateBlock(model, { model.doc.updateBlock(model, {
name: newFileName, name: newFileName,
}); });
if (citationService.isCitationModel(model)) {
citationService.trackEvent('Edit');
}
abort(); abort();
}; };
const onInput = (e: InputEvent) => { const onInput = (e: InputEvent) => {

View File

@@ -1,4 +1,4 @@
import { openSingleFileWith } from '@blocksuite/affine-shared/utils'; import { openFileOrFiles } from '@blocksuite/affine-shared/utils';
import { type SlashMenuConfig } from '@blocksuite/affine-widget-slash-menu'; import { type SlashMenuConfig } from '@blocksuite/affine-widget-slash-menu';
import { ExportToPdfIcon, FileIcon } from '@blocksuite/icons/lit'; import { ExportToPdfIcon, FileIcon } from '@blocksuite/icons/lit';
@@ -18,10 +18,10 @@ export const attachmentSlashMenuConfig: SlashMenuConfig = {
searchAlias: ['file'], searchAlias: ['file'],
group: '4_Content & Media@3', group: '4_Content & Media@3',
when: ({ model }) => when: ({ model }) =>
model.store.schema.flavourSchemaMap.has('affine:attachment'), model.doc.schema.flavourSchemaMap.has('affine:attachment'),
action: ({ std, model }) => { action: ({ std, model }) => {
(async () => { (async () => {
const file = await openSingleFileWith(); const file = await openFileOrFiles();
if (!file) return; if (!file) return;
await addSiblingAttachmentBlocks(std, [file], model); await addSiblingAttachmentBlocks(std, [file], model);
@@ -41,10 +41,10 @@ export const attachmentSlashMenuConfig: SlashMenuConfig = {
}, },
group: '4_Content & Media@4', group: '4_Content & Media@4',
when: ({ model }) => when: ({ model }) =>
model.store.schema.flavourSchemaMap.has('affine:attachment'), model.doc.schema.flavourSchemaMap.has('affine:attachment'),
action: ({ std, model }) => { action: ({ std, model }) => {
(async () => { (async () => {
const file = await openSingleFileWith(); const file = await openFileOrFiles();
if (!file) return; if (!file) return;
await addSiblingAttachmentBlocks(std, [file], model); await addSiblingAttachmentBlocks(std, [file], model);

View File

@@ -77,19 +77,13 @@ export const attachmentViewDropdownMenu = {
const model = ctx.getCurrentModelByType(AttachmentBlockModel); const model = ctx.getCurrentModelByType(AttachmentBlockModel);
if (!model) return; if (!model) return;
const provider = ctx.std.get(AttachmentEmbedProvider); if (!ctx.hasSelectedSurfaceModels) {
// TODO(@fundon): should auto focus image block.
if (
provider.shouldBeConverted(model) &&
!ctx.hasSelectedSurfaceModels
) {
// Clears // Clears
ctx.reset(); ctx.reset();
ctx.select('note'); ctx.select('note');
} }
provider.convertTo(model); ctx.std.get(AttachmentEmbedProvider).convertTo(model);
ctx.track('SelectedView', { ctx.track('SelectedView', {
...trackBaseProps, ...trackBaseProps,
@@ -100,32 +94,18 @@ export const attachmentViewDropdownMenu = {
}, },
], ],
content(ctx) { content(ctx) {
const block = ctx.getCurrentBlockByType(AttachmentBlockComponent); const model = ctx.getCurrentModelByType(AttachmentBlockModel);
if (!block) return null; if (!model) return null;
const model = block.model;
const embedProvider = ctx.std.get(AttachmentEmbedProvider); const embedProvider = ctx.std.get(AttachmentEmbedProvider);
const actions = computed(() => { const actions = this.actions.map(action => ({ ...action }));
const [cardAction, embedAction] = this.actions.map(action => ({ const viewType$ = computed(() => {
...action, const [cardAction, embedAction] = actions;
}));
const ok = block.resourceController.resolvedState$.value.state === 'none';
const sourceId = Boolean(model.props.sourceId$.value);
const embed = model.props.embed$.value ?? false; const embed = model.props.embed$.value ?? false;
// 1. Check whether `sourceId` exists.
// 2. Check if `embedded` is allowed.
// 3. Check `blobState$`
const allowed = ok && sourceId && embedProvider.embedded(model) && !embed;
cardAction.disabled = !embed; cardAction.disabled = !embed;
embedAction.disabled = !allowed; embedAction.disabled = embed && embedProvider.embedded(model);
return [cardAction, embedAction];
});
const viewType$ = computed(() => {
const [cardAction, embedAction] = actions.value;
const embed = model.props.embed$.value ?? false;
return embed ? embedAction.label : cardAction.label; return embed ? embedAction.label : cardAction.label;
}); });
const onToggle = (e: CustomEvent<boolean>) => { const onToggle = (e: CustomEvent<boolean>) => {
@@ -143,7 +123,7 @@ export const attachmentViewDropdownMenu = {
model, model,
html`<affine-view-dropdown-menu html`<affine-view-dropdown-menu
@toggle=${onToggle} @toggle=${onToggle}
.actions=${actions.value} .actions=${actions}
.context=${ctx} .context=${ctx}
.viewType$=${viewType$} .viewType$=${viewType$}
></affine-view-dropdown-menu>` ></affine-view-dropdown-menu>`
@@ -263,13 +243,7 @@ const builtinToolbarConfig = {
icon: ResetIcon(), icon: ResetIcon(),
run(ctx) { run(ctx) {
const block = ctx.getCurrentBlockByType(AttachmentBlockComponent); const block = ctx.getCurrentBlockByType(AttachmentBlockComponent);
block?.reload(); block?.refreshData();
ctx.track('AttachmentReloadedEvent', {
...trackBaseProps,
control: 'reload',
type: block?.model.props.name.split('.').pop() ?? '',
});
}, },
}, },
{ {

View File

@@ -3,10 +3,6 @@ import {
type ImageBlockProps, type ImageBlockProps,
MAX_IMAGE_WIDTH, MAX_IMAGE_WIDTH,
} from '@blocksuite/affine-model'; } from '@blocksuite/affine-model';
import {
EMBED_CARD_HEIGHT,
EMBED_CARD_WIDTH,
} from '@blocksuite/affine-shared/consts';
import { FileSizeLimitProvider } from '@blocksuite/affine-shared/services'; import { FileSizeLimitProvider } from '@blocksuite/affine-shared/services';
import { import {
readImageSize, readImageSize,
@@ -21,7 +17,6 @@ import type { ExtensionType } from '@blocksuite/store';
import { Extension } from '@blocksuite/store'; import { Extension } from '@blocksuite/store';
import type { TemplateResult } from 'lit'; import type { TemplateResult } from 'lit';
import { html } from 'lit'; import { html } from 'lit';
import { styleMap } from 'lit/directives/style-map.js';
import { getAttachmentBlob } from './utils'; import { getAttachmentBlob } from './utils';
@@ -39,22 +34,9 @@ export type AttachmentEmbedConfig = {
std: BlockStdScope std: BlockStdScope
) => Promise<void> | void; ) => Promise<void> | void;
/** /**
* Renders the embed view. * The template will be used to render the embed view.
*/ */
render?: ( template?: (model: AttachmentBlockModel, blobUrl: string) => TemplateResult;
model: AttachmentBlockModel,
blobUrl: string
) => TemplateResult | null;
/**
* Should show status when turned on.
*/
shouldShowStatus?: boolean;
/**
* Should block type conversion be required.
*/
shouldBeConverted?: boolean;
}; };
// Single embed config. // Single embed config.
@@ -115,53 +97,39 @@ export class AttachmentEmbedService extends Extension {
// Converts to embed view. // Converts to embed view.
convertTo(model: AttachmentBlockModel, maxFileSize = this._maxFileSize) { convertTo(model: AttachmentBlockModel, maxFileSize = this._maxFileSize) {
const config = this.values.find(config => config.check(model, maxFileSize)); const config = this.values.find(config => config.check(model, maxFileSize));
if (!config?.action) {
if (config?.action) { model.doc.updateBlock(model, { embed: true });
config.action(model, this.std)?.catch(console.error);
return; return;
} }
config.action(model, this.std)?.catch(console.error);
model.store.updateBlock(model, { embed: true });
} }
embedded(model: AttachmentBlockModel, maxFileSize = this._maxFileSize) { embedded(model: AttachmentBlockModel, maxFileSize = this._maxFileSize) {
return this.values.some(config => config.check(model, maxFileSize)); return this.values.some(config => config.check(model, maxFileSize));
} }
getRender(model: AttachmentBlockModel, maxFileSize = this._maxFileSize) { render(
return (
this.values.find(config => config.check(model, maxFileSize))?.render ??
null
);
}
shouldShowStatus(
model: AttachmentBlockModel, model: AttachmentBlockModel,
blobUrl?: string,
maxFileSize = this._maxFileSize maxFileSize = this._maxFileSize
) { ) {
return ( if (!model.props.embed || !blobUrl) return;
this.values.find(config => config.check(model, maxFileSize))
?.shouldShowStatus ?? false
);
}
shouldBeConverted( const config = this.values.find(config => config.check(model, maxFileSize));
model: AttachmentBlockModel, if (!config || !config.template) {
maxFileSize = this._maxFileSize console.error('No embed view template found!', model, model.props.type);
) { return;
return ( }
this.values.find(config => config.check(model, maxFileSize))
?.shouldBeConverted ?? false return config.template(model, blobUrl);
);
} }
} }
const embedConfig: AttachmentEmbedConfig[] = [ const embedConfig: AttachmentEmbedConfig[] = [
{ {
name: 'image', name: 'image',
shouldBeConverted: true,
check: model => check: model =>
model.store.schema.flavourSchemaMap.has('affine:image') && model.doc.schema.flavourSchemaMap.has('affine:image') &&
model.props.type.startsWith('image/'), model.props.type.startsWith('image/'),
async action(model, std) { async action(model, std) {
const component = std.view.getBlock(model.id); const component = std.view.getBlock(model.id);
@@ -172,30 +140,16 @@ const embedConfig: AttachmentEmbedConfig[] = [
}, },
{ {
name: 'pdf', name: 'pdf',
shouldShowStatus: true,
check: (model, maxFileSize) => check: (model, maxFileSize) =>
model.props.type === 'application/pdf' && model.props.size <= maxFileSize, model.props.type === 'application/pdf' && model.props.size <= maxFileSize,
action: model => { template: (_, blobUrl) => {
const bound = Bound.deserialize(model.props.xywh);
bound.w = EMBED_CARD_WIDTH.pdf;
bound.h = EMBED_CARD_HEIGHT.pdf;
model.store.updateBlock(model, {
embed: true,
style: 'pdf',
xywh: bound.serialize(),
});
},
render: (_, blobUrl) => {
// More options: https://tinytip.co/tips/html-pdf-params/ // 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 // 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'; const parameters = '#toolbar=0';
return html` return html`
<iframe <iframe
style=${styleMap({ style="width: 100%; color-scheme: auto;"
width: '100%', height="480"
minHeight: '480px',
colorScheme: 'auto',
})}
src=${blobUrl + parameters} src=${blobUrl + parameters}
loading="lazy" loading="lazy"
scrolling="no" scrolling="no"
@@ -203,7 +157,6 @@ const embedConfig: AttachmentEmbedConfig[] = [
allowTransparency allowTransparency
allowfullscreen allowfullscreen
type="application/pdf" type="application/pdf"
credentialless
></iframe> ></iframe>
<div class="affine-attachment-embed-event-mask"></div> <div class="affine-attachment-embed-event-mask"></div>
`; `;
@@ -211,44 +164,23 @@ const embedConfig: AttachmentEmbedConfig[] = [
}, },
{ {
name: 'video', name: 'video',
shouldShowStatus: true,
check: (model, maxFileSize) => check: (model, maxFileSize) =>
model.props.type.startsWith('video/') && model.props.size <= maxFileSize, model.props.type.startsWith('video/') && model.props.size <= maxFileSize,
action: model => { template: (_, blobUrl) =>
const bound = Bound.deserialize(model.props.xywh);
bound.w = EMBED_CARD_WIDTH.video;
bound.h = EMBED_CARD_HEIGHT.video;
model.store.updateBlock(model, {
embed: true,
style: 'video',
xywh: bound.serialize(),
});
},
render: (_, blobUrl) =>
html`<video html`<video
style=${styleMap({ style="max-height: max-content;"
display: 'flex', width="100%;"
objectFit: 'cover', height="480"
backgroundSize: 'cover',
width: '100%',
height: '100%',
})}
src=${blobUrl}
width="100%"
height="100%"
controls controls
src=${blobUrl}
></video>`, ></video>`,
}, },
{ {
name: 'audio', name: 'audio',
check: (model, maxFileSize) => check: (model, maxFileSize) =>
model.props.type.startsWith('audio/') && model.props.size <= maxFileSize, model.props.type.startsWith('audio/') && model.props.size <= maxFileSize,
render: (_, blobUrl) => template: (_, blobUrl) =>
html`<audio html`<audio controls src=${blobUrl} style="margin: 4px;"></audio>`,
style=${styleMap({ margin: '4px' })}
src=${blobUrl}
controls
></audio>`,
}, },
]; ];
@@ -256,7 +188,7 @@ const embedConfig: AttachmentEmbedConfig[] = [
* Turn the attachment block into an image block. * Turn the attachment block into an image block.
*/ */
async function turnIntoImageBlock(model: AttachmentBlockModel) { async function turnIntoImageBlock(model: AttachmentBlockModel) {
if (!model.store.schema.flavourSchemaMap.has('affine:image')) { if (!model.doc.schema.flavourSchemaMap.has('affine:image')) {
console.error('The image flavour is not supported!'); console.error('The image flavour is not supported!');
return; return;
} }

View File

@@ -1,6 +1,7 @@
export * from './adapters'; export * from './adapters';
export * from './attachment-block'; export * from './attachment-block';
export * from './attachment-service'; export * from './attachment-service';
export * from './attachment-spec';
export { attachmentViewDropdownMenu } from './configs/toolbar'; export { attachmentViewDropdownMenu } from './configs/toolbar';
export * from './edgeless-clipboard-config'; export * from './edgeless-clipboard-config';
export { export {

View File

@@ -6,9 +6,9 @@ export const styles = css`
border-radius: 8px; border-radius: 8px;
box-sizing: border-box; box-sizing: border-box;
user-select: none; user-select: none;
overflow: hidden;
border: 1px solid ${unsafeCSSVarV2('layer/background/tertiary')}; border: 1px solid ${unsafeCSSVarV2('layer/background/tertiary')};
background: ${unsafeCSSVarV2('layer/background/primary')}; background: ${unsafeCSSVarV2('layer/background/primary')};
overflow: hidden;
&.focused { &.focused {
border-color: ${unsafeCSSVarV2('layer/insideBorder/primaryBorder')}; border-color: ${unsafeCSSVarV2('layer/insideBorder/primaryBorder')};
@@ -30,13 +30,6 @@ export const styles = css`
min-width: 0; min-width: 0;
} }
.truncate {
align-self: stretch;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.affine-attachment-content-title { .affine-attachment-content-title {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@@ -47,10 +40,18 @@ export const styles = css`
.affine-attachment-content-title-icon { .affine-attachment-content-title-icon {
display: flex; display: flex;
width: 16px;
height: 16px;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
color: var(--affine-text-primary-color); color: var(--affine-text-primary-color);
font-size: 16px; }
.truncate {
align-self: stretch;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
} }
.affine-attachment-content-title-text { .affine-attachment-content-title-text {
@@ -91,7 +92,6 @@ export const styles = css`
font-size: var(--affine-font-xs); font-size: var(--affine-font-xs);
font-style: normal; font-style: normal;
font-weight: 500; font-weight: 500;
text-transform: capitalize;
line-height: 20px; line-height: 20px;
svg { svg {
@@ -107,7 +107,7 @@ export const styles = css`
.affine-attachment-card.loading { .affine-attachment-card.loading {
.affine-attachment-content-title-text { .affine-attachment-content-title-text {
color: ${unsafeCSSVarV2('text/placeholder')}; color: var(--affine-placeholder-color);
} }
} }
@@ -143,12 +143,6 @@ export const styles = css`
height: 100%; height: 100%;
} }
.affine-attachment-embed-status {
position: absolute;
left: 14px;
bottom: 64px;
}
.affine-attachment-embed-event-mask { .affine-attachment-embed-event-mask {
position: absolute; position: absolute;
inset: 0; inset: 0;

View File

@@ -13,7 +13,7 @@ import {
FileSizeLimitProvider, FileSizeLimitProvider,
TelemetryProvider, TelemetryProvider,
} from '@blocksuite/affine-shared/services'; } from '@blocksuite/affine-shared/services';
import { formatSize } from '@blocksuite/affine-shared/utils'; import { humanFileSize } from '@blocksuite/affine-shared/utils';
import { Bound, type IVec, Vec } from '@blocksuite/global/gfx'; import { Bound, type IVec, Vec } from '@blocksuite/global/gfx';
import type { BlockStdScope } from '@blocksuite/std'; import type { BlockStdScope } from '@blocksuite/std';
import { GfxControllerIdentifier } from '@blocksuite/std/gfx'; import { GfxControllerIdentifier } from '@blocksuite/std/gfx';
@@ -22,13 +22,15 @@ import type { BlockModel } from '@blocksuite/store';
import type { AttachmentBlockComponent } from './attachment-block'; import type { AttachmentBlockComponent } from './attachment-block';
export async function getAttachmentBlob(model: AttachmentBlockModel) { export async function getAttachmentBlob(model: AttachmentBlockModel) {
const { sourceId$, type$ } = model.props; const {
const sourceId = sourceId$.peek(); sourceId$: { value: sourceId },
const type = type$.peek(); type$: { value: type },
} = model.props;
if (!sourceId) return null; if (!sourceId) return null;
const doc = model.store; const doc = model.doc;
const blob = await doc.blobSync.get(sourceId); let blob = await doc.blobSync.get(sourceId);
if (!blob) return null; if (!blob) return null;
return new Blob([blob], { type }); return new Blob([blob], { type });
@@ -39,9 +41,9 @@ export async function getAttachmentBlob(model: AttachmentBlockModel) {
* the download process may take a long time! * the download process may take a long time!
*/ */
export function downloadAttachmentBlob(block: AttachmentBlockComponent) { export function downloadAttachmentBlob(block: AttachmentBlockComponent) {
const { host, model, blobUrl, resourceController } = block; const { host, model, blobUrl, blobState$ } = block;
if (resourceController.state$.peek().downloading) { if (blobState$.peek().downloading) {
toast(host, 'Download in progress...'); toast(host, 'Download in progress...');
return; return;
} }
@@ -54,7 +56,7 @@ export function downloadAttachmentBlob(block: AttachmentBlockComponent) {
return; return;
} }
resourceController.updateState({ downloading: true }); block.updateBlobState({ downloading: true });
toast(host, `Downloading ${shortName}`); toast(host, `Downloading ${shortName}`);
@@ -65,14 +67,34 @@ export function downloadAttachmentBlob(block: AttachmentBlockComponent) {
tmpLink.dispatchEvent(event); tmpLink.dispatchEvent(event);
tmpLink.remove(); tmpLink.remove();
resourceController.updateState({ downloading: false }); block.updateBlobState({ downloading: false });
} }
export async function refreshData(block: AttachmentBlockComponent) { export async function refreshData(
std: BlockStdScope,
block: AttachmentBlockComponent
) {
const model = block.model; const model = block.model;
const sourceId = model.props.sourceId$.peek();
if (!sourceId) return;
const blobUrl = block.blobUrl;
if (blobUrl) {
URL.revokeObjectURL(blobUrl);
block.blobUrl = null;
}
let blob = await std.store.blobSync.get(sourceId);
if (!blob) {
block.updateBlobState({ errorMessage: 'File not found' });
return;
}
const type = model.props.type$.peek(); const type = model.props.type$.peek();
await block.resourceController.refreshUrlWith(type); blob = new Blob([blob], { type });
block.blobUrl = URL.createObjectURL(blob);
} }
export async function getFileType(file: File) { export async function getFileType(file: File) {
@@ -82,7 +104,7 @@ export async function getFileType(file: File) {
const buffer = await file.arrayBuffer(); const buffer = await file.arrayBuffer();
const FileType = await import('file-type'); const FileType = await import('file-type');
const fileType = await FileType.fileTypeFromBuffer(buffer); const fileType = await FileType.fileTypeFromBuffer(buffer);
return fileType?.mime ?? ''; return fileType ? fileType.mime : '';
} }
function hasExceeded( function hasExceeded(
@@ -93,7 +115,7 @@ function hasExceeded(
const exceeded = files.some(file => file.size > maxFileSize); const exceeded = files.some(file => file.size > maxFileSize);
if (exceeded) { if (exceeded) {
const size = formatSize(maxFileSize); const size = humanFileSize(maxFileSize, true, 0);
toast(std.host, `You can only upload files less than ${size}`); toast(std.host, `You can only upload files less than ${size}`);
} }
@@ -130,7 +152,7 @@ async function buildPropsWith(
std.getOptional(TelemetryProvider)?.track('AttachmentUploadedEvent', { std.getOptional(TelemetryProvider)?.track('AttachmentUploadedEvent', {
page: `${mode} editor`, page: `${mode} editor`,
module: 'attachment', module: 'attachment',
segment: mode, segment: 'attachment',
control: 'uploader', control: 'uploader',
type, type,
category, category,

View File

@@ -7,7 +7,6 @@ import { SlashMenuConfigExtension } from '@blocksuite/affine-widget-slash-menu';
import { BlockViewExtension, FlavourExtension } from '@blocksuite/std'; import { BlockViewExtension, FlavourExtension } from '@blocksuite/std';
import { literal } from 'lit/static-html.js'; import { literal } from 'lit/static-html.js';
import { AttachmentBlockInteraction } from './attachment-edgeless-block.js';
import { AttachmentDropOption } from './attachment-service.js'; import { AttachmentDropOption } from './attachment-service.js';
import { attachmentSlashMenuConfig } from './configs/slash-menu.js'; import { attachmentSlashMenuConfig } from './configs/slash-menu.js';
import { createBuiltinToolbarConfigExtension } from './configs/toolbar'; import { createBuiltinToolbarConfigExtension } from './configs/toolbar';
@@ -45,7 +44,6 @@ export class AttachmentViewExtension extends ViewExtensionProvider {
]); ]);
if (this.isEdgeless(context.scope)) { if (this.isEdgeless(context.scope)) {
context.register(EdgelessClipboardAttachmentConfig); context.register(EdgelessClipboardAttachmentConfig);
context.register(AttachmentBlockInteraction);
} }
} }
} }

View File

@@ -15,6 +15,7 @@
{ "path": "../../widgets/slash-menu" }, { "path": "../../widgets/slash-menu" },
{ "path": "../../../framework/global" }, { "path": "../../../framework/global" },
{ "path": "../../../framework/std" }, { "path": "../../../framework/std" },
{ "path": "../../../framework/store" } { "path": "../../../framework/store" },
{ "path": "../../../framework/sync" }
] ]
} }

View File

@@ -24,7 +24,7 @@
"@blocksuite/store": "workspace:*", "@blocksuite/store": "workspace:*",
"@lit/context": "^1.1.2", "@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0", "@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15", "@toeverything/theme": "^1.1.14",
"lit": "^3.2.0", "lit": "^3.2.0",
"minimatch": "^10.0.1", "minimatch": "^10.0.1",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
@@ -32,10 +32,11 @@
"zod": "^3.23.8" "zod": "^3.23.8"
}, },
"devDependencies": { "devDependencies": {
"vitest": "3.1.3" "vitest": "3.1.2"
}, },
"exports": { "exports": {
".": "./src/index.ts", ".": "./src/index.ts",
"./effects": "./src/effects.ts",
"./store": "./src/store.ts", "./store": "./src/store.ts",
"./view": "./src/view.ts" "./view": "./src/view.ts"
}, },

View File

@@ -10,6 +10,7 @@ import {
isFootnoteDefinitionNode, isFootnoteDefinitionNode,
type MarkdownAST, type MarkdownAST,
} from '@blocksuite/affine-shared/adapters'; } from '@blocksuite/affine-shared/adapters';
import { FeatureFlagService } from '@blocksuite/affine-shared/services';
import { nanoid } from '@blocksuite/store'; import { nanoid } from '@blocksuite/store';
const isUrlFootnoteDefinitionNode = (node: MarkdownAST) => { const isUrlFootnoteDefinitionNode = (node: MarkdownAST) => {
@@ -32,7 +33,15 @@ export const bookmarkBlockMarkdownAdapterMatcher =
toMatch: o => isUrlFootnoteDefinitionNode(o.node), toMatch: o => isUrlFootnoteDefinitionNode(o.node),
toBlockSnapshot: { toBlockSnapshot: {
enter: (o, context) => { enter: (o, context) => {
if (!isFootnoteDefinitionNode(o.node)) { const { provider } = context;
let enableCitation = false;
try {
const featureFlagService = provider?.get(FeatureFlagService);
enableCitation = !!featureFlagService?.getFlag('enable_citation');
} catch {
enableCitation = false;
}
if (!isFootnoteDefinitionNode(o.node) || !enableCitation) {
return; return;
} }

View File

@@ -6,20 +6,16 @@ import type {
BookmarkBlockModel, BookmarkBlockModel,
LinkPreviewData, LinkPreviewData,
} from '@blocksuite/affine-model'; } from '@blocksuite/affine-model';
import { ImageProxyService } from '@blocksuite/affine-shared/adapters';
import { import {
CitationProvider,
DocModeProvider, DocModeProvider,
LinkPreviewServiceIdentifier, LinkPreviewerService,
} from '@blocksuite/affine-shared/services'; } from '@blocksuite/affine-shared/services';
import { normalizeUrl } from '@blocksuite/affine-shared/utils';
import { BlockSelection } from '@blocksuite/std'; import { BlockSelection } from '@blocksuite/std';
import { computed, type ReadonlySignal, signal } from '@preact/signals-core'; import { computed, type ReadonlySignal, signal } from '@preact/signals-core';
import { html } from 'lit'; import { html } from 'lit';
import { property, query } from 'lit/decorators.js'; import { property, query } from 'lit/decorators.js';
import { type ClassInfo, classMap } from 'lit/directives/class-map.js'; import { type ClassInfo, classMap } from 'lit/directives/class-map.js';
import { type StyleInfo, styleMap } from 'lit/directives/style-map.js'; import { type StyleInfo, styleMap } from 'lit/directives/style-map.js';
import { filter } from 'rxjs/operators';
import { refreshBookmarkUrlData } from './utils.js'; import { refreshBookmarkUrlData } from './utils.js';
@@ -75,8 +71,8 @@ export class BookmarkBlockComponent extends CaptionedBlockComponent<BookmarkBloc
this.loading = true; this.loading = true;
this.error = false; this.error = false;
this.std this.std.store
.get(LinkPreviewServiceIdentifier) .get(LinkPreviewerService)
.query(this.model.props.url, this._fetchAbortController.signal) .query(this.model.props.url, this._fetchAbortController.signal)
.then(data => { .then(data => {
this._localLinkPreview$.value = { this._localLinkPreview$.value = {
@@ -102,12 +98,12 @@ export class BookmarkBlockComponent extends CaptionedBlockComponent<BookmarkBloc
selectionManager.setGroup('note', [blockSelection]); selectionManager.setGroup('note', [blockSelection]);
}; };
get link() {
return normalizeUrl(this.model.props.url);
}
open = () => { open = () => {
window.open(this.link, '_blank'); let link = this.model.props.url;
if (!link.match(/^[a-zA-Z]+:\/\//)) {
link = 'https://' + link;
}
window.open(link, '_blank');
}; };
refreshData = () => { refreshData = () => {
@@ -116,25 +112,17 @@ export class BookmarkBlockComponent extends CaptionedBlockComponent<BookmarkBloc
); );
}; };
get citationService() {
return this.std.get(CitationProvider);
}
get isCitation() { get isCitation() {
return this.citationService.isCitationModel(this.model); return (
} !!this.model.props.footnoteIdentifier &&
this.model.props.style === 'citation'
get imageProxyService() { );
return this.std.get(ImageProxyService);
} }
handleClick = (event: MouseEvent) => { handleClick = (event: MouseEvent) => {
event.stopPropagation(); event.stopPropagation();
if ( if (this.model.parent?.flavour !== 'affine:surface' && !this.doc.readonly) {
this.model.parent?.flavour !== 'affine:surface' &&
!this.store.readonly
) {
this.selectBlock(); this.selectBlock();
} }
}; };
@@ -147,10 +135,9 @@ export class BookmarkBlockComponent extends CaptionedBlockComponent<BookmarkBloc
private readonly _renderCitationView = () => { private readonly _renderCitationView = () => {
const { url, footnoteIdentifier } = this.model.props; const { url, footnoteIdentifier } = this.model.props;
const { icon, title, description } = this.linkPreview$.value; const { icon, title, description } = this.linkPreview$.value;
const iconSrc = icon ? this.imageProxyService.buildUrl(icon) : undefined;
return html` return html`
<affine-citation-card <affine-citation-card
.icon=${iconSrc} .icon=${icon}
.citationTitle=${title || url} .citationTitle=${title || url}
.citationContent=${description} .citationContent=${description}
.citationIdentifier=${footnoteIdentifier} .citationIdentifier=${footnoteIdentifier}
@@ -169,31 +156,6 @@ export class BookmarkBlockComponent extends CaptionedBlockComponent<BookmarkBloc
></bookmark-card>`; ></bookmark-card>`;
}; };
private readonly _trackCitationDeleteEvent = () => {
// Check citation delete event
this._disposables.add(
this.std.store.slots.blockUpdated
.pipe(
filter(payload => {
if (!payload.isLocal) return false;
const { flavour, id, type } = payload;
if (
type !== 'delete' ||
flavour !== this.model.flavour ||
id !== this.model.id
)
return false;
const { model } = payload;
if (!this.citationService.isCitationModel(model)) return false;
return true;
})
)
.subscribe(() => {
this.citationService.trackEvent('Delete');
})
);
};
override connectedCallback() { override connectedCallback() {
super.connectedCallback(); super.connectedCallback();
@@ -216,7 +178,7 @@ export class BookmarkBlockComponent extends CaptionedBlockComponent<BookmarkBloc
) { ) {
// When the doc is readonly, and the preview data not provided // When the doc is readonly, and the preview data not provided
// We should fetch the preview data and update the local link preview data // We should fetch the preview data and update the local link preview data
if (this.store.readonly) { if (this.doc.readonly) {
this._updateLocalLinkPreview(); this._updateLocalLinkPreview();
return; return;
} }
@@ -231,8 +193,6 @@ export class BookmarkBlockComponent extends CaptionedBlockComponent<BookmarkBloc
} }
}) })
); );
this._trackCitationDeleteEvent();
} }
override disconnectedCallback(): void { override disconnectedCallback(): void {

View File

@@ -1,10 +1,8 @@
import { BookmarkBlockSchema } from '@blocksuite/affine-model';
import { import {
EMBED_CARD_HEIGHT, EMBED_CARD_HEIGHT,
EMBED_CARD_WIDTH, EMBED_CARD_WIDTH,
} from '@blocksuite/affine-shared/consts'; } from '@blocksuite/affine-shared/consts';
import { toGfxBlockComponent } from '@blocksuite/std'; import { toGfxBlockComponent } from '@blocksuite/std';
import { GfxViewInteractionExtension } from '@blocksuite/std/gfx';
import { type StyleInfo, styleMap } from 'lit/directives/style-map.js'; import { type StyleInfo, styleMap } from 'lit/directives/style-map.js';
import { BookmarkBlockComponent } from './bookmark-block.js'; import { BookmarkBlockComponent } from './bookmark-block.js';
@@ -29,15 +27,6 @@ export class BookmarkEdgelessBlockComponent extends toGfxBlockComponent(
}; };
} }
override connectedCallback(): void {
super.connectedCallback();
this.disposables.add(
this.gfx.selection.slots.updated.subscribe(() => {
this.requestUpdate();
})
);
}
override renderGfxBlock() { override renderGfxBlock() {
const style = this.model.props.style$.value; const style = this.model.props.style$.value;
const width = EMBED_CARD_WIDTH[style]; const width = EMBED_CARD_WIDTH[style];
@@ -45,14 +34,12 @@ export class BookmarkEdgelessBlockComponent extends toGfxBlockComponent(
const bound = this.model.elementBound; const bound = this.model.elementBound;
const scaleX = bound.w / width; const scaleX = bound.w / width;
const scaleY = bound.h / height; const scaleY = bound.h / height;
const isSelected = this.gfx.selection.has(this.model.id);
this.containerStyleMap = styleMap({ this.containerStyleMap = styleMap({
width: `100%`, width: `100%`,
height: `100%`, height: `100%`,
transform: `scale(${scaleX}, ${scaleY})`, transform: `scale(${scaleX}, ${scaleY})`,
transformOrigin: '0 0', transformOrigin: '0 0',
pointerEvents: isSelected ? 'auto' : 'none',
}); });
return this.renderPageContent(); return this.renderPageContent();
@@ -63,24 +50,6 @@ export class BookmarkEdgelessBlockComponent extends toGfxBlockComponent(
}; };
} }
export const BookmarkBlockInteraction = GfxViewInteractionExtension(
BookmarkBlockSchema.model.flavour,
{
resizeConstraint: {
lockRatio: true,
},
handleRotate: () => {
return {
beforeRotate(context) {
context.set({
rotatable: false,
});
},
};
},
}
);
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
'affine-edgeless-bookmark': BookmarkEdgelessBlockComponent; 'affine-edgeless-bookmark': BookmarkEdgelessBlockComponent;

View File

@@ -0,0 +1,22 @@
import { BookmarkBlockSchema } from '@blocksuite/affine-model';
import { BlockViewExtension, FlavourExtension } from '@blocksuite/std';
import type { ExtensionType } from '@blocksuite/store';
import { literal } from 'lit/static-html.js';
import { BookmarkBlockAdapterExtensions } from './adapters/extension';
import { BookmarkSlashMenuConfigExtension } from './configs/slash-menu';
import { createBuiltinToolbarConfigExtension } from './configs/toolbar';
const flavour = BookmarkBlockSchema.model.flavour;
export const BookmarkBlockSpec: ExtensionType[] = [
FlavourExtension(flavour),
BlockViewExtension(flavour, model => {
return model.parent?.flavour === 'affine:surface'
? literal`affine-edgeless-bookmark`
: literal`affine-bookmark`;
}),
BookmarkBlockAdapterExtensions,
createBuiltinToolbarConfigExtension(flavour),
BookmarkSlashMenuConfigExtension,
].flat();

View File

@@ -1,3 +1,5 @@
import '@blocksuite/affine-block-embed/effects';
import { insertEmbedCard } from '@blocksuite/affine-block-embed'; import { insertEmbedCard } from '@blocksuite/affine-block-embed';
import type { EmbedCardStyle } from '@blocksuite/affine-model'; import type { EmbedCardStyle } from '@blocksuite/affine-model';
import { EmbedOptionProvider } from '@blocksuite/affine-shared/services'; import { EmbedOptionProvider } from '@blocksuite/affine-shared/services';

View File

@@ -2,14 +2,9 @@ import { insertEmbedIframeWithUrlCommand } from '@blocksuite/affine-block-embed'
import { import {
type InsertedLinkType, type InsertedLinkType,
insertEmbedLinkedDocCommand, insertEmbedLinkedDocCommand,
insertEmbedSyncedDocCommand,
type LinkableFlavour, type LinkableFlavour,
} from '@blocksuite/affine-block-embed-doc'; } from '@blocksuite/affine-block-embed-doc';
import { import { QuickSearchProvider } from '@blocksuite/affine-shared/services';
DocModeProvider,
EditorSettingProvider,
QuickSearchProvider,
} from '@blocksuite/affine-shared/services';
import type { Command } from '@blocksuite/std'; import type { Command } from '@blocksuite/std';
import { insertBookmarkCommand } from './insert-bookmark'; import { insertBookmarkCommand } from './insert-bookmark';
@@ -31,26 +26,12 @@ export const insertLinkByQuickSearchCommand: Command<
// add linked doc // add linked doc
if ('docId' in result) { if ('docId' in result) {
const editorMode = std.get(DocModeProvider).getEditorMode(); std.command.exec(insertEmbedLinkedDocCommand, {
const editorSettings = std.get(EditorSettingProvider);
let flavour: LinkableFlavour = 'affine:embed-linked-doc';
if (editorMode === 'edgeless') {
flavour =
editorSettings.setting$.value.docCanvasPreferView ?? flavour;
}
const insertCommand =
flavour === 'affine:embed-linked-doc'
? insertEmbedLinkedDocCommand
: insertEmbedSyncedDocCommand;
std.command.exec(insertCommand, {
docId: result.docId, docId: result.docId,
params: result.params, params: result.params,
}); });
return { return {
flavour, flavour: 'affine:embed-linked-doc',
}; };
} }

View File

@@ -1,5 +1,5 @@
import { getEmbedCardIcons } from '@blocksuite/affine-block-embed'; import { getEmbedCardIcons } from '@blocksuite/affine-block-embed';
import { LoadingIcon, WebIcon16 } from '@blocksuite/affine-components/icons'; import { WebIcon16 } from '@blocksuite/affine-components/icons';
import { ImageProxyService } from '@blocksuite/affine-shared/adapters'; import { ImageProxyService } from '@blocksuite/affine-shared/adapters';
import { ThemeProvider } from '@blocksuite/affine-shared/services'; import { ThemeProvider } from '@blocksuite/affine-shared/services';
import { getHostName } from '@blocksuite/affine-shared/utils'; import { getHostName } from '@blocksuite/affine-shared/utils';
@@ -60,11 +60,11 @@ export class BookmarkCard extends SignalWatcher(
: title; : title;
const theme = this.bookmark.std.get(ThemeProvider).theme; const theme = this.bookmark.std.get(ThemeProvider).theme;
const { EmbedCardBannerIcon } = getEmbedCardIcons(theme); const { LoadingIcon, EmbedCardBannerIcon } = getEmbedCardIcons(theme);
const imageProxyService = this.bookmark.store.get(ImageProxyService); const imageProxyService = this.bookmark.doc.get(ImageProxyService);
const titleIcon = this.loading const titleIcon = this.loading
? LoadingIcon() ? LoadingIcon
: icon : icon
? html`<img src=${imageProxyService.buildUrl(icon)} alt="icon" />` ? html`<img src=${imageProxyService.buildUrl(icon)} alt="icon" />`
: WebIcon16; : WebIcon16;

View File

@@ -23,10 +23,10 @@ const bookmarkSlashMenuConfig: SlashMenuConfig = {
}, },
group: '4_Content & Media@2', group: '4_Content & Media@2',
when: ({ model }) => when: ({ model }) =>
model.store.schema.flavourSchemaMap.has('affine:bookmark'), model.doc.schema.flavourSchemaMap.has('affine:bookmark'),
action: ({ std, model }) => { action: ({ std, model }) => {
const { host } = std; const { host } = std;
const parentModel = host.store.getParent(model); const parentModel = host.doc.getParent(model);
if (!parentModel) { if (!parentModel) {
return; return;
} }
@@ -45,7 +45,7 @@ const bookmarkSlashMenuConfig: SlashMenuConfig = {
) )
.then(() => { .then(() => {
if (model.text?.length === 0) { if (model.text?.length === 0) {
model.store.deleteBlock(model); model.doc.deleteBlock(model);
} }
}) })
.catch(console.error); .catch(console.error);

View File

@@ -407,7 +407,7 @@ const builtinSurfaceToolbarConfig = {
if (options?.viewType !== 'embed') return; if (options?.viewType !== 'embed') return;
const { flavour, styles } = options; const { flavour, styles } = options;
let style: EmbedCardStyle = model.props.style; let { style } = model.props;
if (!styles.includes(style)) { if (!styles.includes(style)) {
style = styles[0]; style = styles[0];
@@ -482,26 +482,24 @@ const builtinSurfaceToolbarConfig = {
} satisfies ToolbarActionGroup<ToolbarAction>, } satisfies ToolbarActionGroup<ToolbarAction>,
{ {
id: 'b.style', id: 'b.style',
actions: ( actions: [
[ {
{ id: 'horizontal',
id: 'horizontal', label: 'Large horizontal style',
label: 'Large horizontal style', },
}, {
{ id: 'list',
id: 'list', label: 'Small horizontal style',
label: 'Small horizontal style', },
}, {
{ id: 'vertical',
id: 'vertical', label: 'Large vertical style',
label: 'Large vertical style', },
}, {
{ id: 'cube',
id: 'cube', label: 'Small vertical style',
label: 'Small vertical style', },
}, ].filter(action => BookmarkStyles.includes(action.id as EmbedCardStyle)),
] as const
).filter(action => BookmarkStyles.includes(action.id)),
content(ctx) { content(ctx) {
const model = ctx.getCurrentModelByType(BookmarkBlockModel); const model = ctx.getCurrentModelByType(BookmarkBlockModel);
if (!model) return null; if (!model) return null;

View File

@@ -1,5 +1,6 @@
export * from './adapters'; export * from './adapters';
export * from './bookmark-block'; export * from './bookmark-block';
export * from './bookmark-spec';
export * from './commands'; export * from './commands';
export * from './components'; export * from './components';
export { BookmarkSlashMenuConfigIdentifier } from './configs/slash-menu'; export { BookmarkSlashMenuConfigIdentifier } from './configs/slash-menu';

View File

@@ -1,4 +1,4 @@
import { LinkPreviewServiceIdentifier } from '@blocksuite/affine-shared/services'; import { LinkPreviewerService } from '@blocksuite/affine-shared/services';
import { isAbortError } from '@blocksuite/affine-shared/utils'; import { isAbortError } from '@blocksuite/affine-shared/utils';
import type { BookmarkBlockComponent } from './bookmark-block.js'; import type { BookmarkBlockComponent } from './bookmark-block.js';
@@ -15,7 +15,7 @@ export async function refreshBookmarkUrlData(
try { try {
bookmarkElement.loading = true; bookmarkElement.loading = true;
const linkPreviewer = bookmarkElement.std.get(LinkPreviewServiceIdentifier); const linkPreviewer = bookmarkElement.doc.get(LinkPreviewerService);
const bookmarkUrlData = await linkPreviewer.query( const bookmarkUrlData = await linkPreviewer.query(
bookmarkElement.model.props.url, bookmarkElement.model.props.url,
signal signal
@@ -32,7 +32,7 @@ export async function refreshBookmarkUrlData(
if (signal?.aborted) return; if (signal?.aborted) return;
bookmarkElement.store.updateBlock(bookmarkElement.model, { bookmarkElement.doc.updateBlock(bookmarkElement.model, {
title, title,
description, description,
icon, icon,

View File

@@ -6,7 +6,6 @@ import { BookmarkBlockSchema } from '@blocksuite/affine-model';
import { BlockViewExtension, FlavourExtension } from '@blocksuite/std'; import { BlockViewExtension, FlavourExtension } from '@blocksuite/std';
import { literal } from 'lit/static-html.js'; import { literal } from 'lit/static-html.js';
import { BookmarkBlockInteraction } from './bookmark-edgeless-block';
import { BookmarkSlashMenuConfigExtension } from './configs/slash-menu'; import { BookmarkSlashMenuConfigExtension } from './configs/slash-menu';
import { createBuiltinToolbarConfigExtension } from './configs/toolbar'; import { createBuiltinToolbarConfigExtension } from './configs/toolbar';
import { EdgelessClipboardBookmarkConfig } from './edgeless-clipboard-config'; import { EdgelessClipboardBookmarkConfig } from './edgeless-clipboard-config';
@@ -37,7 +36,6 @@ export class BookmarkViewExtension extends ViewExtensionProvider {
const isEdgeless = this.isEdgeless(context.scope); const isEdgeless = this.isEdgeless(context.scope);
if (isEdgeless) { if (isEdgeless) {
context.register(EdgelessClipboardBookmarkConfig); context.register(EdgelessClipboardBookmarkConfig);
context.register(BookmarkBlockInteraction);
} }
} }
} }

View File

@@ -25,7 +25,7 @@
"@floating-ui/dom": "^1.6.10", "@floating-ui/dom": "^1.6.10",
"@lit/context": "^1.1.2", "@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0", "@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15", "@toeverything/theme": "^1.1.14",
"@types/mdast": "^4.0.4", "@types/mdast": "^4.0.4",
"emoji-mart": "^5.6.0", "emoji-mart": "^5.6.0",
"lit": "^3.2.0", "lit": "^3.2.0",
@@ -35,6 +35,7 @@
}, },
"exports": { "exports": {
".": "./src/index.ts", ".": "./src/index.ts",
"./effects": "./src/effects.ts",
"./view": "./src/view.ts", "./view": "./src/view.ts",
"./store": "./src/store.ts" "./store": "./src/store.ts"
}, },

View File

@@ -2,17 +2,10 @@ import { CalloutBlockSchema } from '@blocksuite/affine-model';
import { import {
BlockMarkdownAdapterExtension, BlockMarkdownAdapterExtension,
type BlockMarkdownAdapterMatcher, type BlockMarkdownAdapterMatcher,
CALLOUT_MARKDOWN_EXPORT_OPTIONS_KEY,
type CalloutAdmonitionType,
CalloutAdmonitionTypeSet,
CalloutExportStyle,
type CalloutMarkdownExportOptions,
calloutMarkdownExportOptionsSchema,
DEFAULT_ADMONITION_TYPE,
getCalloutEmoji, getCalloutEmoji,
isCalloutNode, isCalloutNode,
} from '@blocksuite/affine-shared/adapters'; } from '@blocksuite/affine-shared/adapters';
import { type DeltaInsert, nanoid } from '@blocksuite/store'; import { nanoid } from '@blocksuite/store';
// Currently, the callout block children can only be paragraph block or list block // Currently, the callout block children can only be paragraph block or list block
// In mdast, the node types are `paragraph`, `list`, `heading`, `blockquote` // In mdast, the node types are `paragraph`, `list`, `heading`, `blockquote`
@@ -23,29 +16,6 @@ const CALLOUT_BLOCK_CHILDREN_TYPES = new Set([
'blockquote', 'blockquote',
]); ]);
const ADMONITION_SYMBOL = ':::';
const DEFAULT_OPTIONS: CalloutMarkdownExportOptions = {
style: CalloutExportStyle.GFM,
};
/**
* Get the callout export options from the configs
* @param configs - The configs of the callout block
* @returns The callout export options
*/
function getCalloutExportOptions(
configs: Map<string, unknown>
): CalloutMarkdownExportOptions {
let exportOptions: CalloutMarkdownExportOptions = DEFAULT_OPTIONS;
try {
const options = configs.get(CALLOUT_MARKDOWN_EXPORT_OPTIONS_KEY);
if (options) {
exportOptions = calloutMarkdownExportOptionsSchema.parse(options);
}
} catch {}
return exportOptions;
}
export const calloutBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher = { export const calloutBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher = {
flavour: CalloutBlockSchema.model.flavour, flavour: CalloutBlockSchema.model.flavour,
toMatch: o => isCalloutNode(o.node), toMatch: o => isCalloutNode(o.node),
@@ -87,118 +57,29 @@ export const calloutBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher = {
fromBlockSnapshot: { fromBlockSnapshot: {
enter: (o, context) => { enter: (o, context) => {
const emoji = o.node.props.emoji as string; const emoji = o.node.props.emoji as string;
const { walkerContext, configs } = context; const { walkerContext } = context;
walkerContext
const exportOptions = getCalloutExportOptions(configs); .openNode(
const { style, admonitionType } = exportOptions; {
// If the style is admonitions, we should handle the first child type: 'blockquote',
if (style === CalloutExportStyle.Admonitions) { children: [],
let type = admonitionType ?? DEFAULT_ADMONITION_TYPE; },
let customTitle = ''; 'children'
let restOfText = ''; )
.openNode({
const firstChild = o.node.children[0]; type: 'paragraph',
const isTextNode = !!firstChild.props.text; children: [
// If the first child is a text block, we should get the type and custom title from the first line of the text
// And remove the first child from the children
// Otherwise, we should use the default admonition type as the type
if (isTextNode) {
const textDelta = (firstChild.props.text ?? { delta: [] }) as {
delta: DeltaInsert[];
};
// Get the text of the first child
const text = textDelta.delta.reduce((acc, delta) => {
if (delta.insert) {
acc += delta.insert;
}
return acc;
}, '');
// If the text is not empty, we should try to get type and custom title from the text
if (text) {
// Get the first line of the text
const firstLine = text.includes('\n') ? text.split('\n')[0] : text;
// Get the rest of the text besides the first line
restOfText = text.split('\n').slice(1).join('\n');
// Get the possible type from the first line
const possibleType = firstLine.split(' ')[0].toLowerCase();
// If the type is a valid admonition type, we should use it as the type
if (CalloutAdmonitionTypeSet.has(possibleType)) {
type = possibleType as CalloutAdmonitionType;
// Get the custom title from the first line
customTitle = firstLine.split(' ').slice(1).join(' ').trim();
// Remove the first child from the children
o.node.children = o.node.children.slice(1);
}
}
}
// Add an admonition symbol paragraph to the start of the children
const admonitionSymbol =
`${ADMONITION_SYMBOL} ${type} ${customTitle}`.trim();
walkerContext
.openNode({
type: 'paragraph',
children: [
{
type: 'text',
value: admonitionSymbol,
},
],
})
.closeNode();
// Add the rest of the text to the children content
if (restOfText) {
walkerContext
.openNode({
type: 'paragraph',
children: [{ type: 'text', value: `${restOfText}` }],
})
.closeNode();
}
} else {
walkerContext
.openNode(
{ {
type: 'blockquote', type: 'text',
children: [], value: `[!${emoji}]`,
}, },
'children' ],
) })
.openNode({ .closeNode();
type: 'paragraph',
children: [
{
type: 'text',
value: `[!${emoji}]`,
},
],
})
.closeNode();
}
}, },
leave: (_, context) => { leave: (_, context) => {
const { walkerContext, configs } = context; const { walkerContext } = context;
const exportOptions = getCalloutExportOptions(configs); walkerContext.closeNode();
const { style } = exportOptions;
// If the style is admonitions, we should add an admonition symbol paragraph to the end of the children
if (style === CalloutExportStyle.Admonitions) {
walkerContext
.openNode({
type: 'paragraph',
children: [
{
type: 'text',
value: ADMONITION_SYMBOL,
},
],
})
.closeNode();
} else {
// If the style is gfm, we should close the outer blockquote node
walkerContext.closeNode();
}
}, },
}, },
}; };

View File

@@ -12,7 +12,6 @@ import type { BlockComponent } from '@blocksuite/std';
import { flip, offset } from '@floating-ui/dom'; import { flip, offset } from '@floating-ui/dom';
import { css, html } from 'lit'; import { css, html } from 'lit';
import { query } from 'lit/decorators.js'; import { query } from 'lit/decorators.js';
import { styleMap } from 'lit/directives/style-map.js';
export class CalloutBlockComponent extends CaptionedBlockComponent<CalloutBlockModel> { export class CalloutBlockComponent extends CaptionedBlockComponent<CalloutBlockModel> {
static override styles = css` static override styles = css`
:host { :host {
@@ -110,18 +109,14 @@ export class CalloutBlockComponent extends CaptionedBlockComponent<CalloutBlockM
} }
override renderBlock() { override renderBlock() {
const emoji = this.model.props.emoji$.value;
return html` return html`
<div class="affine-callout-block-container"> <div class="affine-callout-block-container">
<div <div
@click=${this._toggleEmojiMenu} @click=${this._toggleEmojiMenu}
contenteditable="false" contenteditable="false"
class="affine-callout-emoji-container" class="affine-callout-emoji-container"
style=${styleMap({
display: emoji.length === 0 ? 'none' : undefined,
})}
> >
<span class="affine-callout-emoji">${emoji}</span> <span class="affine-callout-emoji">${this.model.props.emoji$}</span>
</div> </div>
<div class="affine-callout-children"> <div class="affine-callout-children">
${this.renderChildren(this.model)} ${this.renderChildren(this.model)}

View File

@@ -0,0 +1,16 @@
import { SlashMenuConfigExtension } from '@blocksuite/affine-widget-slash-menu';
import { BlockViewExtension, FlavourExtension } from '@blocksuite/std';
import type { ExtensionType } from '@blocksuite/store';
import { literal } from 'lit/static-html.js';
import { CalloutBlockMarkdownAdapterExtension } from './adapters/markdown';
import { CalloutKeymapExtension } from './callout-keymap';
import { calloutSlashMenuConfig } from './configs/slash-menu';
export const CalloutBlockSpec: ExtensionType[] = [
FlavourExtension('affine:callout'),
BlockViewExtension('affine:callout', literal`affine-callout`),
CalloutKeymapExtension,
SlashMenuConfigExtension('affine:callout', calloutSlashMenuConfig),
CalloutBlockMarkdownAdapterExtension,
];

View File

@@ -33,24 +33,19 @@ export const calloutSlashMenuConfig: SlashMenuConfig = {
when: ({ std, model }) => { when: ({ std, model }) => {
return ( return (
std.get(FeatureFlagService).getFlag('enable_callout') && std.get(FeatureFlagService).getFlag('enable_callout') &&
!isInsideBlockByFlavour(model.store, model, 'affine:edgeless-text') !isInsideBlockByFlavour(model.doc, model, 'affine:edgeless-text')
); );
}, },
action: ({ model, std }) => { action: ({ model, std }) => {
const { store } = model; const { doc } = model;
const parent = store.getParent(model); const parent = doc.getParent(model);
if (!parent) return; if (!parent) return;
const index = parent.children.indexOf(model); const index = parent.children.indexOf(model);
if (index === -1) return; if (index === -1) return;
const calloutId = store.addBlock( const calloutId = doc.addBlock('affine:callout', {}, parent, index + 1);
'affine:callout',
{},
parent,
index + 1
);
if (!calloutId) return; if (!calloutId) return;
const paragraphId = store.addBlock('affine:paragraph', {}, calloutId); const paragraphId = doc.addBlock('affine:paragraph', {}, calloutId);
if (!paragraphId) return; if (!paragraphId) return;
std.host.updateComplete std.host.updateComplete
.then(() => { .then(() => {

View File

@@ -1,2 +1,3 @@
export * from './callout-block.js'; export * from './callout-block.js';
export * from './callout-spec.js';
export * from './effects.js'; export * from './effects.js';

View File

@@ -27,7 +27,7 @@
"@floating-ui/dom": "^1.6.13", "@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2", "@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0", "@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15", "@toeverything/theme": "^1.1.14",
"@types/mdast": "^4.0.4", "@types/mdast": "^4.0.4",
"lit": "^3.2.0", "lit": "^3.2.0",
"minimatch": "^10.0.1", "minimatch": "^10.0.1",
@@ -37,6 +37,7 @@
}, },
"exports": { "exports": {
".": "./src/index.ts", ".": "./src/index.ts",
"./effects": "./src/effects.ts",
"./turbo-painter": "./src/turbo/code-painter.worker.ts", "./turbo-painter": "./src/turbo/code-painter.worker.ts",
"./view": "./src/view.ts", "./view": "./src/view.ts",
"./store": "./src/store.ts" "./store": "./src/store.ts"

View File

@@ -48,11 +48,7 @@ const codePreprocessor: MarkdownAdapterPreprocessor = {
} }
trimmedLine = trimmedLine.trimEnd(); trimmedLine = trimmedLine.trimEnd();
if ( if (!trimmedLine.startsWith('<') && !trimmedLine.endsWith('>')) {
!trimmedLine.startsWith('<') &&
!trimmedLine.endsWith('>') &&
!trimmedLine.includes(' ')
) {
// check if it is a url link and wrap it with the angle brackets // check if it is a url link and wrap it with the angle brackets
// sometimes the url includes emphasis `_` that will break URL parsing // sometimes the url includes emphasis `_` that will break URL parsing
// //

View File

@@ -0,0 +1,38 @@
import { SlashMenuConfigExtension } from '@blocksuite/affine-widget-slash-menu';
import {
BlockViewExtension,
FlavourExtension,
WidgetViewExtension,
} from '@blocksuite/std';
import type { ExtensionType } from '@blocksuite/store';
import { literal, unsafeStatic } from 'lit/static-html.js';
import { CodeBlockAdapterExtensions } from './adapters/extension.js';
import { getCodeClipboardExtensions } from './clipboard/index.js';
import {
CodeBlockInlineManagerExtension,
CodeBlockUnitSpecExtension,
} from './code-block-inline.js';
import { CodeBlockHighlighter } from './code-block-service.js';
import { CodeKeymapExtension } from './code-keymap.js';
import { AFFINE_CODE_TOOLBAR_WIDGET } from './code-toolbar/index.js';
import { codeSlashMenuConfig } from './configs/slash-menu.js';
export const codeToolbarWidget = WidgetViewExtension(
'affine:code',
AFFINE_CODE_TOOLBAR_WIDGET,
literal`${unsafeStatic(AFFINE_CODE_TOOLBAR_WIDGET)}`
);
export const CodeBlockSpec: ExtensionType[] = [
FlavourExtension('affine:code'),
CodeBlockHighlighter,
BlockViewExtension('affine:code', literal`affine-code`),
codeToolbarWidget,
CodeBlockInlineManagerExtension,
CodeBlockUnitSpecExtension,
CodeBlockAdapterExtensions,
SlashMenuConfigExtension('affine:code', codeSlashMenuConfig),
CodeKeymapExtension,
...getCodeClipboardExtensions(),
].flat();

View File

@@ -26,13 +26,11 @@ import { computed, effect, type Signal, signal } from '@preact/signals-core';
import { html, nothing, type TemplateResult } from 'lit'; import { html, nothing, type TemplateResult } from 'lit';
import { query } from 'lit/decorators.js'; import { query } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js'; import { classMap } from 'lit/directives/class-map.js';
import { styleMap } from 'lit/directives/style-map.js';
import { bundledLanguagesInfo, type ThemedToken } from 'shiki'; import { bundledLanguagesInfo, type ThemedToken } from 'shiki';
import { CodeBlockConfigExtension } from './code-block-config.js'; import { CodeBlockConfigExtension } from './code-block-config.js';
import { CodeBlockInlineManagerExtension } from './code-block-inline.js'; import { CodeBlockInlineManagerExtension } from './code-block-inline.js';
import { CodeBlockHighlighter } from './code-block-service.js'; import { CodeBlockHighlighter } from './code-block-service.js';
import { CodeBlockPreviewIdentifier } from './code-preview-extension.js';
import { codeBlockStyles } from './styles.js'; import { codeBlockStyles } from './styles.js';
export class CodeBlockComponent extends CaptionedBlockComponent<CodeBlockModel> { export class CodeBlockComponent extends CaptionedBlockComponent<CodeBlockModel> {
@@ -40,16 +38,6 @@ export class CodeBlockComponent extends CaptionedBlockComponent<CodeBlockModel>
private _inlineRangeProvider: InlineRangeProvider | null = null; private _inlineRangeProvider: InlineRangeProvider | null = null;
private readonly _localPreview$ = signal<boolean | null>(null);
preview$: Signal<boolean> = computed(() => {
const modelPreview = !!this.model.props.preview$.value;
if (this.store.readonly) {
return this._localPreview$.value ?? modelPreview;
}
return modelPreview;
});
highlightTokens$: Signal<ThemedToken[][]> = signal([]); highlightTokens$: Signal<ThemedToken[][]> = signal([]);
languageName$: Signal<string> = computed(() => { languageName$: Signal<string> = computed(() => {
@@ -78,7 +66,7 @@ export class CodeBlockComponent extends CaptionedBlockComponent<CodeBlockModel>
} }
get readonly() { get readonly() {
return this.store.readonly; return this.doc.readonly;
} }
get langs() { get langs() {
@@ -236,7 +224,7 @@ export class CodeBlockComponent extends CaptionedBlockComponent<CodeBlockModel>
return; return;
}, },
Tab: ctx => { Tab: ctx => {
if (this.store.readonly) return; if (this.doc.readonly) return;
const state = ctx.get('keyboardState'); const state = ctx.get('keyboardState');
const event = state.raw; const event = state.raw;
const inlineEditor = this.inlineEditor; const inlineEditor = this.inlineEditor;
@@ -344,10 +332,10 @@ export class CodeBlockComponent extends CaptionedBlockComponent<CodeBlockModel>
return true; return true;
}, },
Delete: () => { Delete: () => {
return; return true;
}, },
Enter: () => { Enter: () => {
this.store.captureSync(); this.doc.captureSync();
return true; return true;
}, },
'Mod-Enter': () => { 'Mod-Enter': () => {
@@ -358,16 +346,11 @@ export class CodeBlockComponent extends CaptionedBlockComponent<CodeBlockModel>
if (!inlineRange || !inlineEditor) return; if (!inlineRange || !inlineEditor) return;
const isEnd = model.props.text.length === inlineRange.index; const isEnd = model.props.text.length === inlineRange.index;
if (!isEnd) return; if (!isEnd) return;
const parent = this.store.getParent(model); const parent = this.doc.getParent(model);
if (!parent) return; if (!parent) return;
const index = parent.children.indexOf(model); const index = parent.children.indexOf(model);
if (index === -1) return; if (index === -1) return;
const id = this.store.addBlock( const id = this.doc.addBlock('affine:paragraph', {}, parent, index + 1);
'affine:paragraph',
{},
parent,
index + 1
);
focusTextModel(std, id); focusTextModel(std, id);
return true; return true;
}, },
@@ -378,7 +361,7 @@ export class CodeBlockComponent extends CaptionedBlockComponent<CodeBlockModel>
copyCode() { copyCode() {
const model = this.model; const model = this.model;
const slice = Slice.fromModels(model.store, [model]); const slice = Slice.fromModels(model.doc, [model]);
this.std.clipboard this.std.clipboard
.copySlice(slice) .copySlice(slice)
.then(() => { .then(() => {
@@ -398,16 +381,8 @@ export class CodeBlockComponent extends CaptionedBlockComponent<CodeBlockModel>
override renderBlock(): TemplateResult<1> { override renderBlock(): TemplateResult<1> {
const showLineNumbers = const showLineNumbers =
(this.std.getOptional(CodeBlockConfigExtension.identifier) this.std.getOptional(CodeBlockConfigExtension.identifier)
?.showLineNumbers ?? ?.showLineNumbers ?? true;
true) &&
(this.model.props.lineNumber ?? true);
const preview = this.preview$.value;
const previewContext = this.std.getOptional(
CodeBlockPreviewIdentifier(this.model.props.language ?? '')
);
const shouldRenderPreview = preview && previewContext;
return html` return html`
<div <div
@@ -415,50 +390,40 @@ export class CodeBlockComponent extends CaptionedBlockComponent<CodeBlockModel>
'affine-code-block-container': true, 'affine-code-block-container': true,
mobile: IS_MOBILE, mobile: IS_MOBILE,
wrap: this.model.props.wrap, wrap: this.model.props.wrap,
'disable-line-numbers': !showLineNumbers,
})} })}
> >
<rich-text <rich-text
style=${styleMap({
display: shouldRenderPreview ? 'none' : undefined,
})}
.yText=${this.model.props.text.yText} .yText=${this.model.props.text.yText}
.inlineEventSource=${this.topContenteditableElement ?? nothing} .inlineEventSource=${this.topContenteditableElement ?? nothing}
.undoManager=${this.store.history.undoManager} .undoManager=${this.doc.history}
.attributesSchema=${this.inlineManager.getSchema()} .attributesSchema=${this.inlineManager.getSchema()}
.attributeRenderer=${this.inlineManager.getRenderer()} .attributeRenderer=${this.inlineManager.getRenderer()}
.readonly=${this.store.readonly} .readonly=${this.doc.readonly}
.inlineRangeProvider=${this._inlineRangeProvider} .inlineRangeProvider=${this._inlineRangeProvider}
.enableClipboard=${false} .enableClipboard=${false}
.enableUndoRedo=${false} .enableUndoRedo=${false}
.wrapText=${this.model.props.wrap} .wrapText=${this.model.props.wrap}
.verticalScrollContainerGetter=${() => getViewportElement(this.host)} .verticalScrollContainerGetter=${() => getViewportElement(this.host)}
.vLineRenderer=${(vLine: VLine) => { .vLineRenderer=${showLineNumbers
return html` ? (vLine: VLine) => {
<span contenteditable="false" class="line-number" return html`
>${vLine.index + 1}</span <span contenteditable="false" class="line-number"
> >${vLine.index + 1}</span
${vLine.renderVElements()} >
`; ${vLine.renderVElements()}
}} `;
}
: undefined}
> >
</rich-text> </rich-text>
<div
style=${styleMap({
display: shouldRenderPreview ? undefined : 'none',
})}
contenteditable="false"
class="affine-code-block-preview"
>
${previewContext?.renderer(this.model)}
</div>
${this.renderChildren(this.model)} ${Object.values(this.widgets)} ${this.renderChildren(this.model)} ${Object.values(this.widgets)}
</div> </div>
`; `;
} }
setWrap(wrap: boolean) { setWrap(wrap: boolean) {
this.store.updateBlock(this.model, { wrap }); this.doc.updateBlock(this.model, { wrap });
} }
@query('rich-text') @query('rich-text')
@@ -471,14 +436,6 @@ export class CodeBlockComponent extends CaptionedBlockComponent<CodeBlockModel>
override accessor useCaptionEditor = true; override accessor useCaptionEditor = true;
override accessor useZeroWidth = true; override accessor useZeroWidth = true;
setPreviewState(preview: boolean) {
if (this.store.readonly) {
this._localPreview$.value = preview;
} else {
this.store.updateBlock(this.model, { preview });
}
}
} }
declare global { declare global {

View File

@@ -1,27 +0,0 @@
import type { CodeBlockModel } from '@blocksuite/affine-model';
import { createIdentifier } from '@blocksuite/global/di';
import type { ExtensionType } from '@blocksuite/store';
import type { HTMLTemplateResult } from 'lit';
export type CodeBlockPreviewRenderer = (
model: CodeBlockModel
) => HTMLTemplateResult | null;
export type CodeBlockPreviewContext = {
renderer: CodeBlockPreviewRenderer;
lang: string;
};
export const CodeBlockPreviewIdentifier =
createIdentifier<CodeBlockPreviewContext>('CodeBlockPreview');
export function CodeBlockPreviewExtension(
lang: string,
renderer: CodeBlockPreviewRenderer
): ExtensionType {
return {
setup: di => {
di.addImpl(CodeBlockPreviewIdentifier(lang), { renderer, lang });
},
};
}

View File

@@ -4,7 +4,6 @@ import type {
MenuItemGroup, MenuItemGroup,
} from '@blocksuite/affine-components/toolbar'; } from '@blocksuite/affine-components/toolbar';
import { renderGroups } from '@blocksuite/affine-components/toolbar'; import { renderGroups } from '@blocksuite/affine-components/toolbar';
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { WithDisposable } from '@blocksuite/global/lit'; import { WithDisposable } from '@blocksuite/global/lit';
import { noop } from '@blocksuite/global/utils'; import { noop } from '@blocksuite/global/utils';
import { MoreVerticalIcon } from '@blocksuite/icons/lit'; import { MoreVerticalIcon } from '@blocksuite/icons/lit';
@@ -31,11 +30,12 @@ export class AffineCodeToolbar extends WithDisposable(LitElement) {
padding: 4px; padding: 4px;
margin: 0; margin: 0;
display: flex; display: flex;
justify-content: flex-end;
} }
.code-toolbar-button { .code-toolbar-button {
color: ${unsafeCSSVarV2('icon/primary')}; color: var(--affine-icon-color);
background-color: ${unsafeCSSVarV2('button/secondary')}; background-color: var(--affine-background-primary-color);
box-shadow: var(--affine-shadow-1); box-shadow: var(--affine-shadow-1);
border-radius: 4px; border-radius: 4px;
} }

View File

@@ -4,10 +4,6 @@ import {
showPopFilterableList, showPopFilterableList,
} from '@blocksuite/affine-components/filterable-list'; } from '@blocksuite/affine-components/filterable-list';
import { ArrowDownIcon } from '@blocksuite/affine-components/icons'; import { ArrowDownIcon } from '@blocksuite/affine-components/icons';
import {
DocModeProvider,
TelemetryProvider,
} from '@blocksuite/affine-shared/services';
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme'; import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit'; import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
import { noop } from '@blocksuite/global/utils'; import { noop } from '@blocksuite/global/utils';
@@ -22,19 +18,24 @@ export class LanguageListButton extends WithDisposable(
SignalWatcher(LitElement) SignalWatcher(LitElement)
) { ) {
static override styles = css` static override styles = css`
:host {
margin-right: auto;
}
.lang-button { .lang-button {
background-color: var(--affine-background-primary-color);
box-shadow: var(--affine-shadow-1);
display: flex; display: flex;
gap: 4px; gap: 4px;
padding: 2px 4px; padding: 2px 4px;
height: 28px;
} }
.lang-button:hover { .lang-button:hover {
background: ${unsafeCSSVarV2('layer/background/hoverOverlay')}; background: var(--affine-hover-color-filled);
} }
.lang-button[hover] { .lang-button[hover] {
background: ${unsafeCSSVarV2('layer/background/hoverOverlay')}; background: var(--affine-hover-color-filled);
} }
.lang-button-icon { .lang-button-icon {
@@ -52,7 +53,7 @@ export class LanguageListButton extends WithDisposable(
private _abortController?: AbortController; private _abortController?: AbortController;
private readonly _clickLangBtn = () => { private readonly _clickLangBtn = () => {
if (this.blockComponent.store.readonly) return; if (this.blockComponent.doc.readonly) return;
if (this._abortController) { if (this._abortController) {
// Close the language list if it's already opened. // Close the language list if it's already opened.
this._abortController.abort(); this._abortController.abort();
@@ -74,21 +75,9 @@ export class LanguageListButton extends WithDisposable(
sortedBundledLanguages.splice(index, 1); sortedBundledLanguages.splice(index, 1);
sortedBundledLanguages.unshift(item); sortedBundledLanguages.unshift(item);
} }
this.blockComponent.store.transact(() => { this.blockComponent.doc.transact(() => {
this.blockComponent.model.props.language$.value = item.name; this.blockComponent.model.props.language$.value = item.name;
}); });
const std = this.blockComponent.std;
const mode =
std.getOptional(DocModeProvider)?.getEditorMode() ?? 'page';
const telemetryService = std.getOptional(TelemetryProvider);
if (!telemetryService) return;
telemetryService.track('codeBlockLanguageSelect', {
page: mode,
segment: 'code block',
module: 'language selector',
control: item.name,
});
}, },
active: item => item.name === this.blockComponent.model.props.language, active: item => item.name === this.blockComponent.model.props.language,
items: this._sortedBundledLanguages, items: this._sortedBundledLanguages,
@@ -149,10 +138,10 @@ export class LanguageListButton extends WithDisposable(
</div>`} </div>`}
height="24px" height="24px"
@click=${this._clickLangBtn} @click=${this._clickLangBtn}
?disabled=${this.blockComponent.store.readonly} ?disabled=${this.blockComponent.doc.readonly}
> >
<span class="lang-button-icon" slot="suffix"> <span class="lang-button-icon" slot="suffix">
${!this.blockComponent.store.readonly ? ArrowDownIcon : nothing} ${!this.blockComponent.doc.readonly ? ArrowDownIcon : nothing}
</span> </span>
</icon-button> `; </icon-button> `;
} }

View File

@@ -1,112 +0,0 @@
import {
DocModeProvider,
TelemetryProvider,
} from '@blocksuite/affine-shared/services';
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
import { css, html, LitElement, nothing } from 'lit';
import { property } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import type { CodeBlockComponent } from '../../code-block';
import { CodeBlockPreviewIdentifier } from '../../code-preview-extension';
export class PreviewButton extends WithDisposable(SignalWatcher(LitElement)) {
static override styles = css`
:host {
margin-right: auto;
}
.preview-toggle-container {
display: flex;
padding: 2px;
align-items: flex-start;
gap: 4px;
border-radius: 4px;
background: ${unsafeCSSVarV2('segment/background')};
}
.toggle-button {
display: flex;
padding: 0px 4px;
justify-content: center;
align-items: center;
gap: 4px;
border-radius: 4px;
color: ${unsafeCSSVarV2('text/primary')};
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 500;
line-height: 20px;
}
.toggle-button:hover {
background: ${unsafeCSSVarV2('layer/background/hoverOverlay')};
}
.toggle-button.active {
background: ${unsafeCSSVarV2('segment/button')};
box-shadow:
var(--Shadow-buttonShadow-1-x, 0px) var(--Shadow-buttonShadow-1-y, 0px)
var(--Shadow-buttonShadow-1-blur, 1px) 0px
var(--Shadow-buttonShadow-1-color, rgba(0, 0, 0, 0.12)),
var(--Shadow-buttonShadow-2-x, 0px) var(--Shadow-buttonShadow-2-y, 1px)
var(--Shadow-buttonShadow-2-blur, 5px) 0px
var(--Shadow-buttonShadow-2-color, rgba(0, 0, 0, 0.12));
}
`;
private readonly _toggle = (value: boolean) => {
this.blockComponent.setPreviewState(value);
const std = this.blockComponent.std;
const mode = std.getOptional(DocModeProvider)?.getEditorMode() ?? 'page';
const telemetryService = std.getOptional(TelemetryProvider);
if (!telemetryService) return;
telemetryService.track('htmlBlockTogglePreview', {
page: mode,
segment: 'code block',
module: 'code toolbar container',
control: 'preview toggle button',
});
};
get preview() {
return this.blockComponent.preview$.value;
}
override render() {
const lang = this.blockComponent.model.props.language$.value ?? '';
const previewContext = this.blockComponent.std.getOptional(
CodeBlockPreviewIdentifier(lang)
);
if (!previewContext) return nothing;
return html`
<div class="preview-toggle-container">
<div
class=${classMap({
'toggle-button': true,
active: !this.preview,
})}
@click=${() => this._toggle(false)}
>
Code
</div>
<div
class=${classMap({
'toggle-button': true,
active: this.preview,
})}
@click=${() => this._toggle(true)}
>
Preview
</div>
</div>
`;
}
@property({ attribute: false })
accessor blockComponent!: CodeBlockComponent;
}

View File

@@ -9,12 +9,10 @@ import {
import type { MenuItemGroup } from '@blocksuite/affine-components/toolbar'; import type { MenuItemGroup } from '@blocksuite/affine-components/toolbar';
import { isInsidePageEditor } from '@blocksuite/affine-shared/utils'; import { isInsidePageEditor } from '@blocksuite/affine-shared/utils';
import { noop, sleep } from '@blocksuite/global/utils'; import { noop, sleep } from '@blocksuite/global/utils';
import { NumberedListIcon } from '@blocksuite/icons/lit';
import { BlockSelection } from '@blocksuite/std'; import { BlockSelection } from '@blocksuite/std';
import { html } from 'lit'; import { html } from 'lit';
import { ifDefined } from 'lit/directives/if-defined.js'; import { ifDefined } from 'lit/directives/if-defined.js';
import { CodeBlockConfigExtension } from '../code-block-config.js';
import type { CodeBlockToolbarContext } from './context.js'; import type { CodeBlockToolbarContext } from './context.js';
import { duplicateCodeBlock } from './utils.js'; import { duplicateCodeBlock } from './utils.js';
@@ -44,18 +42,6 @@ export const PRIMARY_GROUPS: MenuItemGroup<CodeBlockToolbarContext>[] = [
}; };
}, },
}, },
{
type: 'preview',
generate: ({ blockComponent }) => {
return {
action: noop,
render: () => html`
<preview-button .blockComponent=${blockComponent}>
</preview-button>
`,
};
},
},
{ {
type: 'copy-code', type: 'copy-code',
label: 'Copy code', label: 'Copy code',
@@ -117,76 +103,27 @@ export const PRIMARY_GROUPS: MenuItemGroup<CodeBlockToolbarContext>[] = [
}, },
]; ];
export const toggleGroup: MenuItemGroup<CodeBlockToolbarContext> = {
type: 'toggle',
items: [
{
type: 'wrap',
generate: ({ blockComponent }) => {
return {
action: () => {},
render: () => {
const wrapped = blockComponent.model.props.wrap;
const label = wrapped ? 'Cancel wrap' : 'Wrap';
const icon = wrapped ? CancelWrapIcon : WrapIcon;
return html`
<editor-menu-action
@click=${() => {
blockComponent.setWrap(!wrapped);
}}
aria-label=${label}
>
${icon}
<span class="label">${label}</span>
<toggle-switch
style="margin-left: auto;"
.on="${wrapped}"
></toggle-switch>
</editor-menu-action>
`;
},
};
},
},
{
type: 'line-number',
when: ({ std }) =>
std.getOptional(CodeBlockConfigExtension.identifier)?.showLineNumbers ??
true,
generate: ({ blockComponent }) => {
return {
action: () => {},
render: () => {
const lineNumber = blockComponent.model.props.lineNumber ?? true;
const label = lineNumber ? 'Cancel line number' : 'Line number';
return html`
<editor-menu-action
@click=${() => {
blockComponent.store.updateBlock(blockComponent.model, {
lineNumber: !lineNumber,
});
}}
aria-label=${label}
>
${NumberedListIcon()}
<span class="label">${label}</span>
<toggle-switch
style="margin-left: auto;"
.on="${lineNumber}"
></toggle-switch>
</editor-menu-action>
`;
},
};
},
},
],
};
// Clipboard Group // Clipboard Group
export const clipboardGroup: MenuItemGroup<CodeBlockToolbarContext> = { export const clipboardGroup: MenuItemGroup<CodeBlockToolbarContext> = {
type: 'clipboard', type: 'clipboard',
items: [ items: [
{
type: 'wrap',
generate: ({ blockComponent, close }) => {
const wrapped = blockComponent.model.props.wrap;
const label = wrapped ? 'Cancel wrap' : 'Wrap';
const icon = wrapped ? CancelWrapIcon : WrapIcon;
return {
label,
icon,
action: () => {
blockComponent.setWrap(!wrapped);
close();
},
};
},
},
{ {
type: 'duplicate', type: 'duplicate',
label: 'Duplicate', label: 'Duplicate',
@@ -236,7 +173,6 @@ export const deleteGroup: MenuItemGroup<CodeBlockToolbarContext> = {
}; };
export const MORE_GROUPS: MenuItemGroup<CodeBlockToolbarContext>[] = [ export const MORE_GROUPS: MenuItemGroup<CodeBlockToolbarContext>[] = [
toggleGroup,
clipboardGroup, clipboardGroup,
deleteGroup, deleteGroup,
]; ];

View File

@@ -8,7 +8,7 @@ export class CodeBlockToolbarContext extends MenuContext {
}; };
get doc() { get doc() {
return this.blockComponent.store; return this.blockComponent.doc;
} }
get host() { get host() {

View File

@@ -12,5 +12,5 @@ export const duplicateCodeBlock = (model: CodeBlockModel) => {
...duplicateProps, ...duplicateProps,
}; };
return model.store.addSiblingBlocks(model, [newProps])[0]; return model.doc.addSiblingBlocks(model, [newProps])[0];
}; };

View File

@@ -5,7 +5,6 @@ import {
} from './code-toolbar'; } from './code-toolbar';
import { AffineCodeToolbar } from './code-toolbar/components/code-toolbar'; import { AffineCodeToolbar } from './code-toolbar/components/code-toolbar';
import { LanguageListButton } from './code-toolbar/components/lang-button'; import { LanguageListButton } from './code-toolbar/components/lang-button';
import { PreviewButton } from './code-toolbar/components/preview-button';
import { AffineCodeUnit } from './highlight/affine-code-unit'; import { AffineCodeUnit } from './highlight/affine-code-unit';
export function effects() { export function effects() {
@@ -14,14 +13,12 @@ export function effects() {
customElements.define(AFFINE_CODE_TOOLBAR_WIDGET, AffineCodeToolbarWidget); customElements.define(AFFINE_CODE_TOOLBAR_WIDGET, AffineCodeToolbarWidget);
customElements.define('affine-code-unit', AffineCodeUnit); customElements.define('affine-code-unit', AffineCodeUnit);
customElements.define('affine-code', CodeBlockComponent); customElements.define('affine-code', CodeBlockComponent);
customElements.define('preview-button', PreviewButton);
} }
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
'language-list-button': LanguageListButton; 'language-list-button': LanguageListButton;
'affine-code-toolbar': AffineCodeToolbar; 'affine-code-toolbar': AffineCodeToolbar;
'preview-button': PreviewButton;
[AFFINE_CODE_TOOLBAR_WIDGET]: AffineCodeToolbarWidget; [AFFINE_CODE_TOOLBAR_WIDGET]: AffineCodeToolbarWidget;
} }
} }

View File

@@ -2,7 +2,7 @@ export * from './adapters';
export * from './clipboard'; export * from './clipboard';
export * from './code-block'; export * from './code-block';
export * from './code-block-config'; export * from './code-block-config';
export * from './code-preview-extension'; export * from './code-block-spec';
export * from './code-toolbar'; export * from './code-toolbar';
export * from './turbo/code-layout-handler'; export * from './turbo/code-layout-handler';
export * from './turbo/code-painter.worker'; export * from './turbo/code-painter.worker';

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