Compare commits

..

5 Commits

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

View File

@@ -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.2.14
DB_DATABASE_NAME=affine

View File

@@ -1,65 +0,0 @@
name: affine_dev_services
services:
postgres:
env_file:
- .env
image: pgvector/pgvector:pg${DB_VERSION:-16}
ports:
- 5432:5432
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_USER: ${DB_USERNAME}
POSTGRES_DB: ${DB_DATABASE_NAME}
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:latest
ports:
- 6379:6379
mailhog:
image: mailhog/mailhog:latest
ports:
- 1025:1025
- 8025:8025
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
networks:
dev:
volumes:
postgres_data:
elasticsearch_data:

View File

@@ -24,26 +24,8 @@ services:
- 1025:1025
- 8025:8025
# https://manual.manticoresearch.com/Starting_the_server/Docker
manticoresearch:
image: manticoresearch/manticore:${MANTICORE_VERSION:-9.2.14}
restart: always
ports:
- 9308:9308
ulimits:
nproc: 65535
nofile:
soft: 65535
hard: 65535
memlock:
soft: -1
hard: -1
volumes:
- manticoresearch_data:/var/lib/manticore
networks:
dev:
volumes:
postgres_data:
manticoresearch_data:

View File

@@ -20,9 +20,4 @@ CONFIG_LOCATION=~/.affine/self-host/config
# database credentials
DB_USERNAME=affine
DB_PASSWORD=
DB_DATABASE=affine
# indexer search provider manticoresearch version
MANTICORE_VERSION=9.2.14
# position of the manticoresearch data to persist
MANTICORE_DATA_LOCATION=~/.affine/self-host/manticore
DB_DATABASE=affine

View File

@@ -10,8 +10,6 @@ services:
condition: service_healthy
postgres:
condition: service_healthy
indexer:
condition: service_healthy
affine_migration:
condition: service_completed_successfully
volumes:
@@ -43,8 +41,6 @@ services:
condition: service_healthy
redis:
condition: service_healthy
indexer:
condition: service_healthy
redis:
image: redis
@@ -76,24 +72,3 @@ services:
timeout: 5s
retries: 5
restart: unless-stopped
indexer:
image: manticoresearch/manticore:${MANTICORE_VERSION:-9.2.14}
container_name: affine_indexer
volumes:
- ${MANTICORE_DATA_LOCATION}:/var/lib/manticore
ulimits:
nproc: 65535
nofile:
soft: 65535
hard: 65535
memlock:
soft: -1
hard: -1
healthcheck:
test:
['CMD', 'wget', '-O-', 'http://127.0.0.1:9308']
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped

View File

@@ -70,18 +70,6 @@
"concurrency": 1
}
},
"queues.indexer": {
"type": "object",
"description": "The config for indexer job queue\n@default {\"concurrency\":1}",
"properties": {
"concurrency": {
"type": "number"
}
},
"default": {
"concurrency": 1
}
},
"queues.notification": {
"type": "object",
"description": "The config for notification job queue\n@default {\"concurrency\":10}",
@@ -135,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",
@@ -487,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",
@@ -646,13 +634,6 @@
"apiKey": ""
}
},
"providers.anthropic": {
"type": "object",
"description": "The config for the anthropic provider.\n@default {\"apiKey\":\"\"}",
"default": {
"apiKey": ""
}
},
"unsplash": {
"type": "object",
"description": "The config for the unsplash key.\n@default {\"key\":\"\"}",
@@ -660,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\"}}",
@@ -806,37 +780,6 @@
}
}
},
"indexer": {
"type": "object",
"description": "Configuration for indexer module",
"properties": {
"enabled": {
"type": "boolean",
"description": "Enable indexer plugin\n@default true",
"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.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": ""
}
}
},
"oauth": {
"type": "object",
"description": "Configuration for oauth module",

View File

@@ -16,10 +16,6 @@ const {
REDIS_SERVER_HOST,
REDIS_SERVER_PASSWORD,
STATIC_IP_NAME,
AFFINE_INDEXER_SEARCH_PROVIDER,
AFFINE_INDEXER_SEARCH_ENDPOINT,
AFFINE_INDEXER_SEARCH_USERNAME,
AFFINE_INDEXER_SEARCH_PASSWORD,
} = process.env;
const buildType = BUILD_TYPE || 'canary';
@@ -85,12 +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.username="${AFFINE_INDEXER_SEARCH_USERNAME}"`,
`--set-string global.indexer.password="${AFFINE_INDEXER_SEARCH_PASSWORD}"`,
];
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}\\" }"`,
@@ -140,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}`,

View File

@@ -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,7 +21,6 @@ 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
run: |

View File

@@ -69,17 +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_USERNAME
value: "{{ .Values.global.indexer.username }}"
- name: AFFINE_INDEXER_SEARCH_PASSWORD
valueFrom:
secretKeyRef:
name: indexer
key: indexer-password
- name: AFFINE_SERVER_PORT
value: "{{ .Values.global.docService.port }}"
- name: AFFINE_SERVER_SUB_PATH

View File

@@ -67,17 +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_USERNAME
value: "{{ .Values.global.indexer.username }}"
- name: AFFINE_INDEXER_SEARCH_PASSWORD
valueFrom:
secretKeyRef:
name: indexer
key: indexer-password
- name: AFFINE_SERVER_PORT
value: "{{ .Values.service.port }}"
- name: AFFINE_SERVER_SUB_PATH

View File

@@ -44,17 +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_USERNAME
value: "{{ .Values.global.indexer.username }}"
- name: AFFINE_INDEXER_SEARCH_PASSWORD
valueFrom:
secretKeyRef:
name: indexer
key: indexer-password
resources:
requests:
cpu: '100m'

View File

@@ -69,17 +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_USERNAME
value: "{{ .Values.global.indexer.username }}"
- name: AFFINE_INDEXER_SEARCH_PASSWORD
valueFrom:
secretKeyRef:
name: indexer
key: indexer-password
- name: AFFINE_SERVER_PORT
value: "{{ .Values.service.port }}"
- name: AFFINE_SERVER_SUB_PATH

View File

@@ -69,17 +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_USERNAME
value: "{{ .Values.global.indexer.username }}"
- name: AFFINE_INDEXER_SEARCH_PASSWORD
valueFrom:
secretKeyRef:
name: indexer
key: indexer-password
- name: AFFINE_SERVER_PORT
value: "{{ .Values.service.port }}"
- name: AFFINE_SERVER_HOST

View File

@@ -1,13 +0,0 @@
{{- if .Values.global.indexer.password -}}
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-password: {{ .Values.global.indexer.password | b64enc }}
{{- end }}

View File

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

View File

@@ -136,8 +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 }}
with:
target: ${{ matrix.targets.name }}
package: '@affine/server-native'
@@ -174,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
@@ -257,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

View File

@@ -157,7 +157,6 @@ jobs:
runs-on: ubuntu-latest
needs:
- optimize_ci
- build-server-native
if: needs.optimize_ci.outputs.skip == 'false'
steps:
- uses: actions/checkout@v4
@@ -166,25 +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 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"
@@ -555,8 +542,8 @@ 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
@@ -582,25 +569,7 @@ jobs:
ports:
- 1025:1025
- 8025:8025
manticoresearch:
image: manticoresearch/manticore:9.2.14
ports:
- 9308:9308
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
@@ -662,10 +631,6 @@ jobs:
image: redis
ports:
- 6379:6379
indexer:
image: manticoresearch/manticore:9.2.14
ports:
- 9308:9308
steps:
- uses: actions/checkout@v4
@@ -1103,10 +1068,6 @@ jobs:
ports:
- 1025:1025
- 8025:8025
indexer:
image: manticoresearch/manticore:9.2.14
ports:
- 9308:9308
steps:
- uses: actions/checkout@v4

View File

@@ -103,10 +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_USERNAME: ${{ secrets.AFFINE_INDEXER_SEARCH_USERNAME }}
AFFINE_INDEXER_SEARCH_PASSWORD: ${{ secrets.AFFINE_INDEXER_SEARCH_PASSWORD }}
deploy-done:
needs:
@@ -160,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
@@ -175,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 }}

View File

@@ -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

824
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -43,7 +43,7 @@ _Special thanks to [Blaze](https://runblaze.dev) for their support of this proje
<a href="https://affine.pro/redirect/discord">Discord</a> |
<a href="https://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).
[![Run on ClawCloud](https://raw.githubusercontent.com/ClawCloud/Run-Template/refs/heads/main/Run-on-ClawCloud.svg)](https://template.run.claw.cloud/?openapp=system-fastdeploy%3FtemplateName%3Daffine)
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

View File

@@ -258,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",
@@ -274,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",
@@ -287,6 +284,6 @@
"version": "0.21.0",
"devDependencies": {
"@vanilla-extract/vite-plugin": "^5.0.0",
"vitest": "3.1.3"
"vitest": "3.1.2"
}
}

View File

@@ -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>`

