mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-08 18:43:46 +00:00
Compare commits
5 Commits
v0.22.1
...
04-02-chor
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2bfc7e33f1 | ||
|
|
e320240f24 | ||
|
|
93d93abd8a | ||
|
|
7fbe5173c3 | ||
|
|
359ed9698b |
@@ -6,7 +6,6 @@ yarn install
|
||||
|
||||
# Build Server Dependencies
|
||||
yarn affine @affine/server-native build
|
||||
yarn affine @affine/reader build
|
||||
|
||||
# Create database
|
||||
yarn affine @affine/server prisma migrate reset -f
|
||||
|
||||
@@ -10,7 +10,6 @@ services:
|
||||
environment:
|
||||
DATABASE_URL: postgresql://affine:affine@db:5432/affine
|
||||
REDIS_SERVER_HOST: redis
|
||||
AFFINE_INDEXER_SEARCH_ENDPOINT: http://indexer:9308
|
||||
|
||||
db:
|
||||
image: pgvector/pgvector:pg16
|
||||
@@ -24,19 +23,5 @@ services:
|
||||
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:
|
||||
postgres-data:
|
||||
manticoresearch_data:
|
||||
|
||||
@@ -3,13 +3,4 @@ DB_VERSION=16
|
||||
# database credentials
|
||||
DB_PASSWORD=affine
|
||||
DB_USERNAME=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
|
||||
DB_DATABASE_NAME=affine
|
||||
5
.docker/dev/.gitignore
vendored
5
.docker/dev/.gitignore
vendored
@@ -1,6 +1,3 @@
|
||||
postgres
|
||||
.env
|
||||
compose.yml
|
||||
certs/*
|
||||
!certs/.gitkeep
|
||||
nginx/conf.d/*
|
||||
compose.yml
|
||||
@@ -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
|
||||
```
|
||||
@@ -24,78 +24,8 @@ services:
|
||||
- 1025:1025
|
||||
- 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:
|
||||
dev:
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
manticoresearch_data:
|
||||
elasticsearch_data:
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
log-queries
|
||||
address=/dev.affine.fail/127.0.0.1
|
||||
@@ -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/*";
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -20,4 +20,4 @@ CONFIG_LOCATION=~/.affine/self-host/config
|
||||
# database credentials
|
||||
DB_USERNAME=affine
|
||||
DB_PASSWORD=
|
||||
DB_DATABASE=affine
|
||||
DB_DATABASE=affine
|
||||
1
.docker/selfhost/.gitignore
vendored
1
.docker/selfhost/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
.env
|
||||
@@ -21,7 +21,6 @@ services:
|
||||
environment:
|
||||
- REDIS_SERVER_HOST=redis
|
||||
- DATABASE_URL=postgresql://${DB_USERNAME}:${DB_PASSWORD}@postgres:5432/${DB_DATABASE:-affine}
|
||||
- AFFINE_INDEXER_ENABLED=false
|
||||
restart: unless-stopped
|
||||
|
||||
affine_migration:
|
||||
@@ -37,7 +36,6 @@ services:
|
||||
environment:
|
||||
- REDIS_SERVER_HOST=redis
|
||||
- DATABASE_URL=postgresql://${DB_USERNAME}:${DB_PASSWORD}@postgres:5432/${DB_DATABASE:-affine}
|
||||
- AFFINE_INDEXER_ENABLED=false
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
@@ -55,7 +53,7 @@ services:
|
||||
restart: unless-stopped
|
||||
|
||||
postgres:
|
||||
image: pgvector/pgvector:pg16
|
||||
image: postgres:16
|
||||
container_name: affine_postgres
|
||||
volumes:
|
||||
- ${DB_DATA_LOCATION}:/var/lib/postgresql/data
|
||||
|
||||
@@ -31,13 +31,9 @@
|
||||
"properties": {
|
||||
"queue": {
|
||||
"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": {
|
||||
"attempts": 5,
|
||||
"backoff": {
|
||||
"type": "exponential",
|
||||
"delay": 1000
|
||||
},
|
||||
"removeOnComplete": true,
|
||||
"removeOnFail": {
|
||||
"age": 86400,
|
||||
@@ -52,19 +48,7 @@
|
||||
},
|
||||
"queues.copilot": {
|
||||
"type": "object",
|
||||
"description": "The config for copilot job queue\n@default {\"concurrency\":10}",
|
||||
"properties": {
|
||||
"concurrency": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"concurrency": 10
|
||||
}
|
||||
},
|
||||
"queues.doc": {
|
||||
"type": "object",
|
||||
"description": "The config for doc job queue\n@default {\"concurrency\":1}",
|
||||
"description": "The config for copilot job queue\n@default {\"concurrency\":1}",
|
||||
"properties": {
|
||||
"concurrency": {
|
||||
"type": "number"
|
||||
@@ -74,9 +58,9 @@
|
||||
"concurrency": 1
|
||||
}
|
||||
},
|
||||
"queues.indexer": {
|
||||
"queues.doc": {
|
||||
"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": {
|
||||
"concurrency": {
|
||||
"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": {
|
||||
"type": "object",
|
||||
"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": {
|
||||
"type": "object",
|
||||
"description": "Configuration for server module",
|
||||
@@ -643,41 +627,6 @@
|
||||
"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": {
|
||||
"type": "object",
|
||||
"description": "The config for the perplexity provider.\n@default {\"apiKey\":\"\"}",
|
||||
@@ -685,48 +634,6 @@
|
||||
"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": {
|
||||
"type": "object",
|
||||
"description": "The config for the unsplash key.\n@default {\"key\":\"\"}",
|
||||
@@ -734,13 +641,6 @@
|
||||
"key": ""
|
||||
}
|
||||
},
|
||||
"exa": {
|
||||
"type": "object",
|
||||
"description": "The config for the exa web search key.\n@default {\"key\":\"\"}",
|
||||
"default": {
|
||||
"key": ""
|
||||
}
|
||||
},
|
||||
"storage": {
|
||||
"type": "object",
|
||||
"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 true\n@environment `AFFINE_INDEXER_ENABLED`",
|
||||
"default": true
|
||||
},
|
||||
"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": {
|
||||
"type": "object",
|
||||
"description": "Configuration for oauth module",
|
||||
@@ -965,43 +824,13 @@
|
||||
},
|
||||
"providers.oidc": {
|
||||
"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",
|
||||
"properties": {
|
||||
"clientId": {
|
||||
"type": "string"
|
||||
},
|
||||
"clientSecret": {
|
||||
"type": "string"
|
||||
},
|
||||
"args": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"description": "OIDC OAuth provider config\n@default {\"clientId\":\"\",\"clientSecret\":\"\",\"issuer\":\"\",\"args\":{}}",
|
||||
"default": {
|
||||
"clientId": "",
|
||||
"clientSecret": "",
|
||||
"issuer": "",
|
||||
"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": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
9
.github/actions/deploy/deploy.mjs
vendored
9
.github/actions/deploy/deploy.mjs
vendored
@@ -16,9 +16,6 @@ const {
|
||||
REDIS_SERVER_HOST,
|
||||
REDIS_SERVER_PASSWORD,
|
||||
STATIC_IP_NAME,
|
||||
AFFINE_INDEXER_SEARCH_PROVIDER,
|
||||
AFFINE_INDEXER_SEARCH_ENDPOINT,
|
||||
AFFINE_INDEXER_SEARCH_API_KEY,
|
||||
} = process.env;
|
||||
|
||||
const buildType = BUILD_TYPE || 'canary';
|
||||
@@ -84,11 +81,6 @@ const createHelmCommand = ({ isDryRun }) => {
|
||||
`--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 = [
|
||||
`--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}\\" }"`,
|
||||
@@ -138,7 +130,6 @@ const createHelmCommand = ({ isDryRun }) => {
|
||||
`--set-string global.ingress.host="${host}"`,
|
||||
`--set-string global.version="${APP_VERSION}"`,
|
||||
...redisAndPostgres,
|
||||
...indexerOptions,
|
||||
`--set web.replicaCount=${replica.web}`,
|
||||
`--set-string web.image.tag="${imageTag}"`,
|
||||
`--set graphql.replicaCount=${replica.graphql}`,
|
||||
|
||||
16
.github/actions/server-test-env/action.yml
vendored
16
.github/actions/server-test-env/action.yml
vendored
@@ -4,11 +4,6 @@ description: 'Prepare Server Test Environment'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Bundle @affine/reader
|
||||
shell: bash
|
||||
run: |
|
||||
yarn affine @affine/reader build
|
||||
|
||||
- name: Initialize database
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -26,10 +21,13 @@ runs:
|
||||
yarn affine @affine/server prisma generate
|
||||
yarn affine @affine/server prisma migrate deploy
|
||||
yarn affine @affine/server data-migration run
|
||||
|
||||
- name: Import config
|
||||
shell: bash
|
||||
env:
|
||||
DEFAULT_CONFIG: '{}'
|
||||
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
|
||||
|
||||
@@ -69,15 +69,6 @@ spec:
|
||||
key: redis-password
|
||||
- name: REDIS_SERVER_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
|
||||
value: "{{ .Values.global.docService.port }}"
|
||||
- name: AFFINE_SERVER_SUB_PATH
|
||||
|
||||
@@ -67,15 +67,6 @@ spec:
|
||||
key: redis-password
|
||||
- name: REDIS_SERVER_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
|
||||
value: "{{ .Values.service.port }}"
|
||||
- name: AFFINE_SERVER_SUB_PATH
|
||||
|
||||
@@ -44,15 +44,6 @@ spec:
|
||||
secretKeyRef:
|
||||
name: redis
|
||||
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:
|
||||
requests:
|
||||
cpu: '100m'
|
||||
|
||||
@@ -69,15 +69,6 @@ spec:
|
||||
key: redis-password
|
||||
- name: REDIS_SERVER_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
|
||||
value: "{{ .Values.service.port }}"
|
||||
- name: AFFINE_SERVER_SUB_PATH
|
||||
|
||||
@@ -69,15 +69,6 @@ spec:
|
||||
key: redis-password
|
||||
- name: REDIS_SERVER_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
|
||||
value: "{{ .Values.service.port }}"
|
||||
- name: AFFINE_SERVER_HOST
|
||||
|
||||
@@ -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 }}
|
||||
13
.github/helm/affine/templates/monitoring.yaml
vendored
13
.github/helm/affine/templates/monitoring.yaml
vendored
@@ -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 }}
|
||||
5
.github/helm/affine/values.yaml
vendored
5
.github/helm/affine/values.yaml
vendored
@@ -21,11 +21,6 @@ global:
|
||||
username: ''
|
||||
password: ''
|
||||
database: 0
|
||||
indexer:
|
||||
provider: ''
|
||||
endpoint: ''
|
||||
username: ''
|
||||
password: ''
|
||||
docService:
|
||||
name: 'affine-doc'
|
||||
port: 3020
|
||||
|
||||
8
.github/workflows/build-images.yml
vendored
8
.github/workflows/build-images.yml
vendored
@@ -136,9 +136,6 @@ jobs:
|
||||
extra-flags: workspaces focus @affine/server-native
|
||||
- name: 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:
|
||||
target: ${{ matrix.targets.name }}
|
||||
package: '@affine/server-native'
|
||||
@@ -175,8 +172,6 @@ jobs:
|
||||
path: ./packages/backend/native
|
||||
- name: List server-native files
|
||||
run: ls -alh ./packages/backend/native
|
||||
- name: Build @affine/reader
|
||||
run: yarn workspace @affine/reader build
|
||||
- name: Build Server
|
||||
run: yarn workspace @affine/server build
|
||||
- name: Upload server dist
|
||||
@@ -258,9 +253,6 @@ jobs:
|
||||
- name: Generate Prisma client
|
||||
run: yarn workspace @affine/server prisma generate
|
||||
|
||||
- name: Mv node_modules
|
||||
run: mv ./node_modules ./packages/backend/server
|
||||
|
||||
- name: Setup Version
|
||||
id: version
|
||||
uses: ./.github/actions/setup-version
|
||||
|
||||
168
.github/workflows/build-test.yml
vendored
168
.github/workflows/build-test.yml
vendored
@@ -125,7 +125,6 @@ jobs:
|
||||
- name: Run BS Docs Build
|
||||
run: |
|
||||
yarn affine bs-docs build
|
||||
git checkout packages/frontend/i18n/src/i18n-completenesses.json
|
||||
git status --porcelain | grep . && {
|
||||
echo "Run 'yarn typecheck && yarn affine bs-docs build' and make sure all changes are submitted"
|
||||
exit 1
|
||||
@@ -151,15 +150,13 @@ jobs:
|
||||
- name: Clippy
|
||||
run: |
|
||||
rustup component add clippy
|
||||
cargo clippy --workspace --exclude affine_server_native --all-targets --all-features -- -D warnings
|
||||
cargo clippy -p affine_server_native --all-targets --all-features -- -D warnings
|
||||
cargo clippy --all-targets --all-features -- -D warnings
|
||||
|
||||
check-git-status:
|
||||
name: Check Git Status
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- optimize_ci
|
||||
- build-server-native
|
||||
if: needs.optimize_ci.outputs.skip == 'false'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -168,26 +165,13 @@ jobs:
|
||||
with:
|
||||
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
|
||||
run: |
|
||||
yarn affine init
|
||||
yarn affine gql build
|
||||
yarn affine i18n build
|
||||
yarn affine server genconfig
|
||||
git checkout packages/frontend/i18n/src/i18n-completenesses.json
|
||||
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
|
||||
} || {
|
||||
echo "All changes are submitted"
|
||||
@@ -558,86 +542,12 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
node_index: [0, 1, 2, 3, 4, 5, 6, 7]
|
||||
total_nodes: [8]
|
||||
node_index: [0, 1, 2, 3]
|
||||
total_nodes: [4]
|
||||
env:
|
||||
NODE_ENV: test
|
||||
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
|
||||
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:
|
||||
postgres:
|
||||
image: pgvector/pgvector:pg16
|
||||
@@ -660,20 +570,6 @@ jobs:
|
||||
- 1025:1025
|
||||
- 8025:8025
|
||||
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
|
||||
|
||||
- name: Setup Node.js
|
||||
@@ -691,8 +587,8 @@ jobs:
|
||||
- name: Prepare Server Test Environment
|
||||
uses: ./.github/actions/server-test-env
|
||||
|
||||
- name: Run server tests with elasticsearch only
|
||||
run: yarn affine @affine/server test:coverage "**/*/*elasticsearch.spec.ts" --forbid-only
|
||||
- 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 }}
|
||||
@@ -735,10 +631,6 @@ jobs:
|
||||
image: redis
|
||||
ports:
|
||||
- 6379:6379
|
||||
indexer:
|
||||
image: manticoresearch/manticore:9.3.2
|
||||
ports:
|
||||
- 9308:9308
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -924,7 +816,7 @@ jobs:
|
||||
uses: taiki-e/install-action@nextest
|
||||
|
||||
- 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:
|
||||
name: Server Copilot Api Test
|
||||
@@ -959,10 +851,6 @@ jobs:
|
||||
ports:
|
||||
- 1025:1025
|
||||
- 8025:8025
|
||||
indexer:
|
||||
image: manticoresearch/manticore:9.3.2
|
||||
ports:
|
||||
- 9308:9308
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -1002,7 +890,12 @@ jobs:
|
||||
- name: Prepare Server Test Environment
|
||||
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.apifilter.outputs.changed == 'true' }}
|
||||
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
|
||||
|
||||
- name: Run server tests
|
||||
@@ -1053,10 +946,6 @@ jobs:
|
||||
image: redis
|
||||
ports:
|
||||
- 6379:6379
|
||||
indexer:
|
||||
image: manticoresearch/manticore:9.3.2
|
||||
ports:
|
||||
- 9308:9308
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -1079,7 +968,6 @@ jobs:
|
||||
- 'packages/backend/server/src/plugins/copilot/**'
|
||||
- 'packages/backend/server/tests/copilot.*'
|
||||
- 'packages/frontend/core/src/blocksuite/ai/**'
|
||||
- 'packages/frontend/core/src/modules/workspace-indexer-embedding/**'
|
||||
- 'tests/affine-cloud-copilot/**'
|
||||
|
||||
- name: Setup Node.js
|
||||
@@ -1101,7 +989,12 @@ jobs:
|
||||
- name: Prepare Server Test Environment
|
||||
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.e2efilter.outputs.changed == 'true' }}
|
||||
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
|
||||
|
||||
- name: Run Copilot E2E Test ${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
|
||||
@@ -1175,10 +1068,6 @@ jobs:
|
||||
ports:
|
||||
- 1025:1025
|
||||
- 8025:8025
|
||||
indexer:
|
||||
image: manticoresearch/manticore:9.3.2
|
||||
ports:
|
||||
- 9308:9308
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -1348,13 +1237,6 @@ jobs:
|
||||
target: x86_64-unknown-linux-gnu,
|
||||
test: true,
|
||||
}
|
||||
- {
|
||||
os: windows-latest,
|
||||
platform: windows,
|
||||
arch: x64,
|
||||
target: x86_64-pc-windows-msvc,
|
||||
test: true,
|
||||
}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Node.js
|
||||
@@ -1395,18 +1277,6 @@ jobs:
|
||||
HOIST_NODE_MODULES: 1
|
||||
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)
|
||||
run: |
|
||||
sudo add-apt-repository universe
|
||||
|
||||
22
.github/workflows/copilot-test.yml
vendored
22
.github/workflows/copilot-test.yml
vendored
@@ -59,10 +59,6 @@ jobs:
|
||||
ports:
|
||||
- 1025:1025
|
||||
- 8025:8025
|
||||
indexer:
|
||||
image: manticoresearch/manticore:9.3.2
|
||||
ports:
|
||||
- 9308:9308
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -81,7 +77,12 @@ jobs:
|
||||
|
||||
- name: Prepare Server Test Environment
|
||||
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
|
||||
|
||||
- name: Run server tests
|
||||
@@ -129,10 +130,6 @@ jobs:
|
||||
image: redis
|
||||
ports:
|
||||
- 6379:6379
|
||||
indexer:
|
||||
image: manticoresearch/manticore:9.3.2
|
||||
ports:
|
||||
- 9308:9308
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -151,7 +148,12 @@ jobs:
|
||||
|
||||
- name: Prepare Server Test Environment
|
||||
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
|
||||
|
||||
- name: Run Copilot E2E Test ${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
|
||||
|
||||
7
.github/workflows/deploy.yml
vendored
7
.github/workflows/deploy.yml
vendored
@@ -103,9 +103,6 @@ jobs:
|
||||
CLOUD_SQL_IAM_ACCOUNT: ${{ secrets.CLOUD_SQL_IAM_ACCOUNT }}
|
||||
APP_IAM_ACCOUNT: ${{ secrets.APP_IAM_ACCOUNT }}
|
||||
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:
|
||||
needs:
|
||||
@@ -159,7 +156,7 @@ jobs:
|
||||
BLOCKSUITE_REPO_PATH: ${{ github.workspace }}/blocksuite
|
||||
- name: Post Failed event to a Slack channel
|
||||
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') }}
|
||||
with:
|
||||
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 }}`>"
|
||||
- name: Post Cancel event to a Slack channel
|
||||
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') }}
|
||||
with:
|
||||
token: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||
|
||||
2
.github/workflows/release-desktop.yml
vendored
2
.github/workflows/release-desktop.yml
vendored
@@ -252,7 +252,7 @@ jobs:
|
||||
shell: bash
|
||||
# node_modules of nbstore is not needed for building, and it will make the build process out of memory
|
||||
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
|
||||
|
||||
- name: package
|
||||
|
||||
25
.github/workflows/release-mobile.yml
vendored
25
.github/workflows/release-mobile.yml
vendored
@@ -117,10 +117,31 @@ jobs:
|
||||
name: android
|
||||
path: packages/frontend/apps/android/dist
|
||||
|
||||
ios:
|
||||
runs-on: ${{ github.ref_name == 'canary' && 'macos-latest' || 'blaze/macos-14' }}
|
||||
determine-ios-runner:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- 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:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Download mobile artifact
|
||||
|
||||
@@ -38,5 +38,3 @@ packages/frontend/apps/ios/App/**
|
||||
tests/blocksuite/snapshots
|
||||
blocksuite/docs/api/**
|
||||
packages/frontend/admin/src/config.json
|
||||
**/test-docs.json
|
||||
**/test-blocks.json
|
||||
|
||||
966
Cargo.lock
generated
966
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
14
Cargo.toml
14
Cargo.toml
@@ -47,9 +47,9 @@ log = "0.4"
|
||||
loom = { version = "0.7", features = ["checkpoint"] }
|
||||
mimalloc = "0.1"
|
||||
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-derive = { version = "3.0.0-beta.3" }
|
||||
napi-derive = { version = "3.0.0-alpha.28" }
|
||||
nom = "8"
|
||||
notify = { version = "8", features = ["serde"] }
|
||||
objc2 = "0.6"
|
||||
@@ -57,7 +57,7 @@ objc2-foundation = "0.3"
|
||||
once_cell = "1"
|
||||
ordered-float = "5"
|
||||
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" }
|
||||
phf = { version = "0.11", features = ["macros"] }
|
||||
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"] }
|
||||
strum_macros = "0.27.0"
|
||||
symphonia = { version = "0.5", features = ["all", "opt-simd"] }
|
||||
text-splitter = "0.27"
|
||||
text-splitter = "0.25"
|
||||
thiserror = "2"
|
||||
tiktoken-rs = "0.7"
|
||||
tokio = "1.45"
|
||||
tiktoken-rs = "0.6"
|
||||
tokio = "1.37"
|
||||
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-cpp = { version = "0.23" }
|
||||
tree-sitter-go = { version = "0.23" }
|
||||
|
||||
@@ -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://app.affine.pro">Live Demo</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>
|
||||
<br/>
|
||||
|
||||
@@ -89,7 +89,7 @@ Star us, and you will receive all release notifications from GitHub without any
|
||||
|
||||
**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
|
||||
|
||||
@@ -191,9 +191,7 @@ We would like to express our gratitude to all the individuals who have already c
|
||||
|
||||
## 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).
|
||||
|
||||
[](https://template.run.claw.cloud/?openapp=system-fastdeploy%3FtemplateName%3Daffine)
|
||||
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).
|
||||
|
||||
## Hiring
|
||||
|
||||
|
||||
@@ -33,7 +33,6 @@
|
||||
"@blocksuite/affine-components": "workspace:*",
|
||||
"@blocksuite/affine-ext-loader": "workspace:*",
|
||||
"@blocksuite/affine-foundation": "workspace:*",
|
||||
"@blocksuite/affine-fragment-adapter-panel": "workspace:*",
|
||||
"@blocksuite/affine-fragment-doc-title": "workspace:*",
|
||||
"@blocksuite/affine-fragment-frame-panel": "workspace:*",
|
||||
"@blocksuite/affine-fragment-outline": "workspace:*",
|
||||
@@ -59,14 +58,11 @@
|
||||
"@blocksuite/affine-shared": "workspace:*",
|
||||
"@blocksuite/affine-widget-drag-handle": "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-zoom-toolbar": "workspace:*",
|
||||
"@blocksuite/affine-widget-frame-title": "workspace:*",
|
||||
"@blocksuite/affine-widget-keyboard-toolbar": "workspace:*",
|
||||
"@blocksuite/affine-widget-linked-doc": "workspace:*",
|
||||
"@blocksuite/affine-widget-note-slicer": "workspace:*",
|
||||
"@blocksuite/affine-widget-page-dragging-area": "workspace:*",
|
||||
"@blocksuite/affine-widget-remote-selection": "workspace:*",
|
||||
"@blocksuite/affine-widget-scroll-anchoring": "workspace:*",
|
||||
@@ -182,8 +178,6 @@
|
||||
"./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/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/view": "./src/widgets/edgeless-toolbar/view.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/outline": "./src/fragments/outline/index.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/store": "./src/gfx/text/store.ts",
|
||||
"./gfx/text/view": "./src/gfx/text/view.ts",
|
||||
@@ -266,7 +258,6 @@
|
||||
"./components/toolbar": "./src/components/toolbar.ts",
|
||||
"./components/view-dropdown-menu": "./src/components/view-dropdown-menu.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/effects": "./src/rich-text/effects.ts",
|
||||
"./shared/adapters": "./src/shared/adapters.ts",
|
||||
@@ -282,9 +273,7 @@
|
||||
"./model": "./src/model/index.ts",
|
||||
"./sync": "./src/sync/index.ts",
|
||||
"./extensions/store": "./src/extensions/store.ts",
|
||||
"./extensions/view": "./src/extensions/view.ts",
|
||||
"./foundation/store": "./src/foundation/store.ts",
|
||||
"./foundation/view": "./src/foundation/view.ts"
|
||||
"./extensions/view": "./src/extensions/view.ts"
|
||||
},
|
||||
"files": [
|
||||
"src",
|
||||
@@ -295,7 +284,6 @@
|
||||
"version": "0.21.0",
|
||||
"devDependencies": {
|
||||
"@vanilla-extract/vite-plugin": "^5.0.0",
|
||||
"msw": "^2.8.4",
|
||||
"vitest": "3.1.3"
|
||||
"vitest": "3.1.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2360,65 +2360,6 @@ describe('html to snapshot', () => {
|
||||
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 () => {
|
||||
const html = template(
|
||||
`<p><span>aaa</span><span>bbb</span><span>ccc</span></p>`
|
||||
@@ -2697,335 +2638,4 @@ describe('html to snapshot', () => {
|
||||
});
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,9 +4,6 @@ import {
|
||||
TableModelFlavour,
|
||||
} from '@blocksuite/affine-model';
|
||||
import {
|
||||
CalloutAdmonitionType,
|
||||
CalloutExportStyle,
|
||||
calloutMarkdownExportMiddleware,
|
||||
embedSyncedDocMiddleware,
|
||||
MarkdownAdapter,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
@@ -2452,288 +2449,119 @@ World!
|
||||
expect(target.file).toBe(markdown);
|
||||
});
|
||||
|
||||
describe('callout', () => {
|
||||
test('without export middleware', async () => {
|
||||
const blockSnapshot: BlockSnapshot = {
|
||||
type: 'block',
|
||||
id: 'block:vu6SK6WJpW',
|
||||
flavour: 'affine:page',
|
||||
props: {
|
||||
title: {
|
||||
'$blocksuite:internal:text$': true,
|
||||
delta: [],
|
||||
},
|
||||
test('callout', 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: [],
|
||||
},
|
||||
children: [
|
||||
{
|
||||
type: 'block',
|
||||
id: 'block:Tk4gSPocAt',
|
||||
flavour: 'affine:surface',
|
||||
props: {
|
||||
elements: {},
|
||||
},
|
||||
{
|
||||
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: '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: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: '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
|
||||
|
||||
> \\[!]
|
||||
>
|
||||
> Warning second callout without emoji
|
||||
>
|
||||
> Text in second callout
|
||||
> Second callout without emoji
|
||||
|
||||
> This is a regular blockquote
|
||||
`;
|
||||
|
||||
const mdAdapter = new MarkdownAdapter(createJob(), provider);
|
||||
const target = await mdAdapter.fromBlockSnapshot({
|
||||
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);
|
||||
const mdAdapter = new MarkdownAdapter(createJob(), provider);
|
||||
const target = await mdAdapter.fromBlockSnapshot({
|
||||
snapshot: blockSnapshot,
|
||||
});
|
||||
expect(target.file).toBe(markdown);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4393,61 +4221,6 @@ hhh
|
||||
},
|
||||
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);
|
||||
});
|
||||
|
||||
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 () => {
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import { DefaultTheme, NoteDisplayMode } from '@blocksuite/affine-model';
|
||||
import { NotionHtmlAdapter } from '@blocksuite/affine-shared/adapters';
|
||||
import { DEFAULT_IMAGE_PROXY_ENDPOINT } from '@blocksuite/affine-shared/consts';
|
||||
import {
|
||||
AssetsManager,
|
||||
type BlockSnapshot,
|
||||
MemoryBlobCRUD,
|
||||
} from '@blocksuite/store';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vitest';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import { createJob } from '../utils/create-job.js';
|
||||
import { getProvider } from '../utils/get-provider.js';
|
||||
@@ -1198,71 +1195,43 @@ describe('notion html to snapshot', () => {
|
||||
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
|
||||
});
|
||||
|
||||
describe('image', () => {
|
||||
const originalUrl =
|
||||
'https://raw.githubusercontent.com/toeverything/blocksuite/master/assets/logo.svg';
|
||||
test('image', async () => {
|
||||
const html = `<div class="page-body">
|
||||
<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 imageUrl = `${imageProxy}?url=${encodeURIComponent(originalUrl)}`;
|
||||
|
||||
// Mock the image request
|
||||
const imageRequestHandlers = [
|
||||
http.get(imageUrl.toString(), async () => {
|
||||
// Return a mock image blob
|
||||
const mockImageBlob = new Blob(['mock image data'], {
|
||||
type: 'image/svg+xml',
|
||||
});
|
||||
return new HttpResponse(mockImageBlob, {
|
||||
headers: {
|
||||
'Content-Type': 'image/svg+xml',
|
||||
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]',
|
||||
},
|
||||
});
|
||||
}),
|
||||
];
|
||||
|
||||
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: [],
|
||||
},
|
||||
children: [
|
||||
{
|
||||
type: 'block',
|
||||
id: 'matchesReplaceMap[1]',
|
||||
flavour: 'affine:image',
|
||||
props: {
|
||||
sourceId: 'matchesReplaceMap[2]',
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
],
|
||||
};
|
||||
|
||||
const adapter = new NotionHtmlAdapter(createJob(), provider);
|
||||
const rawBlockSnapshot = await adapter.toBlockSnapshot({
|
||||
file: html,
|
||||
assets: new AssetsManager({ blob: new MemoryBlobCRUD() }),
|
||||
});
|
||||
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
|
||||
const adapter = new NotionHtmlAdapter(createJob(), provider);
|
||||
const rawBlockSnapshot = await adapter.toBlockSnapshot({
|
||||
file: html,
|
||||
assets: new AssetsManager({ blob: new MemoryBlobCRUD() }),
|
||||
});
|
||||
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
|
||||
});
|
||||
|
||||
test('bookmark', async () => {
|
||||
|
||||
@@ -106,65 +106,4 @@ describe('notion-text to snapshot', () => {
|
||||
});
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export * from '@blocksuite/affine-components/resource';
|
||||
@@ -19,7 +19,6 @@ import { SurfaceViewExtension } from '@blocksuite/affine-block-surface/view';
|
||||
import { SurfaceRefViewExtension } from '@blocksuite/affine-block-surface-ref/view';
|
||||
import { TableViewExtension } from '@blocksuite/affine-block-table/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 { FramePanelViewExtension } from '@blocksuite/affine-fragment-frame-panel/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 { DragHandleViewExtension } from '@blocksuite/affine-widget-drag-handle/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 { EdgelessZoomToolbarViewExtension } from '@blocksuite/affine-widget-edgeless-zoom-toolbar/view';
|
||||
import { FrameTitleViewExtension } from '@blocksuite/affine-widget-frame-title/view';
|
||||
import { KeyboardToolbarViewExtension } from '@blocksuite/affine-widget-keyboard-toolbar/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 { RemoteSelectionViewExtension } from '@blocksuite/affine-widget-remote-selection/view';
|
||||
import { ScrollAnchoringViewExtension } from '@blocksuite/affine-widget-scroll-anchoring/view';
|
||||
@@ -103,9 +99,9 @@ export function getInternalViewExtensions() {
|
||||
InlinePresetViewExtension,
|
||||
|
||||
// Widget
|
||||
// order will affect the z-index of the widget
|
||||
DragHandleViewExtension,
|
||||
EdgelessAutoConnectViewExtension,
|
||||
EdgelessToolbarViewExtension,
|
||||
FrameTitleViewExtension,
|
||||
KeyboardToolbarViewExtension,
|
||||
LinkedDocViewExtension,
|
||||
@@ -116,15 +112,10 @@ export function getInternalViewExtensions() {
|
||||
ViewportOverlayViewExtension,
|
||||
EdgelessZoomToolbarViewExtension,
|
||||
PageDraggingAreaViewExtension,
|
||||
EdgelessSelectedRectViewExtension,
|
||||
EdgelessDraggingAreaViewExtension,
|
||||
NoteSlicerViewExtension,
|
||||
EdgelessToolbarViewExtension,
|
||||
|
||||
// Fragment
|
||||
DocTitleViewExtension,
|
||||
FramePanelViewExtension,
|
||||
OutlineViewExtension,
|
||||
AdapterPanelViewExtension,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export * from '@blocksuite/affine-foundation/store';
|
||||
@@ -1 +0,0 @@
|
||||
export * from '@blocksuite/affine-foundation/view';
|
||||
@@ -1 +0,0 @@
|
||||
export * from '@blocksuite/affine-fragment-adapter-panel';
|
||||
@@ -1 +0,0 @@
|
||||
export * from '@blocksuite/affine-fragment-adapter-panel/view';
|
||||
@@ -1 +0,0 @@
|
||||
export * from '@blocksuite/affine-widget-edgeless-dragging-area';
|
||||
@@ -1 +0,0 @@
|
||||
export * from '@blocksuite/affine-widget-edgeless-dragging-area/view';
|
||||
@@ -1 +0,0 @@
|
||||
export * from '@blocksuite/affine-widget-edgeless-selected-rect';
|
||||
@@ -1 +0,0 @@
|
||||
export * from '@blocksuite/affine-widget-edgeless-selected-rect/view';
|
||||
@@ -1 +0,0 @@
|
||||
export * from '@blocksuite/affine-widget-note-slicer';
|
||||
@@ -1 +0,0 @@
|
||||
export * from '@blocksuite/affine-widget-note-slicer/view';
|
||||
@@ -30,7 +30,6 @@
|
||||
{ "path": "../components" },
|
||||
{ "path": "../ext-loader" },
|
||||
{ "path": "../foundation" },
|
||||
{ "path": "../fragments/adapter-panel" },
|
||||
{ "path": "../fragments/doc-title" },
|
||||
{ "path": "../fragments/frame-panel" },
|
||||
{ "path": "../fragments/outline" },
|
||||
@@ -56,14 +55,11 @@
|
||||
{ "path": "../shared" },
|
||||
{ "path": "../widgets/drag-handle" },
|
||||
{ "path": "../widgets/edgeless-auto-connect" },
|
||||
{ "path": "../widgets/edgeless-dragging-area" },
|
||||
{ "path": "../widgets/edgeless-selected-rect" },
|
||||
{ "path": "../widgets/edgeless-toolbar" },
|
||||
{ "path": "../widgets/edgeless-zoom-toolbar" },
|
||||
{ "path": "../widgets/frame-title" },
|
||||
{ "path": "../widgets/keyboard-toolbar" },
|
||||
{ "path": "../widgets/linked-doc" },
|
||||
{ "path": "../widgets/note-slicer" },
|
||||
{ "path": "../widgets/page-dragging-area" },
|
||||
{ "path": "../widgets/remote-selection" },
|
||||
{ "path": "../widgets/scroll-anchoring" },
|
||||
|
||||
@@ -20,11 +20,12 @@
|
||||
"@blocksuite/icons": "^2.2.12",
|
||||
"@blocksuite/std": "workspace:*",
|
||||
"@blocksuite/store": "workspace:*",
|
||||
"@blocksuite/sync": "workspace:*",
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"file-type": "^21.0.0",
|
||||
"@toeverything/theme": "^1.1.14",
|
||||
"file-type": "^20.0.0",
|
||||
"lit": "^3.2.0",
|
||||
"minimatch": "^10.0.1",
|
||||
"rxjs": "^7.8.1",
|
||||
@@ -32,6 +33,7 @@
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./effects": "./src/effects.ts",
|
||||
"./store": "./src/store.ts",
|
||||
"./view": "./src/view.ts"
|
||||
},
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
isFootnoteDefinitionNode,
|
||||
type MarkdownAST,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
import { FeatureFlagService } from '@blocksuite/affine-shared/services';
|
||||
import { nanoid } from '@blocksuite/store';
|
||||
|
||||
const isAttachmentFootnoteDefinitionNode = (node: MarkdownAST) => {
|
||||
@@ -35,7 +36,15 @@ export const attachmentBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher
|
||||
fromMatch: o => o.node.flavour === AttachmentBlockSchema.model.flavour,
|
||||
toBlockSnapshot: {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -64,7 +73,6 @@ export const attachmentBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher
|
||||
name: fileName,
|
||||
sourceId: blobId,
|
||||
footnoteIdentifier,
|
||||
style: 'citation',
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
|
||||
@@ -4,24 +4,19 @@ import {
|
||||
} from '@blocksuite/affine-components/caption';
|
||||
import {
|
||||
getAttachmentFileIcon,
|
||||
LoadingIcon,
|
||||
getLoadingIconWith,
|
||||
} from '@blocksuite/affine-components/icons';
|
||||
import { Peekable } from '@blocksuite/affine-components/peek';
|
||||
import {
|
||||
type ResolvedStateInfo,
|
||||
ResourceController,
|
||||
} from '@blocksuite/affine-components/resource';
|
||||
import { toast } from '@blocksuite/affine-components/toast';
|
||||
import {
|
||||
type AttachmentBlockModel,
|
||||
AttachmentBlockStyles,
|
||||
} from '@blocksuite/affine-model';
|
||||
import {
|
||||
DocModeProvider,
|
||||
FileSizeLimitProvider,
|
||||
TelemetryProvider,
|
||||
ThemeProvider,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import { formatSize } from '@blocksuite/affine-shared/utils';
|
||||
import { humanFileSize } from '@blocksuite/affine-shared/utils';
|
||||
import {
|
||||
AttachmentIcon,
|
||||
ResetIcon,
|
||||
@@ -29,12 +24,13 @@ import {
|
||||
WarningIcon,
|
||||
} from '@blocksuite/icons/lit';
|
||||
import { BlockSelection } from '@blocksuite/std';
|
||||
import { nanoid, Slice } from '@blocksuite/store';
|
||||
import { computed, signal } from '@preact/signals-core';
|
||||
import { Slice } from '@blocksuite/store';
|
||||
import { type BlobState } from '@blocksuite/sync';
|
||||
import { effect, signal } from '@preact/signals-core';
|
||||
import { html, type TemplateResult } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
import { choose } from 'lit/directives/choose.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 { when } from 'lit/directives/when.js';
|
||||
|
||||
@@ -42,13 +38,11 @@ import { AttachmentEmbedProvider } from './embed';
|
||||
import { styles } from './styles';
|
||||
import { downloadAttachmentBlob, refreshData } from './utils';
|
||||
|
||||
type AttachmentResolvedStateInfo = ResolvedStateInfo & {
|
||||
kind?: TemplateResult;
|
||||
};
|
||||
type State = 'loading' | 'uploading' | 'warning' | 'oversize' | 'none';
|
||||
|
||||
@Peekable({
|
||||
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> {
|
||||
@@ -56,18 +50,7 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
|
||||
|
||||
blockDraggable = true;
|
||||
|
||||
resourceController = new ResourceController(
|
||||
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() ?? '';
|
||||
}
|
||||
blobState$ = signal<Partial<BlobState>>({});
|
||||
|
||||
protected containerStyleMap = styleMap({
|
||||
position: 'relative',
|
||||
@@ -90,7 +73,7 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
|
||||
};
|
||||
|
||||
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);
|
||||
toast(this.host, 'Copied to clipboard');
|
||||
};
|
||||
@@ -114,23 +97,33 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
|
||||
window.open(blobUrl, '_blank');
|
||||
};
|
||||
|
||||
// Refreshes data.
|
||||
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.
|
||||
reload = () => {
|
||||
if (this.model.props.embed) {
|
||||
this._refreshKey$.value = nanoid();
|
||||
return;
|
||||
}
|
||||
|
||||
this.refreshData();
|
||||
determineState = (
|
||||
downloading: boolean,
|
||||
uploading: boolean,
|
||||
overSize: boolean,
|
||||
error: boolean
|
||||
): State => {
|
||||
if (overSize) return 'oversize';
|
||||
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() {
|
||||
const selectionManager = this.host.selection;
|
||||
const blockSelection = selectionManager.create(BlockSelection, {
|
||||
@@ -144,26 +137,46 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
|
||||
|
||||
this.contentEditable = 'false';
|
||||
|
||||
this.resourceController.setEngine(this.std.store.blobSync);
|
||||
|
||||
this.disposables.add(this.resourceController.subscribe());
|
||||
this.disposables.add(this.resourceController);
|
||||
this.refreshData();
|
||||
|
||||
this.disposables.add(
|
||||
this.model.props.sourceId$.subscribe(() => {
|
||||
this.refreshData();
|
||||
effect(() => {
|
||||
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) {
|
||||
this.store.withoutTransact(() => {
|
||||
this.store.updateBlock(this.model, {
|
||||
if (!this.model.props.style && !this.doc.readonly) {
|
||||
this.doc.withoutTransact(() => {
|
||||
this.doc.updateBlock(this.model, {
|
||||
style: AttachmentBlockStyles[1],
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
override disconnectedCallback() {
|
||||
const blobUrl = this.blobUrl;
|
||||
if (blobUrl) {
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
}
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
override firstUpdated() {
|
||||
// lazy bindings
|
||||
this.disposables.addFromEvent(this, 'click', this.onClick);
|
||||
@@ -193,22 +206,6 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
|
||||
@click=${(event: MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
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
|
||||
@@ -217,199 +214,150 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
|
||||
);
|
||||
};
|
||||
|
||||
protected renderNormalButton = (needUpload: boolean) => {
|
||||
const label = needUpload ? 'retry' : 'reload';
|
||||
const run = async () => {
|
||||
if (needUpload) {
|
||||
await this.resourceController.upload();
|
||||
return;
|
||||
}
|
||||
|
||||
this.refreshData();
|
||||
};
|
||||
|
||||
protected renderReloadButton = () => {
|
||||
return html`
|
||||
<button
|
||||
class="affine-attachment-content-button"
|
||||
@click=${(event: MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
run().catch(console.error);
|
||||
|
||||
{
|
||||
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,
|
||||
});
|
||||
}
|
||||
this.refreshData();
|
||||
}}
|
||||
>
|
||||
${ResetIcon()} ${label}
|
||||
${ResetIcon()} Reload
|
||||
</button>
|
||||
`;
|
||||
};
|
||||
|
||||
protected renderWithHorizontal(
|
||||
classInfo: ClassInfo,
|
||||
{
|
||||
icon,
|
||||
title,
|
||||
description,
|
||||
kind,
|
||||
state,
|
||||
needUpload,
|
||||
}: AttachmentResolvedStateInfo
|
||||
icon: TemplateResult,
|
||||
title: string,
|
||||
description: string,
|
||||
kind: TemplateResult,
|
||||
state: State
|
||||
) {
|
||||
return html`
|
||||
<div class=${classMap(classInfo)}>
|
||||
<div class="affine-attachment-content">
|
||||
<div class="affine-attachment-content-title">
|
||||
<div class="affine-attachment-content-title-icon">${icon}</div>
|
||||
<div class="affine-attachment-content-title-text truncate">
|
||||
${title}
|
||||
</div>
|
||||
</div>
|
||||
return html`<div class=${classMap(classInfo)}>
|
||||
<div class="affine-attachment-content">
|
||||
<div class="affine-attachment-content-title">
|
||||
<div class="affine-attachment-content-title-icon">${icon}</div>
|
||||
|
||||
<div class="affine-attachment-content-description">
|
||||
<div class="affine-attachment-content-info truncate">
|
||||
${description}
|
||||
</div>
|
||||
${choose(state, [
|
||||
['error', () => this.renderNormalButton(needUpload)],
|
||||
['error:oversize', this.renderUpgradeButton],
|
||||
])}
|
||||
<div class="affine-attachment-content-title-text truncate">
|
||||
${title}
|
||||
</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 class="affine-attachment-banner">${kind}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
protected renderWithVertical(
|
||||
classInfo: ClassInfo,
|
||||
{
|
||||
icon,
|
||||
title,
|
||||
description,
|
||||
kind,
|
||||
state,
|
||||
needUpload,
|
||||
}: AttachmentResolvedStateInfo
|
||||
icon: TemplateResult,
|
||||
title: string,
|
||||
description: string,
|
||||
kind: TemplateResult,
|
||||
state?: State
|
||||
) {
|
||||
return html`
|
||||
<div class=${classMap(classInfo)}>
|
||||
<div class="affine-attachment-content">
|
||||
<div class="affine-attachment-content-title">
|
||||
<div class="affine-attachment-content-title-icon">${icon}</div>
|
||||
<div class="affine-attachment-content-title-text truncate">
|
||||
${title}
|
||||
</div>
|
||||
</div>
|
||||
return html`<div class=${classMap(classInfo)}>
|
||||
<div class="affine-attachment-content">
|
||||
<div class="affine-attachment-content-title">
|
||||
<div class="affine-attachment-content-title-icon">${icon}</div>
|
||||
|
||||
<div class="affine-attachment-content-info truncate">
|
||||
${description}
|
||||
<div class="affine-attachment-content-title-text truncate">
|
||||
${title}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="affine-attachment-banner">
|
||||
${kind}
|
||||
${choose(state, [
|
||||
['error', () => this.renderNormalButton(needUpload)],
|
||||
['error:oversize', this.renderUpgradeButton],
|
||||
])}
|
||||
<div class="affine-attachment-content-info truncate">
|
||||
${description}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
<div class="affine-attachment-banner">
|
||||
${kind}
|
||||
${choose(state, [
|
||||
['oversize', this.renderUpgradeButton],
|
||||
['warning', this.renderReloadButton],
|
||||
])}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
protected resolvedState$ = computed<AttachmentResolvedStateInfo>(() => {
|
||||
const size = this.model.props.size;
|
||||
const name = this.model.props.name$.value;
|
||||
const kind = getAttachmentFileIcon(this.filetype);
|
||||
protected renderCard = () => {
|
||||
const { name, size, style } = this.model.props;
|
||||
const cardStyle = style ?? AttachmentBlockStyles[1];
|
||||
|
||||
const resolvedState = this.resourceController.resolveStateWith({
|
||||
loadingIcon: LoadingIcon(),
|
||||
errorIcon: WarningIcon(),
|
||||
icon: AttachmentIcon(),
|
||||
title: name,
|
||||
description: formatSize(size),
|
||||
});
|
||||
const theme = this.std.get(ThemeProvider).theme$.value;
|
||||
const loadingIcon = getLoadingIconWith(theme);
|
||||
|
||||
return { ...resolvedState, kind };
|
||||
});
|
||||
|
||||
protected renderCardView = () => {
|
||||
const resolvedState = this.resolvedState$.value;
|
||||
const cardStyle = this.model.props.style$.value ?? AttachmentBlockStyles[1];
|
||||
const blobState = this.blobState$.value;
|
||||
const {
|
||||
uploading = false,
|
||||
downloading = false,
|
||||
overSize = false,
|
||||
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 = {
|
||||
'affine-attachment-card': true,
|
||||
[cardStyle]: true,
|
||||
loading: resolvedState.loading,
|
||||
error: resolvedState.error,
|
||||
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(
|
||||
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 = () => {
|
||||
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
|
||||
.icon=${icon}
|
||||
.icon=${fileTypeIcon}
|
||||
.citationTitle=${name}
|
||||
.citationIdentifier=${footnoteIdentifier}
|
||||
.active=${this.selected$.value}
|
||||
@@ -428,12 +376,23 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
|
||||
${when(
|
||||
this.isCitation,
|
||||
() => this._renderCitation(),
|
||||
() => this.renderEmbedView() ?? this.renderCardView()
|
||||
() =>
|
||||
when(
|
||||
this.embedView,
|
||||
() =>
|
||||
html`<div class="affine-attachment-embed-container">
|
||||
${this.embedView}
|
||||
</div>`,
|
||||
this.renderCard
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
accessor blobUrl: string | null = null;
|
||||
|
||||
override accessor selectedStyle = SelectedStyle.Border;
|
||||
|
||||
override accessor useCaptionEditor = true;
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import { EdgelessLegacySlotIdentifier } from '@blocksuite/affine-block-surface';
|
||||
import {
|
||||
AttachmentBlockSchema,
|
||||
AttachmentBlockStyles,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { AttachmentBlockStyles } from '@blocksuite/affine-model';
|
||||
import {
|
||||
EMBED_CARD_HEIGHT,
|
||||
EMBED_CARD_WIDTH,
|
||||
} from '@blocksuite/affine-shared/consts';
|
||||
import { toGfxBlockComponent } from '@blocksuite/std';
|
||||
import { GfxViewInteractionExtension } from '@blocksuite/std/gfx';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
import { AttachmentBlockComponent } from './attachment-block.js';
|
||||
@@ -52,21 +48,3 @@ declare global {
|
||||
'affine-edgeless-attachment': AttachmentEdgelessBlockComponent;
|
||||
}
|
||||
}
|
||||
|
||||
export const AttachmentBlockInteraction = GfxViewInteractionExtension(
|
||||
AttachmentBlockSchema.model.flavour,
|
||||
{
|
||||
resizeConstraint: {
|
||||
lockRatio: true,
|
||||
},
|
||||
handleRotate: () => {
|
||||
return {
|
||||
beforeRotate: context => {
|
||||
context.set({
|
||||
rotatable: false,
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
31
blocksuite/affine/blocks/attachment/src/attachment-spec.ts
Normal file
31
blocksuite/affine/blocks/attachment/src/attachment-spec.ts
Normal 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();
|
||||
@@ -41,7 +41,7 @@ export const RenameModal = ({
|
||||
toast(editorHost, 'File name cannot be empty');
|
||||
return;
|
||||
}
|
||||
model.store.updateBlock(model, {
|
||||
model.doc.updateBlock(model, {
|
||||
name: newFileName,
|
||||
});
|
||||
abort();
|
||||
|
||||
@@ -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 { ExportToPdfIcon, FileIcon } from '@blocksuite/icons/lit';
|
||||
|
||||
@@ -18,10 +18,10 @@ export const attachmentSlashMenuConfig: SlashMenuConfig = {
|
||||
searchAlias: ['file'],
|
||||
group: '4_Content & Media@3',
|
||||
when: ({ model }) =>
|
||||
model.store.schema.flavourSchemaMap.has('affine:attachment'),
|
||||
model.doc.schema.flavourSchemaMap.has('affine:attachment'),
|
||||
action: ({ std, model }) => {
|
||||
(async () => {
|
||||
const file = await openSingleFileWith();
|
||||
const file = await openFileOrFiles();
|
||||
if (!file) return;
|
||||
|
||||
await addSiblingAttachmentBlocks(std, [file], model);
|
||||
@@ -41,10 +41,10 @@ export const attachmentSlashMenuConfig: SlashMenuConfig = {
|
||||
},
|
||||
group: '4_Content & Media@4',
|
||||
when: ({ model }) =>
|
||||
model.store.schema.flavourSchemaMap.has('affine:attachment'),
|
||||
model.doc.schema.flavourSchemaMap.has('affine:attachment'),
|
||||
action: ({ std, model }) => {
|
||||
(async () => {
|
||||
const file = await openSingleFileWith();
|
||||
const file = await openFileOrFiles();
|
||||
if (!file) return;
|
||||
|
||||
await addSiblingAttachmentBlocks(std, [file], model);
|
||||
|
||||
@@ -77,19 +77,13 @@ export const attachmentViewDropdownMenu = {
|
||||
const model = ctx.getCurrentModelByType(AttachmentBlockModel);
|
||||
if (!model) return;
|
||||
|
||||
const provider = ctx.std.get(AttachmentEmbedProvider);
|
||||
|
||||
// TODO(@fundon): should auto focus image block.
|
||||
if (
|
||||
provider.shouldBeConverted(model) &&
|
||||
!ctx.hasSelectedSurfaceModels
|
||||
) {
|
||||
if (!ctx.hasSelectedSurfaceModels) {
|
||||
// Clears
|
||||
ctx.reset();
|
||||
ctx.select('note');
|
||||
}
|
||||
|
||||
provider.convertTo(model);
|
||||
ctx.std.get(AttachmentEmbedProvider).convertTo(model);
|
||||
|
||||
ctx.track('SelectedView', {
|
||||
...trackBaseProps,
|
||||
@@ -100,32 +94,18 @@ export const attachmentViewDropdownMenu = {
|
||||
},
|
||||
],
|
||||
content(ctx) {
|
||||
const block = ctx.getCurrentBlockByType(AttachmentBlockComponent);
|
||||
if (!block) return null;
|
||||
const model = ctx.getCurrentModelByType(AttachmentBlockModel);
|
||||
if (!model) return null;
|
||||
|
||||
const model = block.model;
|
||||
const embedProvider = ctx.std.get(AttachmentEmbedProvider);
|
||||
const actions = computed(() => {
|
||||
const [cardAction, embedAction] = this.actions.map(action => ({
|
||||
...action,
|
||||
}));
|
||||
|
||||
const ok = block.resourceController.resolvedState$.value.state === 'none';
|
||||
const sourceId = Boolean(model.props.sourceId$.value);
|
||||
const actions = this.actions.map(action => ({ ...action }));
|
||||
const viewType$ = computed(() => {
|
||||
const [cardAction, embedAction] = actions;
|
||||
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;
|
||||
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;
|
||||
});
|
||||
const onToggle = (e: CustomEvent<boolean>) => {
|
||||
@@ -143,7 +123,7 @@ export const attachmentViewDropdownMenu = {
|
||||
model,
|
||||
html`<affine-view-dropdown-menu
|
||||
@toggle=${onToggle}
|
||||
.actions=${actions.value}
|
||||
.actions=${actions}
|
||||
.context=${ctx}
|
||||
.viewType$=${viewType$}
|
||||
></affine-view-dropdown-menu>`
|
||||
@@ -263,13 +243,7 @@ const builtinToolbarConfig = {
|
||||
icon: ResetIcon(),
|
||||
run(ctx) {
|
||||
const block = ctx.getCurrentBlockByType(AttachmentBlockComponent);
|
||||
block?.reload();
|
||||
|
||||
ctx.track('AttachmentReloadedEvent', {
|
||||
...trackBaseProps,
|
||||
control: 'reload',
|
||||
type: block?.model.props.name.split('.').pop() ?? '',
|
||||
});
|
||||
block?.refreshData();
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -3,10 +3,6 @@ import {
|
||||
type ImageBlockProps,
|
||||
MAX_IMAGE_WIDTH,
|
||||
} from '@blocksuite/affine-model';
|
||||
import {
|
||||
EMBED_CARD_HEIGHT,
|
||||
EMBED_CARD_WIDTH,
|
||||
} from '@blocksuite/affine-shared/consts';
|
||||
import { FileSizeLimitProvider } from '@blocksuite/affine-shared/services';
|
||||
import {
|
||||
readImageSize,
|
||||
@@ -21,7 +17,6 @@ import type { ExtensionType } from '@blocksuite/store';
|
||||
import { Extension } from '@blocksuite/store';
|
||||
import type { TemplateResult } from 'lit';
|
||||
import { html } from 'lit';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
|
||||
import { getAttachmentBlob } from './utils';
|
||||
|
||||
@@ -39,22 +34,9 @@ export type AttachmentEmbedConfig = {
|
||||
std: BlockStdScope
|
||||
) => Promise<void> | void;
|
||||
/**
|
||||
* Renders the embed view.
|
||||
* The template will be used to render the embed view.
|
||||
*/
|
||||
render?: (
|
||||
model: AttachmentBlockModel,
|
||||
blobUrl: string
|
||||
) => TemplateResult | null;
|
||||
|
||||
/**
|
||||
* Should show status when turned on.
|
||||
*/
|
||||
shouldShowStatus?: boolean;
|
||||
|
||||
/**
|
||||
* Should block type conversion be required.
|
||||
*/
|
||||
shouldBeConverted?: boolean;
|
||||
template?: (model: AttachmentBlockModel, blobUrl: string) => TemplateResult;
|
||||
};
|
||||
|
||||
// Single embed config.
|
||||
@@ -115,53 +97,39 @@ export class AttachmentEmbedService extends Extension {
|
||||
// Converts to embed view.
|
||||
convertTo(model: AttachmentBlockModel, maxFileSize = this._maxFileSize) {
|
||||
const config = this.values.find(config => config.check(model, maxFileSize));
|
||||
|
||||
if (config?.action) {
|
||||
config.action(model, this.std)?.catch(console.error);
|
||||
if (!config?.action) {
|
||||
model.doc.updateBlock(model, { embed: true });
|
||||
return;
|
||||
}
|
||||
|
||||
model.store.updateBlock(model, { embed: true });
|
||||
config.action(model, this.std)?.catch(console.error);
|
||||
}
|
||||
|
||||
embedded(model: AttachmentBlockModel, maxFileSize = this._maxFileSize) {
|
||||
return this.values.some(config => config.check(model, maxFileSize));
|
||||
}
|
||||
|
||||
getRender(model: AttachmentBlockModel, maxFileSize = this._maxFileSize) {
|
||||
return (
|
||||
this.values.find(config => config.check(model, maxFileSize))?.render ??
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
shouldShowStatus(
|
||||
render(
|
||||
model: AttachmentBlockModel,
|
||||
blobUrl?: string,
|
||||
maxFileSize = this._maxFileSize
|
||||
) {
|
||||
return (
|
||||
this.values.find(config => config.check(model, maxFileSize))
|
||||
?.shouldShowStatus ?? false
|
||||
);
|
||||
}
|
||||
if (!model.props.embed || !blobUrl) return;
|
||||
|
||||
shouldBeConverted(
|
||||
model: AttachmentBlockModel,
|
||||
maxFileSize = this._maxFileSize
|
||||
) {
|
||||
return (
|
||||
this.values.find(config => config.check(model, maxFileSize))
|
||||
?.shouldBeConverted ?? false
|
||||
);
|
||||
const config = this.values.find(config => config.check(model, maxFileSize));
|
||||
if (!config || !config.template) {
|
||||
console.error('No embed view template found!', model, model.props.type);
|
||||
return;
|
||||
}
|
||||
|
||||
return config.template(model, blobUrl);
|
||||
}
|
||||
}
|
||||
|
||||
const embedConfig: AttachmentEmbedConfig[] = [
|
||||
{
|
||||
name: 'image',
|
||||
shouldBeConverted: true,
|
||||
check: model =>
|
||||
model.store.schema.flavourSchemaMap.has('affine:image') &&
|
||||
model.doc.schema.flavourSchemaMap.has('affine:image') &&
|
||||
model.props.type.startsWith('image/'),
|
||||
async action(model, std) {
|
||||
const component = std.view.getBlock(model.id);
|
||||
@@ -172,30 +140,16 @@ const embedConfig: AttachmentEmbedConfig[] = [
|
||||
},
|
||||
{
|
||||
name: 'pdf',
|
||||
shouldShowStatus: true,
|
||||
check: (model, maxFileSize) =>
|
||||
model.props.type === 'application/pdf' && model.props.size <= maxFileSize,
|
||||
action: model => {
|
||||
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) => {
|
||||
template: (_, blobUrl) => {
|
||||
// More options: https://tinytip.co/tips/html-pdf-params/
|
||||
// https://chromium.googlesource.com/chromium/src/+/refs/tags/121.0.6153.1/chrome/browser/resources/pdf/open_pdf_params_parser.ts
|
||||
const parameters = '#toolbar=0';
|
||||
return html`
|
||||
<iframe
|
||||
style=${styleMap({
|
||||
width: '100%',
|
||||
minHeight: '480px',
|
||||
colorScheme: 'auto',
|
||||
})}
|
||||
style="width: 100%; color-scheme: auto;"
|
||||
height="480"
|
||||
src=${blobUrl + parameters}
|
||||
loading="lazy"
|
||||
scrolling="no"
|
||||
@@ -203,7 +157,6 @@ const embedConfig: AttachmentEmbedConfig[] = [
|
||||
allowTransparency
|
||||
allowfullscreen
|
||||
type="application/pdf"
|
||||
credentialless
|
||||
></iframe>
|
||||
<div class="affine-attachment-embed-event-mask"></div>
|
||||
`;
|
||||
@@ -211,44 +164,23 @@ const embedConfig: AttachmentEmbedConfig[] = [
|
||||
},
|
||||
{
|
||||
name: 'video',
|
||||
shouldShowStatus: true,
|
||||
check: (model, maxFileSize) =>
|
||||
model.props.type.startsWith('video/') && model.props.size <= maxFileSize,
|
||||
action: model => {
|
||||
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) =>
|
||||
template: (_, blobUrl) =>
|
||||
html`<video
|
||||
style=${styleMap({
|
||||
display: 'flex',
|
||||
objectFit: 'cover',
|
||||
backgroundSize: 'cover',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
})}
|
||||
src=${blobUrl}
|
||||
width="100%"
|
||||
height="100%"
|
||||
style="max-height: max-content;"
|
||||
width="100%;"
|
||||
height="480"
|
||||
controls
|
||||
src=${blobUrl}
|
||||
></video>`,
|
||||
},
|
||||
{
|
||||
name: 'audio',
|
||||
check: (model, maxFileSize) =>
|
||||
model.props.type.startsWith('audio/') && model.props.size <= maxFileSize,
|
||||
render: (_, blobUrl) =>
|
||||
html`<audio
|
||||
style=${styleMap({ margin: '4px' })}
|
||||
src=${blobUrl}
|
||||
controls
|
||||
></audio>`,
|
||||
template: (_, blobUrl) =>
|
||||
html`<audio controls src=${blobUrl} style="margin: 4px;"></audio>`,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -256,7 +188,7 @@ const embedConfig: AttachmentEmbedConfig[] = [
|
||||
* Turn the attachment block into an image block.
|
||||
*/
|
||||
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!');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export * from './adapters';
|
||||
export * from './attachment-block';
|
||||
export * from './attachment-service';
|
||||
export * from './attachment-spec';
|
||||
export { attachmentViewDropdownMenu } from './configs/toolbar';
|
||||
export * from './edgeless-clipboard-config';
|
||||
export {
|
||||
|
||||
@@ -6,9 +6,9 @@ export const styles = css`
|
||||
border-radius: 8px;
|
||||
box-sizing: border-box;
|
||||
user-select: none;
|
||||
overflow: hidden;
|
||||
border: 1px solid ${unsafeCSSVarV2('layer/background/tertiary')};
|
||||
background: ${unsafeCSSVarV2('layer/background/primary')};
|
||||
overflow: hidden;
|
||||
|
||||
&.focused {
|
||||
border-color: ${unsafeCSSVarV2('layer/insideBorder/primaryBorder')};
|
||||
@@ -30,13 +30,6 @@ export const styles = css`
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.truncate {
|
||||
align-self: stretch;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.affine-attachment-content-title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -47,10 +40,18 @@ export const styles = css`
|
||||
|
||||
.affine-attachment-content-title-icon {
|
||||
display: flex;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
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 {
|
||||
@@ -91,7 +92,6 @@ export const styles = css`
|
||||
font-size: var(--affine-font-xs);
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
text-transform: capitalize;
|
||||
line-height: 20px;
|
||||
|
||||
svg {
|
||||
@@ -107,7 +107,7 @@ export const styles = css`
|
||||
|
||||
.affine-attachment-card.loading {
|
||||
.affine-attachment-content-title-text {
|
||||
color: ${unsafeCSSVarV2('text/placeholder')};
|
||||
color: var(--affine-placeholder-color);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,12 +143,6 @@ export const styles = css`
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.affine-attachment-embed-status {
|
||||
position: absolute;
|
||||
left: 14px;
|
||||
bottom: 64px;
|
||||
}
|
||||
|
||||
.affine-attachment-embed-event-mask {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
FileSizeLimitProvider,
|
||||
TelemetryProvider,
|
||||
} 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 type { BlockStdScope } from '@blocksuite/std';
|
||||
import { GfxControllerIdentifier } from '@blocksuite/std/gfx';
|
||||
@@ -22,13 +22,15 @@ import type { BlockModel } from '@blocksuite/store';
|
||||
import type { AttachmentBlockComponent } from './attachment-block';
|
||||
|
||||
export async function getAttachmentBlob(model: AttachmentBlockModel) {
|
||||
const { sourceId$, type$ } = model.props;
|
||||
const sourceId = sourceId$.peek();
|
||||
const type = type$.peek();
|
||||
const {
|
||||
sourceId$: { value: sourceId },
|
||||
type$: { value: type },
|
||||
} = model.props;
|
||||
if (!sourceId) return null;
|
||||
|
||||
const doc = model.store;
|
||||
const blob = await doc.blobSync.get(sourceId);
|
||||
const doc = model.doc;
|
||||
let blob = await doc.blobSync.get(sourceId);
|
||||
|
||||
if (!blob) return null;
|
||||
|
||||
return new Blob([blob], { type });
|
||||
@@ -39,9 +41,9 @@ export async function getAttachmentBlob(model: AttachmentBlockModel) {
|
||||
* the download process may take a long time!
|
||||
*/
|
||||
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...');
|
||||
return;
|
||||
}
|
||||
@@ -54,7 +56,7 @@ export function downloadAttachmentBlob(block: AttachmentBlockComponent) {
|
||||
return;
|
||||
}
|
||||
|
||||
resourceController.updateState({ downloading: true });
|
||||
block.updateBlobState({ downloading: true });
|
||||
|
||||
toast(host, `Downloading ${shortName}`);
|
||||
|
||||
@@ -65,14 +67,34 @@ export function downloadAttachmentBlob(block: AttachmentBlockComponent) {
|
||||
tmpLink.dispatchEvent(event);
|
||||
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 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();
|
||||
|
||||
await block.resourceController.refreshUrlWith(type);
|
||||
blob = new Blob([blob], { type });
|
||||
|
||||
block.blobUrl = URL.createObjectURL(blob);
|
||||
}
|
||||
|
||||
export async function getFileType(file: File) {
|
||||
@@ -82,7 +104,7 @@ export async function getFileType(file: File) {
|
||||
const buffer = await file.arrayBuffer();
|
||||
const FileType = await import('file-type');
|
||||
const fileType = await FileType.fileTypeFromBuffer(buffer);
|
||||
return fileType?.mime ?? '';
|
||||
return fileType ? fileType.mime : '';
|
||||
}
|
||||
|
||||
function hasExceeded(
|
||||
@@ -93,7 +115,7 @@ function hasExceeded(
|
||||
const exceeded = files.some(file => file.size > maxFileSize);
|
||||
|
||||
if (exceeded) {
|
||||
const size = formatSize(maxFileSize);
|
||||
const size = humanFileSize(maxFileSize, true, 0);
|
||||
toast(std.host, `You can only upload files less than ${size}`);
|
||||
}
|
||||
|
||||
@@ -130,7 +152,7 @@ async function buildPropsWith(
|
||||
std.getOptional(TelemetryProvider)?.track('AttachmentUploadedEvent', {
|
||||
page: `${mode} editor`,
|
||||
module: 'attachment',
|
||||
segment: mode,
|
||||
segment: 'attachment',
|
||||
control: 'uploader',
|
||||
type,
|
||||
category,
|
||||
|
||||
@@ -7,7 +7,6 @@ import { SlashMenuConfigExtension } from '@blocksuite/affine-widget-slash-menu';
|
||||
import { BlockViewExtension, FlavourExtension } from '@blocksuite/std';
|
||||
import { literal } from 'lit/static-html.js';
|
||||
|
||||
import { AttachmentBlockInteraction } from './attachment-edgeless-block.js';
|
||||
import { AttachmentDropOption } from './attachment-service.js';
|
||||
import { attachmentSlashMenuConfig } from './configs/slash-menu.js';
|
||||
import { createBuiltinToolbarConfigExtension } from './configs/toolbar';
|
||||
@@ -45,7 +44,6 @@ export class AttachmentViewExtension extends ViewExtensionProvider {
|
||||
]);
|
||||
if (this.isEdgeless(context.scope)) {
|
||||
context.register(EdgelessClipboardAttachmentConfig);
|
||||
context.register(AttachmentBlockInteraction);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
{ "path": "../../widgets/slash-menu" },
|
||||
{ "path": "../../../framework/global" },
|
||||
{ "path": "../../../framework/std" },
|
||||
{ "path": "../../../framework/store" }
|
||||
{ "path": "../../../framework/store" },
|
||||
{ "path": "../../../framework/sync" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"@blocksuite/store": "workspace:*",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.14",
|
||||
"lit": "^3.2.0",
|
||||
"minimatch": "^10.0.1",
|
||||
"rxjs": "^7.8.1",
|
||||
@@ -32,10 +32,11 @@
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vitest": "3.1.3"
|
||||
"vitest": "3.1.2"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./effects": "./src/effects.ts",
|
||||
"./store": "./src/store.ts",
|
||||
"./view": "./src/view.ts"
|
||||
},
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
isFootnoteDefinitionNode,
|
||||
type MarkdownAST,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
import { FeatureFlagService } from '@blocksuite/affine-shared/services';
|
||||
import { nanoid } from '@blocksuite/store';
|
||||
|
||||
const isUrlFootnoteDefinitionNode = (node: MarkdownAST) => {
|
||||
@@ -32,7 +33,15 @@ export const bookmarkBlockMarkdownAdapterMatcher =
|
||||
toMatch: o => isUrlFootnoteDefinitionNode(o.node),
|
||||
toBlockSnapshot: {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,12 +6,10 @@ import type {
|
||||
BookmarkBlockModel,
|
||||
LinkPreviewData,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { ImageProxyService } from '@blocksuite/affine-shared/adapters';
|
||||
import {
|
||||
DocModeProvider,
|
||||
LinkPreviewServiceIdentifier,
|
||||
LinkPreviewerService,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import { normalizeUrl } from '@blocksuite/affine-shared/utils';
|
||||
import { BlockSelection } from '@blocksuite/std';
|
||||
import { computed, type ReadonlySignal, signal } from '@preact/signals-core';
|
||||
import { html } from 'lit';
|
||||
@@ -73,8 +71,8 @@ export class BookmarkBlockComponent extends CaptionedBlockComponent<BookmarkBloc
|
||||
this.loading = true;
|
||||
this.error = false;
|
||||
|
||||
this.std
|
||||
.get(LinkPreviewServiceIdentifier)
|
||||
this.std.store
|
||||
.get(LinkPreviewerService)
|
||||
.query(this.model.props.url, this._fetchAbortController.signal)
|
||||
.then(data => {
|
||||
this._localLinkPreview$.value = {
|
||||
@@ -100,12 +98,12 @@ export class BookmarkBlockComponent extends CaptionedBlockComponent<BookmarkBloc
|
||||
selectionManager.setGroup('note', [blockSelection]);
|
||||
};
|
||||
|
||||
get link() {
|
||||
return normalizeUrl(this.model.props.url);
|
||||
}
|
||||
|
||||
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 = () => {
|
||||
@@ -121,17 +119,10 @@ export class BookmarkBlockComponent extends CaptionedBlockComponent<BookmarkBloc
|
||||
);
|
||||
}
|
||||
|
||||
get imageProxyService() {
|
||||
return this.std.get(ImageProxyService);
|
||||
}
|
||||
|
||||
handleClick = (event: MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
|
||||
if (
|
||||
this.model.parent?.flavour !== 'affine:surface' &&
|
||||
!this.store.readonly
|
||||
) {
|
||||
if (this.model.parent?.flavour !== 'affine:surface' && !this.doc.readonly) {
|
||||
this.selectBlock();
|
||||
}
|
||||
};
|
||||
@@ -144,10 +135,9 @@ export class BookmarkBlockComponent extends CaptionedBlockComponent<BookmarkBloc
|
||||
private readonly _renderCitationView = () => {
|
||||
const { url, footnoteIdentifier } = this.model.props;
|
||||
const { icon, title, description } = this.linkPreview$.value;
|
||||
const iconSrc = icon ? this.imageProxyService.buildUrl(icon) : undefined;
|
||||
return html`
|
||||
<affine-citation-card
|
||||
.icon=${iconSrc}
|
||||
.icon=${icon}
|
||||
.citationTitle=${title || url}
|
||||
.citationContent=${description}
|
||||
.citationIdentifier=${footnoteIdentifier}
|
||||
@@ -188,7 +178,7 @@ export class BookmarkBlockComponent extends CaptionedBlockComponent<BookmarkBloc
|
||||
) {
|
||||
// When the doc is readonly, and the preview data not provided
|
||||
// We should fetch the preview data and update the local link preview data
|
||||
if (this.store.readonly) {
|
||||
if (this.doc.readonly) {
|
||||
this._updateLocalLinkPreview();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { BookmarkBlockSchema } from '@blocksuite/affine-model';
|
||||
import {
|
||||
EMBED_CARD_HEIGHT,
|
||||
EMBED_CARD_WIDTH,
|
||||
} from '@blocksuite/affine-shared/consts';
|
||||
import { toGfxBlockComponent } from '@blocksuite/std';
|
||||
import { GfxViewInteractionExtension } from '@blocksuite/std/gfx';
|
||||
import { type StyleInfo, styleMap } from 'lit/directives/style-map.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() {
|
||||
const style = this.model.props.style$.value;
|
||||
const width = EMBED_CARD_WIDTH[style];
|
||||
@@ -45,14 +34,12 @@ export class BookmarkEdgelessBlockComponent extends toGfxBlockComponent(
|
||||
const bound = this.model.elementBound;
|
||||
const scaleX = bound.w / width;
|
||||
const scaleY = bound.h / height;
|
||||
const isSelected = this.gfx.selection.has(this.model.id);
|
||||
|
||||
this.containerStyleMap = styleMap({
|
||||
width: `100%`,
|
||||
height: `100%`,
|
||||
transform: `scale(${scaleX}, ${scaleY})`,
|
||||
transformOrigin: '0 0',
|
||||
pointerEvents: isSelected ? 'auto' : 'none',
|
||||
});
|
||||
|
||||
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 {
|
||||
interface HTMLElementTagNameMap {
|
||||
'affine-edgeless-bookmark': BookmarkEdgelessBlockComponent;
|
||||
|
||||
22
blocksuite/affine/blocks/bookmark/src/bookmark-spec.ts
Normal file
22
blocksuite/affine/blocks/bookmark/src/bookmark-spec.ts
Normal 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();
|
||||
@@ -1,3 +1,5 @@
|
||||
import '@blocksuite/affine-block-embed/effects';
|
||||
|
||||
import { insertEmbedCard } from '@blocksuite/affine-block-embed';
|
||||
import type { EmbedCardStyle } from '@blocksuite/affine-model';
|
||||
import { EmbedOptionProvider } from '@blocksuite/affine-shared/services';
|
||||
|
||||
@@ -2,14 +2,9 @@ import { insertEmbedIframeWithUrlCommand } from '@blocksuite/affine-block-embed'
|
||||
import {
|
||||
type InsertedLinkType,
|
||||
insertEmbedLinkedDocCommand,
|
||||
insertEmbedSyncedDocCommand,
|
||||
type LinkableFlavour,
|
||||
} from '@blocksuite/affine-block-embed-doc';
|
||||
import {
|
||||
DocModeProvider,
|
||||
EditorSettingProvider,
|
||||
QuickSearchProvider,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import { QuickSearchProvider } from '@blocksuite/affine-shared/services';
|
||||
import type { Command } from '@blocksuite/std';
|
||||
|
||||
import { insertBookmarkCommand } from './insert-bookmark';
|
||||
@@ -31,26 +26,12 @@ export const insertLinkByQuickSearchCommand: Command<
|
||||
|
||||
// add linked doc
|
||||
if ('docId' in result) {
|
||||
const editorMode = std.get(DocModeProvider).getEditorMode();
|
||||
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, {
|
||||
std.command.exec(insertEmbedLinkedDocCommand, {
|
||||
docId: result.docId,
|
||||
params: result.params,
|
||||
});
|
||||
|
||||
return {
|
||||
flavour,
|
||||
flavour: 'affine:embed-linked-doc',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { ThemeProvider } from '@blocksuite/affine-shared/services';
|
||||
import { getHostName } from '@blocksuite/affine-shared/utils';
|
||||
@@ -60,11 +60,11 @@ export class BookmarkCard extends SignalWatcher(
|
||||
: title;
|
||||
|
||||
const theme = this.bookmark.std.get(ThemeProvider).theme;
|
||||
const { EmbedCardBannerIcon } = getEmbedCardIcons(theme);
|
||||
const imageProxyService = this.bookmark.store.get(ImageProxyService);
|
||||
const { LoadingIcon, EmbedCardBannerIcon } = getEmbedCardIcons(theme);
|
||||
const imageProxyService = this.bookmark.doc.get(ImageProxyService);
|
||||
|
||||
const titleIcon = this.loading
|
||||
? LoadingIcon()
|
||||
? LoadingIcon
|
||||
: icon
|
||||
? html`<img src=${imageProxyService.buildUrl(icon)} alt="icon" />`
|
||||
: WebIcon16;
|
||||
|
||||
@@ -23,10 +23,10 @@ const bookmarkSlashMenuConfig: SlashMenuConfig = {
|
||||
},
|
||||
group: '4_Content & Media@2',
|
||||
when: ({ model }) =>
|
||||
model.store.schema.flavourSchemaMap.has('affine:bookmark'),
|
||||
model.doc.schema.flavourSchemaMap.has('affine:bookmark'),
|
||||
action: ({ std, model }) => {
|
||||
const { host } = std;
|
||||
const parentModel = host.store.getParent(model);
|
||||
const parentModel = host.doc.getParent(model);
|
||||
if (!parentModel) {
|
||||
return;
|
||||
}
|
||||
@@ -45,7 +45,7 @@ const bookmarkSlashMenuConfig: SlashMenuConfig = {
|
||||
)
|
||||
.then(() => {
|
||||
if (model.text?.length === 0) {
|
||||
model.store.deleteBlock(model);
|
||||
model.doc.deleteBlock(model);
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
|
||||
@@ -407,7 +407,7 @@ const builtinSurfaceToolbarConfig = {
|
||||
if (options?.viewType !== 'embed') return;
|
||||
|
||||
const { flavour, styles } = options;
|
||||
let style: EmbedCardStyle = model.props.style;
|
||||
let { style } = model.props;
|
||||
|
||||
if (!styles.includes(style)) {
|
||||
style = styles[0];
|
||||
@@ -482,26 +482,24 @@ const builtinSurfaceToolbarConfig = {
|
||||
} satisfies ToolbarActionGroup<ToolbarAction>,
|
||||
{
|
||||
id: 'b.style',
|
||||
actions: (
|
||||
[
|
||||
{
|
||||
id: 'horizontal',
|
||||
label: 'Large horizontal style',
|
||||
},
|
||||
{
|
||||
id: 'list',
|
||||
label: 'Small horizontal style',
|
||||
},
|
||||
{
|
||||
id: 'vertical',
|
||||
label: 'Large vertical style',
|
||||
},
|
||||
{
|
||||
id: 'cube',
|
||||
label: 'Small vertical style',
|
||||
},
|
||||
] as const
|
||||
).filter(action => BookmarkStyles.includes(action.id)),
|
||||
actions: [
|
||||
{
|
||||
id: 'horizontal',
|
||||
label: 'Large horizontal style',
|
||||
},
|
||||
{
|
||||
id: 'list',
|
||||
label: 'Small horizontal style',
|
||||
},
|
||||
{
|
||||
id: 'vertical',
|
||||
label: 'Large vertical style',
|
||||
},
|
||||
{
|
||||
id: 'cube',
|
||||
label: 'Small vertical style',
|
||||
},
|
||||
].filter(action => BookmarkStyles.includes(action.id as EmbedCardStyle)),
|
||||
content(ctx) {
|
||||
const model = ctx.getCurrentModelByType(BookmarkBlockModel);
|
||||
if (!model) return null;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from './adapters';
|
||||
export * from './bookmark-block';
|
||||
export * from './bookmark-spec';
|
||||
export * from './commands';
|
||||
export * from './components';
|
||||
export { BookmarkSlashMenuConfigIdentifier } from './configs/slash-menu';
|
||||
|
||||
@@ -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 type { BookmarkBlockComponent } from './bookmark-block.js';
|
||||
@@ -15,7 +15,7 @@ export async function refreshBookmarkUrlData(
|
||||
try {
|
||||
bookmarkElement.loading = true;
|
||||
|
||||
const linkPreviewer = bookmarkElement.std.get(LinkPreviewServiceIdentifier);
|
||||
const linkPreviewer = bookmarkElement.doc.get(LinkPreviewerService);
|
||||
const bookmarkUrlData = await linkPreviewer.query(
|
||||
bookmarkElement.model.props.url,
|
||||
signal
|
||||
@@ -32,7 +32,7 @@ export async function refreshBookmarkUrlData(
|
||||
|
||||
if (signal?.aborted) return;
|
||||
|
||||
bookmarkElement.store.updateBlock(bookmarkElement.model, {
|
||||
bookmarkElement.doc.updateBlock(bookmarkElement.model, {
|
||||
title,
|
||||
description,
|
||||
icon,
|
||||
|
||||
@@ -6,7 +6,6 @@ import { BookmarkBlockSchema } from '@blocksuite/affine-model';
|
||||
import { BlockViewExtension, FlavourExtension } from '@blocksuite/std';
|
||||
import { literal } from 'lit/static-html.js';
|
||||
|
||||
import { BookmarkBlockInteraction } from './bookmark-edgeless-block';
|
||||
import { BookmarkSlashMenuConfigExtension } from './configs/slash-menu';
|
||||
import { createBuiltinToolbarConfigExtension } from './configs/toolbar';
|
||||
import { EdgelessClipboardBookmarkConfig } from './edgeless-clipboard-config';
|
||||
@@ -37,7 +36,6 @@ export class BookmarkViewExtension extends ViewExtensionProvider {
|
||||
const isEdgeless = this.isEdgeless(context.scope);
|
||||
if (isEdgeless) {
|
||||
context.register(EdgelessClipboardBookmarkConfig);
|
||||
context.register(BookmarkBlockInteraction);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"@floating-ui/dom": "^1.6.10",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.14",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"emoji-mart": "^5.6.0",
|
||||
"lit": "^3.2.0",
|
||||
@@ -35,6 +35,7 @@
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./effects": "./src/effects.ts",
|
||||
"./view": "./src/view.ts",
|
||||
"./store": "./src/store.ts"
|
||||
},
|
||||
|
||||
@@ -2,17 +2,10 @@ import { CalloutBlockSchema } from '@blocksuite/affine-model';
|
||||
import {
|
||||
BlockMarkdownAdapterExtension,
|
||||
type BlockMarkdownAdapterMatcher,
|
||||
CALLOUT_MARKDOWN_EXPORT_OPTIONS_KEY,
|
||||
type CalloutAdmonitionType,
|
||||
CalloutAdmonitionTypeSet,
|
||||
CalloutExportStyle,
|
||||
type CalloutMarkdownExportOptions,
|
||||
calloutMarkdownExportOptionsSchema,
|
||||
DEFAULT_ADMONITION_TYPE,
|
||||
getCalloutEmoji,
|
||||
isCalloutNode,
|
||||
} 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
|
||||
// In mdast, the node types are `paragraph`, `list`, `heading`, `blockquote`
|
||||
@@ -23,29 +16,6 @@ const CALLOUT_BLOCK_CHILDREN_TYPES = new Set([
|
||||
'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 = {
|
||||
flavour: CalloutBlockSchema.model.flavour,
|
||||
toMatch: o => isCalloutNode(o.node),
|
||||
@@ -87,118 +57,29 @@ export const calloutBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher = {
|
||||
fromBlockSnapshot: {
|
||||
enter: (o, context) => {
|
||||
const emoji = o.node.props.emoji as string;
|
||||
const { walkerContext, configs } = context;
|
||||
|
||||
const exportOptions = getCalloutExportOptions(configs);
|
||||
const { style, admonitionType } = exportOptions;
|
||||
// If the style is admonitions, we should handle the first child
|
||||
if (style === CalloutExportStyle.Admonitions) {
|
||||
let type = admonitionType ?? DEFAULT_ADMONITION_TYPE;
|
||||
let customTitle = '';
|
||||
let restOfText = '';
|
||||
|
||||
const firstChild = o.node.children[0];
|
||||
const isTextNode = !!firstChild.props.text;
|
||||
// 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(
|
||||
const { walkerContext } = context;
|
||||
walkerContext
|
||||
.openNode(
|
||||
{
|
||||
type: 'blockquote',
|
||||
children: [],
|
||||
},
|
||||
'children'
|
||||
)
|
||||
.openNode({
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{
|
||||
type: 'blockquote',
|
||||
children: [],
|
||||
type: 'text',
|
||||
value: `[!${emoji}]`,
|
||||
},
|
||||
'children'
|
||||
)
|
||||
.openNode({
|
||||
type: 'paragraph',
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
value: `[!${emoji}]`,
|
||||
},
|
||||
],
|
||||
})
|
||||
.closeNode();
|
||||
}
|
||||
],
|
||||
})
|
||||
.closeNode();
|
||||
},
|
||||
leave: (_, context) => {
|
||||
const { walkerContext, configs } = context;
|
||||
const exportOptions = getCalloutExportOptions(configs);
|
||||
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();
|
||||
}
|
||||
const { walkerContext } = context;
|
||||
walkerContext.closeNode();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -12,7 +12,6 @@ import type { BlockComponent } from '@blocksuite/std';
|
||||
import { flip, offset } from '@floating-ui/dom';
|
||||
import { css, html } from 'lit';
|
||||
import { query } from 'lit/decorators.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
export class CalloutBlockComponent extends CaptionedBlockComponent<CalloutBlockModel> {
|
||||
static override styles = css`
|
||||
:host {
|
||||
@@ -110,18 +109,14 @@ export class CalloutBlockComponent extends CaptionedBlockComponent<CalloutBlockM
|
||||
}
|
||||
|
||||
override renderBlock() {
|
||||
const emoji = this.model.props.emoji$.value;
|
||||
return html`
|
||||
<div class="affine-callout-block-container">
|
||||
<div
|
||||
@click=${this._toggleEmojiMenu}
|
||||
contenteditable="false"
|
||||
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 class="affine-callout-children">
|
||||
${this.renderChildren(this.model)}
|
||||
|
||||
16
blocksuite/affine/blocks/callout/src/callout-spec.ts
Normal file
16
blocksuite/affine/blocks/callout/src/callout-spec.ts
Normal 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,
|
||||
];
|
||||
@@ -33,24 +33,19 @@ export const calloutSlashMenuConfig: SlashMenuConfig = {
|
||||
when: ({ std, model }) => {
|
||||
return (
|
||||
std.get(FeatureFlagService).getFlag('enable_callout') &&
|
||||
!isInsideBlockByFlavour(model.store, model, 'affine:edgeless-text')
|
||||
!isInsideBlockByFlavour(model.doc, model, 'affine:edgeless-text')
|
||||
);
|
||||
},
|
||||
action: ({ model, std }) => {
|
||||
const { store } = model;
|
||||
const parent = store.getParent(model);
|
||||
const { doc } = model;
|
||||
const parent = doc.getParent(model);
|
||||
if (!parent) return;
|
||||
|
||||
const index = parent.children.indexOf(model);
|
||||
if (index === -1) return;
|
||||
const calloutId = store.addBlock(
|
||||
'affine:callout',
|
||||
{},
|
||||
parent,
|
||||
index + 1
|
||||
);
|
||||
const calloutId = doc.addBlock('affine:callout', {}, parent, index + 1);
|
||||
if (!calloutId) return;
|
||||
const paragraphId = store.addBlock('affine:paragraph', {}, calloutId);
|
||||
const paragraphId = doc.addBlock('affine:paragraph', {}, calloutId);
|
||||
if (!paragraphId) return;
|
||||
std.host.updateComplete
|
||||
.then(() => {
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './callout-block.js';
|
||||
export * from './callout-spec.js';
|
||||
export * from './effects.js';
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
"@floating-ui/dom": "^1.6.13",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@toeverything/theme": "^1.1.14",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"lit": "^3.2.0",
|
||||
"minimatch": "^10.0.1",
|
||||
@@ -37,6 +37,7 @@
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./effects": "./src/effects.ts",
|
||||
"./turbo-painter": "./src/turbo/code-painter.worker.ts",
|
||||
"./view": "./src/view.ts",
|
||||
"./store": "./src/store.ts"
|
||||
|
||||
@@ -48,11 +48,7 @@ const codePreprocessor: MarkdownAdapterPreprocessor = {
|
||||
}
|
||||
|
||||
trimmedLine = trimmedLine.trimEnd();
|
||||
if (
|
||||
!trimmedLine.startsWith('<') &&
|
||||
!trimmedLine.endsWith('>') &&
|
||||
!trimmedLine.includes(' ')
|
||||
) {
|
||||
if (!trimmedLine.startsWith('<') && !trimmedLine.endsWith('>')) {
|
||||
// check if it is a url link and wrap it with the angle brackets
|
||||
// sometimes the url includes emphasis `_` that will break URL parsing
|
||||
//
|
||||
|
||||
38
blocksuite/affine/blocks/code/src/code-block-spec.ts
Normal file
38
blocksuite/affine/blocks/code/src/code-block-spec.ts
Normal 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();
|
||||
@@ -26,13 +26,11 @@ import { computed, effect, type Signal, signal } from '@preact/signals-core';
|
||||
import { html, nothing, type TemplateResult } from 'lit';
|
||||
import { query } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import { bundledLanguagesInfo, type ThemedToken } from 'shiki';
|
||||
|
||||
import { CodeBlockConfigExtension } from './code-block-config.js';
|
||||
import { CodeBlockInlineManagerExtension } from './code-block-inline.js';
|
||||
import { CodeBlockHighlighter } from './code-block-service.js';
|
||||
import { CodeBlockPreviewIdentifier } from './code-preview-extension.js';
|
||||
import { codeBlockStyles } from './styles.js';
|
||||
|
||||
export class CodeBlockComponent extends CaptionedBlockComponent<CodeBlockModel> {
|
||||
@@ -68,7 +66,7 @@ export class CodeBlockComponent extends CaptionedBlockComponent<CodeBlockModel>
|
||||
}
|
||||
|
||||
get readonly() {
|
||||
return this.store.readonly;
|
||||
return this.doc.readonly;
|
||||
}
|
||||
|
||||
get langs() {
|
||||
@@ -226,7 +224,7 @@ export class CodeBlockComponent extends CaptionedBlockComponent<CodeBlockModel>
|
||||
return;
|
||||
},
|
||||
Tab: ctx => {
|
||||
if (this.store.readonly) return;
|
||||
if (this.doc.readonly) return;
|
||||
const state = ctx.get('keyboardState');
|
||||
const event = state.raw;
|
||||
const inlineEditor = this.inlineEditor;
|
||||
@@ -334,10 +332,10 @@ export class CodeBlockComponent extends CaptionedBlockComponent<CodeBlockModel>
|
||||
return true;
|
||||
},
|
||||
Delete: () => {
|
||||
return;
|
||||
return true;
|
||||
},
|
||||
Enter: () => {
|
||||
this.store.captureSync();
|
||||
this.doc.captureSync();
|
||||
return true;
|
||||
},
|
||||
'Mod-Enter': () => {
|
||||
@@ -348,16 +346,11 @@ export class CodeBlockComponent extends CaptionedBlockComponent<CodeBlockModel>
|
||||
if (!inlineRange || !inlineEditor) return;
|
||||
const isEnd = model.props.text.length === inlineRange.index;
|
||||
if (!isEnd) return;
|
||||
const parent = this.store.getParent(model);
|
||||
const parent = this.doc.getParent(model);
|
||||
if (!parent) return;
|
||||
const index = parent.children.indexOf(model);
|
||||
if (index === -1) return;
|
||||
const id = this.store.addBlock(
|
||||
'affine:paragraph',
|
||||
{},
|
||||
parent,
|
||||
index + 1
|
||||
);
|
||||
const id = this.doc.addBlock('affine:paragraph', {}, parent, index + 1);
|
||||
focusTextModel(std, id);
|
||||
return true;
|
||||
},
|
||||
@@ -368,7 +361,7 @@ export class CodeBlockComponent extends CaptionedBlockComponent<CodeBlockModel>
|
||||
|
||||
copyCode() {
|
||||
const model = this.model;
|
||||
const slice = Slice.fromModels(model.store, [model]);
|
||||
const slice = Slice.fromModels(model.doc, [model]);
|
||||
this.std.clipboard
|
||||
.copySlice(slice)
|
||||
.then(() => {
|
||||
@@ -388,16 +381,8 @@ export class CodeBlockComponent extends CaptionedBlockComponent<CodeBlockModel>
|
||||
|
||||
override renderBlock(): TemplateResult<1> {
|
||||
const showLineNumbers =
|
||||
(this.std.getOptional(CodeBlockConfigExtension.identifier)
|
||||
?.showLineNumbers ??
|
||||
true) &&
|
||||
(this.model.props.lineNumber ?? true);
|
||||
|
||||
const preview = !!this.model.props.preview;
|
||||
const previewContext = this.std.getOptional(
|
||||
CodeBlockPreviewIdentifier(this.model.props.language ?? '')
|
||||
);
|
||||
const shouldRenderPreview = preview && previewContext;
|
||||
this.std.getOptional(CodeBlockConfigExtension.identifier)
|
||||
?.showLineNumbers ?? true;
|
||||
|
||||
return html`
|
||||
<div
|
||||
@@ -405,50 +390,40 @@ export class CodeBlockComponent extends CaptionedBlockComponent<CodeBlockModel>
|
||||
'affine-code-block-container': true,
|
||||
mobile: IS_MOBILE,
|
||||
wrap: this.model.props.wrap,
|
||||
'disable-line-numbers': !showLineNumbers,
|
||||
})}
|
||||
>
|
||||
<rich-text
|
||||
style=${styleMap({
|
||||
display: shouldRenderPreview ? 'none' : undefined,
|
||||
})}
|
||||
.yText=${this.model.props.text.yText}
|
||||
.inlineEventSource=${this.topContenteditableElement ?? nothing}
|
||||
.undoManager=${this.store.history.undoManager}
|
||||
.undoManager=${this.doc.history}
|
||||
.attributesSchema=${this.inlineManager.getSchema()}
|
||||
.attributeRenderer=${this.inlineManager.getRenderer()}
|
||||
.readonly=${this.store.readonly}
|
||||
.readonly=${this.doc.readonly}
|
||||
.inlineRangeProvider=${this._inlineRangeProvider}
|
||||
.enableClipboard=${false}
|
||||
.enableUndoRedo=${false}
|
||||
.wrapText=${this.model.props.wrap}
|
||||
.verticalScrollContainerGetter=${() => getViewportElement(this.host)}
|
||||
.vLineRenderer=${(vLine: VLine) => {
|
||||
return html`
|
||||
<span contenteditable="false" class="line-number"
|
||||
>${vLine.index + 1}</span
|
||||
>
|
||||
${vLine.renderVElements()}
|
||||
`;
|
||||
}}
|
||||
.vLineRenderer=${showLineNumbers
|
||||
? (vLine: VLine) => {
|
||||
return html`
|
||||
<span contenteditable="false" class="line-number"
|
||||
>${vLine.index + 1}</span
|
||||
>
|
||||
${vLine.renderVElements()}
|
||||
`;
|
||||
}
|
||||
: undefined}
|
||||
>
|
||||
</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)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
setWrap(wrap: boolean) {
|
||||
this.store.updateBlock(this.model, { wrap });
|
||||
this.doc.updateBlock(this.model, { wrap });
|
||||
}
|
||||
|
||||
@query('rich-text')
|
||||
|
||||
@@ -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 });
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import type {
|
||||
MenuItemGroup,
|
||||
} 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 { noop } from '@blocksuite/global/utils';
|
||||
import { MoreVerticalIcon } from '@blocksuite/icons/lit';
|
||||
@@ -31,11 +30,12 @@ export class AffineCodeToolbar extends WithDisposable(LitElement) {
|
||||
padding: 4px;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.code-toolbar-button {
|
||||
color: ${unsafeCSSVarV2('icon/primary')};
|
||||
background-color: ${unsafeCSSVarV2('button/secondary')};
|
||||
color: var(--affine-icon-color);
|
||||
background-color: var(--affine-background-primary-color);
|
||||
box-shadow: var(--affine-shadow-1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
@@ -4,10 +4,6 @@ import {
|
||||
showPopFilterableList,
|
||||
} from '@blocksuite/affine-components/filterable-list';
|
||||
import { ArrowDownIcon } from '@blocksuite/affine-components/icons';
|
||||
import {
|
||||
DocModeProvider,
|
||||
TelemetryProvider,
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
|
||||
import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit';
|
||||
import { noop } from '@blocksuite/global/utils';
|
||||
@@ -22,19 +18,24 @@ export class LanguageListButton extends WithDisposable(
|
||||
SignalWatcher(LitElement)
|
||||
) {
|
||||
static override styles = css`
|
||||
:host {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.lang-button {
|
||||
background-color: var(--affine-background-primary-color);
|
||||
box-shadow: var(--affine-shadow-1);
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
padding: 2px 4px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.lang-button:hover {
|
||||
background: ${unsafeCSSVarV2('layer/background/hoverOverlay')};
|
||||
background: var(--affine-hover-color-filled);
|
||||
}
|
||||
|
||||
.lang-button[hover] {
|
||||
background: ${unsafeCSSVarV2('layer/background/hoverOverlay')};
|
||||
background: var(--affine-hover-color-filled);
|
||||
}
|
||||
|
||||
.lang-button-icon {
|
||||
@@ -52,7 +53,7 @@ export class LanguageListButton extends WithDisposable(
|
||||
private _abortController?: AbortController;
|
||||
|
||||
private readonly _clickLangBtn = () => {
|
||||
if (this.blockComponent.store.readonly) return;
|
||||
if (this.blockComponent.doc.readonly) return;
|
||||
if (this._abortController) {
|
||||
// Close the language list if it's already opened.
|
||||
this._abortController.abort();
|
||||
@@ -74,21 +75,9 @@ export class LanguageListButton extends WithDisposable(
|
||||
sortedBundledLanguages.splice(index, 1);
|
||||
sortedBundledLanguages.unshift(item);
|
||||
}
|
||||
this.blockComponent.store.transact(() => {
|
||||
this.blockComponent.doc.transact(() => {
|
||||
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,
|
||||
items: this._sortedBundledLanguages,
|
||||
@@ -149,10 +138,10 @@ export class LanguageListButton extends WithDisposable(
|
||||
</div>`}
|
||||
height="24px"
|
||||
@click=${this._clickLangBtn}
|
||||
?disabled=${this.blockComponent.store.readonly}
|
||||
?disabled=${this.blockComponent.doc.readonly}
|
||||
>
|
||||
<span class="lang-button-icon" slot="suffix">
|
||||
${!this.blockComponent.store.readonly ? ArrowDownIcon : nothing}
|
||||
${!this.blockComponent.doc.readonly ? ArrowDownIcon : nothing}
|
||||
</span>
|
||||
</icon-button> `;
|
||||
}
|
||||
|
||||
@@ -1,116 +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) => {
|
||||
if (this.blockComponent.store.readonly) return;
|
||||
|
||||
this.blockComponent.store.updateBlock(this.blockComponent.model, {
|
||||
preview: 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.model.props.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;
|
||||
}
|
||||
@@ -9,12 +9,10 @@ import {
|
||||
import type { MenuItemGroup } from '@blocksuite/affine-components/toolbar';
|
||||
import { isInsidePageEditor } from '@blocksuite/affine-shared/utils';
|
||||
import { noop, sleep } from '@blocksuite/global/utils';
|
||||
import { NumberedListIcon } from '@blocksuite/icons/lit';
|
||||
import { BlockSelection } from '@blocksuite/std';
|
||||
import { html } from 'lit';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
|
||||
import { CodeBlockConfigExtension } from '../code-block-config.js';
|
||||
import type { CodeBlockToolbarContext } from './context.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',
|
||||
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
|
||||
export const clipboardGroup: MenuItemGroup<CodeBlockToolbarContext> = {
|
||||
type: 'clipboard',
|
||||
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',
|
||||
label: 'Duplicate',
|
||||
@@ -236,7 +173,6 @@ export const deleteGroup: MenuItemGroup<CodeBlockToolbarContext> = {
|
||||
};
|
||||
|
||||
export const MORE_GROUPS: MenuItemGroup<CodeBlockToolbarContext>[] = [
|
||||
toggleGroup,
|
||||
clipboardGroup,
|
||||
deleteGroup,
|
||||
];
|
||||
|
||||
@@ -8,7 +8,7 @@ export class CodeBlockToolbarContext extends MenuContext {
|
||||
};
|
||||
|
||||
get doc() {
|
||||
return this.blockComponent.store;
|
||||
return this.blockComponent.doc;
|
||||
}
|
||||
|
||||
get host() {
|
||||
|
||||
@@ -12,5 +12,5 @@ export const duplicateCodeBlock = (model: CodeBlockModel) => {
|
||||
...duplicateProps,
|
||||
};
|
||||
|
||||
return model.store.addSiblingBlocks(model, [newProps])[0];
|
||||
return model.doc.addSiblingBlocks(model, [newProps])[0];
|
||||
};
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
} from './code-toolbar';
|
||||
import { AffineCodeToolbar } from './code-toolbar/components/code-toolbar';
|
||||
import { LanguageListButton } from './code-toolbar/components/lang-button';
|
||||
import { PreviewButton } from './code-toolbar/components/preview-button';
|
||||
import { AffineCodeUnit } from './highlight/affine-code-unit';
|
||||
|
||||
export function effects() {
|
||||
@@ -14,14 +13,12 @@ export function effects() {
|
||||
customElements.define(AFFINE_CODE_TOOLBAR_WIDGET, AffineCodeToolbarWidget);
|
||||
customElements.define('affine-code-unit', AffineCodeUnit);
|
||||
customElements.define('affine-code', CodeBlockComponent);
|
||||
customElements.define('preview-button', PreviewButton);
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'language-list-button': LanguageListButton;
|
||||
'affine-code-toolbar': AffineCodeToolbar;
|
||||
'preview-button': PreviewButton;
|
||||
[AFFINE_CODE_TOOLBAR_WIDGET]: AffineCodeToolbarWidget;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ export * from './adapters';
|
||||
export * from './clipboard';
|
||||
export * from './code-block';
|
||||
export * from './code-block-config';
|
||||
export * from './code-preview-extension';
|
||||
export * from './code-block-spec';
|
||||
export * from './code-toolbar';
|
||||
export * from './turbo/code-layout-handler';
|
||||
export * from './turbo/code-painter.worker';
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user