Compare commits

..

10 Commits

Author SHA1 Message Date
DarkSky 67f875e32d fix: ava ts loader 2025-01-22 14:31:03 +08:00
DarkSky a2b9b89695 chore: use tla 2025-01-22 14:31:03 +08:00
DarkSky 8fbc03d631 chore: remove outdated api 2025-01-22 14:31:03 +08:00
DarkSky 26a7f1a75d fix: env typo 2025-01-22 14:31:03 +08:00
DarkSky 94c45d7ea3 feat: add more detail for slack report 2025-01-22 14:31:03 +08:00
DarkSky 2bb3504b9e fix: merge error 2025-01-22 14:31:03 +08:00
DarkSky 6f3f291b18 feat: cron job for copilot test 2025-01-22 14:31:03 +08:00
DarkSky 55fffe0762 fix: typing 2025-01-22 14:28:52 +08:00
DarkSky 96a0650841 fix: rebase error 2025-01-22 14:28:52 +08:00
DarkSky e96f89c26b feat: online ci 2025-01-22 14:28:52 +08:00
2579 changed files with 43501 additions and 80562 deletions
-1
View File
@@ -1,6 +1,5 @@
FROM mcr.microsoft.com/devcontainers/base:bookworm
USER vscode
# Install Homebrew For Linux
RUN /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" && \
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" && \
+1
View File
@@ -1,3 +1,4 @@
DATABASE_LOCATION=./postgres
DB_PASSWORD=affine
DB_USERNAME=affine
DB_DATABASE_NAME=affine
-1
View File
@@ -1,2 +1 @@
/blocksuite/ @toeverything/blocksuite-core
/packages/frontend/core/src/blocksuite @toeverything/blocksuite-core
+4 -9
View File
@@ -19,6 +19,8 @@ const {
COPILOT_FAL_API_KEY,
COPILOT_PERPLEXITY_API_KEY,
COPILOT_UNSPLASH_API_KEY,
SLACK_BOT_TOKEN,
RELEASE_SLACK_CHANNEL_ID,
MAILER_SENDER,
MAILER_USER,
MAILER_PASSWORD,
@@ -47,21 +49,18 @@ const replicaConfig = {
graphql: Number(process.env.PRODUCTION_GRAPHQL_REPLICA) || 3,
sync: Number(process.env.PRODUCTION_SYNC_REPLICA) || 3,
renderer: Number(process.env.PRODUCTION_RENDERER_REPLICA) || 3,
doc: Number(process.env.PRODUCTION_DOC_REPLICA) || 3,
},
beta: {
web: 2,
graphql: Number(process.env.BETA_GRAPHQL_REPLICA) || 2,
sync: Number(process.env.BETA_SYNC_REPLICA) || 2,
renderer: Number(process.env.BETA_RENDERER_REPLICA) || 2,
doc: Number(process.env.BETA_DOC_REPLICA) || 2,
},
canary: {
web: 2,
graphql: 2,
sync: 2,
renderer: 2,
doc: 2,
},
};
@@ -70,14 +69,12 @@ const cpuConfig = {
web: '300m',
graphql: '1',
sync: '1',
doc: '1',
renderer: '300m',
},
canary: {
web: '300m',
graphql: '1',
sync: '1',
doc: '1',
renderer: '300m',
},
};
@@ -116,7 +113,6 @@ const createHelmCommand = ({ isDryRun }) => {
`--set web.resources.requests.cpu="${cpu.web}"`,
`--set graphql.resources.requests.cpu="${cpu.graphql}"`,
`--set sync.resources.requests.cpu="${cpu.sync}"`,
`--set doc.resources.requests.cpu="${cpu.doc}"`,
]
: [];
@@ -156,6 +152,8 @@ const createHelmCommand = ({ isDryRun }) => {
`--set-string graphql.app.copilot.fal.key="${COPILOT_FAL_API_KEY}"`,
`--set-string graphql.app.copilot.perplexity.key="${COPILOT_PERPLEXITY_API_KEY}"`,
`--set-string graphql.app.copilot.unsplash.key="${COPILOT_UNSPLASH_API_KEY}"`,
`--set-string graphql.app.copilot.slack.botToken="${SLACK_BOT_TOKEN}"`,
`--set-string graphql.app.copilot.slack.channelId="${RELEASE_SLACK_CHANNEL_ID}"`,
`--set-string graphql.app.mailer.sender="${MAILER_SENDER}"`,
`--set-string graphql.app.mailer.user="${MAILER_USER}"`,
`--set-string graphql.app.mailer.password="${MAILER_PASSWORD}"`,
@@ -174,9 +172,6 @@ const createHelmCommand = ({ isDryRun }) => {
`--set-string renderer.image.tag="${imageTag}"`,
`--set renderer.app.host=${host}`,
`--set renderer.replicaCount=${replica.renderer}`,
`--set-string doc.image.tag="${imageTag}"`,
`--set doc.app.host=${host}`,
`--set doc.replicaCount=${replica.doc}`,
...serviceAnnotations,
...resources,
`--timeout 10m`,
+1 -1
View File
@@ -3,4 +3,4 @@ name: affine
description: AFFiNE cloud chart
type: application
version: 0.0.0
appVersion: "0.20.0"
appVersion: "0.19.0"
-11
View File
@@ -1,11 +0,0 @@
apiVersion: v2
name: doc
description: AFFiNE doc server
type: application
version: 0.0.0
appVersion: "0.20.0"
dependencies:
- name: gcloud-sql-proxy
version: 0.0.0
repository: "file://../gcloud-sql-proxy"
condition: .global.database.gcloud.enabled
@@ -1,16 +0,0 @@
1. Get the application URL by running these commands:
{{- if contains "NodePort" .Values.service.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "doc.fullname" . }})
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
echo http://$NODE_IP:$NODE_PORT
{{- else if contains "LoadBalancer" .Values.service.type }}
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "doc.fullname" . }}'
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "doc.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
echo http://$SERVICE_IP:{{ .Values.service.port }}
{{- else if contains "ClusterIP" .Values.service.type }}
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "doc.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
echo "Visit http://127.0.0.1:8080 to use your application"
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
{{- end }}
@@ -1,63 +0,0 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "doc.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "doc.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "doc.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "doc.labels" -}}
helm.sh/chart: {{ include "doc.chart" . }}
{{ include "doc.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
monitoring: enabled
{{- end }}
{{/*
Selector labels
*/}}
{{- define "doc.selectorLabels" -}}
app.kubernetes.io/name: {{ include "doc.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "doc.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "doc.fullname" .) .Values.global.docService.name }}
{{- else }}
{{- default "default" .Values.global.docService.name }}
{{- end }}
{{- end }}
@@ -1,105 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "doc.fullname" . }}
labels:
{{- include "doc.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "doc.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "doc.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "doc.serviceAccountName" . }}
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
env:
- name: AFFINE_PRIVATE_KEY
valueFrom:
secretKeyRef:
name: "{{ .Values.global.secret.secretName }}"
key: key
- name: NODE_ENV
value: "{{ .Values.env }}"
- name: NODE_OPTIONS
value: "--max-old-space-size=4096"
- name: NO_COLOR
value: "1"
- name: DEPLOYMENT_TYPE
value: "affine"
- name: SERVER_FLAVOR
value: "doc"
- name: AFFINE_ENV
value: "{{ .Release.Namespace }}"
- name: DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: pg-postgresql
key: postgres-password
- name: DATABASE_URL
value: postgres://{{ .Values.global.database.user }}:$(DATABASE_PASSWORD)@{{ .Values.global.database.url }}:{{ .Values.global.database.port }}/{{ .Values.global.database.name }}
- name: REDIS_SERVER_ENABLED
value: "true"
- name: REDIS_SERVER_HOST
value: "{{ .Values.global.redis.host }}"
- name: REDIS_SERVER_PORT
value: "{{ .Values.global.redis.port }}"
- name: REDIS_SERVER_USER
value: "{{ .Values.global.redis.username }}"
- name: REDIS_SERVER_PASSWORD
valueFrom:
secretKeyRef:
name: redis
key: redis-password
- name: REDIS_SERVER_DATABASE
value: "{{ .Values.global.redis.database }}"
- name: AFFINE_SERVER_PORT
value: "{{ .Values.global.docService.port }}"
- name: AFFINE_SERVER_SUB_PATH
value: "{{ .Values.app.path }}"
- name: AFFINE_SERVER_HOST
value: "{{ .Values.app.host }}"
- name: AFFINE_SERVER_HTTPS
value: "{{ .Values.app.https }}"
ports:
- name: http
containerPort: {{ .Values.global.docService.port }}
protocol: TCP
livenessProbe:
httpGet:
path: /info
port: http
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
readinessProbe:
httpGet:
path: /info
port: http
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
@@ -1,19 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "doc.fullname" . }}
labels:
{{- include "doc.labels" . | nindent 4 }}
{{- with .Values.service.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.global.docService.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "doc.selectorLabels" . | nindent 4 }}
@@ -1,12 +0,0 @@
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "doc.serviceAccountName" . }}
labels:
{{- include "doc.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}
@@ -1,15 +0,0 @@
apiVersion: v1
kind: Pod
metadata:
name: "{{ include "doc.fullname" . }}-test-connection"
labels:
{{- include "doc.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": test
spec:
containers:
- name: wget
image: busybox
command: ['wget']
args: ['{{ include "doc.fullname" . }}:{{ .Values.global.docService.port }}']
restartPolicy: Never
@@ -1,37 +0,0 @@
replicaCount: 1
image:
repository: ghcr.io/toeverything/affine-graphql
pullPolicy: IfNotPresent
tag: ''
imagePullSecrets: []
nameOverride: ''
fullnameOverride: ''
# map to NODE_ENV environment variable
env: 'production'
app:
# AFFINE_SERVER_SUB_PATH
path: ''
# AFFINE_SERVER_HOST
host: '0.0.0.0'
https: true
serviceAccount:
create: true
annotations: {}
podAnnotations: {}
podSecurityContext:
fsGroup: 2000
resources:
requests:
cpu: '2'
memory: 4Gi
probe:
initialDelaySeconds: 20
nodeSelector: {}
tolerations: []
affinity: {}
@@ -3,7 +3,7 @@ name: graphql
description: AFFiNE GraphQL server
type: application
version: 0.0.0
appVersion: "0.20.0"
appVersion: "0.19.0"
dependencies:
- name: gcloud-sql-proxy
version: 0.0.0
@@ -9,4 +9,6 @@ data:
falSecret: {{ .Values.app.copilot.fal.key | b64enc }}
perplexitySecret: {{ .Values.app.copilot.perplexity.key | b64enc }}
unsplashSecret: {{ .Values.app.copilot.unsplash.key | b64enc }}
slackBotToken: {{ .Values.app.copilot.slack.botToken | b64enc }}
slackChannelId: {{ .Values.app.copilot.slack.channelId | b64enc }}
{{- end }}
@@ -0,0 +1,66 @@
{{ if .Values.app.copilot.enabled }}
apiVersion: batch/v1
kind: CronJob
metadata:
name: {{ include "graphql.fullname" . }}-copilot-test
labels:
{{- include "graphql.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": post-install,post-upgrade
"helm.sh/hook-weight": "1"
"helm.sh/hook-delete-policy": before-hook-creation
spec:
schedule: "0 8 * * *"
jobTemplate:
spec:
template:
spec:
serviceAccountName: {{ include "graphql.serviceAccountName" . }}
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
command: ["yarn", "test:copilot:e2e:cron"]
env:
- name: AFFINE_ENV
value: "{{ .Release.Namespace }}"
- name: SLACK_BOT_TOKEN
valueFrom:
secretKeyRef:
name: "{{ .Values.app.copilot.secretName }}"
key: slackBotToken
- name: CHANNEL_ID
valueFrom:
secretKeyRef:
name: "{{ .Values.app.copilot.secretName }}"
key: slackChannelId
- name: COPILOT_E2E_ENDPOINT
value: "http://{{ include "graphql.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local:3000"
- name: DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: pg-postgresql
key: postgres-password
- name: DATABASE_URL
value: postgres://{{ .Values.global.database.user }}:$(DATABASE_PASSWORD)@{{ .Values.global.database.url }}:{{ .Values.global.database.port }}/{{ .Values.global.database.name }}
- name: COPILOT_OPENAI_API_KEY
valueFrom:
secretKeyRef:
name: "{{ .Values.app.copilot.secretName }}"
key: openaiSecret
- name: COPILOT_FAL_API_KEY
valueFrom:
secretKeyRef:
name: "{{ .Values.app.copilot.secretName }}"
key: falSecret
- name: COPILOT_UNSPLASH_API_KEY
valueFrom:
secretKeyRef:
name: "{{ .Values.app.copilot.secretName }}"
key: unsplashSecret
resources:
requests:
cpu: '100m'
memory: '200Mi'
restartPolicy: Never
backoffLimit: 1
{{ end }}
@@ -116,8 +116,8 @@ spec:
secretKeyRef:
name: "{{ .Values.app.payment.stripe.secretName }}"
key: stripeWebhookKey
- name: DOC_SERVICE_ENDPOINT
value: "http://{{ .Values.global.docService.name }}:{{ .Values.global.docService.port }}"
- name: DOC_MERGE_INTERVAL
value: "{{ .Values.app.doc.mergeInterval }}"
{{ if .Values.app.experimental.enableJwstCodec }}
- name: DOC_MERGE_USE_JWST_CODEC
value: "true"
@@ -17,6 +17,8 @@ app:
# AFFINE_SERVER_HOST
host: '0.0.0.0'
https: true
doc:
mergeInterval: "3000"
captcha:
enabled: false
secretName: captcha
@@ -94,8 +94,6 @@ spec:
name: "{{ .Values.global.objectStorage.r2.secretName }}"
key: secretAccessKey
{{ end }}
- name: DOC_SERVICE_ENDPOINT
value: "http://{{ .Values.global.docService.name }}:{{ .Values.global.docService.port }}"
ports:
- name: http
containerPort: {{ .Values.service.port }}
+1 -1
View File
@@ -3,7 +3,7 @@ name: sync
description: AFFiNE Sync Server
type: application
version: 0.0.0
appVersion: "0.20.0"
appVersion: "0.19.0"
dependencies:
- name: gcloud-sql-proxy
version: 0.0.0
@@ -73,8 +73,6 @@ spec:
value: "{{ .Values.service.port }}"
- name: AFFINE_SERVER_HOST
value: "{{ .Values.app.host }}"
- name: DOC_SERVICE_ENDPOINT
value: "http://{{ .Values.global.docService.name }}:{{ .Values.global.docService.port }}"
ports:
- name: http
containerPort: {{ .Values.service.port }}
-9
View File
@@ -39,9 +39,6 @@ global:
secretAccessKey: ''
gke:
enabled: true
docService:
name: 'affine-doc'
port: 3020
graphql:
service:
@@ -64,12 +61,6 @@ renderer:
annotations:
cloud.google.com/backend-config: '{"default": "affine-api-backendconfig"}'
doc:
service:
type: ClusterIP
annotations:
cloud.google.com/backend-config: '{"default": "affine-api-backendconfig"}'
web:
service:
type: ClusterIP
+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
extra-flags: workspaces focus @affine/server
- name: Build Server
run: |
find packages/backend/server -type d -name "__tests__" -exec rm -rf {} +
rm -rf packages/backend/server/src/__tests__
yarn workspace @affine/server build
- name: Upload server dist
uses: actions/upload-artifact@v4
+11 -44
View File
@@ -118,26 +118,6 @@ jobs:
- name: Run Type Check
run: yarn typecheck
lint-rust:
name: Lint Rust
runs-on: ubuntu-latest
needs: optimize_ci
if: needs.optimize_ci.outputs.skip == 'false'
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/build-rust
with:
no-build: 'true'
- name: fmt check
run: |
rustup toolchain add nightly
rustup component add --toolchain nightly-x86_64-unknown-linux-gnu rustfmt
cargo +nightly fmt --all -- --check
- name: Clippy
run: |
rustup component add clippy
cargo clippy --all-targets --all-features -- -D warnings
check-yarn-binary:
name: Check yarn binary
runs-on: ubuntu-latest
@@ -175,7 +155,7 @@ jobs:
run: yarn workspace @blocksuite/legacy-e2e test --forbid-only --shard=${{ matrix.shard }}/${{ strategy.job-total }}
- name: Upload test results
if: always()
if: ${{ failure() }}
uses: actions/upload-artifact@v4
with:
name: test-results-e2e-legacy-bs-${{ matrix.shard }}
@@ -207,7 +187,7 @@ jobs:
run: yarn affine @affine-test/affine-local e2e --forbid-only --shard=${{ matrix.shard }}/${{ strategy.job-total }}
- name: Upload test results
if: always()
if: ${{ failure() }}
uses: actions/upload-artifact@v4
with:
name: test-results-e2e-${{ matrix.shard }}
@@ -239,7 +219,7 @@ jobs:
run: yarn affine @affine-test/affine-mobile e2e --forbid-only --shard=${{ matrix.shard }}/${{ strategy.job-total }}
- name: Upload test results
if: always()
if: ${{ failure() }}
uses: actions/upload-artifact@v4
with:
name: test-results-e2e-mobile-${{ matrix.shard }}
@@ -328,7 +308,6 @@ jobs:
package: '@affine/native'
- name: Upload ${{ steps.filename.outputs.filename }}
uses: actions/upload-artifact@v4
if: always()
with:
name: ${{ steps.filename.outputs.filename }}
path: ${{ env.DEV_DRIVE_WORKSPACE || github.workspace }}/packages/frontend/native/${{ steps.filename.outputs.filename }}
@@ -355,7 +334,6 @@ jobs:
package: '@affine/server-native'
- name: Upload server-native.node
uses: actions/upload-artifact@v4
if: always()
with:
name: server-native.node
path: ./packages/backend/native/server-native.node
@@ -381,7 +359,6 @@ jobs:
run: tar -czf dist.tar.gz --directory=packages/frontend/apps/electron-renderer/dist .
- name: Upload web artifact
uses: actions/upload-artifact@v4
if: always()
with:
name: web
path: dist.tar.gz
@@ -394,11 +371,6 @@ jobs:
- optimize_ci
- build-server-native
if: needs.optimize_ci.outputs.skip == 'false'
strategy:
fail-fast: false
matrix:
node_index: [0, 1, 2]
total_nodes: [3]
env:
NODE_ENV: test
DISTRIBUTION: web
@@ -448,8 +420,6 @@ jobs:
env:
CARGO_TARGET_DIR: '${{ github.workspace }}/target'
COPILOT_OPENAI_API_KEY: 'use_fake_openai_api_key'
CI_NODE_INDEX: ${{ matrix.node_index }}
CI_NODE_TOTAL: ${{ matrix.total_nodes }}
- name: Upload server test coverage results
uses: codecov/codecov-action@v5
@@ -534,7 +504,8 @@ jobs:
filters: |
changed:
- 'packages/backend/server/src/plugins/copilot/**'
- 'packages/backend/server/tests/copilot.*'
- 'packages/backend/server/tests/copilot*'
- 'tests/affine-cloud-copilot/**'
- name: Setup Node.js
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.apifilter.outputs.changed == 'true' }}
@@ -556,7 +527,7 @@ jobs:
- name: Run server tests
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.apifilter.outputs.changed == 'true' }}
run: yarn affine @affine/server test:copilot:coverage --forbid-only
run: yarn affine @affine/server test:copilot:spec:coverage --forbid-only
env:
CARGO_TARGET_DIR: '${{ github.workspace }}/target'
COPILOT_OPENAI_API_KEY: ${{ secrets.COPILOT_OPENAI_API_KEY }}
@@ -624,7 +595,8 @@ jobs:
with:
filters: |
changed:
- 'packages/frontend/core/src/blocksuite/ai/**'
- 'packages/frontend/core/src/blocksuite/presets/ai/**'
- 'packages/frontend/core/src/components/blocksuite/block-suite-editor/ai/**'
- 'tests/affine-cloud-copilot/**'
- name: Setup Node.js
@@ -669,16 +641,12 @@ jobs:
matrix:
tests:
- name: 'Server E2E Test 1/3'
shard: 1
script: yarn affine @affine-test/affine-cloud e2e --forbid-only --shard=1/3
- name: 'Server E2E Test 2/3'
shard: 2
script: yarn affine @affine-test/affine-cloud e2e --forbid-only --shard=2/3
- name: 'Server E2E Test 3/3'
shard: 3
script: yarn affine @affine-test/affine-cloud e2e --forbid-only --shard=3/3
- name: 'Server Desktop E2E Test'
shard: desktop
script: |
yarn affine @affine/electron build:dev
# Workaround for Electron apps failing to initialize on Ubuntu 24.04 due to AppArmor restrictions
@@ -741,10 +709,10 @@ jobs:
COPILOT_PERPLEXITY_API_KEY: 1
- name: Upload test results
if: always()
if: ${{ failure() }}
uses: actions/upload-artifact@v4
with:
name: test-results-e2e-server-${{ matrix.tests.shard }}
name: test-results-e2e-server
path: ./test-results
if-no-files-found: ignore
@@ -866,7 +834,7 @@ jobs:
yarn affine @affine/electron node ./scripts/macos-arm64-output-check.ts
- name: Upload test results
if: always()
if: ${{ failure() }}
uses: actions/upload-artifact@v4
with:
name: test-results-e2e-${{ matrix.spec.os }}-${{ matrix.spec.arch }}
@@ -888,7 +856,6 @@ jobs:
needs:
- analyze
- lint
- lint-rust
- check-yarn-binary
- e2e-test
- e2e-legacy-blocksuite-test
+16 -5
View File
@@ -38,6 +38,16 @@ jobs:
DISTRIBUTION: web
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
REDIS_SERVER_HOST: localhost
strategy:
fail-fast: false
matrix:
spec:
- {
name: e2e,
package: '@affine-test/affine-cloud-copilot',
type: e2e,
}
- { name: spec, package: '@affine/server', type: copilot:spec }
services:
postgres:
image: postgres
@@ -78,13 +88,14 @@ jobs:
- name: Prepare Server Test Environment
uses: ./.github/actions/server-test-env
- name: Run server tests
run: yarn affine @affine/server test:copilot:coverage --forbid-only
- name: Run copilot api ${{ matrix.spec.name }} tests
run: yarn affine ${{ matrix.spec.package }} test:${{ matrix.spec.type }}:coverage --forbid-only
env:
CARGO_TARGET_DIR: '${{ github.workspace }}/target'
COPILOT_OPENAI_API_KEY: ${{ secrets.COPILOT_OPENAI_API_KEY }}
COPILOT_FAL_API_KEY: ${{ secrets.COPILOT_FAL_API_KEY }}
COPILOT_PERPLEXITY_API_KEY: ${{ secrets.COPILOT_PERPLEXITY_API_KEY }}
COPILOT_E2E_ENDPOINT: ${{ secrets.COPILOT_E2E_ENDPOINT }}
- name: Upload server test coverage results
uses: codecov/codecov-action@v5
@@ -170,7 +181,7 @@ jobs:
if: ${{ always() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }}
run: node ./tools/copilot-result/index.js
env:
CHANNEL_ID: ${{ secrets.RELEASE_SLACK_CHNNEL_ID }}
CHANNEL_ID: ${{ secrets.RELEASE_SLACK_CHANNEL_ID }}
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
BRANCH_SHA: ${{ github.sha }}
BRANCH_NAME: ${{ github.ref }}
@@ -180,7 +191,7 @@ jobs:
if: ${{ always() && contains(needs.*.result, 'failure') }}
run: node ./tools/copilot-result/index.js
env:
CHANNEL_ID: ${{ secrets.RELEASE_SLACK_CHNNEL_ID }}
CHANNEL_ID: ${{ secrets.RELEASE_SLACK_CHANNEL_ID }}
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
BRANCH_SHA: ${{ github.sha }}
BRANCH_NAME: ${{ github.ref }}
@@ -190,7 +201,7 @@ jobs:
if: ${{ always() && contains(needs.*.result, 'cancelled') && !contains(needs.*.result, 'failure') }}
run: node ./tools/copilot-result/index.js
env:
CHANNEL_ID: ${{ secrets.RELEASE_SLACK_CHNNEL_ID }}
CHANNEL_ID: ${{ secrets.RELEASE_SLACK_CHANNEL_ID }}
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
BRANCH_SHA: ${{ github.sha }}
BRANCH_NAME: ${{ github.ref }}
+6 -3
View File
@@ -100,6 +100,9 @@ jobs:
COPILOT_FAL_API_KEY: ${{ secrets.COPILOT_FAL_API_KEY }}
COPILOT_PERPLEXITY_API_KEY: ${{ secrets.COPILOT_PERPLEXITY_API_KEY }}
COPILOT_UNSPLASH_API_KEY: ${{ secrets.COPILOT_UNSPLASH_API_KEY }}
# used for slack notifications
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
RELEASE_SLACK_CHANNEL_ID: ${{ secrets.RELEASE_SLACK_CHANNEL_ID }}
METRICS_CUSTOMER_IO_TOKEN: ${{ secrets.METRICS_CUSTOMER_IO_TOKEN }}
MAILER_SENDER: ${{ secrets.OAUTH_EMAIL_SENDER }}
MAILER_USER: ${{ secrets.OAUTH_EMAIL_LOGIN }}
@@ -162,7 +165,7 @@ jobs:
if: ${{ always() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }}
run: node ./tools/changelog/index.js
env:
CHANNEL_ID: ${{ secrets.RELEASE_SLACK_CHNNEL_ID }}
CHANNEL_ID: ${{ secrets.RELEASE_SLACK_CHANNEL_ID }}
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
DEPLOYED_URL: ${{ steps.set_info.outputs.deployed_url }}
PREV_VERSION: ${{ needs.output-prev-version.outputs.prev }}
@@ -178,7 +181,7 @@ jobs:
method: chat.postMessage
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: |
channel: ${{ secrets.RELEASE_SLACK_CHNNEL_ID }}
channel: ${{ secrets.RELEASE_SLACK_CHANNEL_ID }}
text: "<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|Backend deploy failed `${{ github.event.inputs.flavor }}`>"
blocks:
- type: section
@@ -193,7 +196,7 @@ jobs:
token: ${{ secrets.SLACK_BOT_TOKEN }}
method: chat.postMessage
payload: |
channel: ${{ secrets.RELEASE_SLACK_CHNNEL_ID }}
channel: ${{ secrets.RELEASE_SLACK_CHANNEL_ID }}
text: "<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|Backend deploy cancelled `${{ github.event.inputs.flavor }}`>"
blocks:
- type: section
+2 -2
View File
@@ -231,7 +231,7 @@ jobs:
- name: Build
run: |
echo -n "${{ env.AFFINE_ANDROID_SIGN_KEYSTORE }}" | base64 --decode > packages/frontend/apps/android/affine.keystore
yarn workspace @affine/android cap build android --flavor ${{ env.BUILD_TYPE }} --androidreleasetype AAB
yarn workspace @affine/android cap build android
env:
AFFINE_ANDROID_KEYSTORE_PASSWORD: ${{ secrets.AFFINE_ANDROID_KEYSTORE_PASSWORD }}
AFFINE_ANDROID_KEYSTORE_ALIAS_PASSWORD: ${{ secrets.AFFINE_ANDROID_KEYSTORE_ALIAS_PASSWORD }}
@@ -243,7 +243,7 @@ jobs:
with:
serviceAccountJson: ${{ steps.auth.outputs.credentials_file_path }}
packageName: app.affine.pro
releaseFiles: packages/frontend/apps/android/App/app/build/outputs/bundle/${{ env.BUILD_TYPE }}Release/app-${{ env.BUILD_TYPE }}-release-signed.aab
releaseFiles: packages/frontend/apps/android/App/app/build/outputs/bundle/release/app-release-signed.aab
track: internal
status: draft
existingEditId: ${{ steps.bump.outputs.EDIT_ID }}
+23
View File
@@ -0,0 +1,23 @@
name: Deploy Cloudflare Worker
on:
push:
branches:
- canary
paths:
- tools/workers/**
jobs:
deploy:
runs-on: ubuntu-latest
name: Deploy
environment: stable
steps:
- uses: actions/checkout@v4
- name: Publish
uses: cloudflare/wrangler-action@v3.13.1
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: ${{ secrets.CF_ACCOUNT_ID }}
workingDirectory: 'tools/workers'
packageManager: 'yarn'
+1 -1
View File
@@ -1 +1 @@
22.14.0
22.13.0
-1
View File
@@ -29,7 +29,6 @@ test-results
tools/cli/src/webpack/error-handler.js
packages/backend/native/index.d.ts
packages/backend/server/src/__tests__/__snapshots__
packages/common/native/fixtures/**
packages/frontend/native/index.d.ts
packages/frontend/native/index.js
packages/frontend/graphql/src/graphql/index.ts
Generated
+378 -1543
View File
File diff suppressed because it is too large Load Diff
+5 -11
View File
@@ -15,28 +15,22 @@ affine_common = { path = "./packages/common/native" }
affine_nbstore = { path = "./packages/frontend/native/nbstore" }
anyhow = "1"
base64-simd = "0.8"
block2 = "0.6"
chrono = "0.4"
core-foundation = "0.10"
coreaudio-rs = "0.12"
criterion2 = { version = "2", default-features = false }
dispatch2 = "0.2"
dotenvy = "0.15"
file-format = { version = "0.26", features = ["reader"] }
homedir = "0.3"
mimalloc = "0.1"
napi = { version = "3.0.0-alpha.12", features = ["async", "chrono_date", "error_anyhow", "napi9", "serde"] }
napi-build = { version = "2" }
napi-derive = { version = "3.0.0-alpha.12" }
notify = { version = "8", features = ["serde"] }
objc2 = "0.6"
objc2-foundation = "0.3"
objc2 = "0.5.2"
objc2-foundation = "0.2.2"
once_cell = "1"
parking_lot = "0.12"
rand = "0.9"
homedir = "0.3"
rand = "0.8"
rayon = "1.10"
rubato = "0.16"
screencapturekit = "0.3"
serde = "1"
serde_json = "1"
sha3 = "0.10"
@@ -44,7 +38,7 @@ sqlx = { version = "0.8", default-features = false, features = ["chr
thiserror = "2"
tiktoken-rs = "0.6"
tokio = "1.37"
uniffi = "0.29"
uniffi = "0.28"
uuid = "1.8"
v_htmlescape = "0.15"
y-octo = { git = "https://github.com/y-crdt/y-octo.git", branch = "main" }
+6 -1
View File
@@ -17,6 +17,7 @@
"@blocksuite/blocks": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/inline": "workspace:*",
"@blocksuite/presets": "workspace:*",
"@blocksuite/store": "workspace:*",
"@blocksuite/sync": "workspace:*"
},
@@ -36,6 +37,7 @@
"./inline": "./src/inline/index.ts",
"./inline/consts": "./src/inline/consts.ts",
"./inline/types": "./src/inline/types.ts",
"./presets": "./src/presets/index.ts",
"./blocks": "./src/blocks/index.ts",
"./blocks/schemas": "./src/blocks/schemas.ts",
"./sync": "./src/sync/index.ts"
@@ -81,6 +83,9 @@
"inline/types": [
"dist/inline/types.d.ts"
],
"presets": [
"dist/presets/index.d.ts"
],
"blocks": [
"dist/blocks/index.d.ts"
],
@@ -98,5 +103,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.20.0"
"version": "0.19.0"
}
+2
View File
@@ -1,5 +1,7 @@
import { effects as blocksEffects } from '@blocksuite/blocks/effects';
import { effects as presetsEffects } from '@blocksuite/presets/effects';
export function effects() {
blocksEffects();
presetsEffects();
}
@@ -0,0 +1 @@
export * from '@blocksuite/presets';
+1
View File
@@ -11,6 +11,7 @@
{ "path": "../../blocks" },
{ "path": "../../framework/global" },
{ "path": "../../framework/inline" },
{ "path": "../../presets" },
{ "path": "../../framework/store" },
{ "path": "../../framework/sync" }
]
@@ -23,10 +23,10 @@
"@blocksuite/icons": "^2.2.1",
"@blocksuite/inline": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",
"@floating-ui/dom": "^1.6.10",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.12",
"@toeverything/theme": "^1.1.7",
"file-type": "^20.0.0",
"lit": "^3.2.0",
"minimatch": "^10.0.1",
@@ -42,5 +42,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.20.0"
"version": "0.19.0"
}
@@ -3,7 +3,7 @@ import { CaptionedBlockComponent } from '@blocksuite/affine-components/caption';
import { HoverController } from '@blocksuite/affine-components/hover';
import {
AttachmentIcon16,
getAttachmentFileIcon,
getAttachmentFileIcons,
} from '@blocksuite/affine-components/icons';
import { Peekable } from '@blocksuite/affine-components/peek';
import { toast } from '@blocksuite/affine-components/toast';
@@ -11,12 +11,13 @@ import {
type AttachmentBlockModel,
AttachmentBlockStyles,
} from '@blocksuite/affine-model';
import {
FileSizeLimitService,
ThemeProvider,
} from '@blocksuite/affine-shared/services';
import { ThemeProvider } from '@blocksuite/affine-shared/services';
import { humanFileSize } from '@blocksuite/affine-shared/utils';
import { BlockSelection, TextSelection } from '@blocksuite/block-std';
import {
BlockSelection,
SurfaceSelection,
TextSelection,
} from '@blocksuite/block-std';
import { Slice } from '@blocksuite/store';
import { flip, offset } from '@floating-ui/dom';
import { html, nothing } from 'lit';
@@ -25,19 +26,25 @@ import { classMap } from 'lit/directives/class-map.js';
import { ref } from 'lit/directives/ref.js';
import { styleMap } from 'lit/directives/style-map.js';
import type { AttachmentBlockService } from './attachment-service.js';
import { AttachmentOptionsTemplate } from './components/options.js';
import { AttachmentEmbedProvider } from './embed.js';
import { styles } from './styles.js';
import { checkAttachmentBlob, downloadAttachmentBlob } from './utils.js';
@Peekable()
export class AttachmentBlockComponent extends CaptionedBlockComponent<AttachmentBlockModel> {
export class AttachmentBlockComponent extends CaptionedBlockComponent<
AttachmentBlockModel,
AttachmentBlockService
> {
static override styles = styles;
protected _isDragging = false;
protected _isResizing = false;
protected _isSelected = false;
protected _whenHover: HoverController | null = new HoverController(
this,
({ abortController }) => {
@@ -83,14 +90,10 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
margin: '18px 0px',
});
private get _maxFileSize() {
return this.std.store.get(FileSizeLimitService).maxFileSize;
}
convertTo = () => {
return this.std
.get(AttachmentEmbedProvider)
.convertTo(this.model, this._maxFileSize);
.convertTo(this.model, this.service.maxFileSize);
};
copy = () => {
@@ -106,7 +109,7 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
embedded = () => {
return this.std
.get(AttachmentEmbedProvider)
.embedded(this.model, this._maxFileSize);
.embedded(this.model, this.service.maxFileSize);
};
open = () => {
@@ -123,7 +126,7 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
protected get embedView() {
return this.std
.get(AttachmentEmbedProvider)
.render(this.model, this.blobUrl, this._maxFileSize);
.render(this.model, this.blobUrl, this.service.maxFileSize);
}
private _selectBlock() {
@@ -167,21 +170,26 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
// this is required to prevent iframe from capturing pointer events
this.disposables.add(
this.selected$.subscribe(selected => {
this._showOverlay = this._isResizing || this._isDragging || !selected;
this.std.selection.slots.changed.on(() => {
this._isSelected =
!!this.selected?.is(BlockSelection) ||
!!this.selected?.is(SurfaceSelection);
this._showOverlay =
this._isResizing || this._isDragging || !this._isSelected;
})
);
// this is required to prevent iframe from capturing pointer events
this.handleEvent('dragStart', () => {
this._isDragging = true;
this._showOverlay =
this._isResizing || this._isDragging || !this.selected$.peek();
this._isResizing || this._isDragging || !this._isSelected;
});
this.handleEvent('dragEnd', () => {
this._isDragging = false;
this._showOverlay =
this._isResizing || this._isDragging || !this.selected$.peek();
this._isResizing || this._isDragging || !this._isSelected;
});
}
@@ -218,7 +226,7 @@ export class AttachmentBlockComponent extends CaptionedBlockComponent<Attachment
const infoText = this.error ? 'File loading failed.' : humanFileSize(size);
const fileType = name.split('.').pop() ?? '';
const FileTypeIcon = getAttachmentFileIcon(fileType);
const FileTypeIcon = getAttachmentFileIcons(fileType);
const embedView = this.embedView;
@@ -35,7 +35,7 @@ export class AttachmentEdgelessBlockComponent extends toGfxBlockComponent(
this.slots.elementResizeEnd.on(() => {
this._isResizing = false;
this._showOverlay =
this._isResizing || this._isDragging || !this.selected$.peek();
this._isResizing || this._isDragging || !this._isSelected;
})
);
}
@@ -1,18 +1,24 @@
import { SurfaceBlockModel } from '@blocksuite/affine-block-surface';
import { FileDropConfigExtension } from '@blocksuite/affine-components/drop-indicator';
import { FileDropConfigExtension } from '@blocksuite/affine-components/drag-indicator';
import { AttachmentBlockSchema } from '@blocksuite/affine-model';
import {
FileSizeLimitService,
TelemetryProvider,
} from '@blocksuite/affine-shared/services';
import { TelemetryProvider } from '@blocksuite/affine-shared/services';
import {
isInsideEdgelessEditor,
matchModels,
matchFlavours,
} from '@blocksuite/affine-shared/utils';
import { BlockService } from '@blocksuite/block-std';
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
import { addAttachments, addSiblingAttachmentBlocks } from './utils.js';
// bytes.parse('2GB')
const maxFileSize = 2147483648;
export class AttachmentBlockService extends BlockService {
static override readonly flavour = AttachmentBlockSchema.model.flavour;
maxFileSize = maxFileSize;
}
export const AttachmentDropOption = FileDropConfigExtension({
flavour: AttachmentBlockSchema.model.flavour,
onDrop: ({ files, targetModel, placement, point, std }) => {
@@ -22,12 +28,11 @@ export const AttachmentDropOption = FileDropConfigExtension({
);
if (!attachmentFiles.length) return false;
const maxFileSize = std.store.get(FileSizeLimitService).maxFileSize;
if (targetModel && !matchModels(targetModel, [SurfaceBlockModel])) {
if (targetModel && !matchFlavours(targetModel, ['affine:surface'])) {
addSiblingAttachmentBlocks(
std.host,
attachmentFiles,
// TODO: use max file size from service
maxFileSize,
targetModel,
placement
@@ -3,7 +3,10 @@ import type { ExtensionType } from '@blocksuite/store';
import { literal } from 'lit/static-html.js';
import { AttachmentBlockNotionHtmlAdapterExtension } from './adapters/notion-html.js';
import { AttachmentDropOption } from './attachment-service.js';
import {
AttachmentBlockService,
AttachmentDropOption,
} from './attachment-service.js';
import {
AttachmentEmbedConfigExtension,
AttachmentEmbedService,
@@ -11,6 +14,7 @@ import {
export const AttachmentBlockSpec: ExtensionType[] = [
FlavourExtension('affine:attachment'),
AttachmentBlockService,
BlockViewExtension('affine:attachment', model => {
return model.parent?.flavour === 'affine:surface'
? literal`affine-edgeless-attachment`
@@ -2,6 +2,8 @@ import {
CaptionIcon,
DownloadIcon,
EditIcon,
MoreVerticalIcon,
SmallArrowDownIcon,
} from '@blocksuite/affine-components/icons';
import { createLitPortal } from '@blocksuite/affine-components/portal';
import {
@@ -19,7 +21,6 @@ import {
EMBED_CARD_WIDTH,
} from '@blocksuite/affine-shared/consts';
import { Bound } from '@blocksuite/global/utils';
import { ArrowDownSmallIcon, MoreVerticalIcon } from '@blocksuite/icons/lit';
import { flip, offset } from '@floating-ui/dom';
import { html, nothing } from 'lit';
import { join } from 'lit/directives/join.js';
@@ -87,7 +88,7 @@ export function attachmentViewToggleMenu({
<span style="text-transform: capitalize">${viewType}</span>
view
</div>
${ArrowDownSmallIcon({ width: '16px', height: '16px' })}
${SmallArrowDownIcon}
</editor-icon-button>
`}
>
@@ -199,12 +200,8 @@ export function AttachmentOptionsTemplate({
<editor-menu-button
.contentPadding=${'8px'}
.button=${html`
<editor-icon-button
aria-label="More"
.tooltip=${'More'}
.iconSize=${'20px'}
>
${MoreVerticalIcon()}
<editor-icon-button aria-label="More" .tooltip=${'More'}>
${MoreVerticalIcon}
</editor-icon-button>
`}
>
@@ -1,5 +1,6 @@
import { AttachmentBlockComponent } from './attachment-block';
import { AttachmentEdgelessBlockComponent } from './attachment-edgeless-block';
import type { AttachmentBlockService } from './attachment-service';
export function effects() {
customElements.define(
@@ -8,3 +9,11 @@ export function effects() {
);
customElements.define('affine-attachment', AttachmentBlockComponent);
}
declare global {
namespace BlockSuite {
interface BlockServices {
'affine:attachment': AttachmentBlockService;
}
}
}
+27 -67
View File
@@ -1,25 +1,18 @@
import {
type AttachmentBlockModel,
type ImageBlockProps,
MAX_IMAGE_WIDTH,
import type {
AttachmentBlockModel,
ImageBlockProps,
} from '@blocksuite/affine-model';
import { FileSizeLimitService } from '@blocksuite/affine-shared/services';
import {
readImageSize,
transformModel,
withTempBlobData,
} from '@blocksuite/affine-shared/utils';
import { type BlockStdScope, StdIdentifier } from '@blocksuite/block-std';
import type { Container } from '@blocksuite/global/di';
import { createIdentifier } from '@blocksuite/global/di';
import { Bound } from '@blocksuite/global/utils';
import type { ExtensionType } from '@blocksuite/store';
import { Extension } from '@blocksuite/store';
import type { TemplateResult } from 'lit';
import { html } from 'lit';
import { getAttachmentBlob } from './utils';
export type AttachmentEmbedConfig = {
name: string;
/**
@@ -29,10 +22,7 @@ export type AttachmentEmbedConfig = {
/**
* The action will be executed when the 「Turn into embed view」 button is clicked.
*/
action?: (
model: AttachmentBlockModel,
std: BlockStdScope
) => Promise<void> | void;
action?: (model: AttachmentBlockModel) => Promise<void> | void;
/**
* The template will be used to render the embed view.
*/
@@ -67,9 +57,8 @@ export const AttachmentEmbedProvider = createIdentifier<AttachmentEmbedService>(
);
export class AttachmentEmbedService extends Extension {
private get _maxFileSize() {
return this.std.store.get(FileSizeLimitService).maxFileSize;
}
// 10MB
static MAX_EMBED_SIZE = 10 * 1024 * 1024;
get keys() {
return this.configs.keys();
@@ -79,11 +68,7 @@ export class AttachmentEmbedService extends Extension {
return this.configs.values();
}
get configs(): Map<string, AttachmentEmbedConfig> {
return this.std.get(AttachmentEmbedConfigMapIdentifier);
}
constructor(private readonly std: BlockStdScope) {
constructor(private readonly configs: Map<string, AttachmentEmbedConfig>) {
super();
}
@@ -91,27 +76,35 @@ export class AttachmentEmbedService extends Extension {
di.addImpl(AttachmentEmbedConfigMapIdentifier, provider =>
provider.getAll(AttachmentEmbedConfigIdentifier)
);
di.addImpl(AttachmentEmbedProvider, this, [StdIdentifier]);
di.addImpl(AttachmentEmbedProvider, AttachmentEmbedService, [
AttachmentEmbedConfigMapIdentifier,
]);
}
// Converts to embed view.
convertTo(model: AttachmentBlockModel, maxFileSize = this._maxFileSize) {
convertTo(
model: AttachmentBlockModel,
maxFileSize = AttachmentEmbedService.MAX_EMBED_SIZE
) {
const config = this.values.find(config => config.check(model, maxFileSize));
if (!config?.action) {
if (!config || !config.action) {
model.doc.updateBlock(model, { embed: true });
return;
}
config.action(model, this.std)?.catch(console.error);
config.action(model)?.catch(console.error);
}
embedded(model: AttachmentBlockModel, maxFileSize = this._maxFileSize) {
embedded(
model: AttachmentBlockModel,
maxFileSize = AttachmentEmbedService.MAX_EMBED_SIZE
) {
return this.values.some(config => config.check(model, maxFileSize));
}
render(
model: AttachmentBlockModel,
blobUrl?: string,
maxFileSize = this._maxFileSize
maxFileSize = AttachmentEmbedService.MAX_EMBED_SIZE
) {
if (!model.embed || !blobUrl) return;
@@ -131,12 +124,7 @@ const embedConfig: AttachmentEmbedConfig[] = [
check: model =>
model.doc.schema.flavourSchemaMap.has('affine:image') &&
model.type.startsWith('image/'),
async action(model, std) {
const component = std.view.getBlock(model.id);
if (!component) return;
await turnIntoImageBlock(model);
},
action: model => turnIntoImageBlock(model),
},
{
name: 'pdf',
@@ -164,13 +152,7 @@ const embedConfig: AttachmentEmbedConfig[] = [
check: (model, maxFileSize) =>
model.type.startsWith('video/') && model.size <= maxFileSize,
template: (_, blobUrl) =>
html`<video
style="max-height: max-content;"
width="100%;"
height="480"
controls
src=${blobUrl}
></video>`,
html`<video width="100%;" height="480" controls src=${blobUrl}></video>`,
},
{
name: 'audio',
@@ -184,7 +166,7 @@ const embedConfig: AttachmentEmbedConfig[] = [
/**
* Turn the attachment block into an image block.
*/
export async function turnIntoImageBlock(model: AttachmentBlockModel) {
export function turnIntoImageBlock(model: AttachmentBlockModel) {
if (!model.doc.schema.flavourSchemaMap.has('affine:image')) {
console.error('The image flavour is not supported!');
return;
@@ -196,37 +178,15 @@ export async function turnIntoImageBlock(model: AttachmentBlockModel) {
const { saveAttachmentData, getImageData } = withTempBlobData();
saveAttachmentData(sourceId, { name: model.name });
let imageSize = model.sourceId ? getImageData(model.sourceId) : undefined;
const bounds = model.xywh
? Bound.fromXYWH(model.deserializedXYWH)
const imageConvertData = model.sourceId
? getImageData(model.sourceId)
: undefined;
if (bounds) {
if (!imageSize?.width || !imageSize?.height) {
const blob = await getAttachmentBlob(model);
if (blob) {
imageSize = await readImageSize(blob);
}
}
if (imageSize?.width && imageSize?.height) {
const p = imageSize.height / imageSize.width;
imageSize.width = Math.min(imageSize.width, MAX_IMAGE_WIDTH);
imageSize.height = imageSize.width * p;
bounds.w = imageSize.width;
bounds.h = imageSize.height;
}
}
const others = bounds ? { xywh: bounds.serialize() } : undefined;
const imageProp: Partial<ImageBlockProps> = {
sourceId,
caption: model.caption,
size: model.size,
...imageSize,
...others,
...imageConvertData,
};
transformModel(model, 'affine:image', imageProp);
}
@@ -1,3 +1,7 @@
import type * as SurfaceEffects from '@blocksuite/affine-block-surface/effects';
declare type _GLOBAL_ = typeof SurfaceEffects;
export * from './adapters/notion-html';
export * from './attachment-block';
export * from './attachment-service';
@@ -1,3 +1,7 @@
import {
EMBED_CARD_HEIGHT,
EMBED_CARD_WIDTH,
} from '@blocksuite/affine-shared/consts';
import { css } from 'lit';
export const styles = css`
@@ -8,7 +12,7 @@ export const styles = css`
gap: 12px;
width: 100%;
height: 100%;
height: ${EMBED_CARD_HEIGHT.horizontalThin}px;
padding: 12px;
border-radius: 8px;
@@ -117,6 +121,9 @@ export const styles = css`
}
.affine-attachment-card.cubeThick {
width: ${EMBED_CARD_WIDTH.cubeThick}px;
height: ${EMBED_CARD_HEIGHT.cubeThick}px;
flex-direction: column-reverse;
.affine-attachment-content {
+12 -19
View File
@@ -8,10 +8,7 @@ import {
EMBED_CARD_HEIGHT,
EMBED_CARD_WIDTH,
} from '@blocksuite/affine-shared/consts';
import {
FileSizeLimitService,
TelemetryProvider,
} from '@blocksuite/affine-shared/services';
import { TelemetryProvider } from '@blocksuite/affine-shared/services';
import { humanFileSize } from '@blocksuite/affine-shared/utils';
import type { BlockStdScope, EditorHost } from '@blocksuite/block-std';
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
@@ -97,7 +94,7 @@ export async function uploadAttachmentBlob(
}
}
export async function getAttachmentBlob(model: AttachmentBlockModel) {
async function getAttachmentBlob(model: AttachmentBlockModel) {
const sourceId = model.sourceId;
if (!sourceId) {
return null;
@@ -215,8 +212,7 @@ export async function addSiblingAttachmentBlocks(
files: File[],
maxFileSize: number,
targetModel: BlockModel,
place: 'before' | 'after' = 'after',
isEmbed?: boolean
place: 'before' | 'after' = 'after'
) {
if (!files.length) {
return;
@@ -246,7 +242,6 @@ export async function addSiblingAttachmentBlocks(
name: file.name,
size: file.size,
type: types[index],
embed: isEmbed,
}));
const blockIds = doc.addSiblingBlocks(
@@ -266,13 +261,18 @@ export async function addSiblingAttachmentBlocks(
export async function addAttachments(
std: BlockStdScope,
files: File[],
point?: IVec,
transformPoint?: boolean // determines whether we should use `toModelCoord` to convert the point
point?: IVec
): Promise<string[]> {
if (!files.length) return [];
const attachmentService = std.getService('affine:attachment');
const gfx = std.get(GfxControllerIdentifier);
const maxFileSize = std.store.get(FileSizeLimitService).maxFileSize;
if (!attachmentService) {
console.error('Attachment service not found');
return [];
}
const maxFileSize = attachmentService.maxFileSize;
const isSizeExceeded = files.some(file => file.size > maxFileSize);
if (isSizeExceeded) {
toast(
@@ -287,14 +287,7 @@ export async function addAttachments(
}
let { x, y } = gfx.viewport.center;
if (point) {
let transform = transformPoint ?? true;
if (transform) {
[x, y] = gfx.viewport.toModelCoord(...point);
} else {
[x, y] = point;
}
}
if (point) [x, y] = gfx.viewport.toModelCoord(...point);
const CARD_STACK_GAP = 32;
@@ -14,6 +14,7 @@
"license": "MIT",
"dependencies": {
"@blocksuite/affine-block-embed": "workspace:*",
"@blocksuite/affine-block-surface": "workspace:*",
"@blocksuite/affine-components": "workspace:*",
"@blocksuite/affine-model": "workspace:*",
"@blocksuite/affine-shared": "workspace:*",
@@ -22,10 +23,10 @@
"@blocksuite/icons": "^2.2.1",
"@blocksuite/inline": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",
"@floating-ui/dom": "^1.6.10",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.12",
"@toeverything/theme": "^1.1.7",
"lit": "^3.2.0",
"minimatch": "^10.0.1",
"zod": "^3.23.8"
@@ -40,5 +41,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.20.0"
"version": "0.19.0"
}
@@ -4,10 +4,10 @@ import {
} from '@blocksuite/affine-components/caption';
import type { BookmarkBlockModel } from '@blocksuite/affine-model';
import { DocModeProvider } from '@blocksuite/affine-shared/services';
import { computed, type ReadonlySignal } from '@preact/signals-core';
import { BlockSelection } from '@blocksuite/block-std';
import { html } from 'lit';
import { property, query } from 'lit/decorators.js';
import { type ClassInfo, classMap } from 'lit/directives/class-map.js';
import { classMap } from 'lit/directives/class-map.js';
import { type StyleInfo, styleMap } from 'lit/directives/style-map.js';
import { refreshBookmarkUrlData } from './utils.js';
@@ -15,12 +15,6 @@ import { refreshBookmarkUrlData } from './utils.js';
export const BOOKMARK_MIN_WIDTH = 450;
export class BookmarkBlockComponent extends CaptionedBlockComponent<BookmarkBlockModel> {
selectedStyle$: ReadonlySignal<ClassInfo> | null = computed<ClassInfo>(
() => ({
'selected-style': this.selected$.value,
})
);
private _fetchAbortController?: AbortController;
blockDraggable = true;
@@ -76,12 +70,13 @@ export class BookmarkBlockComponent extends CaptionedBlockComponent<BookmarkBloc
}
override renderBlock() {
const selected = !!this.selected?.is(BlockSelection);
return html`
<div
draggable="${this.blockDraggable ? 'true' : 'false'}"
class=${classMap({
'affine-bookmark-container': true,
...this.selectedStyle$?.value,
'selected-style': selected,
})}
style=${this.containerStyleMap}
>
@@ -3,15 +3,13 @@ import {
EMBED_CARD_WIDTH,
} from '@blocksuite/affine-shared/consts';
import { toGfxBlockComponent } from '@blocksuite/block-std';
import { type StyleInfo, styleMap } from 'lit/directives/style-map.js';
import { styleMap } from 'lit/directives/style-map.js';
import { BookmarkBlockComponent } from './bookmark-block.js';
export class BookmarkEdgelessBlockComponent extends toGfxBlockComponent(
BookmarkBlockComponent
) {
override selectedStyle$ = null;
override blockDraggable = false;
override getRenderingRect() {
@@ -45,9 +43,7 @@ export class BookmarkEdgelessBlockComponent extends toGfxBlockComponent(
return this.renderPageContent();
}
protected override accessor blockContainerStyles: StyleInfo = {
height: '100%',
};
protected override accessor blockContainerStyles = {};
}
declare global {
@@ -1,11 +1,17 @@
import { BlockViewExtension, FlavourExtension } from '@blocksuite/block-std';
import {
BlockViewExtension,
CommandExtension,
FlavourExtension,
} from '@blocksuite/block-std';
import type { ExtensionType } from '@blocksuite/store';
import { literal } from 'lit/static-html.js';
import { BookmarkBlockAdapterExtensions } from './adapters/extension.js';
import { commands } from './commands/index.js';
export const BookmarkBlockSpec: ExtensionType[] = [
FlavourExtension('affine:bookmark'),
CommandExtension(commands),
BlockViewExtension('affine:bookmark', model => {
return model.parent?.flavour === 'affine:surface'
? literal`affine-edgeless-bookmark`
@@ -1,2 +1,9 @@
export { insertBookmarkCommand } from './insert-bookmark.js';
export { insertLinkByQuickSearchCommand } from './insert-link-by-quick-search.js';
import type { BlockCommands } from '@blocksuite/block-std';
import { insertBookmarkCommand } from './insert-bookmark.js';
import { insertLinkByQuickSearchCommand } from './insert-link-by-quick-search.js';
export const commands: BlockCommands = {
insertBookmark: insertBookmarkCommand,
insertLinkByQuickSearch: insertLinkByQuickSearchCommand,
};
@@ -5,7 +5,11 @@ import type { EmbedCardStyle } from '@blocksuite/affine-model';
import { EmbedOptionProvider } from '@blocksuite/affine-shared/services';
import type { Command } from '@blocksuite/block-std';
export const insertBookmarkCommand: Command<{ url: string }> = (ctx, next) => {
export const insertBookmarkCommand: Command<
never,
'insertedLinkType',
{ url: string }
> = (ctx, next) => {
const { url, std } = ctx;
const embedOptions = std.get(EmbedOptionProvider).getEmbedBlockOptions(url);
@@ -1,15 +1,10 @@
import {
type InsertedLinkType,
insertEmbedLinkedDocCommand,
} from '@blocksuite/affine-block-embed';
import type { InsertedLinkType } from '@blocksuite/affine-block-embed';
import { QuickSearchProvider } from '@blocksuite/affine-shared/services';
import type { Command } from '@blocksuite/block-std';
import { insertBookmarkCommand } from './insert-bookmark';
export const insertLinkByQuickSearchCommand: Command<
{},
{ insertedLinkType: Promise<InsertedLinkType> }
never,
'insertedLinkType'
> = (ctx, next) => {
const { std } = ctx;
const quickSearchService = std.getOptional(QuickSearchProvider);
@@ -25,7 +20,7 @@ export const insertLinkByQuickSearchCommand: Command<
// add linked doc
if ('docId' in result) {
std.command.exec(insertEmbedLinkedDocCommand, {
std.command.exec('insertEmbedLinkedDoc', {
docId: result.docId,
params: result.params,
});
@@ -36,7 +31,7 @@ export const insertLinkByQuickSearchCommand: Command<
// add normal link;
if ('externalUrl' in result) {
std.command.exec(insertBookmarkCommand, { url: result.externalUrl });
std.command.exec('insertBookmark', { url: result.externalUrl });
return {
flavour: 'affine:bookmark',
};
@@ -2,11 +2,15 @@ import { getEmbedCardIcons } from '@blocksuite/affine-block-embed';
import { WebIcon16 } from '@blocksuite/affine-components/icons';
import { ThemeProvider } from '@blocksuite/affine-shared/services';
import { getHostName } from '@blocksuite/affine-shared/utils';
import { BlockSelection, ShadowlessElement } from '@blocksuite/block-std';
import {
BlockSelection,
ShadowlessElement,
SurfaceSelection,
} from '@blocksuite/block-std';
import { WithDisposable } from '@blocksuite/global/utils';
import { OpenInNewIcon } from '@blocksuite/icons/lit';
import { html } from 'lit';
import { property } from 'lit/decorators.js';
import { property, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import type { BookmarkBlockComponent } from '../bookmark-block.js';
@@ -51,6 +55,14 @@ export class BookmarkCard extends WithDisposable(ShadowlessElement) {
.get(ThemeProvider)
.theme$.subscribe(() => this.requestUpdate())
);
this.disposables.add(
this.bookmark.selection.slots.changed.on(() => {
this._isSelected =
!!this.bookmark.selected?.is(BlockSelection) ||
!!this.bookmark.selected?.is(SurfaceSelection);
})
);
}
override render() {
@@ -60,7 +72,7 @@ export class BookmarkCard extends WithDisposable(ShadowlessElement) {
loading: this.loading,
error: this.error,
[style]: true,
selected: this.bookmark.selected$.value,
selected: this._isSelected,
});
const domainName = url.match(
@@ -136,6 +148,9 @@ export class BookmarkCard extends WithDisposable(ShadowlessElement) {
`;
}
@state()
private accessor _isSelected = false;
@property({ attribute: false })
accessor bookmark!: BookmarkBlockComponent;
@@ -49,7 +49,7 @@ export class EmbedCardEditCaptionEditModal extends WithDisposable(
.catch(console.error);
this.disposables.addFromEvent(this, 'keydown', this._onKeydown);
this.disposables.addFromEvent(this, 'cut', stopPropagation);
this.disposables.addFromEvent(this, 'cup', stopPropagation);
this.disposables.addFromEvent(this, 'copy', stopPropagation);
this.disposables.addFromEvent(this, 'paste', stopPropagation);
}
@@ -1,15 +1,21 @@
import { EdgelessCRUDIdentifier } from '@blocksuite/affine-block-surface';
import { toast } from '@blocksuite/affine-components/toast';
import type { EmbedCardStyle } from '@blocksuite/affine-model';
import {
EMBED_CARD_HEIGHT,
EMBED_CARD_WIDTH,
} from '@blocksuite/affine-shared/consts';
import { EmbedOptionProvider } from '@blocksuite/affine-shared/services';
import { isValidUrl, stopPropagation } from '@blocksuite/affine-shared/utils';
import type { EditorHost } from '@blocksuite/block-std';
import { ShadowlessElement } from '@blocksuite/block-std';
import { GfxControllerIdentifier } from '@blocksuite/block-std/gfx';
import { WithDisposable } from '@blocksuite/global/utils';
import { Bound, Vec, WithDisposable } from '@blocksuite/global/utils';
import type { BlockModel } from '@blocksuite/store';
import { html } from 'lit';
import { property, query, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { toast } from '../toast';
import { embedCardModalStyles } from './styles.js';
export class EmbedCardCreateModal extends WithDisposable(ShadowlessElement) {
@@ -49,13 +55,37 @@ export class EmbedCardCreateModal extends WithDisposable(ShadowlessElement) {
index
);
} else if (mode === 'edgeless') {
let flavour = 'affine:bookmark',
targetStyle: EmbedCardStyle = 'vertical';
if (embedOptions) {
flavour = embedOptions.flavour;
targetStyle = embedOptions.styles[0];
}
const gfx = this.host.std.get(GfxControllerIdentifier);
const crud = this.host.std.get(EdgelessCRUDIdentifier);
const viewport = gfx.viewport;
const surfaceModel = gfx.surface;
if (!surfaceModel) {
return;
}
this.createOptions.onSave(url);
const center = Vec.toVec(viewport.center);
crud.addBlock(
flavour,
{
url,
xywh: Bound.fromCenter(
center,
EMBED_CARD_WIDTH[targetStyle],
EMBED_CARD_HEIGHT[targetStyle]
).serialize(),
style: targetStyle,
},
surfaceModel
);
gfx.tool.setTool(
// @ts-expect-error FIXME: resolve after gfx tool refactor
@@ -92,7 +122,7 @@ export class EmbedCardCreateModal extends WithDisposable(ShadowlessElement) {
})
.catch(console.error);
this.disposables.addFromEvent(this, 'keydown', this._onDocumentKeydown);
this.disposables.addFromEvent(this, 'cut', stopPropagation);
this.disposables.addFromEvent(this, 'cup', stopPropagation);
this.disposables.addFromEvent(this, 'copy', stopPropagation);
this.disposables.addFromEvent(this, 'paste', stopPropagation);
}
@@ -150,7 +180,6 @@ export class EmbedCardCreateModal extends WithDisposable(ShadowlessElement) {
}
| {
mode: 'edgeless';
onSave: (url: string) => void;
};
@property({ attribute: false })
@@ -181,7 +210,6 @@ export async function toggleEmbedCardCreateModal(
}
| {
mode: 'edgeless';
onSave: (url: string) => void;
}
): Promise<void> {
host.selection.clear();
@@ -1,8 +1,16 @@
import type { AliasInfo, LinkableEmbedModel } from '@blocksuite/affine-model';
import {
EmbedLinkedDocBlockComponent,
EmbedSyncedDocBlockComponent,
} from '@blocksuite/affine-block-embed';
import {
notifyLinkedDocClearedAliases,
notifyLinkedDocSwitchedToCard,
} from '@blocksuite/affine-components/notification';
import { toast } from '@blocksuite/affine-components/toast';
import type { AliasInfo } from '@blocksuite/affine-model';
import {
EmbedLinkedDocModel,
EmbedSyncedDocModel,
isInternalEmbedModel,
} from '@blocksuite/affine-model';
import {
type LinkEventType,
@@ -29,7 +37,8 @@ import { choose } from 'lit/directives/choose.js';
import { classMap } from 'lit/directives/class-map.js';
import { live } from 'lit/directives/live.js';
import { toast } from '../toast';
import type { LinkableEmbedModel } from './type.js';
import { isInternalEmbedModel } from './type.js';
export class EmbedCardEditModal extends SignalWatcher(
WithDisposable(LitElement)
@@ -162,8 +171,14 @@ export class EmbedCardEditModal extends SignalWatcher(
this.model.doc.updateBlock(this.model, { title: null, description: null });
this.onReset?.(std, blockComponent);
if (
this.isEmbedLinkedDocModel &&
blockComponent instanceof EmbedLinkedDocBlockComponent
) {
blockComponent.refreshData();
notifyLinkedDocClearedAliases(std);
}
blockComponent.requestUpdate();
track(std, this.model, this.viewType, 'ResetedAlias', { control: 'reset' });
@@ -192,7 +207,17 @@ export class EmbedCardEditModal extends SignalWatcher(
const props: AliasInfo = { title };
if (description) props.description = description;
this.onSave?.(std, blockComponent, props);
if (
this.isEmbedSyncedDocModel &&
blockComponent instanceof EmbedSyncedDocBlockComponent
) {
blockComponent.convertToCard(props);
notifyLinkedDocSwitchedToCard(std);
} else {
this.model.doc.updateBlock(this.model, props);
blockComponent.requestUpdate();
}
track(std, this.model, this.viewType, 'SavedAlias', { control: 'save' });
@@ -281,7 +306,7 @@ export class EmbedCardEditModal extends SignalWatcher(
this.disposables.add(listenClickAway(this, this._hide));
this.disposables.addFromEvent(this, 'keydown', this._onKeydown);
this.disposables.addFromEvent(this, 'pointerdown', stopPropagation);
this.disposables.addFromEvent(this, 'cut', stopPropagation);
this.disposables.addFromEvent(this, 'cup', stopPropagation);
this.disposables.addFromEvent(this, 'copy', stopPropagation);
this.disposables.addFromEvent(this, 'paste', stopPropagation);
@@ -368,20 +393,6 @@ export class EmbedCardEditModal extends SignalWatcher(
@property({ attribute: false })
accessor originalDocInfo: AliasInfo | undefined = undefined;
@property({ attribute: false })
accessor onReset:
| ((std: BlockStdScope, component: BlockComponent) => void)
| undefined = undefined;
@property({ attribute: false })
accessor onSave:
| ((
std: BlockStdScope,
component: BlockComponent,
props: AliasInfo
) => void)
| undefined = undefined;
accessor resetButtonDisabled$ = computed<boolean>(
() =>
!(
@@ -407,13 +418,7 @@ export function toggleEmbedCardEditModal(
host: EditorHost,
embedCardModel: LinkableEmbedModel,
viewType: string,
originalDocInfo?: AliasInfo,
onReset?: (std: BlockStdScope, component: BlockComponent) => void,
onSave?: (
std: BlockStdScope,
component: BlockComponent,
props: AliasInfo
) => void
originalDocInfo?: AliasInfo
) {
document.body.querySelector('embed-card-edit-modal')?.remove();
@@ -422,8 +427,6 @@ export function toggleEmbedCardEditModal(
embedCardEditModal.host = host;
embedCardEditModal.viewType = viewType;
embedCardEditModal.originalDocInfo = originalDocInfo;
embedCardEditModal.onReset = onReset;
embedCardEditModal.onSave = onSave;
document.body.append(embedCardEditModal);
}
@@ -0,0 +1,4 @@
export * from './embed-card-caption-edit-modal';
export * from './embed-card-create-modal';
export * from './embed-card-edit-modal';
export * from './type';
@@ -1,4 +1,3 @@
import { BookmarkBlockComponent } from '@blocksuite/affine-block-bookmark';
import {
EmbedFigmaBlockComponent,
EmbedGithubBlockComponent,
@@ -8,8 +7,22 @@ import {
EmbedSyncedDocBlockComponent,
EmbedYoutubeBlockComponent,
} from '@blocksuite/affine-block-embed';
import type {
BookmarkBlockModel,
EmbedFigmaModel,
EmbedGithubModel,
EmbedHtmlModel,
EmbedLoomModel,
EmbedYoutubeModel,
} from '@blocksuite/affine-model';
import {
EmbedLinkedDocModel,
EmbedSyncedDocModel,
} from '@blocksuite/affine-model';
import type { BlockComponent } from '@blocksuite/block-std';
import { BookmarkBlockComponent } from '../../bookmark-block';
export type ExternalEmbedBlockComponent =
| BookmarkBlockComponent
| EmbedFigmaBlockComponent
@@ -29,6 +42,19 @@ export type BuiltInEmbedBlockComponent =
| LinkableEmbedBlockComponent
| EmbedHtmlBlockComponent;
export type ExternalEmbedModel =
| BookmarkBlockModel
| EmbedFigmaModel
| EmbedGithubModel
| EmbedLoomModel
| EmbedYoutubeModel;
export type InternalEmbedModel = EmbedLinkedDocModel | EmbedSyncedDocModel;
export type LinkableEmbedModel = ExternalEmbedModel | InternalEmbedModel;
export type BuiltInEmbedModel = LinkableEmbedModel | EmbedHtmlModel;
export function isEmbedCardBlockComponent(
block: BlockComponent
): block is BuiltInEmbedBlockComponent {
@@ -43,3 +69,11 @@ export function isEmbedCardBlockComponent(
block instanceof EmbedSyncedDocBlockComponent
);
}
export function isInternalEmbedModel(
model: BuiltInEmbedModel
): model is InternalEmbedModel {
return (
model instanceof EmbedLinkedDocModel || model instanceof EmbedSyncedDocModel
);
}
@@ -1 +1,2 @@
export * from './bookmark-card';
export * from './embed-card-modal';
@@ -1,6 +1,13 @@
import { BookmarkBlockComponent } from './bookmark-block';
import { BookmarkEdgelessBlockComponent } from './bookmark-edgeless-block';
import type { insertBookmarkCommand } from './commands/insert-bookmark';
import type { insertLinkByQuickSearchCommand } from './commands/insert-link-by-quick-search';
import { BookmarkCard } from './components/bookmark-card';
import {
EmbedCardCreateModal,
EmbedCardEditCaptionEditModal,
EmbedCardEditModal,
} from './components/embed-card-modal';
export function effects() {
customElements.define(
@@ -9,4 +16,20 @@ export function effects() {
);
customElements.define('affine-bookmark', BookmarkBlockComponent);
customElements.define('bookmark-card', BookmarkCard);
customElements.define('embed-card-create-modal', EmbedCardCreateModal);
customElements.define('embed-card-edit-modal', EmbedCardEditModal);
customElements.define(
'embed-card-caption-edit-modal',
EmbedCardEditCaptionEditModal
);
}
declare global {
namespace BlockSuite {
interface Commands {
insertBookmark: typeof insertBookmarkCommand;
insertLinkByQuickSearch: typeof insertLinkByQuickSearchCommand;
}
}
}
@@ -1,5 +1,4 @@
export * from './adapters';
export * from './bookmark-block';
export * from './bookmark-spec';
export * from './commands';
export * from './components';
+12 -7
View File
@@ -1,20 +1,19 @@
import {
EMBED_CARD_HEIGHT,
EMBED_CARD_WIDTH,
} from '@blocksuite/affine-shared/consts';
import { unsafeCSSVar } from '@blocksuite/affine-shared/theme';
import { baseTheme } from '@toeverything/theme';
import { css, unsafeCSS } from 'lit';
export const styles = css`
bookmark-card {
display: block;
height: 100%;
width: 100%;
}
.affine-bookmark-card {
container: affine-bookmark-card / inline-size;
margin: 0 auto;
box-sizing: border-box;
display: flex;
width: 100%;
height: ${EMBED_CARD_HEIGHT.horizontal}px;
border-radius: 8px;
border: 1px solid var(--affine-background-tertiary-color);
@@ -188,6 +187,8 @@ export const styles = css`
}
.affine-bookmark-card.list {
height: ${EMBED_CARD_HEIGHT.list}px;
.affine-bookmark-content {
width: 100%;
flex-direction: row;
@@ -214,8 +215,9 @@ export const styles = css`
}
.affine-bookmark-card.vertical {
width: ${EMBED_CARD_WIDTH.vertical}px;
height: ${EMBED_CARD_HEIGHT.vertical}px;
flex-direction: column-reverse;
height: 100%;
.affine-bookmark-content {
width: 100%;
@@ -246,6 +248,9 @@ export const styles = css`
}
.affine-bookmark-card.cube {
width: ${EMBED_CARD_WIDTH.cube}px;
height: ${EMBED_CARD_HEIGHT.cube}px;
.affine-bookmark-content {
width: 100%;
flex-direction: column;
@@ -8,6 +8,7 @@
"include": ["./src"],
"references": [
{ "path": "../block-embed" },
{ "path": "../block-surface" },
{ "path": "../components" },
{ "path": "../model" },
{ "path": "../shared" },
+3 -4
View File
@@ -18,13 +18,12 @@
"@blocksuite/affine-shared": "workspace:*",
"@blocksuite/block-std": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.3",
"@blocksuite/inline": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",
"@floating-ui/dom": "^1.6.10",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.12",
"@toeverything/theme": "^1.1.7",
"@types/mdast": "^4.0.4",
"lit": "^3.2.0",
"minimatch": "^10.0.1",
@@ -41,5 +40,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.20.0"
"version": "0.19.0"
}
@@ -2,7 +2,6 @@ import { CodeBlockSchema } from '@blocksuite/affine-model';
import {
BlockHtmlAdapterExtension,
type BlockHtmlAdapterMatcher,
CODE_BLOCK_WRAP_KEY,
HastUtils,
} from '@blocksuite/affine-shared/adapters';
import type { DeltaInsert } from '@blocksuite/inline';
@@ -38,8 +37,7 @@ export const codeBlockHtmlAdapterMatcher: BlockHtmlAdapterMatcher = {
? codeLang.replace('code-', '')
: undefined;
const { walkerContext, deltaConverter, configs } = context;
const wrap = configs.get(CODE_BLOCK_WRAP_KEY) === 'true';
const { walkerContext, deltaConverter } = context;
walkerContext
.openNode(
{
@@ -48,7 +46,6 @@ export const codeBlockHtmlAdapterMatcher: BlockHtmlAdapterMatcher = {
flavour: 'affine:code',
props: {
language: codeLang ?? 'Plain Text',
wrap,
text: {
'$blocksuite:internal:text$': true,
delta: deltaConverter.astToDelta(codeText, {
@@ -2,7 +2,6 @@ import { CodeBlockSchema } from '@blocksuite/affine-model';
import {
BlockMarkdownAdapterExtension,
type BlockMarkdownAdapterMatcher,
CODE_BLOCK_WRAP_KEY,
type MarkdownAST,
} from '@blocksuite/affine-shared/adapters';
import type { DeltaInsert } from '@blocksuite/inline';
@@ -20,8 +19,7 @@ export const codeBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher = {
if (!isCodeNode(o.node)) {
return;
}
const { walkerContext, configs } = context;
const wrap = configs.get(CODE_BLOCK_WRAP_KEY) === 'true';
const { walkerContext } = context;
walkerContext
.openNode(
{
@@ -30,7 +28,6 @@ export const codeBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher = {
flavour: 'affine:code',
props: {
language: o.node.lang ?? 'Plain Text',
wrap,
text: {
'$blocksuite:internal:text$': true,
delta: [
@@ -2,7 +2,6 @@ import { CodeBlockSchema } from '@blocksuite/affine-model';
import {
BlockNotionHtmlAdapterExtension,
type BlockNotionHtmlAdapterMatcher,
CODE_BLOCK_WRAP_KEY,
HastUtils,
} from '@blocksuite/affine-shared/adapters';
import { nanoid } from '@blocksuite/store';
@@ -21,8 +20,7 @@ export const codeBlockNotionHtmlAdapterMatcher: BlockNotionHtmlAdapterMatcher =
if (!code) {
return;
}
const { walkerContext, deltaConverter, configs } = context;
const wrap = configs.get(CODE_BLOCK_WRAP_KEY) === 'true';
const { walkerContext, deltaConverter } = context;
const codeText =
code.children.length === 1 && code.children[0].type === 'text'
? code.children[0]
@@ -35,7 +33,6 @@ export const codeBlockNotionHtmlAdapterMatcher: BlockNotionHtmlAdapterMatcher =
flavour: CodeBlockSchema.model.flavour,
props: {
language: 'Plain Text',
wrap,
text: {
'$blocksuite:internal:text$': true,
delta: deltaConverter.astToDelta(codeText, {
@@ -1,14 +1,8 @@
import { deleteTextCommand } from '@blocksuite/affine-components/rich-text';
import {
HtmlAdapter,
pasteMiddleware,
PlainTextAdapter,
} from '@blocksuite/affine-shared/adapters';
import {
getBlockIndexCommand,
getBlockSelectionsCommand,
getTextSelectionCommand,
} from '@blocksuite/affine-shared/commands';
import {
type BlockComponent,
Clipboard,
@@ -46,13 +40,13 @@ export class CodeClipboardController {
this._std.command
.chain()
.try(cmd => [
cmd.pipe(getTextSelectionCommand).pipe((ctx, next) => {
cmd.getTextSelection().inline<'currentSelectionPath'>((ctx, next) => {
const textSelection = ctx.currentTextSelection;
if (!textSelection) return;
const end = textSelection.to ?? textSelection.from;
next({ currentSelectionPath: end.blockId });
}),
cmd.pipe(getBlockSelectionsCommand).pipe((ctx, next) => {
cmd.getBlockSelections().inline<'currentSelectionPath'>((ctx, next) => {
const currentBlockSelections = ctx.currentBlockSelections;
if (!currentBlockSelections) return;
const blockSelection = currentBlockSelections.at(-1);
@@ -60,9 +54,9 @@ export class CodeClipboardController {
next({ currentSelectionPath: blockSelection.blockId });
}),
])
.pipe(getBlockIndexCommand)
.try(cmd => [cmd.pipe(getTextSelectionCommand).pipe(deleteTextCommand)])
.pipe((ctx, next) => {
.getBlockIndex()
.try(cmd => [cmd.getTextSelection().deleteText()])
.inline((ctx, next) => {
if (!ctx.parentBlock) {
return;
}
@@ -1,4 +1,3 @@
import { ConfigExtensionFactory } from '@blocksuite/block-std';
import type { BundledLanguageInfo, ThemeInput } from 'shiki';
export interface CodeBlockConfig {
@@ -14,6 +13,3 @@ export interface CodeBlockConfig {
*/
showLineNumbers?: boolean;
}
export const CodeBlockConfigExtension =
ConfigExtensionFactory<CodeBlockConfig>('affine:code');
@@ -12,7 +12,6 @@ import {
} from 'shiki';
import getWasm from 'shiki/wasm';
import { CodeBlockConfigExtension } from './code-block-config.js';
import {
CODE_BLOCK_DEFAULT_DARK_THEME,
CODE_BLOCK_DEFAULT_LIGHT_THEME,
@@ -28,10 +27,7 @@ export class CodeBlockService extends BlockService {
highlighter$: Signal<HighlighterCore | null> = signal(null);
get langs() {
return (
this.std.getOptional(CodeBlockConfigExtension.identifier)?.langs ??
bundledLanguagesInfo
);
return this.std.getConfig('affine:code')?.langs ?? bundledLanguagesInfo;
}
get themeKey() {
@@ -50,9 +46,7 @@ export class CodeBlockService extends BlockService {
engine: createOnigurumaEngine(() => getWasm),
})
.then(async highlighter => {
const config = this.std.getOptional(
CodeBlockConfigExtension.identifier
);
const config = this.std.getConfig('affine:code');
const darkTheme = config?.theme?.dark ?? CODE_BLOCK_DEFAULT_DARK_THEME;
const lightTheme =
config?.theme?.light ?? CODE_BLOCK_DEFAULT_LIGHT_THEME;
@@ -1,7 +1,7 @@
import {
BlockViewExtension,
FlavourExtension,
WidgetViewExtension,
WidgetViewMapExtension,
} from '@blocksuite/block-std';
import type { ExtensionType } from '@blocksuite/store';
import { literal, unsafeStatic } from 'lit/static-html.js';
@@ -14,17 +14,13 @@ import {
import { CodeBlockService } from './code-block-service.js';
import { AFFINE_CODE_TOOLBAR_WIDGET } from './code-toolbar/index.js';
export const codeToolbarWidget = WidgetViewExtension(
'affine:code',
AFFINE_CODE_TOOLBAR_WIDGET,
literal`${unsafeStatic(AFFINE_CODE_TOOLBAR_WIDGET)}`
);
export const CodeBlockSpec: ExtensionType[] = [
FlavourExtension('affine:code'),
CodeBlockService,
BlockViewExtension('affine:code', literal`affine-code`),
codeToolbarWidget,
WidgetViewMapExtension('affine:code', {
codeToolbar: literal`${unsafeStatic(AFFINE_CODE_TOOLBAR_WIDGET)}`,
}),
CodeBlockInlineManagerExtension,
CodeBlockUnitSpecExtension,
CodeBlockAdapterExtensions,
@@ -32,7 +32,6 @@ import { classMap } from 'lit/directives/class-map.js';
import type { ThemedToken } from 'shiki';
import { CodeClipboardController } from './clipboard/index.js';
import { CodeBlockConfigExtension } from './code-block-config.js';
import { CodeBlockInlineManagerExtension } from './code-block-inline.js';
import type { CodeBlockService } from './code-block-service.js';
import { codeBlockStyles } from './styles.js';
@@ -384,8 +383,7 @@ export class CodeBlockComponent extends CaptionedBlockComponent<
override renderBlock(): TemplateResult<1> {
const showLineNumbers =
this.std.getOptional(CodeBlockConfigExtension.identifier)
?.showLineNumbers ?? true;
this.std.getConfig('affine:code')?.showLineNumbers ?? true;
return html`
<div
@@ -1,3 +1,4 @@
import { MoreVerticalIcon } from '@blocksuite/affine-components/icons';
import { createLitPortal } from '@blocksuite/affine-components/portal';
import type {
EditorIconButton,
@@ -5,7 +6,6 @@ import type {
} from '@blocksuite/affine-components/toolbar';
import { renderGroups } from '@blocksuite/affine-components/toolbar';
import { noop, WithDisposable } from '@blocksuite/global/utils';
import { MoreVerticalIcon } from '@blocksuite/icons/lit';
import { flip, offset } from '@floating-ui/dom';
import { css, html, LitElement } from 'lit';
import { property, query, state } from 'lit/decorators.js';
@@ -132,7 +132,7 @@ export class AffineCodeToolbar extends WithDisposable(LitElement) {
?disabled=${this.context.doc.readonly}
@click=${() => this._toggleMoreMenu()}
>
${MoreVerticalIcon()}
${MoreVerticalIcon}
</editor-icon-button>
</editor-toolbar>
`;
@@ -1,4 +1,7 @@
import type * as CommandsType from '@blocksuite/affine-shared/commands';
import { CodeBlockComponent } from './code-block';
import type { CodeBlockConfig } from './code-block-config';
import {
AFFINE_CODE_TOOLBAR_WIDGET,
AffineCodeToolbarWidget,
@@ -15,7 +18,15 @@ export function effects() {
customElements.define('affine-code', CodeBlockComponent);
}
declare type _GLOBAL_ = typeof CommandsType;
declare global {
namespace BlockSuite {
interface BlockConfigs {
'affine:code': CodeBlockConfig;
}
}
interface HTMLElementTagNameMap {
'language-list-button': LanguageListButton;
'affine-code-toolbar': AffineCodeToolbar;
@@ -20,13 +20,12 @@
"@blocksuite/block-std": "workspace:*",
"@blocksuite/data-view": "workspace:*",
"@blocksuite/global": "workspace:*",
"@blocksuite/icons": "^2.2.3",
"@blocksuite/inline": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",
"@floating-ui/dom": "^1.6.10",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.12",
"@toeverything/theme": "^1.1.7",
"@types/mdast": "^4.0.4",
"lit": "^3.2.0",
"minimatch": "^10.0.1",
@@ -42,5 +41,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.20.0"
"version": "0.19.0"
}
@@ -9,7 +9,11 @@ import {
popMenu,
popupTargetFromElement,
} from '@blocksuite/affine-components/context-menu';
import { CopyIcon, DeleteIcon } from '@blocksuite/affine-components/icons';
import {
CopyIcon,
DeleteIcon,
MoreHorizontalIcon,
} from '@blocksuite/affine-components/icons';
import { PeekViewProvider } from '@blocksuite/affine-components/peek';
import { toast } from '@blocksuite/affine-components/toast';
import { NOTE_SELECTOR } from '@blocksuite/affine-shared/consts';
@@ -38,7 +42,6 @@ import {
uniMap,
} from '@blocksuite/data-view';
import { widgetPresets } from '@blocksuite/data-view/widget-presets';
import { MoreHorizontalIcon } from '@blocksuite/icons/lit';
import { Slice } from '@blocksuite/store';
import { computed, signal } from '@preact/signals-core';
import { css, nothing, unsafeCSS } from 'lit';
@@ -241,7 +244,7 @@ export class DataViewBlockComponent extends CaptionedBlockComponent<DataViewBloc
return nothing;
}
return html` <div class="database-ops" @click="${this._clickDatabaseOps}">
${MoreHorizontalIcon()}
${MoreHorizontalIcon}
</div>`;
}
@@ -1,5 +1,14 @@
import { DataViewBlockComponent } from './data-view-block';
import type { DataViewBlockModel } from './data-view-model';
export function effects() {
customElements.define('affine-data-view', DataViewBlockComponent);
}
declare global {
namespace BlockSuite {
interface BlockModels {
'affine:data-view': DataViewBlockModel;
}
}
}
@@ -23,10 +23,10 @@
"@blocksuite/icons": "^2.2.1",
"@blocksuite/inline": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",
"@floating-ui/dom": "^1.6.10",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.12",
"@toeverything/theme": "^1.1.7",
"@types/mdast": "^4.0.4",
"date-fns": "^4.0.0",
"lit": "^3.2.0",
@@ -44,5 +44,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.20.0"
"version": "0.19.0"
}
@@ -6,17 +6,162 @@ import {
import {
BlockHtmlAdapterExtension,
type BlockHtmlAdapterMatcher,
HastUtils,
type InlineHtmlAST,
TextUtils,
} from '@blocksuite/affine-shared/adapters';
import { nanoid } from '@blocksuite/store';
import type { Element } from 'hast';
import { processTable } from './utils';
const DATABASE_NODE_TYPES = new Set(['table', 'thead', 'tbody', 'th', 'tr']);
export const databaseBlockHtmlAdapterMatcher: BlockHtmlAdapterMatcher = {
flavour: DatabaseBlockSchema.model.flavour,
toMatch: () => false,
toMatch: o =>
HastUtils.isElement(o.node) && DATABASE_NODE_TYPES.has(o.node.tagName),
fromMatch: o => o.node.flavour === DatabaseBlockSchema.model.flavour,
toBlockSnapshot: {},
toBlockSnapshot: {
enter: (o, context) => {
if (!HastUtils.isElement(o.node)) {
return;
}
const { walkerContext } = context;
if (o.node.tagName === 'table') {
const tableHeader = HastUtils.querySelector(o.node, 'thead');
if (!tableHeader) {
return;
}
const tableHeaderRow = HastUtils.querySelector(tableHeader, 'tr');
if (!tableHeaderRow) {
return;
}
// Table header row as database header row
const viewsColumns = tableHeaderRow.children.map(() => {
return {
id: nanoid(),
hide: false,
width: 180,
};
});
// Build database cells from table body rows
const cells = Object.create(null);
const tableBody = HastUtils.querySelector(o.node, 'tbody');
tableBody?.children.forEach(row => {
const rowId = nanoid();
cells[rowId] = Object.create(null);
(row as Element).children.forEach((cell, index) => {
const column = viewsColumns[index];
if (!column) {
return;
}
cells[rowId][column.id] = {
columnId: column.id,
value: TextUtils.createText(
(cell as Element).children
.map(child => ('value' in child ? child.value : ''))
.join('')
),
};
});
});
// Build database columns from table header row
const columns = tableHeaderRow.children.flatMap((_child, index) => {
const column = viewsColumns[index];
if (!column) {
return [];
}
return {
type: index === 0 ? 'title' : 'rich-text',
name: (_child as Element).children
.map(child => ('value' in child ? child.value : ''))
.join(''),
data: {},
id: column.id,
};
});
walkerContext.openNode(
{
type: 'block',
id: nanoid(),
flavour: 'affine:database',
props: {
views: [
{
id: nanoid(),
name: 'Table View',
mode: 'table',
columns: [],
filter: {
type: 'group',
op: 'and',
conditions: [],
},
header: {
titleColumn: viewsColumns[0]?.id,
iconColumn: 'type',
},
},
],
title: {
'$blocksuite:internal:text$': true,
delta: [],
},
cells,
columns,
},
children: [],
},
'children'
);
walkerContext.setNodeContext('affine:table:rowid', Object.keys(cells));
walkerContext.skipChildren(1);
}
// The first child of each table body row is the database title cell
if (o.node.tagName === 'tr') {
const { deltaConverter } = context;
const firstChild = o.node.children[0];
if (!firstChild) {
return;
}
walkerContext
.openNode({
type: 'block',
id:
(
walkerContext.getNodeContext(
'affine:table:rowid'
) as Array<string>
).shift() ?? nanoid(),
flavour: 'affine:paragraph',
props: {
text: {
'$blocksuite:internal:text$': true,
delta: deltaConverter.astToDelta(firstChild),
},
type: 'text',
},
children: [],
})
.closeNode();
walkerContext.skipAllChildren();
}
},
leave: (o, context) => {
if (!HastUtils.isElement(o.node)) {
return;
}
const { walkerContext } = context;
if (o.node.tagName === 'table') {
walkerContext.closeNode();
}
},
},
fromBlockSnapshot: {
enter: (o, context) => {
const { walkerContext } = context;
@@ -7,7 +7,9 @@ import {
BlockMarkdownAdapterExtension,
type BlockMarkdownAdapterMatcher,
type MarkdownAST,
TextUtils,
} from '@blocksuite/affine-shared/adapters';
import { nanoid } from '@blocksuite/store';
import type { TableRow } from 'mdast';
import { processTable } from './utils';
@@ -22,7 +24,123 @@ export const databaseBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher =
flavour: DatabaseBlockSchema.model.flavour,
toMatch: o => isDatabaseNode(o.node),
fromMatch: o => o.node.flavour === DatabaseBlockSchema.model.flavour,
toBlockSnapshot: {},
toBlockSnapshot: {
enter: (o, context) => {
const { walkerContext } = context;
if (o.node.type === 'table') {
const viewsColumns = o.node.children[0]?.children.map(() => {
return {
id: nanoid(),
hide: false,
width: 180,
};
});
const cells = Object.create(null);
o.node.children.slice(1).forEach(row => {
const rowId = nanoid();
cells[rowId] = Object.create(null);
row.children.slice(1).forEach((cell, index) => {
const column = viewsColumns?.[index + 1];
if (!column) {
return;
}
cells[rowId][column.id] = {
columnId: column.id,
value: TextUtils.createText(
cell.children
.map(child => ('value' in child ? child.value : ''))
.join('')
),
};
});
});
const columns = o.node.children[0]?.children.map((_child, index) => {
return {
type: index === 0 ? 'title' : 'rich-text',
name: _child.children
.map(child => ('value' in child ? child.value : ''))
.join(''),
data: {},
id: viewsColumns?.[index]?.id,
};
});
walkerContext.openNode(
{
type: 'block',
id: nanoid(),
flavour: 'affine:database',
props: {
views: [
{
id: nanoid(),
name: 'Table View',
mode: 'table',
columns: [],
filter: {
type: 'group',
op: 'and',
conditions: [],
},
header: {
titleColumn: viewsColumns?.[0]?.id,
iconColumn: 'type',
},
},
],
title: {
'$blocksuite:internal:text$': true,
delta: [],
},
cells,
columns,
},
children: [],
},
'children'
);
walkerContext.setNodeContext(
'affine:table:rowid',
Object.keys(cells)
);
walkerContext.skipChildren(1);
}
if (o.node.type === 'tableRow') {
const { deltaConverter } = context;
const firstChild = o.node.children[0];
if (!firstChild) {
return;
}
walkerContext
.openNode({
type: 'block',
id:
(
walkerContext.getNodeContext(
'affine:table:rowid'
) as Array<string>
).shift() ?? nanoid(),
flavour: 'affine:paragraph',
props: {
text: {
'$blocksuite:internal:text$': true,
delta: deltaConverter.astToDelta(firstChild),
},
type: 'text',
},
children: [],
})
.closeNode();
walkerContext.skipAllChildren();
}
},
leave: (o, context) => {
const { walkerContext } = context;
if (o.node.type === 'table') {
walkerContext.closeNode();
}
},
},
fromBlockSnapshot: {
enter: (o, context) => {
const { walkerContext, deltaConverter } = context;
@@ -1,9 +1,9 @@
import { DatabaseBlockSchema } from '@blocksuite/affine-model';
import {
AdapterTextUtils,
BlockNotionHtmlAdapterExtension,
type BlockNotionHtmlAdapterMatcher,
HastUtils,
TextUtils,
} from '@blocksuite/affine-shared/adapters';
import { getTagColor } from '@blocksuite/data-view';
import { type BlockSnapshot, nanoid } from '@blocksuite/store';
@@ -219,7 +219,7 @@ export const databaseBlockNotionHtmlAdapterMatcher: BlockNotionHtmlAdapterMatche
column.type = 'rich-text';
row[column.id] = {
columnId: column.id,
value: AdapterTextUtils.createText(text),
value: TextUtils.createText(text),
};
} else {
row[column.id] = {
@@ -235,11 +235,11 @@ export const databaseBlockNotionHtmlAdapterMatcher: BlockNotionHtmlAdapterMatche
}
if (
column.type === 'rich-text' &&
!AdapterTextUtils.isText(row[column.id].value)
!TextUtils.isText(row[column.id].value)
) {
row[column.id] = {
columnId: column.id,
value: AdapterTextUtils.createText(row[column.id].value),
value: TextUtils.createText(row[column.id].value),
};
}
});
@@ -1,5 +1,5 @@
import type { DatabaseBlockModel } from '@blocksuite/affine-model';
import type { Command } from '@blocksuite/block-std';
import type { BlockCommands, Command } from '@blocksuite/block-std';
import type { BlockModel, Store } from '@blocksuite/store';
import {
@@ -8,14 +8,12 @@ import {
} from './data-source';
export const insertDatabaseBlockCommand: Command<
'selectedModels',
'insertedDatabaseBlockId',
{
selectedModels?: BlockModel[];
viewType: string;
place?: 'after' | 'before';
removeEmptyLine?: boolean;
},
{
insertedDatabaseBlockId: string;
}
> = (ctx, next) => {
const { selectedModels, viewType, place, removeEmptyLine, std } = ctx;
@@ -67,3 +65,7 @@ export const initDatabaseBlock = (
doc.addBlock('affine:paragraph', {}, parent.id);
}
};
export const commands: BlockCommands = {
insertDatabaseBlock: insertDatabaseBlockCommand,
};
@@ -1,10 +1,6 @@
import type { MenuOptions } from '@blocksuite/affine-components/context-menu';
import { type DatabaseBlockModel } from '@blocksuite/affine-model';
import { ConfigExtensionFactory } from '@blocksuite/block-std';
export interface DatabaseOptionsConfig {
configure: (model: DatabaseBlockModel, options: MenuOptions) => MenuOptions;
}
export const DatabaseConfigExtension =
ConfigExtensionFactory<DatabaseOptionsConfig>('affine:database');
@@ -3,7 +3,6 @@ import type {
ColumnUpdater,
DatabaseBlockModel,
} from '@blocksuite/affine-model';
import { getSelectedModelsCommand } from '@blocksuite/affine-shared/commands';
import { FeatureFlagService } from '@blocksuite/affine-shared/services';
import {
insertPositionToIndex,
@@ -34,6 +33,8 @@ import {
} from './properties/index.js';
import {
addProperty,
applyCellsUpdate,
applyPropertyUpdate,
copyCellsByProperty,
deleteRows,
deleteView,
@@ -167,6 +168,7 @@ export class DatabaseBlockDataSource extends DataSourceBase {
columnId: propertyId,
value: newValue,
});
applyCellsUpdate(this._model);
}
}
@@ -196,6 +198,7 @@ export class DatabaseBlockDataSource extends DataSourceBase {
insertToPosition,
property.create(this.newPropertyName())
);
applyPropertyUpdate(this._model);
return result;
}
@@ -279,6 +282,7 @@ export class DatabaseBlockDataSource extends DataSourceBase {
propertyDataSet(propertyId: string, data: Record<string, unknown>): void {
this._runCapture();
this.updateProperty(propertyId, () => ({ data }));
applyPropertyUpdate(this._model);
}
propertyDataTypeGet(propertyId: string): TypeInstance | undefined {
@@ -332,6 +336,7 @@ export class DatabaseBlockDataSource extends DataSourceBase {
schema
);
copyCellsByProperty(this._model, copyId, id);
applyPropertyUpdate(this._model);
return id;
}
@@ -360,6 +365,7 @@ export class DatabaseBlockDataSource extends DataSourceBase {
propertyNameSet(propertyId: string, name: string): void {
this.doc.captureSync();
this.updateProperty(propertyId, () => ({ name }));
applyPropertyUpdate(this._model);
}
override propertyReadonlyGet(propertyId: string): boolean {
@@ -414,6 +420,7 @@ export class DatabaseBlockDataSource extends DataSourceBase {
}
});
updateCells(this._model, propertyId, cells);
applyPropertyUpdate(this._model);
}
rowAdd(insertPosition: InsertToPosition | number): string {
@@ -510,9 +517,12 @@ export const databaseViewInitTemplate = (
datasource.viewManager.viewAdd(viewType);
};
export const convertToDatabase = (host: EditorHost, viewType: string) => {
const [_, ctx] = host.std.command.exec(getSelectedModelsCommand, {
types: ['block', 'text'],
});
const [_, ctx] = host.std.command
.chain()
.getSelectedModels({
types: ['block', 'text'],
})
.run();
const { selectedModels } = ctx;
const firstModel = selectedModels?.[0];
if (!firstModel) return;
@@ -4,7 +4,7 @@ import {
popMenu,
popupTargetFromElement,
} from '@blocksuite/affine-components/context-menu';
import { DropIndicator } from '@blocksuite/affine-components/drop-indicator';
import { DragIndicator } from '@blocksuite/affine-components/drag-indicator';
import { PeekViewProvider } from '@blocksuite/affine-components/peek';
import { toast } from '@blocksuite/affine-components/toast';
import type { DatabaseBlockModel } from '@blocksuite/affine-model';
@@ -48,10 +48,7 @@ import { computed, signal } from '@preact/signals-core';
import { css, html, nothing, unsafeCSS } from 'lit';
import { popSideDetail } from './components/layout.js';
import {
DatabaseConfigExtension,
type DatabaseOptionsConfig,
} from './config.js';
import type { DatabaseOptionsConfig } from './config.js';
import { HostContextKey } from './context/host-context.js';
import { DatabaseBlockDataSource } from './data-source.js';
import { BlockRenderer } from './detail-panel/block-renderer.js';
@@ -244,7 +241,7 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent<DatabaseBloc
}
);
indicator = new DropIndicator();
indicator = new DragIndicator();
onDrag = (evt: MouseEvent, id: string): (() => void) => {
const result = getDropResult(evt);
@@ -337,6 +334,11 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent<DatabaseBloc
this._dataSource.contextSet(HostContextKey, this.host);
const id = currentViewStorage.getCurrentView(this.model.id);
if (id && this.dataSource.viewManager.viewGet(id)) {
console.log(
'set current view',
id,
this._dataSource.viewManager.viewGet(id)
);
this.dataSource.viewManager.setCurrentView(id);
}
}
@@ -346,7 +348,7 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent<DatabaseBloc
get optionsConfig(): DatabaseOptionsConfig {
return {
configure: (_model, options) => options,
...this.std.getOptional(DatabaseConfigExtension.identifier),
...this.std.getConfig('affine:database'),
};
}
@@ -1,58 +0,0 @@
import type { DatabaseBlockModel } from '@blocksuite/affine-model';
import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme';
import { BlockComponent } from '@blocksuite/block-std';
import { DatabaseListViewIcon } from '@blocksuite/icons/lit';
import { css, html } from 'lit';
export class DatabaseDndPreviewBlockComponent extends BlockComponent<DatabaseBlockModel> {
static override styles = css`
.affine-database-preview-container {
display: flex;
flex-direction: column;
align-items: flex-start;
padding: 4px;
box-sizing: border-box;
border-radius: 8px;
background-color: ${unsafeCSSVarV2(
'layer/background/overlayPanel',
'#FBFBFC'
)};
}
.database-preview-content {
padding: 4px 16px;
display: flex;
gap: 8px;
}
.database-preview-content > svg {
color: ${unsafeCSSVarV2('icon/primary', '#77757D')};
}
.database-preview-content > .text {
color: var(--affine-text-primary-color);
color: ${unsafeCSSVarV2('text/primary', '#121212')};
font-size: 14px;
line-height: 24px;
}
`;
override renderBlock() {
return html`<div
class="affine-database-preview-container"
contenteditable="false"
>
<div class="database-preview-content">
${DatabaseListViewIcon({ width: '24px', height: '24px' })}
<span class="text">Database Block</span>
</div>
</div>`;
}
}
declare global {
interface HTMLElementTagNameMap {
'affine-dnd-preview-database': DatabaseDndPreviewBlockComponent;
}
}
@@ -1,11 +1,17 @@
import { BlockViewExtension, FlavourExtension } from '@blocksuite/block-std';
import {
BlockViewExtension,
CommandExtension,
FlavourExtension,
} from '@blocksuite/block-std';
import type { ExtensionType } from '@blocksuite/store';
import { literal } from 'lit/static-html.js';
import { DatabaseBlockAdapterExtensions } from './adapters/extension.js';
import { commands } from './commands.js';
export const DatabaseBlockSpec: ExtensionType[] = [
FlavourExtension('affine:database'),
CommandExtension(commands),
BlockViewExtension('affine:database', literal`affine-database`),
DatabaseBlockAdapterExtensions,
].flat();
@@ -76,6 +76,10 @@ export class BlockRenderer
return this.host?.doc.getBlock(this.rowId)?.model;
}
get service() {
return this.host.std.getService('affine:database');
}
override connectedCallback() {
super.connectedCallback();
if (this.model && this.model.text) {
@@ -127,7 +131,7 @@ export class BlockRenderer
.attributesSchema=${this.attributesSchema}
.attributeRenderer=${this.attributeRenderer}
.embedChecker=${this.inlineManager.embedChecker}
.markdownMatches=${this.inlineManager.markdownMatches}
.markdownShortcutHandler=${this.inlineManager.markdownShortcutHandler}
class="inline-editor"
></rich-text>
`;
@@ -1,14 +1,14 @@
import {
CodeBlockModel,
type DatabaseBlockModel,
ListBlockModel,
ParagraphBlockModel,
type RootBlockModel,
import type {
DatabaseBlockModel,
RootBlockModel,
} from '@blocksuite/affine-model';
import { REFERENCE_NODE } from '@blocksuite/affine-shared/consts';
import { TelemetryProvider } from '@blocksuite/affine-shared/services';
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
import { createDefaultDoc, matchModels } from '@blocksuite/affine-shared/utils';
import {
createDefaultDoc,
matchFlavours,
} from '@blocksuite/affine-shared/utils';
import { type EditorHost, ShadowlessElement } from '@blocksuite/block-std';
import type { DetailSlotProps, SingleView } from '@blocksuite/data-view';
import { SignalWatcher, WithDisposable } from '@blocksuite/global/utils';
@@ -72,10 +72,10 @@ export class NoteRenderer
note.root.children
.find(child => child.flavour === 'affine:note')
?.children.find(block =>
matchModels(block, [
ParagraphBlockModel,
ListBlockModel,
CodeBlockModel,
matchFlavours(block, [
'affine:paragraph',
'affine:list',
'affine:code',
])
);
}
@@ -1,7 +1,8 @@
import type { insertDatabaseBlockCommand } from './commands';
import { CenterPeek } from './components/layout';
import { DatabaseTitle } from './components/title';
import type { DatabaseOptionsConfig } from './config';
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 { LinkCell, LinkCellEditing } from './properties/link/cell-renderer';
@@ -36,9 +37,27 @@ export function effects() {
customElements.define('database-datasource-block-renderer', BlockRenderer);
customElements.define('affine-database-link-node', LinkNode);
customElements.define('affine-database', DatabaseBlockComponent);
customElements.define(
'affine-dnd-preview-database',
DatabaseDndPreviewBlockComponent
);
}
declare global {
namespace BlockSuite {
interface BlockConfigs {
'affine:database': Partial<DatabaseOptionsConfig>;
}
interface CommandContext {
insertedDatabaseBlockId?: string;
}
interface Commands {
/**
* insert a database block after or before the current block selection
* @param latex the LaTeX content. A input dialog will be shown if not provided
* @param removeEmptyLine remove the current block if it is empty
* @param place where to insert the LaTeX block
* @returns the id of the inserted LaTeX block
*/
insertDatabaseBlock: typeof insertDatabaseBlockCommand;
}
}
}
@@ -1,6 +1,9 @@
import type * as CommandType from '@blocksuite/affine-shared/commands';
declare type _GLOBAL_ = typeof CommandType;
export * from './adapters';
export * from './commands';
export * from './config';
export type { DatabaseOptionsConfig } from './config';
export * from './data-source';
export * from './database-block';
export * from './database-spec';
@@ -221,7 +221,7 @@ export class RichTextCell extends BaseRichTextCell {
.attributesSchema=${this.attributesSchema}
.attributeRenderer=${this.attributeRenderer}
.embedChecker=${this.inlineManager?.embedChecker}
.markdownMatches=${this.inlineManager?.markdownMatches}
.markdownShortcutHandler=${this.inlineManager?.markdownShortcutHandler}
.readonly=${true}
class="affine-database-rich-text inline-editor"
></rich-text>`
@@ -518,14 +518,12 @@ export class RichTextCellEditing extends BaseRichTextCell {
override render() {
return html`<rich-text
data-disable-ask-ai
data-not-block-text
.yText=${this.value}
.inlineEventSource=${this.topContenteditableElement}
.attributesSchema=${this.attributesSchema}
.attributeRenderer=${this.attributeRenderer}
.embedChecker=${this.inlineManager?.embedChecker}
.markdownMatches=${this.inlineManager?.markdownMatches}
.markdownShortcutHandler=${this.inlineManager?.markdownShortcutHandler}
.verticalScrollContainerGetter=${() =>
this.topContenteditableElement?.host
? getViewportElement(this.topContenteditableElement.host)
@@ -123,6 +123,10 @@ abstract class BaseTextCell extends BaseCellRenderer<Text> {
return this.host?.std.get(DefaultInlineManagerExtension.identifier);
}
get service() {
return this.host?.std.getService('affine:database');
}
get topContenteditableElement() {
const databaseBlock =
this.closest<DatabaseBlockComponent>('affine-database');
@@ -187,7 +191,7 @@ export class HeaderAreaTextCell extends BaseTextCell {
.attributesSchema="${this.attributesSchema}"
.attributeRenderer="${this.attributeRenderer}"
.embedChecker="${this.inlineManager?.embedChecker}"
.markdownMatches="${this.inlineManager?.markdownMatches}"
.markdownShortcutHandler="${this.inlineManager?.markdownShortcutHandler}"
.readonly="${true}"
class="data-view-header-area-rich-text"
></rich-text>`;
@@ -384,14 +388,12 @@ export class HeaderAreaTextCellEditing extends BaseTextCell {
override renderBlockText() {
return html` <rich-text
data-disable-ask-ai
data-not-block-text
.yText="${this.value}"
.inlineEventSource="${this.topContenteditableElement}"
.attributesSchema="${this.attributesSchema}"
.attributeRenderer="${this.attributeRenderer}"
.embedChecker="${this.inlineManager?.embedChecker}"
.markdownMatches="${this.inlineManager?.markdownMatches}"
.markdownShortcutHandler="${this.inlineManager?.markdownShortcutHandler}"
.readonly="${this.readonly}"
.enableClipboard="${false}"
.verticalScrollContainerGetter="${() =>
@@ -405,8 +407,6 @@ export class HeaderAreaTextCellEditing extends BaseTextCell {
override renderLinkedDoc(): TemplateResult {
return html` <rich-text
data-disable-ask-ai
data-not-block-text
.yText="${this.linkedDocTitle$.value}"
.inlineEventSource="${this.topContenteditableElement}"
.readonly="${this.readonly}"
@@ -65,4 +65,12 @@ export class DatabaseSelection extends BaseSelection {
}
}
declare global {
namespace BlockSuite {
interface Selection {
database: typeof DatabaseSelection;
}
}
}
export const DatabaseSelectionExtension = SelectionExtension(DatabaseSelection);
@@ -37,6 +37,24 @@ export function addProperty(
return id;
}
export function applyCellsUpdate(model: DatabaseBlockModel) {
model.doc.updateBlock(model, {
cells: model.cells,
});
}
export function applyPropertyUpdate(model: DatabaseBlockModel) {
model.doc.updateBlock(model, {
columns: model.columns,
});
}
export function applyViewsUpdate(model: DatabaseBlockModel) {
model.doc.updateBlock(model, {
views: model.views,
});
}
export function copyCellsByProperty(
model: DatabaseBlockModel,
fromId: Column['id'],
@@ -138,6 +156,7 @@ export function moveViewTo(
arr => insertPositionToIndex(position, arr)
);
});
applyViewsUpdate(model);
}
export function updateCell(
@@ -236,5 +255,6 @@ export const updateView = <ViewData extends ViewBasicDataType>(
return { ...v, ...update(v as ViewData) };
});
});
applyViewsUpdate(model);
};
export const DATABASE_CONVERT_WHITE_LIST = ['affine:list', 'affine:paragraph'];
+3 -3
View File
@@ -20,10 +20,10 @@
"@blocksuite/global": "workspace:*",
"@blocksuite/inline": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",
"@floating-ui/dom": "^1.6.10",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.12",
"@toeverything/theme": "^1.1.7",
"@types/mdast": "^4.0.4",
"lit": "^3.2.0",
"minimatch": "^10.0.1",
@@ -39,5 +39,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.20.0"
"version": "0.19.0"
}
@@ -22,10 +22,10 @@
"@blocksuite/icons": "^2.2.1",
"@blocksuite/inline": "workspace:*",
"@blocksuite/store": "workspace:*",
"@floating-ui/dom": "^1.6.13",
"@floating-ui/dom": "^1.6.10",
"@lit/context": "^1.1.2",
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.12",
"@toeverything/theme": "^1.1.7",
"lit": "^3.2.0",
"minimatch": "^10.0.1",
"zod": "^3.23.8"
@@ -40,5 +40,5 @@
"!src/__tests__",
"!dist/__tests__"
],
"version": "0.20.0"
"version": "0.19.0"
}

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