View File

@@ -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);
});
});

View File

@@ -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);
});
});

View File

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

View File

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

View File

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

View File

@@ -20,6 +20,7 @@
"@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",

View File

@@ -7,10 +7,6 @@ import {
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,
@@ -28,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';
@@ -41,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> {
@@ -55,13 +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;
}
blobState$ = signal<Partial<BlobState>>({});
protected containerStyleMap = styleMap({
position: 'relative',
@@ -84,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');
};
@@ -108,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, {
@@ -138,22 +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();
if (!this.model.props.style && !this.store.readonly) {
this.store.withoutTransact(() => {
this.store.updateBlock(this.model, {
this.disposables.add(
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.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);
@@ -207,135 +230,128 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
protected renderWithHorizontal(
classInfo: ClassInfo,
{ icon, title, description, kind, state }: 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.renderReloadButton],
['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 }: 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.renderReloadButton],
['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>(() => {
protected renderCard = () => {
const { name, size, style } = this.model.props;
const cardStyle = style ?? AttachmentBlockStyles[1];
const theme = this.std.get(ThemeProvider).theme$.value;
const loadingIcon = getLoadingIconWith(theme);
const size = this.model.props.size;
const name = this.model.props.name$.value;
const kind = getAttachmentFileIcon(name.split('.').pop() ?? '');
const resolvedState = this.resourceController.resolveStateWith({
loadingIcon,
errorIcon: WarningIcon(),
icon: AttachmentIcon(),
title: name,
description: humanFileSize(size),
});
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;
return html`
<affine-resource-status
class="affine-attachment-embed-status"
.message=${message}
.reload=${() => this.reload()}
></affine-resource-status>
`;
})}
`;
};
private readonly _renderCitation = () => {
const { name, footnoteIdentifier } = this.model.props;
const fileType = name.split('.').pop() ?? '';
@@ -360,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;

View File

@@ -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,
});
},
};
},
}
);

View File

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

View File

@@ -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();

View File

@@ -18,7 +18,7 @@ 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 openFileOrFiles();
@@ -41,7 +41,7 @@ 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 openFileOrFiles();

View File

@@ -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,7 +243,7 @@ const builtinToolbarConfig = {
icon: ResetIcon(),
run(ctx) {
const block = ctx.getCurrentBlockByType(AttachmentBlockComponent);
block?.reload();
block?.refreshData();
},
},
{

View File

@@ -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;
}

View File

@@ -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 {

View File

@@ -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;
@@ -54,6 +47,13 @@ export const styles = css`
color: var(--affine-text-primary-color);
}
.truncate {
align-self: stretch;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.affine-attachment-content-title-text {
color: var(--affine-text-primary-color);
font-family: var(--affine-font-family);
@@ -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;

View File

@@ -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(

View File

@@ -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);
}
}
}

View File

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

View File

@@ -32,7 +32,7 @@
"zod": "^3.23.8"
},
"devDependencies": {
"vitest": "3.1.3"
"vitest": "3.1.2"
},
"exports": {
".": "./src/index.ts",

View File

@@ -6,10 +6,9 @@ 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 { BlockSelection } from '@blocksuite/std';
import { computed, type ReadonlySignal, signal } from '@preact/signals-core';
@@ -72,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 = {
@@ -120,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();
}
};
@@ -143,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}
@@ -187,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;
}

View File

@@ -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';
@@ -52,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;

View File

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

View File

@@ -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',
};
}

View File

@@ -61,7 +61,7 @@ export class BookmarkCard extends SignalWatcher(
const theme = this.bookmark.std.get(ThemeProvider).theme;
const { LoadingIcon, EmbedCardBannerIcon } = getEmbedCardIcons(theme);
const imageProxyService = this.bookmark.store.get(ImageProxyService);
const imageProxyService = this.bookmark.doc.get(ImageProxyService);
const titleIcon = this.loading
? LoadingIcon

View File

@@ -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);

View File

@@ -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';

View File

@@ -1,4 +1,4 @@
import { LinkPreviewServiceIdentifier } from '@blocksuite/affine-shared/services';
import { LinkPreviewerService } from '@blocksuite/affine-shared/services';
import { isAbortError } from '@blocksuite/affine-shared/utils';
import 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,

View File

@@ -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);
}
}
}

View File

@@ -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();
},
},
};

View File

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

View File

@@ -33,24 +33,19 @@ export const calloutSlashMenuConfig: SlashMenuConfig = {
when: ({ std, model }) => {
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(() => {

View File

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

View File

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

View File

@@ -26,13 +26,11 @@ import { computed, effect, type Signal, signal } from '@preact/signals-core';
import { html, nothing, type TemplateResult } from 'lit';
import { 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(() => {
@@ -391,12 +384,6 @@ export class CodeBlockComponent extends CaptionedBlockComponent<CodeBlockModel>
this.std.getOptional(CodeBlockConfigExtension.identifier)
?.showLineNumbers ?? true;
const preview = !!this.model.props.preview;
const previewContext = this.std.getOptional(
CodeBlockPreviewIdentifier(this.model.props.language ?? '')
);
const shouldRenderPreview = preview && previewContext;
return html`
<div
class=${classMap({
@@ -406,15 +393,12 @@ export class CodeBlockComponent extends CaptionedBlockComponent<CodeBlockModel>
})}
>
<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}
@@ -432,22 +416,14 @@ export class CodeBlockComponent extends CaptionedBlockComponent<CodeBlockModel>
: 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')

View File

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

View File

@@ -30,6 +30,7 @@ export class AffineCodeToolbar extends WithDisposable(LitElement) {
padding: 4px;
margin: 0;
display: flex;
justify-content: flex-end;
}
.code-toolbar-button {
@@ -38,10 +39,6 @@ export class AffineCodeToolbar extends WithDisposable(LitElement) {
box-shadow: var(--affine-shadow-1);
border-radius: 4px;
}
.copy-code {
margin-left: auto;
}
`;
private _currentOpenMenu: AbortController | null = null;

View File

@@ -18,13 +18,16 @@ 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 {
@@ -50,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();
@@ -72,7 +75,7 @@ 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;
});
},
@@ -135,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> `;
}

View File

@@ -1,97 +0,0 @@
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`
.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,
});
};
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;
}

View File

@@ -42,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',
@@ -122,28 +110,16 @@ export const clipboardGroup: MenuItemGroup<CodeBlockToolbarContext> = {
{
type: 'wrap',
generate: ({ blockComponent, close }) => {
const wrapped = blockComponent.model.props.wrap;
const label = wrapped ? 'Cancel wrap' : 'Wrap';
const icon = wrapped ? CancelWrapIcon : WrapIcon;
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);
close();
}}
aria-label=${label}
>
${icon}
<span class="label">${label}</span>
<toggle-switch
style="margin-left: auto;"
.on="${wrapped}"
></toggle-switch>
</editor-menu-action>
`;
label,
icon,
action: () => {
blockComponent.setWrap(!wrapped);
close();
},
};
},

View File

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

View File

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

View File

@@ -5,7 +5,6 @@ import {
} from './code-toolbar';
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;
}
}

View File

@@ -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';

View File

@@ -6,7 +6,7 @@ export const codeBlockStyles = css`
font-size: var(--affine-font-xs);
line-height: var(--affine-line-height);
position: relative;
padding: 32px 20px;
padding: 28px 24px;
background: var(--affine-background-code-block);
border-radius: 10px;
box-sizing: border-box;
@@ -36,8 +36,8 @@ export const codeBlockStyles = css`
.affine-code-block-container .line-number {
position: sticky;
text-align: left;
padding-right: 12px;
width: 32px;
padding-right: 4px;
width: 24px;
word-break: break-word;
white-space: nowrap;
left: -0.5px;
@@ -49,8 +49,4 @@ export const codeBlockStyles = css`
box-sizing: border-box;
user-select: none;
}
affine-code .affine-code-block-preview {
padding: 12px;
}
`;

View File

@@ -11,7 +11,6 @@ import {
import { literal, unsafeStatic } from 'lit/static-html.js';
import { getCodeClipboardExtensions } from './clipboard/index.js';
import { CodeBlockConfigExtension } from './code-block-config';
import {
CodeBlockInlineManagerExtension,
CodeBlockUnitSpecExtension,
@@ -22,7 +21,7 @@ import { AFFINE_CODE_TOOLBAR_WIDGET } from './code-toolbar/index.js';
import { codeSlashMenuConfig } from './configs/slash-menu.js';
import { effects } from './effects.js';
const codeToolbarWidget = WidgetViewExtension(
export const codeToolbarWidget = WidgetViewExtension(
'affine:code',
AFFINE_CODE_TOOLBAR_WIDGET,
literal`${unsafeStatic(AFFINE_CODE_TOOLBAR_WIDGET)}`
@@ -52,12 +51,6 @@ export class CodeBlockViewExtension extends ViewExtensionProvider {
]);
if (!this.isMobile(context.scope)) {
context.register(codeToolbarWidget);
} else {
context.register(
CodeBlockConfigExtension({
showLineNumbers: false,
})
);
}
}
}

View File

@@ -21,21 +21,21 @@ export const dataViewSlashMenuConfig: SlashMenuConfig = {
},
group: '7_Database@1',
when: ({ model, std }) =>
!isInsideBlockByFlavour(model.store, model, 'affine:edgeless-text') &&
!isInsideBlockByFlavour(model.doc, model, 'affine:edgeless-text') &&
!!std.get(FeatureFlagService).getFlag('enable_block_query'),
action: ({ model, std }) => {
const { host } = std;
const parent = host.store.getParent(model);
const parent = host.doc.getParent(model);
if (!parent) return;
const index = parent.children.indexOf(model);
const id = host.store.addBlock(
const id = host.doc.addBlock(
'affine:data-view',
{},
host.store.getParent(model),
host.doc.getParent(model),
index + 1
);
const dataViewModel = host.store.getBlock(id)!;
const dataViewModel = host.doc.getBlock(id)!;
const dataView = std.view.getBlock(
dataViewModel.id
@@ -43,7 +43,7 @@ export const dataViewSlashMenuConfig: SlashMenuConfig = {
dataView?.dataSource.viewManager.viewAdd('table');
if (model.text?.length === 0) {
model.store.deleteBlock(model);
model.doc.deleteBlock(model);
}
},
},

View File

@@ -60,7 +60,7 @@ export class BlockQueryDataSource extends DataSourceBase {
}
get workspace() {
return this.host.store.workspace;
return this.host.doc.workspace;
}
constructor(
@@ -83,18 +83,14 @@ export class BlockQueryDataSource extends DataSourceBase {
this.workspace.docs.forEach(doc => {
this.listenToDoc(doc.getStore());
});
this.workspace.slots.docListUpdated.subscribe(() => {
this.workspace.docs.forEach(doc => {
if (!this.docDisposeMap.has(doc.id)) {
this.listenToDoc(doc.getStore());
}
});
this.docDisposeMap.forEach((_, id) => {
if (!this.workspace.docs.has(id)) {
this.docDisposeMap.get(id)?.();
this.docDisposeMap.delete(id);
}
});
this.workspace.slots.docCreated.subscribe(id => {
const doc = this.workspace.getDoc(id);
if (doc) {
this.listenToDoc(doc.getStore());
}
});
this.workspace.slots.docRemoved.subscribe(id => {
this.docDisposeMap.get(id)?.();
});
}
@@ -171,13 +167,9 @@ export class BlockQueryDataSource extends DataSourceBase {
propertyAdd(
insertToPosition: InsertToPosition,
ops?: {
type?: string;
name?: string;
}
type: string | undefined
): string {
const { type } = ops ?? {};
const doc = this.block.store;
const doc = this.block.doc;
doc.captureSync();
const column = DatabaseBlockDataSource.propertiesMap.value[
type ?? propertyPresets.multiSelectPropertyConfig.type
@@ -300,7 +292,7 @@ export class BlockQueryDataSource extends DataSourceBase {
].config.propertyData.default(),
cells: currentCells.map(() => undefined),
};
this.block.store.captureSync();
this.block.doc.captureSync();
viewColumn.type = toType;
viewColumn.data = result.property;
currentCells.forEach((value, i) => {

View File

@@ -104,7 +104,7 @@ export class DataViewBlockComponent extends CaptionedBlockComponent<DataViewBloc
prefix: CopyIcon,
name: 'Copy',
select: () => {
const slice = Slice.fromModels(this.store, [this.model]);
const slice = Slice.fromModels(this.doc, [this.model]);
this.std.clipboard.copySlice(slice).catch(console.error);
},
}),
@@ -119,9 +119,9 @@ export class DataViewBlockComponent extends CaptionedBlockComponent<DataViewBloc
name: 'Delete Database',
select: () => {
this.model.children.slice().forEach(block => {
this.store.deleteBlock(block);
this.doc.deleteBlock(block);
});
this.store.deleteBlock(this.model);
this.doc.deleteBlock(this.model);
},
}),
],
@@ -237,7 +237,7 @@ export class DataViewBlockComponent extends CaptionedBlockComponent<DataViewBloc
}
private renderDatabaseOps() {
if (this.store.readonly) {
if (this.doc.readonly) {
return nothing;
}
return html` <div class="database-ops" @click="${this._clickDatabaseOps}">

View File

@@ -24,21 +24,21 @@ export class DataViewBlockModel extends BlockModel<Props> {
}
applyViewsUpdate() {
this.store.updateBlock(this, {
this.doc.updateBlock(this, {
views: this.props.views,
});
}
deleteView(id: string) {
this.store.captureSync();
this.store.transact(() => {
this.doc.captureSync();
this.doc.transact(() => {
this.props.views = this.props.views.filter(v => v.id !== id);
});
}
duplicateView(id: string): string {
const newId = this.store.workspace.idGenerator();
this.store.transact(() => {
const newId = this.doc.workspace.idGenerator();
this.doc.transact(() => {
const index = this.props.views.findIndex(v => v.id === id);
const view = this.props.views[index];
if (view) {
@@ -53,7 +53,7 @@ export class DataViewBlockModel extends BlockModel<Props> {
}
moveViewTo(id: string, position: InsertToPosition) {
this.store.transact(() => {
this.doc.transact(() => {
this.props.views = arrayMove(
this.props.views,
v => v.id === id,
@@ -67,7 +67,7 @@ export class DataViewBlockModel extends BlockModel<Props> {
id: string,
update: (data: DataViewDataType) => Partial<DataViewDataType>
) {
this.store.transact(() => {
this.doc.transact(() => {
this.props.views = this.props.views.map(v => {
if (v.id !== id) {
return v;

View File

@@ -24,12 +24,12 @@
"@blocksuite/icons": "^2.2.12",
"@blocksuite/std": "workspace:*",
"@blocksuite/store": "workspace:*",
"@emotion/css": "^11.13.5",
"@floating-ui/dom": "^1.6.13",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.14",
"@types/mdast": "^4.0.4",
"@vanilla-extract/css": "^1.17.0",
"date-fns": "^4.0.0",
"lit": "^3.2.0",
"minimatch": "^10.0.1",

View File

@@ -25,7 +25,7 @@ export const databaseSlashMenuConfig: SlashMenuConfig = {
},
group: '7_Database@0',
when: ({ model }) =>
!isInsideBlockByFlavour(model.store, model, 'affine:edgeless-text'),
!isInsideBlockByFlavour(model.doc, model, 'affine:edgeless-text'),
action: ({ std }) => {
std.command
.chain()
@@ -58,7 +58,7 @@ export const databaseSlashMenuConfig: SlashMenuConfig = {
},
group: '7_Database@2',
when: ({ model }) =>
!isInsideBlockByFlavour(model.store, model, 'affine:edgeless-text'),
!isInsideBlockByFlavour(model.doc, model, 'affine:edgeless-text'),
action: ({ std }) => {
std.command
.chain()

View File

@@ -1,4 +1,7 @@
import { createIdentifier } from '@blocksuite/global/di';
import { createContextKey } from '@blocksuite/data-view';
import type { EditorHost } from '@blocksuite/std';
export const EditorHostKey = createIdentifier<EditorHost>('editor-host');
export const HostContextKey = createContextKey<EditorHost | undefined>(
'editor-host',
undefined
);

View File

@@ -2,7 +2,6 @@ import type {
ColumnDataType,
ColumnUpdater,
DatabaseBlockModel,
ParagraphBlockModel,
} from '@blocksuite/affine-model';
import { getSelectedModelsCommand } from '@blocksuite/affine-shared/commands';
import { FeatureFlagService } from '@blocksuite/affine-shared/services';
@@ -52,70 +51,7 @@ import {
databaseBlockViews,
} from './views/index.js';
type SpacialProperty = {
valueSet: (rowId: string, propertyId: string, value: unknown) => void;
valueGet: (rowId: string, propertyId: string) => unknown;
};
export class DatabaseBlockDataSource extends DataSourceBase {
override get parentProvider() {
return this._model.store.provider;
}
spacialProperties: Record<string, SpacialProperty> = {
'created-time': {
valueSet: () => {},
valueGet: (rowId: string) => {
const model = this.getModelById(rowId) as ParagraphBlockModel;
if (!model) {
return null;
}
return model.props['meta:createdAt'];
},
},
'created-by': {
valueSet: () => {},
valueGet: (rowId: string) => {
const model = this.getModelById(rowId) as
| ParagraphBlockModel
| undefined;
return model ? model.props['meta:createdBy'] : null;
},
},
type: {
valueSet: () => {},
valueGet: (rowId: string) => {
const model = this.getModelById(rowId);
if (!model) {
return;
}
return getIcon(model);
},
},
title: {
valueSet: () => {},
valueGet: (rowId: string) => {
const model = this.getModelById(rowId);
if (!model) {
return;
}
return model.text;
},
},
};
isSpacialProperty(propertyType: string): boolean {
return this.spacialProperties[propertyType] !== undefined;
}
spacialValueGet(
rowId: string,
propertyId: string,
propertyType: string
): unknown {
return this.spacialProperties[propertyType]?.valueGet(rowId, propertyId);
}
static externalProperties = signal<PropertyMetaConfig[]>([]);
static propertiesList = computed(() => {
return [
@@ -163,7 +99,7 @@ export class DatabaseBlockDataSource extends DataSourceBase {
readonly$: ReadonlySignal<boolean> = computed(() => {
return (
this._model.store.readonly ||
this._model.doc.readonly ||
// TODO(@L-Sun): use block level readonly
IS_MOBILE
);
@@ -184,7 +120,7 @@ export class DatabaseBlockDataSource extends DataSourceBase {
viewMetas = databaseBlockViews;
get doc() {
return this._model.store;
return this._model.doc;
}
allPropertyMetas$ = computed<PropertyMetaConfig<any, any, any, any>[]>(() => {
@@ -197,13 +133,9 @@ export class DatabaseBlockDataSource extends DataSourceBase {
);
});
constructor(
model: DatabaseBlockModel,
init?: (dataSource: DatabaseBlockDataSource) => void
) {
constructor(model: DatabaseBlockModel) {
super();
this._model = model; // ensure invariants first
init?.(this); // then allow external initialisation
this._model = model;
}
private _runCapture() {
@@ -221,20 +153,16 @@ export class DatabaseBlockDataSource extends DataSourceBase {
return this._model.children[this._model.childMap.value.get(rowId) ?? -1];
}
private newPropertyName(prefix = 'Column'): string {
private newPropertyName() {
let i = 1;
const hasSameName = (name: string) => {
return this._model.props.columns$.value.some(
column => column.name === name
);
};
while (true) {
let name = i === 1 ? prefix : `${prefix} ${i}`;
if (!hasSameName(name)) {
return name;
}
while (
this._model.props.columns$.value.some(
column => column.name === `Column ${i}`
)
) {
i++;
}
return `Column ${i}`;
}
cellValueChange(rowId: string, propertyId: string, value: unknown): void {
@@ -268,15 +196,20 @@ export class DatabaseBlockDataSource extends DataSourceBase {
}
cellValueGet(rowId: string, propertyId: string): unknown {
if (this.isSpacialProperty(propertyId)) {
return this.spacialValueGet(rowId, propertyId, propertyId);
if (propertyId === 'type') {
const model = this.getModelById(rowId);
if (!model) {
return;
}
return getIcon(model);
}
const type = this.propertyTypeGet(propertyId);
if (!type) {
return;
}
if (this.isSpacialProperty(type)) {
return this.spacialValueGet(rowId, propertyId, type);
if (type === 'title') {
const model = this.getModelById(rowId);
return model?.text;
}
const meta = this.propertyMetaGet(type);
if (!meta) {
@@ -295,13 +228,9 @@ export class DatabaseBlockDataSource extends DataSourceBase {
propertyAdd(
insertToPosition: InsertToPosition,
ops?: {
type?: string;
name?: string;
}
type?: string
): string | undefined {
this.doc.captureSync();
const { type, name } = ops ?? {};
const property = this.propertyMetaGet(
type ?? propertyPresets.multiSelectPropertyConfig.type
);
@@ -311,7 +240,7 @@ export class DatabaseBlockDataSource extends DataSourceBase {
const result = addProperty(
this._model,
insertToPosition,
property.create(this.newPropertyName(name))
property.create(this.newPropertyName())
);
return result;
}
@@ -373,7 +302,7 @@ export class DatabaseBlockDataSource extends DataSourceBase {
return;
}
const { column: prevColumn, index } = result;
this._model.store.transact(() => {
this._model.doc.transact(() => {
if (index >= 0) {
const result = updater(prevColumn);
this._model.props.columns[index] = { ...prevColumn, ...result };
@@ -571,15 +500,15 @@ export class DatabaseBlockDataSource extends DataSourceBase {
}
viewDataAdd(viewData: DataViewDataType): string {
this._model.store.captureSync();
this._model.store.transact(() => {
this._model.doc.captureSync();
this._model.doc.transact(() => {
this._model.props.views = [...this._model.props.views, viewData];
});
return viewData.id;
}
viewDataDelete(viewId: string): void {
this._model.store.captureSync();
this._model.doc.captureSync();
deleteView(this._model, viewId);
}
@@ -639,20 +568,20 @@ export const convertToDatabase = (host: EditorHost, viewType: string) => {
const firstModel = selectedModels?.[0];
if (!firstModel) return;
host.store.captureSync();
host.doc.captureSync();
const parentModel = host.store.getParent(firstModel);
const parentModel = host.doc.getParent(firstModel);
if (!parentModel) {
return;
}
const id = host.store.addBlock(
const id = host.doc.addBlock(
'affine:database',
{},
parentModel,
parentModel.children.indexOf(firstModel)
);
const databaseModel = host.store.getBlock(id)?.model as
const databaseModel = host.doc.getBlock(id)?.model as
| DatabaseBlockModel
| undefined;
if (!databaseModel) {
@@ -660,7 +589,7 @@ export const convertToDatabase = (host: EditorHost, viewType: string) => {
}
const datasource = new DatabaseBlockDataSource(databaseModel);
datasource.viewManager.viewAdd(viewType);
host.store.moveBlocks(selectedModels, databaseModel);
host.doc.moveBlocks(selectedModels, databaseModel);
const selectionManager = host.selection;
selectionManager.clear();

View File

@@ -27,7 +27,6 @@ import {
type DataViewWidget,
type DataViewWidgetProps,
defineUniComponent,
ExternalGroupByConfigProvider,
renderUniLit,
type SingleView,
uniMap,
@@ -48,7 +47,7 @@ import { css, html, nothing, unsafeCSS } from 'lit';
import { popSideDetail } from './components/layout.js';
import { DatabaseConfigExtension } from './config.js';
import { EditorHostKey } from './context/host-context.js';
import { HostContextKey } from './context/host-context.js';
import { DatabaseBlockDataSource } from './data-source.js';
import { BlockRenderer } from './detail-panel/block-renderer.js';
import { NoteRenderer } from './detail-panel/note-renderer.js';
@@ -121,7 +120,7 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent<DatabaseBloc
prefix: CopyIcon(),
name: 'Copy',
select: () => {
const slice = Slice.fromModels(this.store, [this.model]);
const slice = Slice.fromModels(this.doc, [this.model]);
this.std.clipboard
.copySlice(slice)
.then(() => {
@@ -140,9 +139,9 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent<DatabaseBloc
name: 'Delete Database',
select: () => {
this.model.children.slice().forEach(block => {
this.store.deleteBlock(block);
this.doc.deleteBlock(block);
});
this.store.deleteBlock(this.model);
this.doc.deleteBlock(this.model);
},
}),
],
@@ -259,18 +258,18 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent<DatabaseBloc
);
return () => {
this.indicator.remove();
const model = this.store.getBlock(id)?.model;
const model = this.doc.getBlock(id)?.model;
const target = result.modelState.model;
let parent = this.store.getParent(target.id);
let parent = this.doc.getParent(target.id);
const shouldInsertIn = result.placement === 'in';
if (shouldInsertIn) {
parent = target;
}
if (model && target && parent) {
if (shouldInsertIn) {
this.store.moveBlocks([model], parent);
this.doc.moveBlocks([model], parent);
} else {
this.store.moveBlocks(
this.doc.moveBlocks(
[model],
parent,
target,
@@ -334,17 +333,8 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent<DatabaseBloc
get dataSource(): DatabaseBlockDataSource {
if (!this._dataSource) {
this._dataSource = new DatabaseBlockDataSource(this.model, dataSource => {
dataSource.serviceSet(EditorHostKey, this.host);
this.std.provider
.getAll(ExternalGroupByConfigProvider)
.forEach(config => {
dataSource.serviceSet(
ExternalGroupByConfigProvider(config.name),
config
);
});
});
this._dataSource = new DatabaseBlockDataSource(this.model);
this._dataSource.contextSet(HostContextKey, this.host);
const id = currentViewStorage.getCurrentView(this.model.id);
if (id && this.dataSource.viewManager.viewGet(id)) {
this.dataSource.viewManager.setCurrentView(id);
@@ -444,13 +434,13 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent<DatabaseBloc
return peekViewService.peek({
docId,
databaseId: this.blockId,
databaseDocId: this.model.store.id,
databaseDocId: this.model.doc.id,
databaseRowId: data.rowId,
target: this,
});
};
const doc = getSingleDocIdFromText(
this.model.store.getBlock(data.rowId)?.model?.text
this.model.doc.getBlock(data.rowId)?.model?.text
);
if (doc) {
return openDoc(doc);

View File

@@ -0,0 +1,14 @@
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 { DatabaseBlockAdapterExtensions } from './adapters/extension.js';
import { databaseSlashMenuConfig } from './configs/slash-menu.js';
export const DatabaseBlockSpec: ExtensionType[] = [
FlavourExtension('affine:database'),
BlockViewExtension('affine:database', literal`affine-database`),
DatabaseBlockAdapterExtensions,
SlashMenuConfigExtension('affine:database', databaseSlashMenuConfig),
].flat();

View File

@@ -74,7 +74,7 @@ export class BlockRenderer
}
get model() {
return this.host?.store.getBlock(this.rowId)?.model;
return this.host?.doc.getBlock(this.rowId)?.model;
}
override connectedCallback() {
@@ -140,7 +140,7 @@ export class BlockRenderer
return;
}
return html` <div class="database-block-detail-header-icon">
${this.view.cellGetOrCreate(this.rowId, iconColumn).value$.value}
${this.view.cellValueGet(this.rowId, iconColumn)}
</div>`;
}

View File

@@ -36,7 +36,7 @@ export class NoteRenderer
accessor rowId!: string;
rowText$ = computed(() => {
return this.databaseBlock.store.getBlock(this.rowId)?.model?.text;
return this.databaseBlock.doc.getBlock(this.rowId)?.model?.text;
});
allowCreateDoc$ = computed(() => {

View File

@@ -4,7 +4,6 @@ import { DatabaseBlockComponent } from './database-block';
import { DatabaseDndPreviewBlockComponent } from './database-dnd-preview-block';
import { BlockRenderer } from './detail-panel/block-renderer';
import { NoteRenderer } from './detail-panel/note-renderer';
import { CreatedTimeCell } from './properties/created-time/cell-renderer';
import { LinkCell } from './properties/link/cell-renderer';
import { RichTextCell } from './properties/rich-text/cell-renderer';
import { IconCell } from './properties/title/icon';
@@ -16,7 +15,6 @@ export function effects() {
customElements.define('affine-database-link-cell', LinkCell);
customElements.define('data-view-header-area-text', HeaderAreaTextCell);
customElements.define('affine-database-rich-text-cell', RichTextCell);
customElements.define('affine-database-created-time-cell', CreatedTimeCell);
customElements.define('center-peek', CenterPeek);
customElements.define('database-datasource-note-renderer', NoteRenderer);
customElements.define('database-datasource-block-renderer', BlockRenderer);

View File

@@ -4,6 +4,7 @@ export * from './config';
export * from './context';
export * from './data-source';
export * from './database-block';
export * from './database-spec';
export * from './detail-panel/block-renderer';
export * from './detail-panel/note-renderer';
export * from './properties';

View File

@@ -1,54 +0,0 @@
import {
BaseCellRenderer,
createFromBaseCellRenderer,
createIcon,
} from '@blocksuite/data-view';
import { css } from '@emotion/css';
import { format } from 'date-fns/format';
import { html } from 'lit';
import { createdTimePropertyModelConfig } from './define.js';
const createdTimeCellStyle = css({
display: 'flex',
alignItems: 'center',
width: '100%',
height: '100%',
});
const textStyle = css({
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
height: '100%',
});
export class CreatedTimeCell extends BaseCellRenderer<number, number> {
renderContent() {
const formattedDate = this.value
? format(this.value, 'yyyy-MM-dd HH:mm:ss')
: '';
return html`<div class="${textStyle}">${formattedDate}</div>`;
}
override connectedCallback(): void {
super.connectedCallback();
this.classList.add(createdTimeCellStyle);
}
override beforeEnterEditMode() {
return false;
}
override render() {
return html`<div class="date-container">${this.renderContent()}</div>`;
}
}
export const createdTimeColumnConfig =
createdTimePropertyModelConfig.createPropertyMeta({
icon: createIcon('DateTimeIcon'),
cellRenderer: {
view: createFromBaseCellRenderer(CreatedTimeCell),
},
});

View File

@@ -1,30 +0,0 @@
import { propertyType, t } from '@blocksuite/data-view';
import { format } from 'date-fns/format';
import zod from 'zod';
export const createdTimeColumnType = propertyType('created-time');
export const createdTimePropertyModelConfig = createdTimeColumnType.modelConfig(
{
name: 'Created Time',
propertyData: {
schema: zod.object({}),
default: () => ({}),
},
jsonValue: {
schema: zod.number().nullable(),
isEmpty: () => false,
type: () => t.date.instance(),
},
rawValue: {
schema: zod.number().nullable(),
default: () => null,
toString: ({ value }) =>
value != null ? format(value, 'yyyy-MM-dd HH:mm:ss') : '',
fromString: () => {
return { value: null };
},
toJson: ({ value }) => value,
setValue: () => {},
},
}
);

View File

@@ -1,6 +1,5 @@
import { propertyPresets } from '@blocksuite/data-view/property-presets';
import { createdTimeColumnConfig } from './created-time/cell-renderer.js';
import { linkColumnConfig } from './link/cell-renderer.js';
import { richTextColumnConfig } from './rich-text/cell-renderer.js';
import { titleColumnConfig } from './title/cell-renderer.js';
@@ -25,5 +24,4 @@ export const databaseBlockProperties = {
linkColumnConfig,
richTextColumnConfig,
titleColumnConfig,
createdTimeColumnConfig,
};

View File

@@ -1,15 +1,15 @@
import { cssVarV2 } from '@blocksuite/affine-shared/theme';
import { css } from '@emotion/css';
import { baseTheme } from '@toeverything/theme';
import { style } from '@vanilla-extract/css';
export const linkCellStyle = css({
export const linkCellStyle = style({
width: '100%',
height: '100%',
userSelect: 'none',
position: 'relative',
});
export const linkContainerStyle = css({
export const linkContainerStyle = style({
display: 'flex',
position: 'relative',
alignItems: 'center',
@@ -21,8 +21,7 @@ export const linkContainerStyle = css({
lineHeight: 'var(--data-view-cell-text-line-height)',
wordBreak: 'break-all',
});
export const linkIconContainerStyle = css({
export const linkIconContainerStyle = style({
position: 'absolute',
right: '8px',
top: '8px',
@@ -35,8 +34,7 @@ export const linkIconContainerStyle = css({
overflow: 'hidden',
zIndex: 1,
});
export const linkIconStyle = css({
export const linkIconStyle = style({
width: '100%',
height: '100%',
display: 'flex',
@@ -50,13 +48,15 @@ export const linkIconStyle = css({
},
});
export const showLinkIconStyle = css({
[`.${linkCellStyle}:hover &`]: {
visibility: 'visible',
export const showLinkIconStyle = style({
selectors: {
[`${linkCellStyle}:hover &`]: {
visibility: 'visible',
},
},
});
export const linkedDocStyle = css({
export const linkedDocStyle = style({
textDecoration: 'underline',
textDecorationColor: 'var(--affine-divider-color)',
transition: 'text-decoration-color 0.2s ease-out',
@@ -66,7 +66,7 @@ export const linkedDocStyle = css({
},
});
export const linkEditingStyle = css({
export const linkEditingStyle = style({
display: 'flex',
alignItems: 'center',
width: '100%',
@@ -74,7 +74,7 @@ export const linkEditingStyle = css({
border: 'none',
fontFamily: baseTheme.fontSansFamily,
color: 'var(--affine-text-primary-color)',
fontWeight: 400,
fontWeight: '400',
backgroundColor: 'transparent',
fontSize: 'var(--data-view-cell-text-size)',
lineHeight: 'var(--data-view-cell-text-line-height)',
@@ -84,7 +84,7 @@ export const linkEditingStyle = css({
},
});
export const inlineLinkNodeStyle = css({
export const inlineLinkNodeStyle = style({
wordBreak: 'break-all',
color: 'var(--affine-link-color)',
fill: 'var(--affine-link-color)',
@@ -94,6 +94,6 @@ export const inlineLinkNodeStyle = css({
textDecoration: 'none',
});
export const normalTextStyle = css({
export const normalTextStyle = style({
wordBreak: 'break-all',
});

View File

@@ -15,7 +15,7 @@ import { computed } from '@preact/signals-core';
import { html, nothing, type PropertyValues } from 'lit';
import { createRef, ref } from 'lit/directives/ref.js';
import { EditorHostKey } from '../../context/host-context.js';
import { HostContextKey } from '../../context/host-context.js';
import {
inlineLinkNodeStyle,
linkCellStyle,
@@ -26,7 +26,7 @@ import {
linkIconStyle,
normalTextStyle,
showLinkIconStyle,
} from './cell-renderer-css.js';
} from './cell-renderer.css.js';
import { linkPropertyModelConfig } from './define.js';
export class LinkCell extends BaseCellRenderer<string, string> {
@@ -88,7 +88,7 @@ export class LinkCell extends BaseCellRenderer<string, string> {
};
get std() {
const host = this.view.serviceGet(EditorHostKey);
const host = this.view.contextGet(HostContextKey);
return host?.std;
}

View File

@@ -1,13 +1,13 @@
import { css } from '@emotion/css';
import { style } from '@vanilla-extract/css';
export const richTextCellStyle = css({
export const richTextCellStyle = style({
display: 'flex',
alignItems: 'center',
width: '100%',
userSelect: 'none',
});
export const richTextContainerStyle = css({
export const richTextContainerStyle = style({
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',

View File

@@ -24,12 +24,12 @@ import { computed, effect, signal } from '@preact/signals-core';
import { ref } from 'lit/directives/ref.js';
import { html } from 'lit/static-html.js';
import { EditorHostKey } from '../../context/host-context.js';
import { HostContextKey } from '../../context/host-context.js';
import type { DatabaseBlockComponent } from '../../database-block.js';
import {
richTextCellStyle,
richTextContainerStyle,
} from './cell-renderer-css.js';
} from './cell-renderer.css.js';
import { richTextPropertyModelConfig } from './define.js';
function toggleStyle(
@@ -87,7 +87,7 @@ export class RichTextCell extends BaseCellRenderer<Text, string> {
get inlineManager() {
return this.view
.serviceGet(EditorHostKey)
.contextGet(HostContextKey)
?.std.get(DefaultInlineManagerExtension.identifier);
}
@@ -98,7 +98,7 @@ export class RichTextCell extends BaseCellRenderer<Text, string> {
}
get host() {
return this.view.serviceGet(EditorHostKey);
return this.view.contextGet(HostContextKey);
}
private readonly richText$ = signal<RichText>();
@@ -310,6 +310,7 @@ export class RichTextCell extends BaseCellRenderer<Text, string> {
});
}
} else {
console.log(text);
inlineEditor.insertText(inlineRange, text);
inlineEditor.setInlineRange({
index: inlineRange.index + text.length,
@@ -398,7 +399,7 @@ export class RichTextCell extends BaseCellRenderer<Text, string> {
}
private get std() {
return this.view.serviceGet(EditorHostKey)?.std;
return this.view.contextGet(HostContextKey)?.std;
}
insertDelta = (delta: DeltaInsert<AffineTextAttributes>) => {

View File

@@ -5,7 +5,7 @@ import { Text } from '@blocksuite/store';
import * as Y from 'yjs';
import zod from 'zod';
import { EditorHostKey } from '../../context/host-context.js';
import { HostContextKey } from '../../context/host-context.js';
import { isLinkedDoc } from '../../utils/title-doc.js';
export const richTextColumnType = propertyType('rich-text');
@@ -43,7 +43,7 @@ export const richTextPropertyModelConfig = richTextColumnType.modelConfig({
},
toJson: ({ value, dataSource }) => {
if (!value) return null;
const host = dataSource.serviceGet(EditorHostKey);
const host = dataSource.contextGet(HostContextKey);
if (host) {
const collection = host.std.workspace;
const yText = toYText(value);

View File

@@ -1,12 +1,12 @@
import { cssVarV2 } from '@blocksuite/affine-shared/theme';
import { css } from '@emotion/css';
import { style } from '@vanilla-extract/css';
export const titleCellStyle = css({
export const titleCellStyle = style({
width: '100%',
display: 'flex',
});
export const titleRichTextStyle = css({
export const titleRichTextStyle = style({
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
@@ -18,7 +18,7 @@ export const titleRichTextStyle = css({
lineHeight: 'var(--data-view-cell-text-line-height)',
});
export const headerAreaIconStyle = css({
export const headerAreaIconStyle = style({
height: 'max-content',
display: 'flex',
alignItems: 'center',

View File

@@ -3,7 +3,7 @@ import { Text } from '@blocksuite/store';
import { Doc } from 'yjs';
import zod from 'zod';
import { EditorHostKey } from '../../context/host-context.js';
import { HostContextKey } from '../../context/host-context.js';
import { isLinkedDoc } from '../../utils/title-doc.js';
export const titleColumnType = propertyType('title');
@@ -28,7 +28,7 @@ export const titlePropertyModelConfig = titleColumnType.modelConfig({
},
toJson: ({ value, dataSource }) => {
if (!value) return '';
const host = dataSource.serviceGet(EditorHostKey);
const host = dataSource.contextGet(HostContextKey);
if (host) {
const collection = host.std.workspace;
const deltas = value.deltas$.value;

View File

@@ -17,14 +17,14 @@ import { property } from 'lit/decorators.js';
import { createRef, ref } from 'lit/directives/ref.js';
import { html } from 'lit/static-html.js';
import { EditorHostKey } from '../../context/host-context.js';
import { HostContextKey } from '../../context/host-context.js';
import type { DatabaseBlockComponent } from '../../database-block.js';
import { getSingleDocIdFromText } from '../../utils/title-doc.js';
import {
headerAreaIconStyle,
titleCellStyle,
titleRichTextStyle,
} from './cell-renderer-css.js';
} from './cell-renderer.css.js';
export class HeaderAreaTextCell extends BaseCellRenderer<Text, string> {
activity = true;
@@ -32,7 +32,7 @@ export class HeaderAreaTextCell extends BaseCellRenderer<Text, string> {
docId$ = signal<string>();
get host() {
return this.view.serviceGet(EditorHostKey);
return this.view.contextGet(HostContextKey);
}
get inlineEditor() {
@@ -50,7 +50,7 @@ export class HeaderAreaTextCell extends BaseCellRenderer<Text, string> {
}
get std() {
return this.view.serviceGet(EditorHostKey)?.std;
return this.view.contextGet(HostContextKey)?.std;
}
private readonly _onCopy = (e: ClipboardEvent) => {
@@ -271,8 +271,7 @@ export class HeaderAreaTextCell extends BaseCellRenderer<Text, string> {
const iconColumn = this.view.mainProperties$.value.iconColumn;
if (!iconColumn) return;
const icon = this.view.cellGetOrCreate(this.cell.rowId, iconColumn).value$
.value;
const icon = this.view.cellValueGet(this.cell.rowId, iconColumn) as string;
if (!icon) return;
return icon;
});

View File

@@ -19,11 +19,11 @@ export function addProperty(
id?: string;
}
): string {
const id = column.id ?? model.store.workspace.idGenerator();
const id = column.id ?? model.doc.workspace.idGenerator();
if (model.props.columns.some(v => v.id === id)) {
return id;
}
model.store.transact(() => {
model.doc.transact(() => {
const col: ColumnDataType = {
...column,
id,
@@ -42,7 +42,7 @@ export function copyCellsByProperty(
fromId: ColumnDataType['id'],
toId: ColumnDataType['id']
) {
model.store.transact(() => {
model.doc.transact(() => {
Object.keys(model.props.cells).forEach(rowId => {
const cell = model.props.cells[rowId]?.[fromId];
if (cell && model.props.cells[rowId]) {
@@ -62,13 +62,13 @@ export function deleteColumn(
const index = model.props.columns.findIndex(v => v.id === columnId);
if (index < 0) return;
model.store.transact(() => {
model.doc.transact(() => {
model.props.columns.splice(index, 1);
});
}
export function deleteRows(model: DatabaseBlockModel, rowIds: string[]) {
model.store.transact(() => {
model.doc.transact(() => {
for (const rowId of rowIds) {
delete model.props.cells[rowId];
}
@@ -76,15 +76,15 @@ export function deleteRows(model: DatabaseBlockModel, rowIds: string[]) {
}
export function deleteView(model: DatabaseBlockModel, id: string) {
model.store.captureSync();
model.store.transact(() => {
model.doc.captureSync();
model.doc.transact(() => {
model.props.views = model.props.views.filter(v => v.id !== id);
});
}
export function duplicateView(model: DatabaseBlockModel, id: string): string {
const newId = model.store.workspace.idGenerator();
model.store.transact(() => {
const newId = model.doc.workspace.idGenerator();
model.doc.transact(() => {
const index = model.props.views.findIndex(v => v.id === id);
const view = model.props.views[index];
if (view) {
@@ -131,7 +131,7 @@ export function moveViewTo(
id: string,
position: InsertToPosition
) {
model.store.transact(() => {
model.doc.transact(() => {
model.props.views = arrayMove(
model.props.views,
v => v.id === id,
@@ -145,7 +145,7 @@ export function updateCell(
rowId: string,
cell: CellDataType
) {
model.store.transact(() => {
model.doc.transact(() => {
const columnId = cell.columnId;
if (
rowId === '__proto__' ||
@@ -180,7 +180,7 @@ export function updateCells(
columnId: string,
cells: Record<string, unknown>
) {
model.store.transact(() => {
model.doc.transact(() => {
Object.entries(cells).forEach(([rowId, value]) => {
if (
rowId === '__proto__' ||
@@ -212,7 +212,7 @@ export function updateProperty(
if (index == null) {
return;
}
model.store.transact(() => {
model.doc.transact(() => {
const column = model.props.columns[index];
if (!column) {
return;
@@ -228,7 +228,7 @@ export const updateView = <ViewData extends ViewBasicDataType>(
id: string,
update: (data: ViewData) => Partial<ViewData>
) => {
model.store.transact(() => {
model.doc.transact(() => {
model.props.views = model.props.views.map(v => {
if (v.id !== id) {
return v;

View File

@@ -0,0 +1,10 @@
import { BlockViewExtension } from '@blocksuite/std';
import type { ExtensionType } from '@blocksuite/store';
import { literal } from 'lit/static-html.js';
import { DividerBlockAdapterExtensions } from './adapters/extension.js';
export const DividerBlockSpec: ExtensionType[] = [
BlockViewExtension('affine:divider', literal`affine-divider`),
DividerBlockAdapterExtensions,
].flat();

View File

@@ -1,2 +1,3 @@
export * from './adapters';
export * from './divider-block';
export * from './divider-spec';

View File

@@ -5,7 +5,6 @@ import {
EDGELESS_TEXT_BLOCK_MIN_HEIGHT,
EDGELESS_TEXT_BLOCK_MIN_WIDTH,
type EdgelessTextBlockModel,
EdgelessTextBlockSchema,
ListBlockModel,
ParagraphBlockModel,
} from '@blocksuite/affine-model';
@@ -22,10 +21,7 @@ import {
GfxBlockComponent,
TextSelection,
} from '@blocksuite/std';
import {
GfxViewInteractionExtension,
type SelectedContext,
} from '@blocksuite/std/gfx';
import type { SelectedContext } from '@blocksuite/std/gfx';
import { css, html } from 'lit';
import { query, state } from 'lit/decorators.js';
import { type StyleInfo, styleMap } from 'lit/directives/style-map.js';
@@ -48,7 +44,7 @@ export class EdgelessTextBlockComponent extends GfxBlockComponent<EdgelessTextBl
`;
private readonly _resizeObserver = new ResizeObserver(() => {
if (this.store.readonly) {
if (this.doc.readonly) {
return;
}
@@ -64,7 +60,7 @@ export class EdgelessTextBlockComponent extends GfxBlockComponent<EdgelessTextBl
const rect = this._textContainer.getBoundingClientRect();
bound.h = rect.height / this.gfx.viewport.zoom;
this.store.updateBlock(this.model, {
this.doc.updateBlock(this.model, {
xywh: bound.serialize(),
});
}
@@ -77,7 +73,7 @@ export class EdgelessTextBlockComponent extends GfxBlockComponent<EdgelessTextBl
EDGELESS_TEXT_BLOCK_MIN_WIDTH * this.gfx.viewport.zoom
);
this.store.updateBlock(this.model, {
this.doc.updateBlock(this.model, {
xywh: bound.serialize(),
});
}
@@ -173,7 +169,7 @@ export class EdgelessTextBlockComponent extends GfxBlockComponent<EdgelessTextBl
!firstChild ||
!matchModels(firstChild, [ListBlockModel, ParagraphBlockModel])
) {
newParagraphId = this.store.addBlock(
newParagraphId = this.doc.addBlock(
'affine:paragraph',
{},
this.model.id,
@@ -186,7 +182,7 @@ export class EdgelessTextBlockComponent extends GfxBlockComponent<EdgelessTextBl
!lastChild ||
!matchModels(lastChild, [ListBlockModel, ParagraphBlockModel])
) {
newParagraphId = this.store.addBlock(
newParagraphId = this.doc.addBlock(
'affine:paragraph',
{},
this.model.id
@@ -288,7 +284,7 @@ export class EdgelessTextBlockComponent extends GfxBlockComponent<EdgelessTextBl
}
if (this.model.children.length === 0) {
const blockId = this.store.addBlock(
const blockId = this.doc.addBlock(
'affine:paragraph',
{ type: 'text' },
this.model.id
@@ -343,7 +339,7 @@ export class EdgelessTextBlockComponent extends GfxBlockComponent<EdgelessTextBl
minWidth: !hasMaxWidth ? '220px' : undefined,
};
this.contentEditable = String(editing && !this.store.readonly$.value);
this.contentEditable = String(editing && !this.doc.readonly$.value);
return html`
<div
@@ -424,69 +420,3 @@ declare global {
'affine-edgeless-text': EdgelessTextBlockComponent;
}
}
export const EdgelessTextInteraction =
GfxViewInteractionExtension<EdgelessTextBlockComponent>(
EdgelessTextBlockSchema.model.flavour,
{
resizeConstraint: {
lockRatio: ['top-left', 'top-right', 'bottom-left', 'bottom-right'],
allowedHandlers: [
'top-left',
'top-right',
'left',
'right',
'bottom-left',
'bottom-right',
],
minWidth: EDGELESS_TEXT_BLOCK_MIN_WIDTH,
},
handleResize: context => {
const { model, view } = context;
const initialScale = model.props.scale;
return {
onResizeStart(context) {
context.default(context);
model.stash('scale');
model.stash('hasMaxWidth');
},
onResizeMove(context) {
const { originalBound, newBound, constraint, lockRatio } = context;
if (lockRatio) {
const originalRealWidth = originalBound.w / initialScale;
const newScale = newBound.w / originalRealWidth;
model.props.scale = newScale;
model.props.xywh = newBound.serialize();
} else {
if (!view.checkWidthOverflow(newBound.w)) {
return;
}
const newRealWidth = clamp(
newBound.w / initialScale,
constraint.minWidth,
constraint.maxWidth
);
const curBound = Bound.deserialize(model.xywh);
model.props.xywh = Bound.serialize({
...newBound,
w: newRealWidth * initialScale,
h: curBound.h,
});
model.props.hasMaxWidth = true;
}
},
onResizeEnd(context) {
context.default(context);
model.pop('scale');
model.pop('hasMaxWidth');
},
};
},
}
);

View File

@@ -0,0 +1,7 @@
import { BlockViewExtension } from '@blocksuite/std';
import type { ExtensionType } from '@blocksuite/store';
import { literal } from 'lit/static-html.js';
export const EdgelessTextBlockSpec: ExtensionType[] = [
BlockViewExtension('affine:edgeless-text', literal`affine-edgeless-text`),
];

View File

@@ -1,3 +1,4 @@
export * from './edgeless-clipboard-config';
export * from './edgeless-text-block.js';
export * from './edgeless-text-spec.js';
export * from './edgeless-toolbar';

View File

@@ -6,7 +6,6 @@ import { BlockViewExtension } from '@blocksuite/std';
import { literal } from 'lit/static-html.js';
import { EdgelessClipboardEdgelessTextConfig } from './edgeless-clipboard-config';
import { EdgelessTextInteraction } from './edgeless-text-block';
import { edgelessTextToolbarExtension } from './edgeless-toolbar';
import { effects } from './effects';
@@ -31,7 +30,6 @@ export class EdgelessTextViewExtension extends ViewExtensionProvider {
]);
context.register(edgelessTextToolbarExtension);
context.register(EdgelessClipboardEdgelessTextConfig);
context.register(EdgelessTextInteraction);
}
}
}

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