mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-27 02:42:25 +08:00
Compare commits
1 Commits
v0.26.3-be
...
renovate/a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e1229599c |
@@ -222,7 +222,7 @@
|
|||||||
},
|
},
|
||||||
"SMTP.sender": {
|
"SMTP.sender": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Sender of all the emails (e.g. \"AFFiNE Self Hosted <noreply@example.com>\")\n@default \"AFFiNE Self Hosted <noreply@example.com>\"\n@environment `MAILER_SENDER`",
|
"description": "Sender of all the emails (e.g. \"AFFiNE Self Hosted <noreply@example.com>\")\n@default \"AFFiNE Self Hosted <noreply@example.com>\"\n@environment `MAILER_SENDER`",
|
||||||
"default": "AFFiNE Self Hosted <noreply@example.com>"
|
"default": "AFFiNE Self Hosted <noreply@example.com>"
|
||||||
},
|
},
|
||||||
"SMTP.ignoreTLS": {
|
"SMTP.ignoreTLS": {
|
||||||
@@ -262,7 +262,7 @@
|
|||||||
},
|
},
|
||||||
"fallbackSMTP.sender": {
|
"fallbackSMTP.sender": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Sender of all the emails (e.g. \"AFFiNE Self Hosted <noreply@example.com>\")\n@default \"\"",
|
"description": "Sender of all the emails (e.g. \"AFFiNE Self Hosted <noreply@example.com>\")\n@default \"\"",
|
||||||
"default": ""
|
"default": ""
|
||||||
},
|
},
|
||||||
"fallbackSMTP.ignoreTLS": {
|
"fallbackSMTP.ignoreTLS": {
|
||||||
|
|||||||
33
.github/actions/deploy/deploy.mjs
vendored
33
.github/actions/deploy/deploy.mjs
vendored
@@ -25,30 +25,30 @@ const buildType = BUILD_TYPE || 'canary';
|
|||||||
|
|
||||||
const isProduction = buildType === 'stable';
|
const isProduction = buildType === 'stable';
|
||||||
const isBeta = buildType === 'beta';
|
const isBeta = buildType === 'beta';
|
||||||
const isCanary = buildType === 'canary';
|
|
||||||
const isInternal = buildType === 'internal';
|
const isInternal = buildType === 'internal';
|
||||||
const isSpotEnabled = isBeta || isCanary;
|
|
||||||
|
|
||||||
const replicaConfig = {
|
const replicaConfig = {
|
||||||
stable: {
|
stable: {
|
||||||
front: Number(process.env.PRODUCTION_FRONT_REPLICA) || 2,
|
front: Number(process.env.PRODUCTION_FRONT_REPLICA) || 2,
|
||||||
graphql: Number(process.env.PRODUCTION_GRAPHQL_REPLICA) || 2,
|
graphql: Number(process.env.PRODUCTION_GRAPHQL_REPLICA) || 2,
|
||||||
|
doc: Number(process.env.PRODUCTION_DOC_REPLICA) || 2,
|
||||||
},
|
},
|
||||||
beta: {
|
beta: {
|
||||||
front: Number(process.env.BETA_FRONT_REPLICA) || 1,
|
front: Number(process.env.BETA_FRONT_REPLICA) || 1,
|
||||||
graphql: Number(process.env.BETA_GRAPHQL_REPLICA) || 1,
|
graphql: Number(process.env.BETA_GRAPHQL_REPLICA) || 1,
|
||||||
|
doc: Number(process.env.BETA_DOC_REPLICA) || 1,
|
||||||
},
|
},
|
||||||
canary: { front: 1, graphql: 1 },
|
canary: { front: 1, graphql: 1, doc: 1 },
|
||||||
};
|
};
|
||||||
|
|
||||||
const cpuConfig = {
|
const cpuConfig = {
|
||||||
beta: { front: '1', graphql: '1' },
|
beta: { front: '1', graphql: '1', doc: '1' },
|
||||||
canary: { front: '500m', graphql: '1' },
|
canary: { front: '500m', graphql: '1', doc: '500m' },
|
||||||
};
|
};
|
||||||
|
|
||||||
const memoryConfig = {
|
const memoryConfig = {
|
||||||
beta: { front: '2Gi', graphql: '1Gi' },
|
beta: { front: '1Gi', graphql: '1Gi', doc: '1Gi' },
|
||||||
canary: { front: '512Mi', graphql: '512Mi' },
|
canary: { front: '512Mi', graphql: '512Mi', doc: '512Mi' },
|
||||||
};
|
};
|
||||||
|
|
||||||
const createHelmCommand = ({ isDryRun }) => {
|
const createHelmCommand = ({ isDryRun }) => {
|
||||||
@@ -72,12 +72,10 @@ const createHelmCommand = ({ isDryRun }) => {
|
|||||||
`--set-string global.indexer.endpoint="${AFFINE_INDEXER_SEARCH_ENDPOINT}"`,
|
`--set-string global.indexer.endpoint="${AFFINE_INDEXER_SEARCH_ENDPOINT}"`,
|
||||||
`--set-string global.indexer.apiKey="${AFFINE_INDEXER_SEARCH_API_KEY}"`,
|
`--set-string global.indexer.apiKey="${AFFINE_INDEXER_SEARCH_API_KEY}"`,
|
||||||
];
|
];
|
||||||
const cloudSqlNodeSelector = isBeta
|
|
||||||
? `{ \\"iam.gke.io/gke-metadata-server-enabled\\": \\"true\\", \\"cloud.google.com/gke-spot\\": \\"true\\" }`
|
|
||||||
: `{ \\"iam.gke.io/gke-metadata-server-enabled\\": \\"true\\" }`;
|
|
||||||
const serviceAnnotations = [
|
const serviceAnnotations = [
|
||||||
`--set-json front.serviceAccount.annotations="{ \\"iam.gke.io/gcp-service-account\\": \\"${APP_IAM_ACCOUNT}\\" }"`,
|
`--set-json front.serviceAccount.annotations="{ \\"iam.gke.io/gcp-service-account\\": \\"${APP_IAM_ACCOUNT}\\" }"`,
|
||||||
`--set-json graphql.serviceAccount.annotations="{ \\"iam.gke.io/gcp-service-account\\": \\"${APP_IAM_ACCOUNT}\\" }"`,
|
`--set-json graphql.serviceAccount.annotations="{ \\"iam.gke.io/gcp-service-account\\": \\"${APP_IAM_ACCOUNT}\\" }"`,
|
||||||
|
`--set-json doc.serviceAccount.annotations="{ \\"iam.gke.io/gcp-service-account\\": \\"${APP_IAM_ACCOUNT}\\" }"`,
|
||||||
].concat(
|
].concat(
|
||||||
isProduction || isBeta || isInternal
|
isProduction || isBeta || isInternal
|
||||||
? [
|
? [
|
||||||
@@ -86,17 +84,10 @@ const createHelmCommand = ({ isDryRun }) => {
|
|||||||
`--set-json front.services.renderer.annotations="{ \\"cloud.google.com/neg\\": \\"{\\\\\\"ingress\\\\\\": true}\\" }"`,
|
`--set-json front.services.renderer.annotations="{ \\"cloud.google.com/neg\\": \\"{\\\\\\"ingress\\\\\\": true}\\" }"`,
|
||||||
`--set-json graphql.service.annotations="{ \\"cloud.google.com/neg\\": \\"{\\\\\\"ingress\\\\\\": true}\\" }"`,
|
`--set-json graphql.service.annotations="{ \\"cloud.google.com/neg\\": \\"{\\\\\\"ingress\\\\\\": true}\\" }"`,
|
||||||
`--set-json cloud-sql-proxy.serviceAccount.annotations="{ \\"iam.gke.io/gcp-service-account\\": \\"${CLOUD_SQL_IAM_ACCOUNT}\\" }"`,
|
`--set-json cloud-sql-proxy.serviceAccount.annotations="{ \\"iam.gke.io/gcp-service-account\\": \\"${CLOUD_SQL_IAM_ACCOUNT}\\" }"`,
|
||||||
`--set-json cloud-sql-proxy.nodeSelector="${cloudSqlNodeSelector}"`,
|
`--set-json cloud-sql-proxy.nodeSelector="{ \\"iam.gke.io/gke-metadata-server-enabled\\": \\"true\\" }"`,
|
||||||
]
|
]
|
||||||
: []
|
: []
|
||||||
);
|
);
|
||||||
const spotNodeSelector = `{ \\"cloud.google.com/gke-spot\\": \\"true\\" }`;
|
|
||||||
const spotScheduling = isSpotEnabled
|
|
||||||
? [
|
|
||||||
`--set-json front.nodeSelector="${spotNodeSelector}"`,
|
|
||||||
`--set-json graphql.nodeSelector="${spotNodeSelector}"`,
|
|
||||||
]
|
|
||||||
: [];
|
|
||||||
|
|
||||||
const cpu = cpuConfig[buildType];
|
const cpu = cpuConfig[buildType];
|
||||||
const memory = memoryConfig[buildType];
|
const memory = memoryConfig[buildType];
|
||||||
@@ -105,12 +96,14 @@ const createHelmCommand = ({ isDryRun }) => {
|
|||||||
resources = resources.concat([
|
resources = resources.concat([
|
||||||
`--set front.resources.requests.cpu="${cpu.front}"`,
|
`--set front.resources.requests.cpu="${cpu.front}"`,
|
||||||
`--set graphql.resources.requests.cpu="${cpu.graphql}"`,
|
`--set graphql.resources.requests.cpu="${cpu.graphql}"`,
|
||||||
|
`--set doc.resources.requests.cpu="${cpu.doc}"`,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
if (memory) {
|
if (memory) {
|
||||||
resources = resources.concat([
|
resources = resources.concat([
|
||||||
`--set front.resources.requests.memory="${memory.front}"`,
|
`--set front.resources.requests.memory="${memory.front}"`,
|
||||||
`--set graphql.resources.requests.memory="${memory.graphql}"`,
|
`--set graphql.resources.requests.memory="${memory.graphql}"`,
|
||||||
|
`--set doc.resources.requests.memory="${memory.doc}"`,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,8 +142,10 @@ const createHelmCommand = ({ isDryRun }) => {
|
|||||||
`--set graphql.replicaCount=${replica.graphql}`,
|
`--set graphql.replicaCount=${replica.graphql}`,
|
||||||
`--set-string graphql.image.tag="${imageTag}"`,
|
`--set-string graphql.image.tag="${imageTag}"`,
|
||||||
`--set-string graphql.app.host="${primaryHost}"`,
|
`--set-string graphql.app.host="${primaryHost}"`,
|
||||||
|
`--set-string doc.image.tag="${imageTag}"`,
|
||||||
|
`--set-string doc.app.host="${primaryHost}"`,
|
||||||
|
`--set doc.replicaCount=${replica.doc}`,
|
||||||
...serviceAnnotations,
|
...serviceAnnotations,
|
||||||
...spotScheduling,
|
|
||||||
...resources,
|
...resources,
|
||||||
`--timeout 10m`,
|
`--timeout 10m`,
|
||||||
flag,
|
flag,
|
||||||
|
|||||||
2
.github/deployment/node/Dockerfile
vendored
2
.github/deployment/node/Dockerfile
vendored
@@ -1,4 +1,4 @@
|
|||||||
# syntax=docker/dockerfile:1.7
|
# syntax=docker/dockerfile:1.21
|
||||||
|
|
||||||
FROM node:22-bookworm-slim AS assets
|
FROM node:22-bookworm-slim AS assets
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|||||||
16
.github/helm/affine/charts/doc/templates/NOTES.txt
vendored
Normal file
16
.github/helm/affine/charts/doc/templates/NOTES.txt
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
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 }}
|
||||||
63
.github/helm/affine/charts/doc/templates/_helpers.tpl
vendored
Normal file
63
.github/helm/affine/charts/doc/templates/_helpers.tpl
vendored
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
{{/*
|
||||||
|
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 }}
|
||||||
118
.github/helm/affine/charts/doc/templates/deployment.yaml
vendored
Normal file
118
.github/helm/affine/charts/doc/templates/deployment.yaml
vendored
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
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: "{{ .Values.global.deployment.type }}"
|
||||||
|
- name: DEPLOYMENT_PLATFORM
|
||||||
|
value: "{{ .Values.global.deployment.platform }}"
|
||||||
|
- 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.host }}:{{ .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_INDEXER_SEARCH_PROVIDER
|
||||||
|
value: "{{ .Values.global.indexer.provider }}"
|
||||||
|
- name: AFFINE_INDEXER_SEARCH_ENDPOINT
|
||||||
|
value: "{{ .Values.global.indexer.endpoint }}"
|
||||||
|
- name: AFFINE_INDEXER_SEARCH_API_KEY
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: indexer
|
||||||
|
key: indexer-apiKey
|
||||||
|
- name: AFFINE_SERVER_PORT
|
||||||
|
value: "{{ .Values.global.docService.port }}"
|
||||||
|
- name: AFFINE_SERVER_SUB_PATH
|
||||||
|
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 }}
|
||||||
|
timeoutSeconds: {{ .Values.probe.timeoutSeconds }}
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /info
|
||||||
|
port: http
|
||||||
|
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
|
||||||
|
timeoutSeconds: {{ .Values.probe.timeoutSeconds }}
|
||||||
|
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 +1,19 @@
|
|||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: Service
|
kind: Service
|
||||||
metadata:
|
metadata:
|
||||||
name: {{ .Values.global.docService.name }}
|
name: {{ include "doc.fullname" . }}
|
||||||
labels:
|
labels:
|
||||||
{{- include "front.labels" . | nindent 4 }}
|
{{- include "doc.labels" . | nindent 4 }}
|
||||||
{{- with .Values.services.doc.annotations }}
|
{{- with .Values.service.annotations }}
|
||||||
annotations:
|
annotations:
|
||||||
{{- toYaml . | nindent 4 }}
|
{{- toYaml . | nindent 4 }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
spec:
|
spec:
|
||||||
type: {{ .Values.services.doc.type }}
|
type: {{ .Values.service.type }}
|
||||||
ports:
|
ports:
|
||||||
- port: {{ .Values.global.docService.port }}
|
- port: {{ .Values.global.docService.port }}
|
||||||
targetPort: http
|
targetPort: http
|
||||||
protocol: TCP
|
protocol: TCP
|
||||||
name: http
|
name: http
|
||||||
selector:
|
selector:
|
||||||
{{- include "front.selectorLabels" . | nindent 4 }}
|
{{- include "doc.selectorLabels" . | nindent 4 }}
|
||||||
12
.github/helm/affine/charts/doc/templates/serviceaccount.yaml
vendored
Normal file
12
.github/helm/affine/charts/doc/templates/serviceaccount.yaml
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{{- 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 }}
|
||||||
15
.github/helm/affine/charts/doc/templates/tests/test-connection.yaml
vendored
Normal file
15
.github/helm/affine/charts/doc/templates/tests/test-connection.yaml
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
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
|
||||||
5
.github/helm/affine/charts/doc/values.yaml
vendored
5
.github/helm/affine/charts/doc/values.yaml
vendored
@@ -30,12 +30,9 @@ podSecurityContext:
|
|||||||
fsGroup: 2000
|
fsGroup: 2000
|
||||||
|
|
||||||
resources:
|
resources:
|
||||||
limits:
|
|
||||||
cpu: '1'
|
|
||||||
memory: 4Gi
|
|
||||||
requests:
|
requests:
|
||||||
cpu: '1'
|
cpu: '1'
|
||||||
memory: 2Gi
|
memory: 4Gi
|
||||||
|
|
||||||
probe:
|
probe:
|
||||||
initialDelaySeconds: 20
|
initialDelaySeconds: 20
|
||||||
|
|||||||
@@ -88,6 +88,8 @@ spec:
|
|||||||
value: "{{ .Values.app.host }}"
|
value: "{{ .Values.app.host }}"
|
||||||
- name: AFFINE_SERVER_HTTPS
|
- name: AFFINE_SERVER_HTTPS
|
||||||
value: "{{ .Values.app.https }}"
|
value: "{{ .Values.app.https }}"
|
||||||
|
- name: DOC_SERVICE_ENDPOINT
|
||||||
|
value: "http://{{ .Values.global.docService.name }}:{{ .Values.global.docService.port }}"
|
||||||
ports:
|
ports:
|
||||||
- name: http
|
- name: http
|
||||||
containerPort: {{ .Values.app.port }}
|
containerPort: {{ .Values.app.port }}
|
||||||
|
|||||||
6
.github/helm/affine/charts/front/values.yaml
vendored
6
.github/helm/affine/charts/front/values.yaml
vendored
@@ -29,9 +29,6 @@ podSecurityContext:
|
|||||||
fsGroup: 2000
|
fsGroup: 2000
|
||||||
|
|
||||||
resources:
|
resources:
|
||||||
limits:
|
|
||||||
cpu: '1'
|
|
||||||
memory: 2Gi
|
|
||||||
requests:
|
requests:
|
||||||
cpu: '1'
|
cpu: '1'
|
||||||
memory: 2Gi
|
memory: 2Gi
|
||||||
@@ -57,9 +54,6 @@ services:
|
|||||||
type: ClusterIP
|
type: ClusterIP
|
||||||
port: 8080
|
port: 8080
|
||||||
annotations: {}
|
annotations: {}
|
||||||
doc:
|
|
||||||
type: ClusterIP
|
|
||||||
annotations: {}
|
|
||||||
|
|
||||||
nodeSelector: {}
|
nodeSelector: {}
|
||||||
tolerations: []
|
tolerations: []
|
||||||
|
|||||||
@@ -27,11 +27,8 @@ podSecurityContext:
|
|||||||
fsGroup: 2000
|
fsGroup: 2000
|
||||||
|
|
||||||
resources:
|
resources:
|
||||||
limits:
|
|
||||||
cpu: '1'
|
|
||||||
memory: 4Gi
|
|
||||||
requests:
|
requests:
|
||||||
cpu: '1'
|
cpu: '2'
|
||||||
memory: 2Gi
|
memory: 2Gi
|
||||||
|
|
||||||
probe:
|
probe:
|
||||||
|
|||||||
10
.github/helm/affine/values.yaml
vendored
10
.github/helm/affine/values.yaml
vendored
@@ -47,6 +47,12 @@ graphql:
|
|||||||
annotations:
|
annotations:
|
||||||
cloud.google.com/backend-config: '{"default": "affine-api-backendconfig"}'
|
cloud.google.com/backend-config: '{"default": "affine-api-backendconfig"}'
|
||||||
|
|
||||||
|
doc:
|
||||||
|
service:
|
||||||
|
type: ClusterIP
|
||||||
|
annotations:
|
||||||
|
cloud.google.com/backend-config: '{"default": "affine-api-backendconfig"}'
|
||||||
|
|
||||||
front:
|
front:
|
||||||
services:
|
services:
|
||||||
sync:
|
sync:
|
||||||
@@ -65,7 +71,3 @@ front:
|
|||||||
name: affine-web
|
name: affine-web
|
||||||
type: ClusterIP
|
type: ClusterIP
|
||||||
port: 8080
|
port: 8080
|
||||||
doc:
|
|
||||||
type: ClusterIP
|
|
||||||
annotations:
|
|
||||||
cloud.google.com/backend-config: '{"default": "affine-api-backendconfig"}'
|
|
||||||
|
|||||||
6
.github/workflows/auto-labeler.yml
vendored
6
.github/workflows/auto-labeler.yml
vendored
@@ -1,10 +1,6 @@
|
|||||||
name: 'Pull Request Labeler'
|
name: 'Pull Request Labeler'
|
||||||
on:
|
on:
|
||||||
pull_request_target:
|
- pull_request_target
|
||||||
types:
|
|
||||||
- opened
|
|
||||||
- reopened
|
|
||||||
- synchronize
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
triage:
|
triage:
|
||||||
|
|||||||
296
.github/workflows/build-test.yml
vendored
296
.github/workflows/build-test.yml
vendored
@@ -210,13 +210,18 @@ jobs:
|
|||||||
e2e-blocksuite-cross-browser-test:
|
e2e-blocksuite-cross-browser-test:
|
||||||
name: E2E BlockSuite Cross Browser Test
|
name: E2E BlockSuite Cross Browser Test
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
shard: [1]
|
||||||
|
browser: ['chromium', 'firefox', 'webkit']
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: ./.github/actions/setup-node
|
uses: ./.github/actions/setup-node
|
||||||
with:
|
with:
|
||||||
playwright-install: true
|
playwright-install: true
|
||||||
playwright-platform: 'chromium,firefox,webkit'
|
playwright-platform: ${{ matrix.browser }}
|
||||||
electron-install: false
|
electron-install: false
|
||||||
full-cache: true
|
full-cache: true
|
||||||
|
|
||||||
@@ -224,64 +229,18 @@ jobs:
|
|||||||
run: yarn workspace @blocksuite/playground build
|
run: yarn workspace @blocksuite/playground build
|
||||||
|
|
||||||
- name: Run playwright tests
|
- name: Run playwright tests
|
||||||
run: |
|
env:
|
||||||
yarn workspace @blocksuite/integration-test test:unit
|
BROWSER: ${{ matrix.browser }}
|
||||||
yarn workspace @affine-test/blocksuite test "cross-platform/" --forbid-only
|
run: yarn workspace @affine-test/blocksuite test "cross-platform/" --forbid-only --shard=${{ matrix.shard }}/${{ strategy.job-total }}
|
||||||
|
|
||||||
- name: Upload test results
|
- name: Upload test results
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: test-results-e2e-bs-cross-browser
|
name: test-results-e2e-bs-cross-browser-${{ matrix.browser }}-${{ matrix.shard }}
|
||||||
path: ./test-results
|
path: ./test-results
|
||||||
if-no-files-found: ignore
|
if-no-files-found: ignore
|
||||||
|
|
||||||
bundler-matrix:
|
|
||||||
name: Bundler Matrix (${{ matrix.bundler }})
|
|
||||||
runs-on: ubuntu-24.04-arm
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
bundler: [webpack, rspack]
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: ./.github/actions/setup-node
|
|
||||||
with:
|
|
||||||
playwright-install: false
|
|
||||||
electron-install: false
|
|
||||||
full-cache: true
|
|
||||||
|
|
||||||
- name: Run frontend build matrix
|
|
||||||
env:
|
|
||||||
AFFINE_BUNDLER: ${{ matrix.bundler }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
packages=(
|
|
||||||
"@affine/web"
|
|
||||||
"@affine/mobile"
|
|
||||||
"@affine/ios"
|
|
||||||
"@affine/android"
|
|
||||||
"@affine/admin"
|
|
||||||
"@affine/electron-renderer"
|
|
||||||
)
|
|
||||||
summary="test-results-bundler-${AFFINE_BUNDLER}.txt"
|
|
||||||
: > "$summary"
|
|
||||||
for pkg in "${packages[@]}"; do
|
|
||||||
start=$(date +%s)
|
|
||||||
yarn affine "$pkg" build
|
|
||||||
end=$(date +%s)
|
|
||||||
echo "${pkg},$((end-start))" >> "$summary"
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: Upload bundler timing
|
|
||||||
if: always()
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: test-results-bundler-${{ matrix.bundler }}
|
|
||||||
path: ./test-results-bundler-${{ matrix.bundler }}.txt
|
|
||||||
if-no-files-found: ignore
|
|
||||||
|
|
||||||
e2e-test:
|
e2e-test:
|
||||||
name: E2E Test
|
name: E2E Test
|
||||||
runs-on: ubuntu-24.04-arm
|
runs-on: ubuntu-24.04-arm
|
||||||
@@ -348,7 +307,7 @@ jobs:
|
|||||||
name: Unit Test
|
name: Unit Test
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
- build-native-linux
|
- build-native
|
||||||
env:
|
env:
|
||||||
DISTRIBUTION: web
|
DISTRIBUTION: web
|
||||||
strategy:
|
strategy:
|
||||||
@@ -362,7 +321,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
electron-install: true
|
electron-install: true
|
||||||
playwright-install: true
|
playwright-install: true
|
||||||
playwright-platform: 'chromium,firefox,webkit'
|
|
||||||
full-cache: true
|
full-cache: true
|
||||||
|
|
||||||
- name: Download affine.linux-x64-gnu.node
|
- name: Download affine.linux-x64-gnu.node
|
||||||
@@ -383,39 +341,7 @@ jobs:
|
|||||||
name: affine
|
name: affine
|
||||||
fail_ci_if_error: false
|
fail_ci_if_error: false
|
||||||
|
|
||||||
build-native-linux:
|
build-native:
|
||||||
name: Build AFFiNE native (x86_64-unknown-linux-gnu)
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
env:
|
|
||||||
CARGO_PROFILE_RELEASE_DEBUG: '1'
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: ./.github/actions/setup-node
|
|
||||||
with:
|
|
||||||
extra-flags: workspaces focus @affine/native
|
|
||||||
electron-install: false
|
|
||||||
- name: Setup filename
|
|
||||||
id: filename
|
|
||||||
working-directory: ${{ github.workspace }}
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
export PLATFORM_ARCH_ABI=$(node -e "console.log(require('@napi-rs/cli').parseTriple('x86_64-unknown-linux-gnu').platformArchABI)")
|
|
||||||
echo "filename=affine.$PLATFORM_ARCH_ABI.node" >> "$GITHUB_OUTPUT"
|
|
||||||
- name: Build AFFiNE native
|
|
||||||
uses: ./.github/actions/build-rust
|
|
||||||
with:
|
|
||||||
target: x86_64-unknown-linux-gnu
|
|
||||||
package: '@affine/native'
|
|
||||||
- name: Upload ${{ steps.filename.outputs.filename }}
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
if: always()
|
|
||||||
with:
|
|
||||||
name: ${{ steps.filename.outputs.filename }}
|
|
||||||
path: ${{ github.workspace }}/packages/frontend/native/${{ steps.filename.outputs.filename }}
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
build-native-macos:
|
|
||||||
name: Build AFFiNE native (${{ matrix.spec.target }})
|
name: Build AFFiNE native (${{ matrix.spec.target }})
|
||||||
runs-on: ${{ matrix.spec.os }}
|
runs-on: ${{ matrix.spec.os }}
|
||||||
env:
|
env:
|
||||||
@@ -424,6 +350,7 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
spec:
|
spec:
|
||||||
|
- { os: ubuntu-latest, target: x86_64-unknown-linux-gnu }
|
||||||
- { os: macos-latest, target: x86_64-apple-darwin }
|
- { os: macos-latest, target: x86_64-apple-darwin }
|
||||||
- { os: macos-latest, target: aarch64-apple-darwin }
|
- { os: macos-latest, target: aarch64-apple-darwin }
|
||||||
|
|
||||||
@@ -456,7 +383,7 @@ jobs:
|
|||||||
|
|
||||||
# Split Windows build because it's too slow
|
# Split Windows build because it's too slow
|
||||||
# and other ci jobs required linux native
|
# and other ci jobs required linux native
|
||||||
build-native-windows:
|
build-windows-native:
|
||||||
name: Build AFFiNE native (${{ matrix.spec.target }})
|
name: Build AFFiNE native (${{ matrix.spec.target }})
|
||||||
runs-on: ${{ matrix.spec.os }}
|
runs-on: ${{ matrix.spec.os }}
|
||||||
env:
|
env:
|
||||||
@@ -556,7 +483,7 @@ jobs:
|
|||||||
name: Native Unit Test
|
name: Native Unit Test
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
- build-native-linux
|
- build-native
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
@@ -650,6 +577,8 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
- build-server-native
|
- build-server-native
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
env:
|
env:
|
||||||
NODE_ENV: test
|
NODE_ENV: test
|
||||||
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
|
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
|
||||||
@@ -890,51 +819,11 @@ jobs:
|
|||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: cargo nextest run --workspace --exclude affine_server_native --features use-as-lib --release --no-fail-fast
|
run: cargo nextest run --workspace --exclude affine_server_native --features use-as-lib --release --no-fail-fast
|
||||||
|
|
||||||
copilot-test-filter:
|
|
||||||
name: Copilot test filter
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
outputs:
|
|
||||||
run-api: ${{ steps.decision.outputs.run_api }}
|
|
||||||
run-e2e: ${{ steps.decision.outputs.run_e2e }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- uses: dorny/paths-filter@v3
|
|
||||||
id: copilot-filter
|
|
||||||
with:
|
|
||||||
filters: |
|
|
||||||
api:
|
|
||||||
- 'packages/backend/server/src/plugins/copilot/**'
|
|
||||||
- 'packages/backend/server/tests/copilot.*'
|
|
||||||
e2e:
|
|
||||||
- 'packages/backend/server/src/plugins/copilot/**'
|
|
||||||
- 'packages/backend/server/tests/copilot.*'
|
|
||||||
- 'packages/frontend/core/src/blocksuite/ai/**'
|
|
||||||
- 'packages/frontend/core/src/modules/workspace-indexer-embedding/**'
|
|
||||||
- 'tests/affine-cloud-copilot/**'
|
|
||||||
|
|
||||||
- name: Decide test scope
|
|
||||||
id: decision
|
|
||||||
run: |
|
|
||||||
if [[ "${{ steps.copilot-filter.outputs.api }}" == "true" ]]; then
|
|
||||||
echo "run_api=true" >> "$GITHUB_OUTPUT"
|
|
||||||
else
|
|
||||||
echo "run_api=false" >> "$GITHUB_OUTPUT"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "${{ steps.copilot-filter.outputs.e2e }}" == "true" ]]; then
|
|
||||||
echo "run_e2e=true" >> "$GITHUB_OUTPUT"
|
|
||||||
else
|
|
||||||
echo "run_e2e=false" >> "$GITHUB_OUTPUT"
|
|
||||||
fi
|
|
||||||
|
|
||||||
copilot-api-test:
|
copilot-api-test:
|
||||||
name: Server Copilot Api Test
|
name: Server Copilot Api Test
|
||||||
if: ${{ needs.copilot-test-filter.outputs.run-api == 'true' }}
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
- build-server-native
|
- build-server-native
|
||||||
- copilot-test-filter
|
|
||||||
env:
|
env:
|
||||||
NODE_ENV: test
|
NODE_ENV: test
|
||||||
DISTRIBUTION: web
|
DISTRIBUTION: web
|
||||||
@@ -968,29 +857,53 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Check blocksuite update
|
||||||
|
id: check-blocksuite-update
|
||||||
|
env:
|
||||||
|
BASE_REF: ${{ github.base_ref }}
|
||||||
|
run: |
|
||||||
|
if node ./scripts/detect-blocksuite-update.mjs "$BASE_REF"; then
|
||||||
|
echo "skip=false" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "skip=true" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- uses: dorny/paths-filter@v3
|
||||||
|
id: apifilter
|
||||||
|
with:
|
||||||
|
filters: |
|
||||||
|
changed:
|
||||||
|
- 'packages/backend/server/src/plugins/copilot/**'
|
||||||
|
- 'packages/backend/server/tests/copilot.*'
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
|
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.apifilter.outputs.changed == 'true' }}
|
||||||
uses: ./.github/actions/setup-node
|
uses: ./.github/actions/setup-node
|
||||||
with:
|
with:
|
||||||
electron-install: false
|
electron-install: false
|
||||||
full-cache: true
|
full-cache: true
|
||||||
|
|
||||||
- name: Download server-native.node
|
- name: Download server-native.node
|
||||||
|
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.apifilter.outputs.changed == 'true' }}
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: server-native.node
|
name: server-native.node
|
||||||
path: ./packages/backend/native
|
path: ./packages/backend/native
|
||||||
|
|
||||||
- name: Prepare Server Test Environment
|
- name: Prepare Server Test Environment
|
||||||
|
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.apifilter.outputs.changed == 'true' }}
|
||||||
env:
|
env:
|
||||||
SERVER_CONFIG: ${{ secrets.TEST_SERVER_CONFIG }}
|
SERVER_CONFIG: ${{ secrets.TEST_SERVER_CONFIG }}
|
||||||
uses: ./.github/actions/server-test-env
|
uses: ./.github/actions/server-test-env
|
||||||
|
|
||||||
- name: Run server tests
|
- 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:coverage --forbid-only
|
||||||
env:
|
env:
|
||||||
CARGO_TARGET_DIR: '${{ github.workspace }}/target'
|
CARGO_TARGET_DIR: '${{ github.workspace }}/target'
|
||||||
|
|
||||||
- name: Upload server test coverage results
|
- name: Upload server test coverage results
|
||||||
|
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.apifilter.outputs.changed == 'true' }}
|
||||||
uses: codecov/codecov-action@v5
|
uses: codecov/codecov-action@v5
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
@@ -1001,7 +914,6 @@ jobs:
|
|||||||
|
|
||||||
copilot-e2e-test:
|
copilot-e2e-test:
|
||||||
name: Frontend Copilot E2E Test
|
name: Frontend Copilot E2E Test
|
||||||
if: ${{ needs.copilot-test-filter.outputs.run-e2e == 'true' }}
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
DISTRIBUTION: web
|
DISTRIBUTION: web
|
||||||
@@ -1016,7 +928,6 @@ jobs:
|
|||||||
shardTotal: [5]
|
shardTotal: [5]
|
||||||
needs:
|
needs:
|
||||||
- build-server-native
|
- build-server-native
|
||||||
- copilot-test-filter
|
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: pgvector/pgvector:pg16
|
image: pgvector/pgvector:pg16
|
||||||
@@ -1040,7 +951,30 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Check blocksuite update
|
||||||
|
id: check-blocksuite-update
|
||||||
|
env:
|
||||||
|
BASE_REF: ${{ github.base_ref }}
|
||||||
|
run: |
|
||||||
|
if node ./scripts/detect-blocksuite-update.mjs "$BASE_REF"; then
|
||||||
|
echo "skip=false" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "skip=true" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- uses: dorny/paths-filter@v3
|
||||||
|
id: e2efilter
|
||||||
|
with:
|
||||||
|
filters: |
|
||||||
|
changed:
|
||||||
|
- 'packages/backend/server/src/plugins/copilot/**'
|
||||||
|
- 'packages/backend/server/tests/copilot.*'
|
||||||
|
- 'packages/frontend/core/src/blocksuite/ai/**'
|
||||||
|
- 'packages/frontend/core/src/modules/workspace-indexer-embedding/**'
|
||||||
|
- 'tests/affine-cloud-copilot/**'
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
|
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.e2efilter.outputs.changed == 'true' }}
|
||||||
uses: ./.github/actions/setup-node
|
uses: ./.github/actions/setup-node
|
||||||
with:
|
with:
|
||||||
playwright-install: true
|
playwright-install: true
|
||||||
@@ -1049,17 +983,20 @@ jobs:
|
|||||||
hard-link-nm: false
|
hard-link-nm: false
|
||||||
|
|
||||||
- name: Download server-native.node
|
- name: Download server-native.node
|
||||||
|
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.e2efilter.outputs.changed == 'true' }}
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: server-native.node
|
name: server-native.node
|
||||||
path: ./packages/backend/native
|
path: ./packages/backend/native
|
||||||
|
|
||||||
- name: Prepare Server Test Environment
|
- name: Prepare Server Test Environment
|
||||||
|
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.e2efilter.outputs.changed == 'true' }}
|
||||||
env:
|
env:
|
||||||
SERVER_CONFIG: ${{ secrets.TEST_SERVER_CONFIG }}
|
SERVER_CONFIG: ${{ secrets.TEST_SERVER_CONFIG }}
|
||||||
uses: ./.github/actions/server-test-env
|
uses: ./.github/actions/server-test-env
|
||||||
|
|
||||||
- name: Run Copilot E2E Test ${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
|
- name: Run Copilot E2E Test ${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
|
||||||
|
if: ${{ steps.check-blocksuite-update.outputs.skip != 'true' || steps.e2efilter.outputs.changed == 'true' }}
|
||||||
uses: ./.github/actions/copilot-test
|
uses: ./.github/actions/copilot-test
|
||||||
with:
|
with:
|
||||||
script: yarn affine @affine-test/affine-cloud-copilot e2e --forbid-only --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
|
script: yarn affine @affine-test/affine-cloud-copilot e2e --forbid-only --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
|
||||||
@@ -1069,7 +1006,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
- build-server-native
|
- build-server-native
|
||||||
- build-native-linux
|
- build-native
|
||||||
env:
|
env:
|
||||||
DISTRIBUTION: web
|
DISTRIBUTION: web
|
||||||
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
|
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
|
||||||
@@ -1162,9 +1099,7 @@ jobs:
|
|||||||
runs-on: ${{ matrix.spec.os }}
|
runs-on: ${{ matrix.spec.os }}
|
||||||
needs:
|
needs:
|
||||||
- build-electron-renderer
|
- build-electron-renderer
|
||||||
- build-native-linux
|
- build-native
|
||||||
- build-native-macos
|
|
||||||
- build-native-windows
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -1247,6 +1182,84 @@ jobs:
|
|||||||
if: ${{ matrix.spec.test && matrix.spec.os != 'ubuntu-latest' }}
|
if: ${{ matrix.spec.test && matrix.spec.os != 'ubuntu-latest' }}
|
||||||
run: yarn affine @affine-test/affine-desktop e2e
|
run: yarn affine @affine-test/affine-desktop e2e
|
||||||
|
|
||||||
|
- name: Upload test results
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: test-results-e2e-${{ matrix.spec.os }}-${{ matrix.spec.arch }}
|
||||||
|
path: ./test-results
|
||||||
|
if-no-files-found: ignore
|
||||||
|
|
||||||
|
desktop-bundle-check:
|
||||||
|
name: Desktop bundle check (${{ matrix.spec.os }}, ${{ matrix.spec.platform }}, ${{ matrix.spec.arch }}, ${{ matrix.spec.target }}, ${{ matrix.spec.test }})
|
||||||
|
runs-on: ${{ matrix.spec.os }}
|
||||||
|
needs:
|
||||||
|
- build-electron-renderer
|
||||||
|
- build-native
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
spec:
|
||||||
|
- {
|
||||||
|
os: macos-latest,
|
||||||
|
platform: macos,
|
||||||
|
arch: x64,
|
||||||
|
target: x86_64-apple-darwin,
|
||||||
|
test: false,
|
||||||
|
}
|
||||||
|
- {
|
||||||
|
os: macos-latest,
|
||||||
|
platform: macos,
|
||||||
|
arch: arm64,
|
||||||
|
target: aarch64-apple-darwin,
|
||||||
|
test: true,
|
||||||
|
}
|
||||||
|
- {
|
||||||
|
os: ubuntu-latest,
|
||||||
|
platform: linux,
|
||||||
|
arch: x64,
|
||||||
|
target: x86_64-unknown-linux-gnu,
|
||||||
|
test: true,
|
||||||
|
}
|
||||||
|
- {
|
||||||
|
os: windows-latest,
|
||||||
|
platform: windows,
|
||||||
|
arch: x64,
|
||||||
|
target: x86_64-pc-windows-msvc,
|
||||||
|
test: true,
|
||||||
|
}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: ./.github/actions/setup-node
|
||||||
|
timeout-minutes: 10
|
||||||
|
with:
|
||||||
|
extra-flags: workspaces focus @affine/electron @affine/monorepo @affine-test/affine-desktop @affine/nbstore @toeverything/infra
|
||||||
|
playwright-install: true
|
||||||
|
hard-link-nm: false
|
||||||
|
enableScripts: false
|
||||||
|
|
||||||
|
- name: Setup filename
|
||||||
|
id: filename
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
export PLATFORM_ARCH_ABI=$(node -e "console.log(require('@napi-rs/cli').parseTriple('${{ matrix.spec.target }}').platformArchABI)")
|
||||||
|
echo "filename=affine.$PLATFORM_ARCH_ABI.node" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Download ${{ steps.filename.outputs.filename }}
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ steps.filename.outputs.filename }}
|
||||||
|
path: ./packages/frontend/native
|
||||||
|
|
||||||
|
- name: Download web artifact
|
||||||
|
uses: ./.github/actions/download-web
|
||||||
|
with:
|
||||||
|
path: packages/frontend/apps/electron/resources/web-static
|
||||||
|
|
||||||
|
- name: Build Desktop Layers
|
||||||
|
run: yarn affine @affine/electron build
|
||||||
|
|
||||||
- name: Make bundle (macOS)
|
- name: Make bundle (macOS)
|
||||||
if: ${{ matrix.spec.target == 'aarch64-apple-darwin' }}
|
if: ${{ matrix.spec.target == 'aarch64-apple-darwin' }}
|
||||||
env:
|
env:
|
||||||
@@ -1286,14 +1299,6 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
yarn affine @affine/electron node ./scripts/macos-arm64-output-check.ts
|
yarn affine @affine/electron node ./scripts/macos-arm64-output-check.ts
|
||||||
|
|
||||||
- name: Upload test results
|
|
||||||
if: always()
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: test-results-e2e-${{ matrix.spec.os }}-${{ matrix.spec.arch }}
|
|
||||||
path: ./test-results
|
|
||||||
if-no-files-found: ignore
|
|
||||||
|
|
||||||
test-done:
|
test-done:
|
||||||
needs:
|
needs:
|
||||||
- analyze
|
- analyze
|
||||||
@@ -1307,9 +1312,8 @@ jobs:
|
|||||||
- e2e-blocksuite-cross-browser-test
|
- e2e-blocksuite-cross-browser-test
|
||||||
- e2e-mobile-test
|
- e2e-mobile-test
|
||||||
- unit-test
|
- unit-test
|
||||||
- build-native-linux
|
- build-native
|
||||||
- build-native-macos
|
- build-windows-native
|
||||||
- build-native-windows
|
|
||||||
- build-server-native
|
- build-server-native
|
||||||
- build-electron-renderer
|
- build-electron-renderer
|
||||||
- native-unit-test
|
- native-unit-test
|
||||||
@@ -1319,10 +1323,10 @@ jobs:
|
|||||||
- server-test
|
- server-test
|
||||||
- server-e2e-test
|
- server-e2e-test
|
||||||
- rust-test
|
- rust-test
|
||||||
- copilot-test-filter
|
|
||||||
- copilot-api-test
|
- copilot-api-test
|
||||||
- copilot-e2e-test
|
- copilot-e2e-test
|
||||||
- desktop-test
|
- desktop-test
|
||||||
|
- desktop-bundle-check
|
||||||
- cloud-e2e-test
|
- cloud-e2e-test
|
||||||
if: always()
|
if: always()
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
1
.github/workflows/pr-title-lint.yml
vendored
1
.github/workflows/pr-title-lint.yml
vendored
@@ -16,7 +16,6 @@ jobs:
|
|||||||
check-pull-request-title:
|
check-pull-request-title:
|
||||||
name: Check pull request title
|
name: Check pull request title
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ github.event.action != 'edited' || github.event.changes.title != null }}
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
|
|||||||
78
.github/workflows/release-desktop.yml
vendored
78
.github/workflows/release-desktop.yml
vendored
@@ -201,44 +201,13 @@ jobs:
|
|||||||
nmHoistingLimits: workspaces
|
nmHoistingLimits: workspaces
|
||||||
env:
|
env:
|
||||||
npm_config_arch: ${{ matrix.spec.arch }}
|
npm_config_arch: ${{ matrix.spec.arch }}
|
||||||
- name: Download packaged artifacts
|
- name: Download and overwrite packaged artifacts
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: packaged-${{ matrix.spec.platform }}-${{ matrix.spec.arch }}
|
|
||||||
path: packaged-unsigned
|
|
||||||
- name: unzip packaged artifacts
|
|
||||||
run: Expand-Archive -Path packaged-unsigned/archive.zip -DestinationPath packages/frontend/apps/electron/out
|
|
||||||
- name: Download signed packaged file diff
|
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: signed-packaged-${{ matrix.spec.platform }}-${{ matrix.spec.arch }}
|
name: signed-packaged-${{ matrix.spec.platform }}-${{ matrix.spec.arch }}
|
||||||
path: signed-packaged-diff
|
path: .
|
||||||
- name: Apply signed packaged file diff
|
- name: unzip file
|
||||||
shell: pwsh
|
run: Expand-Archive -Path signed.zip -DestinationPath packages/frontend/apps/electron/out
|
||||||
run: |
|
|
||||||
$DiffRoot = 'signed-packaged-diff/files'
|
|
||||||
$TargetRoot = 'packages/frontend/apps/electron/out'
|
|
||||||
if (!(Test-Path -LiteralPath $DiffRoot)) {
|
|
||||||
throw "Signed diff directory not found: $DiffRoot"
|
|
||||||
}
|
|
||||||
|
|
||||||
Copy-Item -Path (Join-Path $DiffRoot '*') -Destination $TargetRoot -Recurse -Force
|
|
||||||
|
|
||||||
$ManifestPath = 'signed-packaged-diff/manifest.json'
|
|
||||||
if (Test-Path -LiteralPath $ManifestPath) {
|
|
||||||
$ManifestEntries = @(Get-Content -LiteralPath $ManifestPath | ConvertFrom-Json)
|
|
||||||
foreach ($Entry in $ManifestEntries) {
|
|
||||||
$TargetPath = Join-Path $TargetRoot $Entry.path
|
|
||||||
if (!(Test-Path -LiteralPath $TargetPath -PathType Leaf)) {
|
|
||||||
throw "Applied signed file not found: $($Entry.path)"
|
|
||||||
}
|
|
||||||
|
|
||||||
$TargetHash = (Get-FileHash -Algorithm SHA256 -LiteralPath $TargetPath).Hash
|
|
||||||
if ($TargetHash -ne $Entry.sha256) {
|
|
||||||
throw "Signed file hash mismatch: $($Entry.path)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- name: Make squirrel.windows installer
|
- name: Make squirrel.windows installer
|
||||||
run: yarn affine @affine/electron make-squirrel --platform=${{ matrix.spec.platform }} --arch=${{ matrix.spec.arch }}
|
run: yarn affine @affine/electron make-squirrel --platform=${{ matrix.spec.platform }} --arch=${{ matrix.spec.arch }}
|
||||||
@@ -298,44 +267,13 @@ jobs:
|
|||||||
arch: arm64
|
arch: arm64
|
||||||
runs-on: ${{ matrix.spec.runner }}
|
runs-on: ${{ matrix.spec.runner }}
|
||||||
steps:
|
steps:
|
||||||
- name: Download installer artifacts
|
- name: Download and overwrite installer artifacts
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: installer-${{ matrix.spec.platform }}-${{ matrix.spec.arch }}
|
|
||||||
path: installer-unsigned
|
|
||||||
- name: unzip installer artifacts
|
|
||||||
run: Expand-Archive -Path installer-unsigned/archive.zip -DestinationPath packages/frontend/apps/electron/out/${{ env.BUILD_TYPE }}/make
|
|
||||||
- name: Download signed installer file diff
|
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: signed-installer-${{ matrix.spec.platform }}-${{ matrix.spec.arch }}
|
name: signed-installer-${{ matrix.spec.platform }}-${{ matrix.spec.arch }}
|
||||||
path: signed-installer-diff
|
path: .
|
||||||
- name: Apply signed installer file diff
|
- name: unzip file
|
||||||
shell: pwsh
|
run: Expand-Archive -Path signed.zip -DestinationPath packages/frontend/apps/electron/out/${{ env.BUILD_TYPE }}/make
|
||||||
run: |
|
|
||||||
$DiffRoot = 'signed-installer-diff/files'
|
|
||||||
$TargetRoot = 'packages/frontend/apps/electron/out/${{ env.BUILD_TYPE }}/make'
|
|
||||||
if (!(Test-Path -LiteralPath $DiffRoot)) {
|
|
||||||
throw "Signed diff directory not found: $DiffRoot"
|
|
||||||
}
|
|
||||||
|
|
||||||
Copy-Item -Path (Join-Path $DiffRoot '*') -Destination $TargetRoot -Recurse -Force
|
|
||||||
|
|
||||||
$ManifestPath = 'signed-installer-diff/manifest.json'
|
|
||||||
if (Test-Path -LiteralPath $ManifestPath) {
|
|
||||||
$ManifestEntries = @(Get-Content -LiteralPath $ManifestPath | ConvertFrom-Json)
|
|
||||||
foreach ($Entry in $ManifestEntries) {
|
|
||||||
$TargetPath = Join-Path $TargetRoot $Entry.path
|
|
||||||
if (!(Test-Path -LiteralPath $TargetPath -PathType Leaf)) {
|
|
||||||
throw "Applied signed file not found: $($Entry.path)"
|
|
||||||
}
|
|
||||||
|
|
||||||
$TargetHash = (Get-FileHash -Algorithm SHA256 -LiteralPath $TargetPath).Hash
|
|
||||||
if ($TargetHash -ne $Entry.sha256) {
|
|
||||||
throw "Signed file hash mismatch: $($Entry.path)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- name: Save artifacts
|
- name: Save artifacts
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
2
.github/workflows/release-mobile.yml
vendored
2
.github/workflows/release-mobile.yml
vendored
@@ -180,7 +180,7 @@ jobs:
|
|||||||
run: yarn workspace @affine/android cap sync
|
run: yarn workspace @affine/android cap sync
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: '3.13'
|
python-version: '3.14'
|
||||||
- name: Auth gcloud
|
- name: Auth gcloud
|
||||||
id: auth
|
id: auth
|
||||||
uses: google-github-actions/auth@v2
|
uses: google-github-actions/auth@v2
|
||||||
|
|||||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -148,7 +148,7 @@ jobs:
|
|||||||
name: Wait for approval
|
name: Wait for approval
|
||||||
with:
|
with:
|
||||||
secret: ${{ secrets.GITHUB_TOKEN }}
|
secret: ${{ secrets.GITHUB_TOKEN }}
|
||||||
approvers: darkskygit
|
approvers: darkskygit,pengx17,L-Sun,EYHN
|
||||||
minimum-approvals: 1
|
minimum-approvals: 1
|
||||||
fail-on-denial: true
|
fail-on-denial: true
|
||||||
issue-title: Please confirm to release docker image
|
issue-title: Please confirm to release docker image
|
||||||
|
|||||||
40
.github/workflows/windows-signer.yml
vendored
40
.github/workflows/windows-signer.yml
vendored
@@ -30,43 +30,13 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
cd ${{ env.ARCHIVE_DIR }}/out
|
cd ${{ env.ARCHIVE_DIR }}/out
|
||||||
signtool sign /tr http://timestamp.globalsign.com/tsa/r6advanced1 /td sha256 /fd sha256 /a ${{ inputs.files }}
|
signtool sign /tr http://timestamp.globalsign.com/tsa/r6advanced1 /td sha256 /fd sha256 /a ${{ inputs.files }}
|
||||||
- name: collect signed file diff
|
- name: zip file
|
||||||
shell: powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File {0}
|
shell: cmd
|
||||||
run: |
|
run: |
|
||||||
$OutDir = Join-Path '${{ env.ARCHIVE_DIR }}' 'out'
|
cd ${{ env.ARCHIVE_DIR }}
|
||||||
$DiffDir = Join-Path '${{ env.ARCHIVE_DIR }}' 'signed-diff'
|
7za a signed.zip .\out\*
|
||||||
$FilesDir = Join-Path $DiffDir 'files'
|
|
||||||
New-Item -ItemType Directory -Path $FilesDir -Force | Out-Null
|
|
||||||
|
|
||||||
$SignedFiles = [regex]::Matches('${{ inputs.files }}', '"([^"]+)"') | ForEach-Object { $_.Groups[1].Value }
|
|
||||||
if ($SignedFiles.Count -eq 0) {
|
|
||||||
throw 'No files to sign were provided.'
|
|
||||||
}
|
|
||||||
|
|
||||||
$Manifest = @()
|
|
||||||
foreach ($RelativePath in $SignedFiles) {
|
|
||||||
$SourcePath = Join-Path $OutDir $RelativePath
|
|
||||||
if (!(Test-Path -LiteralPath $SourcePath -PathType Leaf)) {
|
|
||||||
throw "Signed file not found: $RelativePath"
|
|
||||||
}
|
|
||||||
|
|
||||||
$TargetPath = Join-Path $FilesDir $RelativePath
|
|
||||||
$TargetDir = Split-Path -Parent $TargetPath
|
|
||||||
if ($TargetDir) {
|
|
||||||
New-Item -ItemType Directory -Path $TargetDir -Force | Out-Null
|
|
||||||
}
|
|
||||||
|
|
||||||
Copy-Item -LiteralPath $SourcePath -Destination $TargetPath -Force
|
|
||||||
$Manifest += [PSCustomObject]@{
|
|
||||||
path = $RelativePath
|
|
||||||
sha256 = (Get-FileHash -Algorithm SHA256 -LiteralPath $TargetPath).Hash
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$Manifest | ConvertTo-Json -Depth 4 | Out-File -FilePath (Join-Path $DiffDir 'manifest.json') -Encoding utf8
|
|
||||||
Write-Host "Collected $($SignedFiles.Count) signed files."
|
|
||||||
- name: upload
|
- name: upload
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: signed-${{ inputs.artifact-name }}
|
name: signed-${{ inputs.artifact-name }}
|
||||||
path: ${{ env.ARCHIVE_DIR }}/signed-diff
|
path: ${{ env.ARCHIVE_DIR }}/signed.zip
|
||||||
|
|||||||
2
.vscode/settings.template.json
vendored
2
.vscode/settings.template.json
vendored
@@ -17,7 +17,7 @@
|
|||||||
"explorer.fileNesting.enabled": true,
|
"explorer.fileNesting.enabled": true,
|
||||||
"explorer.fileNesting.patterns": {
|
"explorer.fileNesting.patterns": {
|
||||||
"*.js": "${capture}.js.map, ${capture}.min.js, ${capture}.d.ts, ${capture}.d.ts.map",
|
"*.js": "${capture}.js.map, ${capture}.min.js, ${capture}.d.ts, ${capture}.d.ts.map",
|
||||||
"package.json": ".browserslist*, .circleci*, .codecov, .commitlint*, .cz-config.js, .czrc, .dlint.json, .dprint.json, .editorconfig, .eslint*, eslint.*, .firebase*, .flowconfig, .github*, .gitlab*, .gitpod*, .huskyrc*, .jslint*, .lighthouserc.*, .lintstagedrc*, .markdownlint*, .mocha*, .node-version, .nodemon*, .npm*, .nvmrc, .pm2*, .pnp.*, .pnpm*, .prettier*, .releaserc*, .sentry*, .stackblitz*, .styleci*, .stylelint*, .tazerc*, .textlint*, .tool-versions, .travis*, .versionrc*, .vscode*, .watchman*, .xo-config*, .yamllint*, .yarnrc*, Procfile, api-extractor.json, apollo.config.*, appveyor*, ava.config.*, azure-pipelines*, bower.json, build.config.*, commitlint*, dangerfile*, dlint.json, dprint.json, firebase.json, grunt*, gulp*, histoire.config.*, jasmine.*, jenkins*, jest.config.*, jsconfig.*, karma*, lerna*, lighthouserc.*, lint-staged*, nest-cli.*, netlify*, nodemon*, nx.*, package-lock.json, package.nls*.json, phpcs.xml, playwright.config.*, pm2.*, pnpm*, prettier*, pullapprove*, puppeteer.config.*, pyrightconfig.json, release-tasks.sh, renovate*, rollup.config.*, stylelint*, tsconfig.*, tsdoc.*, tslint*, tsup.config.*, turbo*, typedoc*, unlighthouse*, vercel*, vetur.config.*, vitest.*, webpack*, workspace.json, xo.config.*, yarn*, babel.*, .babelrc, project.json, .oxlintrc.json, oxlint.json, nyc.config.*",
|
"package.json": ".browserslist*, .circleci*, .codecov, .commitlint*, .cz-config.js, .czrc, .dlint.json, .dprint.json, .editorconfig, .eslint*, eslint.*, .firebase*, .flowconfig, .github*, .gitlab*, .gitpod*, .huskyrc*, .jslint*, .lighthouserc.*, .lintstagedrc*, .markdownlint*, .mocha*, .node-version, .nodemon*, .npm*, .nvmrc, .pm2*, .pnp.*, .pnpm*, .prettier*, .releaserc*, .sentry*, .stackblitz*, .styleci*, .stylelint*, .tazerc*, .textlint*, .tool-versions, .travis*, .versionrc*, .vscode*, .watchman*, .xo-config*, .yamllint*, .yarnrc*, Procfile, api-extractor.json, apollo.config.*, appveyor*, ava.config.*, azure-pipelines*, bower.json, build.config.*, commitlint*, dangerfile*, dlint.json, dprint.json, firebase.json, grunt*, gulp*, histoire.config.*, jasmine.*, jenkins*, jest.config.*, jsconfig.*, karma*, lerna*, lighthouserc.*, lint-staged*, nest-cli.*, netlify*, nodemon*, nx.*, package-lock.json, package.nls*.json, phpcs.xml, playwright.config.*, pm2.*, pnpm*, prettier*, pullapprove*, puppeteer.config.*, pyrightconfig.json, release-tasks.sh, renovate*, rollup.config.*, stylelint*, tsconfig.*, tsdoc.*, tslint*, tsup.config.*, turbo*, typedoc*, unlighthouse*, vercel*, vetur.config.*, vitest.*, webpack*, workspace.json, xo.config.*, yarn*, babel.*, .babelrc, project.json, oxlint.json, nyc.config.*",
|
||||||
"Cargo.toml": "Cargo.lock, rust-toolchain*, rustfmt.toml, .taplo.toml",
|
"Cargo.toml": "Cargo.lock, rust-toolchain*, rustfmt.toml, .taplo.toml",
|
||||||
"README.md": "LICENSE*, CHANGELOG.md, CODE_OF_CONDUCT.md, CONTRIBUTING.md, SECURITY.md, README.*",
|
"README.md": "LICENSE*, CHANGELOG.md, CODE_OF_CONDUCT.md, CONTRIBUTING.md, SECURITY.md, README.*",
|
||||||
".gitignore": ".gitattributes, .dockerignore, .eslintignore, .prettierignore, .stylelintignore, .tslintignore, .yarnignore"
|
".gitignore": ".gitattributes, .dockerignore, .eslintignore, .prettierignore, .stylelintignore, .tslintignore, .yarnignore"
|
||||||
|
|||||||
1031
Cargo.lock
generated
1031
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
37
Cargo.toml
37
Cargo.toml
@@ -30,9 +30,9 @@ resolver = "3"
|
|||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
clap = { version = "4.4", features = ["derive"] }
|
clap = { version = "4.4", features = ["derive"] }
|
||||||
core-foundation = "0.10"
|
core-foundation = "0.10"
|
||||||
coreaudio-rs = "0.12"
|
coreaudio-rs = "0.14"
|
||||||
cpal = "0.15"
|
cpal = "0.17"
|
||||||
criterion = { version = "0.5", features = ["html_reports"] }
|
criterion = { version = "0.8", features = ["html_reports"] }
|
||||||
criterion2 = { version = "3", default-features = false }
|
criterion2 = { version = "3", default-features = false }
|
||||||
crossbeam-channel = "0.5"
|
crossbeam-channel = "0.5"
|
||||||
dispatch2 = "0.3"
|
dispatch2 = "0.3"
|
||||||
@@ -46,7 +46,6 @@ resolver = "3"
|
|||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
loom = { version = "0.7", features = ["checkpoint"] }
|
loom = { version = "0.7", features = ["checkpoint"] }
|
||||||
lru = "0.16"
|
|
||||||
memory-indexer = "0.3.0"
|
memory-indexer = "0.3.0"
|
||||||
mimalloc = "0.1"
|
mimalloc = "0.1"
|
||||||
mp4parse = "0.17"
|
mp4parse = "0.17"
|
||||||
@@ -69,18 +68,18 @@ resolver = "3"
|
|||||||
parking_lot = "0.12"
|
parking_lot = "0.12"
|
||||||
path-ext = "0.1.2"
|
path-ext = "0.1.2"
|
||||||
pdf-extract = { git = "https://github.com/toeverything/pdf-extract", branch = "darksky/improve-font-decoding" }
|
pdf-extract = { git = "https://github.com/toeverything/pdf-extract", branch = "darksky/improve-font-decoding" }
|
||||||
phf = { version = "0.11", features = ["macros"] }
|
phf = { version = "0.13", features = ["macros"] }
|
||||||
proptest = "1.3"
|
proptest = "1.3"
|
||||||
proptest-derive = "0.5"
|
proptest-derive = "0.8"
|
||||||
pulldown-cmark = "0.13"
|
pulldown-cmark = "0.13"
|
||||||
rand = "0.9"
|
rand = "0.10"
|
||||||
rand_chacha = "0.9"
|
rand_chacha = "0.10"
|
||||||
rand_distr = "0.5"
|
rand_distr = "0.5"
|
||||||
rayon = "1.10"
|
rayon = "1.10"
|
||||||
readability = { version = "0.3.0", default-features = false }
|
readability = { version = "0.3.0", default-features = false }
|
||||||
regex = "1.10"
|
regex = "1.10"
|
||||||
rubato = "0.16"
|
rubato = "0.16"
|
||||||
screencapturekit = "0.3"
|
screencapturekit = "0.4"
|
||||||
serde = "1"
|
serde = "1"
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
sha3 = "0.10"
|
sha3 = "0.10"
|
||||||
@@ -95,27 +94,27 @@ resolver = "3"
|
|||||||
] }
|
] }
|
||||||
strum_macros = "0.27.0"
|
strum_macros = "0.27.0"
|
||||||
symphonia = { version = "0.5", features = ["all", "opt-simd"] }
|
symphonia = { version = "0.5", features = ["all", "opt-simd"] }
|
||||||
text-splitter = "0.27"
|
text-splitter = "0.29"
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
tiktoken-rs = "0.7"
|
tiktoken-rs = "0.9"
|
||||||
tokio = "1.45"
|
tokio = "1.45"
|
||||||
tree-sitter = { version = "0.25" }
|
tree-sitter = { version = "0.26" }
|
||||||
tree-sitter-c = { version = "0.24" }
|
tree-sitter-c = { version = "0.24" }
|
||||||
tree-sitter-c-sharp = { version = "0.23" }
|
tree-sitter-c-sharp = { version = "0.23" }
|
||||||
tree-sitter-cpp = { version = "0.23" }
|
tree-sitter-cpp = { version = "0.23" }
|
||||||
tree-sitter-go = { version = "0.23" }
|
tree-sitter-go = { version = "0.25" }
|
||||||
tree-sitter-java = { version = "0.23" }
|
tree-sitter-java = { version = "0.23" }
|
||||||
tree-sitter-javascript = { version = "0.23" }
|
tree-sitter-javascript = { version = "0.25" }
|
||||||
tree-sitter-kotlin-ng = { version = "1.1" }
|
tree-sitter-kotlin-ng = { version = "1.1" }
|
||||||
tree-sitter-python = { version = "0.23" }
|
tree-sitter-python = { version = "0.25" }
|
||||||
tree-sitter-rust = { version = "0.24" }
|
tree-sitter-rust = { version = "0.24" }
|
||||||
tree-sitter-scala = { version = "0.24" }
|
tree-sitter-scala = { version = "0.24" }
|
||||||
tree-sitter-typescript = { version = "0.23" }
|
tree-sitter-typescript = { version = "0.23" }
|
||||||
uniffi = "0.29"
|
uniffi = "0.31"
|
||||||
url = { version = "2.5" }
|
url = { version = "2.5" }
|
||||||
uuid = "1.8"
|
uuid = "1.8"
|
||||||
v_htmlescape = "0.15"
|
v_htmlescape = "0.15"
|
||||||
windows = { version = "0.61", features = [
|
windows = { version = "0.62", features = [
|
||||||
"Win32_Devices_FunctionDiscovery",
|
"Win32_Devices_FunctionDiscovery",
|
||||||
"Win32_Foundation",
|
"Win32_Foundation",
|
||||||
"Win32_Media_Audio",
|
"Win32_Media_Audio",
|
||||||
@@ -127,10 +126,10 @@ resolver = "3"
|
|||||||
"Win32_System_Variant",
|
"Win32_System_Variant",
|
||||||
"Win32_UI_Shell_PropertiesSystem",
|
"Win32_UI_Shell_PropertiesSystem",
|
||||||
] }
|
] }
|
||||||
windows-core = { version = "0.61" }
|
windows-core = { version = "0.62" }
|
||||||
y-octo = { path = "./packages/common/y-octo/core" }
|
y-octo = { path = "./packages/common/y-octo/core" }
|
||||||
y-sync = { version = "0.4" }
|
y-sync = { version = "0.4" }
|
||||||
yrs = "0.23.0"
|
yrs = "0.25.0"
|
||||||
|
|
||||||
[profile.dev.package.sqlx-macros]
|
[profile.dev.package.sqlx-macros]
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
|
|||||||
@@ -2101,157 +2101,6 @@ describe('html to snapshot', () => {
|
|||||||
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
|
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('paragraph with br should split into multiple blocks', async () => {
|
|
||||||
const html = template(`<p>aaa<br>bbb<br>ccc</p>`);
|
|
||||||
|
|
||||||
const blockSnapshot: BlockSnapshot = {
|
|
||||||
type: 'block',
|
|
||||||
id: 'matchesReplaceMap[0]',
|
|
||||||
flavour: 'affine:note',
|
|
||||||
props: {
|
|
||||||
xywh: '[0,0,800,95]',
|
|
||||||
background: DefaultTheme.noteBackgrounColor,
|
|
||||||
index: 'a0',
|
|
||||||
hidden: false,
|
|
||||||
displayMode: NoteDisplayMode.DocAndEdgeless,
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
type: 'block',
|
|
||||||
id: 'matchesReplaceMap[1]',
|
|
||||||
flavour: 'affine:paragraph',
|
|
||||||
props: {
|
|
||||||
type: 'text',
|
|
||||||
text: {
|
|
||||||
'$blocksuite:internal:text$': true,
|
|
||||||
delta: [{ insert: 'aaa' }],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
children: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'block',
|
|
||||||
id: 'matchesReplaceMap[2]',
|
|
||||||
flavour: 'affine:paragraph',
|
|
||||||
props: {
|
|
||||||
type: 'text',
|
|
||||||
text: {
|
|
||||||
'$blocksuite:internal:text$': true,
|
|
||||||
delta: [{ insert: 'bbb' }],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
children: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'block',
|
|
||||||
id: 'matchesReplaceMap[3]',
|
|
||||||
flavour: 'affine:paragraph',
|
|
||||||
props: {
|
|
||||||
type: 'text',
|
|
||||||
text: {
|
|
||||||
'$blocksuite:internal:text$': true,
|
|
||||||
delta: [{ insert: 'ccc' }],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
children: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const htmlAdapter = new HtmlAdapter(createJob(), provider);
|
|
||||||
const rawBlockSnapshot = await htmlAdapter.toBlockSnapshot({
|
|
||||||
file: html,
|
|
||||||
});
|
|
||||||
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('paragraph with br should keep inline styles in each split line', async () => {
|
|
||||||
const html = template(
|
|
||||||
`<p><strong>aaa</strong><br><a href="https://www.google.com/">bbb</a><br><em>ccc</em></p>`
|
|
||||||
);
|
|
||||||
|
|
||||||
const blockSnapshot: BlockSnapshot = {
|
|
||||||
type: 'block',
|
|
||||||
id: 'matchesReplaceMap[0]',
|
|
||||||
flavour: 'affine:note',
|
|
||||||
props: {
|
|
||||||
xywh: '[0,0,800,95]',
|
|
||||||
background: DefaultTheme.noteBackgrounColor,
|
|
||||||
index: 'a0',
|
|
||||||
hidden: false,
|
|
||||||
displayMode: NoteDisplayMode.DocAndEdgeless,
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
type: 'block',
|
|
||||||
id: 'matchesReplaceMap[1]',
|
|
||||||
flavour: 'affine:paragraph',
|
|
||||||
props: {
|
|
||||||
type: 'text',
|
|
||||||
text: {
|
|
||||||
'$blocksuite:internal:text$': true,
|
|
||||||
delta: [
|
|
||||||
{
|
|
||||||
insert: 'aaa',
|
|
||||||
attributes: {
|
|
||||||
bold: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
children: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'block',
|
|
||||||
id: 'matchesReplaceMap[2]',
|
|
||||||
flavour: 'affine:paragraph',
|
|
||||||
props: {
|
|
||||||
type: 'text',
|
|
||||||
text: {
|
|
||||||
'$blocksuite:internal:text$': true,
|
|
||||||
delta: [
|
|
||||||
{
|
|
||||||
insert: 'bbb',
|
|
||||||
attributes: {
|
|
||||||
link: 'https://www.google.com/',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
children: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'block',
|
|
||||||
id: 'matchesReplaceMap[3]',
|
|
||||||
flavour: 'affine:paragraph',
|
|
||||||
props: {
|
|
||||||
type: 'text',
|
|
||||||
text: {
|
|
||||||
'$blocksuite:internal:text$': true,
|
|
||||||
delta: [
|
|
||||||
{
|
|
||||||
insert: 'ccc',
|
|
||||||
attributes: {
|
|
||||||
italic: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
children: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const htmlAdapter = new HtmlAdapter(createJob(), provider);
|
|
||||||
const rawBlockSnapshot = await htmlAdapter.toBlockSnapshot({
|
|
||||||
file: html,
|
|
||||||
});
|
|
||||||
expect(nanoidReplacement(rawBlockSnapshot)).toEqual(blockSnapshot);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('nested list', async () => {
|
test('nested list', async () => {
|
||||||
const html = template(`<ul><li>111<ul><li>222</li></ul></li></ul>`);
|
const html = template(`<ul><li>111<ul><li>222</li></ul></li></ul>`);
|
||||||
|
|
||||||
|
|||||||
@@ -26,11 +26,6 @@ import {
|
|||||||
|
|
||||||
@Peekable()
|
@Peekable()
|
||||||
export class ImageEdgelessBlockComponent extends GfxBlockComponent<ImageBlockModel> {
|
export class ImageEdgelessBlockComponent extends GfxBlockComponent<ImageBlockModel> {
|
||||||
private static readonly LOD_MIN_IMAGE_BYTES = 1024 * 1024;
|
|
||||||
private static readonly LOD_MIN_IMAGE_PIXELS = 1920 * 1080;
|
|
||||||
private static readonly LOD_MAX_ZOOM = 0.4;
|
|
||||||
private static readonly LOD_THUMBNAIL_MAX_EDGE = 256;
|
|
||||||
|
|
||||||
static override styles = css`
|
static override styles = css`
|
||||||
affine-edgeless-image {
|
affine-edgeless-image {
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -68,11 +63,6 @@ export class ImageEdgelessBlockComponent extends GfxBlockComponent<ImageBlockMod
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
affine-edgeless-image .resizable-img {
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
resourceController = new ResourceController(
|
resourceController = new ResourceController(
|
||||||
@@ -80,12 +70,6 @@ export class ImageEdgelessBlockComponent extends GfxBlockComponent<ImageBlockMod
|
|||||||
'Image'
|
'Image'
|
||||||
);
|
);
|
||||||
|
|
||||||
private _lodThumbnailUrl: string | null = null;
|
|
||||||
private _lodSourceUrl: string | null = null;
|
|
||||||
private _lodGeneratingSourceUrl: string | null = null;
|
|
||||||
private _lodGenerationToken = 0;
|
|
||||||
private _lastShouldUseLod = false;
|
|
||||||
|
|
||||||
get blobUrl() {
|
get blobUrl() {
|
||||||
return this.resourceController.blobUrl$.value;
|
return this.resourceController.blobUrl$.value;
|
||||||
}
|
}
|
||||||
@@ -112,134 +96,6 @@ export class ImageEdgelessBlockComponent extends GfxBlockComponent<ImageBlockMod
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _isLargeImage() {
|
|
||||||
const { width = 0, height = 0, size = 0 } = this.model.props;
|
|
||||||
const pixels = width * height;
|
|
||||||
return (
|
|
||||||
size >= ImageEdgelessBlockComponent.LOD_MIN_IMAGE_BYTES ||
|
|
||||||
pixels >= ImageEdgelessBlockComponent.LOD_MIN_IMAGE_PIXELS
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _shouldUseLod(blobUrl: string | null, zoom = this.gfx.viewport.zoom) {
|
|
||||||
return (
|
|
||||||
Boolean(blobUrl) &&
|
|
||||||
this._isLargeImage() &&
|
|
||||||
zoom <= ImageEdgelessBlockComponent.LOD_MAX_ZOOM
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _revokeLodThumbnail() {
|
|
||||||
if (!this._lodThumbnailUrl) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
URL.revokeObjectURL(this._lodThumbnailUrl);
|
|
||||||
this._lodThumbnailUrl = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _resetLodSource(blobUrl: string | null) {
|
|
||||||
if (this._lodSourceUrl === blobUrl) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._lodGenerationToken += 1;
|
|
||||||
this._lodGeneratingSourceUrl = null;
|
|
||||||
this._lodSourceUrl = blobUrl;
|
|
||||||
this._revokeLodThumbnail();
|
|
||||||
}
|
|
||||||
|
|
||||||
private _createImageElement(src: string) {
|
|
||||||
return new Promise<HTMLImageElement>((resolve, reject) => {
|
|
||||||
const image = new Image();
|
|
||||||
image.decoding = 'async';
|
|
||||||
image.onload = () => resolve(image);
|
|
||||||
image.onerror = () => reject(new Error('Failed to load image'));
|
|
||||||
image.src = src;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private _createThumbnailBlob(image: HTMLImageElement) {
|
|
||||||
const maxEdge = ImageEdgelessBlockComponent.LOD_THUMBNAIL_MAX_EDGE;
|
|
||||||
const longestEdge = Math.max(image.naturalWidth, image.naturalHeight);
|
|
||||||
const scale = longestEdge > maxEdge ? maxEdge / longestEdge : 1;
|
|
||||||
const targetWidth = Math.max(1, Math.round(image.naturalWidth * scale));
|
|
||||||
const targetHeight = Math.max(1, Math.round(image.naturalHeight * scale));
|
|
||||||
|
|
||||||
const canvas = document.createElement('canvas');
|
|
||||||
canvas.width = targetWidth;
|
|
||||||
canvas.height = targetHeight;
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
if (!ctx) {
|
|
||||||
return Promise.resolve<Blob | null>(null);
|
|
||||||
}
|
|
||||||
ctx.imageSmoothingEnabled = true;
|
|
||||||
ctx.imageSmoothingQuality = 'low';
|
|
||||||
ctx.drawImage(image, 0, 0, targetWidth, targetHeight);
|
|
||||||
|
|
||||||
return new Promise<Blob | null>(resolve => {
|
|
||||||
canvas.toBlob(resolve);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private _ensureLodThumbnail(blobUrl: string) {
|
|
||||||
if (
|
|
||||||
this._lodThumbnailUrl ||
|
|
||||||
this._lodGeneratingSourceUrl === blobUrl ||
|
|
||||||
!this._shouldUseLod(blobUrl)
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = ++this._lodGenerationToken;
|
|
||||||
this._lodGeneratingSourceUrl = blobUrl;
|
|
||||||
|
|
||||||
void this._createImageElement(blobUrl)
|
|
||||||
.then(image => this._createThumbnailBlob(image))
|
|
||||||
.then(blob => {
|
|
||||||
if (!blob || token !== this._lodGenerationToken || !this.isConnected) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const thumbnailUrl = URL.createObjectURL(blob);
|
|
||||||
if (token !== this._lodGenerationToken || !this.isConnected) {
|
|
||||||
URL.revokeObjectURL(thumbnailUrl);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._revokeLodThumbnail();
|
|
||||||
this._lodThumbnailUrl = thumbnailUrl;
|
|
||||||
|
|
||||||
if (this._shouldUseLod(this.blobUrl)) {
|
|
||||||
this.requestUpdate();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
if (token !== this._lodGenerationToken || !this.isConnected) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.error(err);
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
if (token === this._lodGenerationToken) {
|
|
||||||
this._lodGeneratingSourceUrl = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private _updateLodFromViewport(zoom: number) {
|
|
||||||
const shouldUseLod = this._shouldUseLod(this.blobUrl, zoom);
|
|
||||||
if (shouldUseLod === this._lastShouldUseLod) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._lastShouldUseLod = shouldUseLod;
|
|
||||||
if (shouldUseLod && this.blobUrl) {
|
|
||||||
this._ensureLodThumbnail(this.blobUrl);
|
|
||||||
}
|
|
||||||
this.requestUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
override connectedCallback() {
|
override connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
|
|
||||||
@@ -252,32 +108,14 @@ export class ImageEdgelessBlockComponent extends GfxBlockComponent<ImageBlockMod
|
|||||||
|
|
||||||
this.disposables.add(
|
this.disposables.add(
|
||||||
this.model.props.sourceId$.subscribe(() => {
|
this.model.props.sourceId$.subscribe(() => {
|
||||||
this._resetLodSource(null);
|
|
||||||
this.refreshData();
|
this.refreshData();
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
this.disposables.add(
|
|
||||||
this.gfx.viewport.viewportUpdated.subscribe(({ zoom }) => {
|
|
||||||
this._updateLodFromViewport(zoom);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
this._lastShouldUseLod = this._shouldUseLod(this.blobUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
override disconnectedCallback() {
|
|
||||||
this._lodGenerationToken += 1;
|
|
||||||
this._lodGeneratingSourceUrl = null;
|
|
||||||
this._lodSourceUrl = null;
|
|
||||||
this._revokeLodThumbnail();
|
|
||||||
super.disconnectedCallback();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override renderGfxBlock() {
|
override renderGfxBlock() {
|
||||||
const blobUrl = this.blobUrl;
|
const blobUrl = this.blobUrl;
|
||||||
const { rotate = 0, size = 0, caption = 'Image' } = this.model.props;
|
const { rotate = 0, size = 0, caption = 'Image' } = this.model.props;
|
||||||
this._resetLodSource(blobUrl);
|
|
||||||
|
|
||||||
const containerStyleMap = styleMap({
|
const containerStyleMap = styleMap({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -300,13 +138,6 @@ export class ImageEdgelessBlockComponent extends GfxBlockComponent<ImageBlockMod
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { loading, icon, description, error, needUpload } = resovledState;
|
const { loading, icon, description, error, needUpload } = resovledState;
|
||||||
const shouldUseLod = this._shouldUseLod(blobUrl);
|
|
||||||
if (shouldUseLod && blobUrl) {
|
|
||||||
this._ensureLodThumbnail(blobUrl);
|
|
||||||
}
|
|
||||||
this._lastShouldUseLod = shouldUseLod;
|
|
||||||
const imageUrl =
|
|
||||||
shouldUseLod && this._lodThumbnailUrl ? this._lodThumbnailUrl : blobUrl;
|
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="affine-image-container" style=${containerStyleMap}>
|
<div class="affine-image-container" style=${containerStyleMap}>
|
||||||
@@ -318,7 +149,7 @@ export class ImageEdgelessBlockComponent extends GfxBlockComponent<ImageBlockMod
|
|||||||
class="drag-target"
|
class="drag-target"
|
||||||
draggable="false"
|
draggable="false"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
src=${imageUrl ?? ''}
|
src=${blobUrl}
|
||||||
alt=${caption}
|
alt=${caption}
|
||||||
@error=${this._handleError}
|
@error=${this._handleError}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -37,126 +37,6 @@ const tagsInAncestor = (o: NodeProps<HtmlAST>, tagNames: Array<string>) => {
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const splitDeltaByNewline = (delta: DeltaInsert[]) => {
|
|
||||||
const lines: DeltaInsert[][] = [[]];
|
|
||||||
const pending = [...delta];
|
|
||||||
|
|
||||||
while (pending.length > 0) {
|
|
||||||
const op = pending.shift();
|
|
||||||
if (!op) continue;
|
|
||||||
|
|
||||||
const insert = op.insert;
|
|
||||||
if (typeof insert !== 'string') {
|
|
||||||
lines[lines.length - 1].push(op);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!insert.includes('\n')) {
|
|
||||||
if (insert.length === 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
lines[lines.length - 1].push(op);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const splitIndex = insert.indexOf('\n');
|
|
||||||
const linePart = insert.slice(0, splitIndex);
|
|
||||||
const remainPart = insert.slice(splitIndex + 1);
|
|
||||||
if (linePart.length > 0) {
|
|
||||||
lines[lines.length - 1].push({ ...op, insert: linePart });
|
|
||||||
}
|
|
||||||
lines.push([]);
|
|
||||||
if (remainPart) {
|
|
||||||
pending.unshift({ ...op, insert: remainPart });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return lines;
|
|
||||||
};
|
|
||||||
|
|
||||||
const hasBlockElementDescendant = (node: HtmlAST): boolean => {
|
|
||||||
if (!HastUtils.isElement(node)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return node.children.some(child => {
|
|
||||||
if (!HastUtils.isElement(child)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
(HastUtils.isTagBlock(child.tagName) && child.tagName !== 'br') ||
|
|
||||||
hasBlockElementDescendant(child)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const getParagraphDeltas = (
|
|
||||||
node: HtmlAST,
|
|
||||||
delta: DeltaInsert[]
|
|
||||||
): DeltaInsert[][] => {
|
|
||||||
if (!HastUtils.isElement(node)) return [delta];
|
|
||||||
if (hasBlockElementDescendant(node)) return [delta];
|
|
||||||
|
|
||||||
const hasBr = !!HastUtils.querySelector(node, 'br');
|
|
||||||
if (!hasBr) return [delta];
|
|
||||||
|
|
||||||
const hasNewline = delta.some(
|
|
||||||
op => typeof op.insert === 'string' && op.insert.includes('\n')
|
|
||||||
);
|
|
||||||
if (!hasNewline) return [delta];
|
|
||||||
|
|
||||||
return splitDeltaByNewline(delta);
|
|
||||||
};
|
|
||||||
|
|
||||||
const openParagraphBlocks = (
|
|
||||||
deltas: DeltaInsert[][],
|
|
||||||
type: string,
|
|
||||||
// AST walker context from html adapter transform pipeline.
|
|
||||||
walkerContext: any
|
|
||||||
) => {
|
|
||||||
for (const delta of deltas) {
|
|
||||||
walkerContext
|
|
||||||
.openNode(
|
|
||||||
{
|
|
||||||
type: 'block',
|
|
||||||
id: nanoid(),
|
|
||||||
flavour: 'affine:paragraph',
|
|
||||||
props: { type, text: { '$blocksuite:internal:text$': true, delta } },
|
|
||||||
children: [],
|
|
||||||
},
|
|
||||||
'children'
|
|
||||||
)
|
|
||||||
.closeNode();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const MULTI_PARAGRAPH_EMITTED_NODES_CONTEXT_KEY =
|
|
||||||
'affine:paragraph:multi-emitted-nodes';
|
|
||||||
|
|
||||||
const markMultiParagraphEmitted = (walkerContext: any, node: HtmlAST) => {
|
|
||||||
const emittedNodes =
|
|
||||||
(walkerContext.getGlobalContext(
|
|
||||||
MULTI_PARAGRAPH_EMITTED_NODES_CONTEXT_KEY
|
|
||||||
) as WeakSet<object> | undefined) ?? new WeakSet<object>();
|
|
||||||
emittedNodes.add(node as object);
|
|
||||||
walkerContext.setGlobalContext(
|
|
||||||
MULTI_PARAGRAPH_EMITTED_NODES_CONTEXT_KEY,
|
|
||||||
emittedNodes
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const consumeMultiParagraphEmittedMark = (
|
|
||||||
walkerContext: any,
|
|
||||||
node: HtmlAST
|
|
||||||
) => {
|
|
||||||
const emittedNodes = walkerContext.getGlobalContext(
|
|
||||||
MULTI_PARAGRAPH_EMITTED_NODES_CONTEXT_KEY
|
|
||||||
) as WeakSet<object> | undefined;
|
|
||||||
if (!emittedNodes) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return emittedNodes.delete(node as object);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const paragraphBlockHtmlAdapterMatcher: BlockHtmlAdapterMatcher = {
|
export const paragraphBlockHtmlAdapterMatcher: BlockHtmlAdapterMatcher = {
|
||||||
flavour: ParagraphBlockSchema.model.flavour,
|
flavour: ParagraphBlockSchema.model.flavour,
|
||||||
toMatch: o =>
|
toMatch: o =>
|
||||||
@@ -208,37 +88,41 @@ export const paragraphBlockHtmlAdapterMatcher: BlockHtmlAdapterMatcher = {
|
|||||||
!tagsInAncestor(o, ['p', 'li']) &&
|
!tagsInAncestor(o, ['p', 'li']) &&
|
||||||
HastUtils.isParagraphLike(o.node)
|
HastUtils.isParagraphLike(o.node)
|
||||||
) {
|
) {
|
||||||
const delta = deltaConverter.astToDelta(o.node);
|
walkerContext
|
||||||
const deltas = getParagraphDeltas(o.node, delta);
|
.openNode(
|
||||||
openParagraphBlocks(deltas, 'text', walkerContext);
|
{
|
||||||
|
type: 'block',
|
||||||
|
id: nanoid(),
|
||||||
|
flavour: 'affine:paragraph',
|
||||||
|
props: {
|
||||||
|
type: 'text',
|
||||||
|
text: {
|
||||||
|
'$blocksuite:internal:text$': true,
|
||||||
|
delta: deltaConverter.astToDelta(o.node),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
'children'
|
||||||
|
)
|
||||||
|
.closeNode();
|
||||||
walkerContext.skipAllChildren();
|
walkerContext.skipAllChildren();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'p': {
|
case 'p': {
|
||||||
const type = walkerContext.getGlobalContext('hast:blockquote')
|
|
||||||
? 'quote'
|
|
||||||
: 'text';
|
|
||||||
const delta = deltaConverter.astToDelta(o.node);
|
|
||||||
const deltas = getParagraphDeltas(o.node, delta);
|
|
||||||
|
|
||||||
if (deltas.length > 1) {
|
|
||||||
openParagraphBlocks(deltas, type, walkerContext);
|
|
||||||
markMultiParagraphEmitted(walkerContext, o.node);
|
|
||||||
walkerContext.skipAllChildren();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
walkerContext.openNode(
|
walkerContext.openNode(
|
||||||
{
|
{
|
||||||
type: 'block',
|
type: 'block',
|
||||||
id: nanoid(),
|
id: nanoid(),
|
||||||
flavour: 'affine:paragraph',
|
flavour: 'affine:paragraph',
|
||||||
props: {
|
props: {
|
||||||
type,
|
type: walkerContext.getGlobalContext('hast:blockquote')
|
||||||
|
? 'quote'
|
||||||
|
: 'text',
|
||||||
text: {
|
text: {
|
||||||
'$blocksuite:internal:text$': true,
|
'$blocksuite:internal:text$': true,
|
||||||
delta,
|
delta: deltaConverter.astToDelta(o.node),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
@@ -308,9 +192,6 @@ export const paragraphBlockHtmlAdapterMatcher: BlockHtmlAdapterMatcher = {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'p': {
|
case 'p': {
|
||||||
if (consumeMultiParagraphEmittedMark(walkerContext, o.node)) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (
|
if (
|
||||||
o.next?.type === 'element' &&
|
o.next?.type === 'element' &&
|
||||||
o.next.tagName === 'div' &&
|
o.next.tagName === 'div' &&
|
||||||
|
|||||||
@@ -86,7 +86,6 @@ export class PageClipboard extends ReadOnlyClipboard {
|
|||||||
|
|
||||||
if (this.std.store.readonly) return;
|
if (this.std.store.readonly) return;
|
||||||
this.std.store.captureSync();
|
this.std.store.captureSync();
|
||||||
let hasPasteTarget = false;
|
|
||||||
this.std.command
|
this.std.command
|
||||||
.chain()
|
.chain()
|
||||||
.try<{}>(cmd => [
|
.try<{}>(cmd => [
|
||||||
@@ -145,39 +144,18 @@ export class PageClipboard extends ReadOnlyClipboard {
|
|||||||
if (!ctx.parentBlock) {
|
if (!ctx.parentBlock) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
hasPasteTarget = true;
|
|
||||||
this.std.clipboard
|
this.std.clipboard
|
||||||
.paste(
|
.paste(
|
||||||
e,
|
e,
|
||||||
this.std.store,
|
this.std.store,
|
||||||
ctx.parentBlock.model.id,
|
ctx.parentBlock.model.id,
|
||||||
ctx.blockIndex !== undefined ? ctx.blockIndex + 1 : 1
|
ctx.blockIndex ? ctx.blockIndex + 1 : 1
|
||||||
)
|
)
|
||||||
.catch(console.error);
|
.catch(console.error);
|
||||||
|
|
||||||
return next();
|
return next();
|
||||||
})
|
})
|
||||||
.run();
|
.run();
|
||||||
|
|
||||||
if (hasPasteTarget) return;
|
|
||||||
|
|
||||||
// If no valid selection target exists (for example, stale block selection
|
|
||||||
// right after cut), create/focus the default paragraph and paste after it.
|
|
||||||
const firstParagraphId = document
|
|
||||||
.querySelector('affine-page-root')
|
|
||||||
?.focusFirstParagraph?.()?.id;
|
|
||||||
const parentModel = firstParagraphId
|
|
||||||
? this.std.store.getParent(firstParagraphId)
|
|
||||||
: null;
|
|
||||||
const paragraphIndex =
|
|
||||||
firstParagraphId && parentModel
|
|
||||||
? parentModel.children.findIndex(child => child.id === firstParagraphId)
|
|
||||||
: -1;
|
|
||||||
const insertIndex = paragraphIndex >= 0 ? paragraphIndex + 1 : undefined;
|
|
||||||
|
|
||||||
this.std.clipboard
|
|
||||||
.paste(e, this.std.store, parentModel?.id, insertIndex)
|
|
||||||
.catch(console.error);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
override mounted() {
|
override mounted() {
|
||||||
|
|||||||
@@ -33,11 +33,7 @@ import {
|
|||||||
ReleaseFromGroupIcon,
|
ReleaseFromGroupIcon,
|
||||||
UnlockIcon,
|
UnlockIcon,
|
||||||
} from '@blocksuite/icons/lit';
|
} from '@blocksuite/icons/lit';
|
||||||
import {
|
import type { GfxModel } from '@blocksuite/std/gfx';
|
||||||
batchAddChildren,
|
|
||||||
batchRemoveChildren,
|
|
||||||
type GfxModel,
|
|
||||||
} from '@blocksuite/std/gfx';
|
|
||||||
import { html } from 'lit';
|
import { html } from 'lit';
|
||||||
|
|
||||||
import { renderAlignmentMenu } from './alignment';
|
import { renderAlignmentMenu } from './alignment';
|
||||||
@@ -65,13 +61,14 @@ export const builtinMiscToolbarConfig = {
|
|||||||
|
|
||||||
const group = firstModel.group;
|
const group = firstModel.group;
|
||||||
|
|
||||||
batchRemoveChildren(group, [firstModel]);
|
// oxlint-disable-next-line unicorn/prefer-dom-node-remove
|
||||||
|
group.removeChild(firstModel);
|
||||||
|
|
||||||
firstModel.index = ctx.gfx.layer.generateIndex();
|
firstModel.index = ctx.gfx.layer.generateIndex();
|
||||||
|
|
||||||
const parent = group.group;
|
const parent = group.group;
|
||||||
if (parent && parent instanceof GroupElementModel) {
|
if (parent && parent instanceof GroupElementModel) {
|
||||||
batchAddChildren(parent, [firstModel]);
|
parent.addChild(firstModel);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -258,12 +255,9 @@ export const builtinMiscToolbarConfig = {
|
|||||||
|
|
||||||
// release other elements from their groups and group with top element
|
// release other elements from their groups and group with top element
|
||||||
otherElements.forEach(element => {
|
otherElements.forEach(element => {
|
||||||
if (element.group) {
|
// oxlint-disable-next-line unicorn/prefer-dom-node-remove
|
||||||
batchRemoveChildren(element.group, [element]);
|
element.group?.removeChild(element);
|
||||||
}
|
topElement.group?.addChild(element);
|
||||||
if (topElement.group) {
|
|
||||||
batchAddChildren(topElement.group, [element]);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (otherElements.length === 0) {
|
if (otherElements.length === 0) {
|
||||||
|
|||||||
@@ -40,146 +40,10 @@ export const SurfaceBlockSchemaExtension =
|
|||||||
|
|
||||||
export class SurfaceBlockModel extends BaseSurfaceModel {
|
export class SurfaceBlockModel extends BaseSurfaceModel {
|
||||||
private readonly _disposables: DisposableGroup = new DisposableGroup();
|
private readonly _disposables: DisposableGroup = new DisposableGroup();
|
||||||
private readonly _connectorIdsByEndpoint = new Map<string, Set<string>>();
|
|
||||||
private readonly _connectorIndexDisposables = new DisposableGroup();
|
|
||||||
private readonly _connectorEndpoints = new Map<
|
|
||||||
string,
|
|
||||||
{ sourceId: string | null; targetId: string | null }
|
|
||||||
>();
|
|
||||||
|
|
||||||
private _addConnectorEndpoint(endpointId: string, connectorId: string) {
|
|
||||||
const connectorIds = this._connectorIdsByEndpoint.get(endpointId);
|
|
||||||
|
|
||||||
if (connectorIds) {
|
|
||||||
connectorIds.add(connectorId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._connectorIdsByEndpoint.set(endpointId, new Set([connectorId]));
|
|
||||||
}
|
|
||||||
|
|
||||||
private _isConnectorModel(model: unknown): model is ConnectorElementModel {
|
|
||||||
return (
|
|
||||||
!!model &&
|
|
||||||
typeof model === 'object' &&
|
|
||||||
'type' in model &&
|
|
||||||
(model as { type?: string }).type === 'connector'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _removeConnectorEndpoint(endpointId: string, connectorId: string) {
|
|
||||||
const connectorIds = this._connectorIdsByEndpoint.get(endpointId);
|
|
||||||
|
|
||||||
if (!connectorIds) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
connectorIds.delete(connectorId);
|
|
||||||
|
|
||||||
if (connectorIds.size === 0) {
|
|
||||||
this._connectorIdsByEndpoint.delete(endpointId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _removeConnectorFromIndex(connectorId: string) {
|
|
||||||
const endpoints = this._connectorEndpoints.get(connectorId);
|
|
||||||
|
|
||||||
if (!endpoints) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (endpoints.sourceId) {
|
|
||||||
this._removeConnectorEndpoint(endpoints.sourceId, connectorId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (endpoints.targetId) {
|
|
||||||
this._removeConnectorEndpoint(endpoints.targetId, connectorId);
|
|
||||||
}
|
|
||||||
|
|
||||||
this._connectorEndpoints.delete(connectorId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _rebuildConnectorIndex() {
|
|
||||||
this._connectorIdsByEndpoint.clear();
|
|
||||||
this._connectorEndpoints.clear();
|
|
||||||
|
|
||||||
this.getElementsByType('connector').forEach(connector => {
|
|
||||||
this._setConnectorEndpoints(connector as ConnectorElementModel);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private _setConnectorEndpoints(connector: ConnectorElementModel) {
|
|
||||||
const sourceId = connector.source?.id ?? null;
|
|
||||||
const targetId = connector.target?.id ?? null;
|
|
||||||
const previousEndpoints = this._connectorEndpoints.get(connector.id);
|
|
||||||
|
|
||||||
if (
|
|
||||||
previousEndpoints?.sourceId === sourceId &&
|
|
||||||
previousEndpoints?.targetId === targetId
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (previousEndpoints?.sourceId) {
|
|
||||||
this._removeConnectorEndpoint(previousEndpoints.sourceId, connector.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (previousEndpoints?.targetId) {
|
|
||||||
this._removeConnectorEndpoint(previousEndpoints.targetId, connector.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sourceId) {
|
|
||||||
this._addConnectorEndpoint(sourceId, connector.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetId) {
|
|
||||||
this._addConnectorEndpoint(targetId, connector.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
this._connectorEndpoints.set(connector.id, {
|
|
||||||
sourceId,
|
|
||||||
targetId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
override _init() {
|
override _init() {
|
||||||
this._extendElement(elementsCtorMap);
|
this._extendElement(elementsCtorMap);
|
||||||
super._init();
|
super._init();
|
||||||
this._rebuildConnectorIndex();
|
|
||||||
this._connectorIndexDisposables.add(
|
|
||||||
this.elementAdded.subscribe(({ id }) => {
|
|
||||||
const model = this.getElementById(id);
|
|
||||||
|
|
||||||
if (this._isConnectorModel(model)) {
|
|
||||||
this._setConnectorEndpoints(model);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
this._connectorIndexDisposables.add(
|
|
||||||
this.elementUpdated.subscribe(({ id, props }) => {
|
|
||||||
if (!props['source'] && !props['target']) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const model = this.getElementById(id);
|
|
||||||
|
|
||||||
if (this._isConnectorModel(model)) {
|
|
||||||
this._setConnectorEndpoints(model);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
this._connectorIndexDisposables.add(
|
|
||||||
this.elementRemoved.subscribe(({ id, type }) => {
|
|
||||||
if (type === 'connector') {
|
|
||||||
this._removeConnectorFromIndex(id);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
this.deleted.subscribe(() => {
|
|
||||||
this._connectorIndexDisposables.dispose();
|
|
||||||
this._connectorIdsByEndpoint.clear();
|
|
||||||
this._connectorEndpoints.clear();
|
|
||||||
});
|
|
||||||
this.store.provider
|
this.store.provider
|
||||||
.getAll(surfaceMiddlewareIdentifier)
|
.getAll(surfaceMiddlewareIdentifier)
|
||||||
.forEach(({ middleware }) => {
|
.forEach(({ middleware }) => {
|
||||||
@@ -188,31 +52,13 @@ export class SurfaceBlockModel extends BaseSurfaceModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getConnectors(id: string) {
|
getConnectors(id: string) {
|
||||||
const connectorIds = this._connectorIdsByEndpoint.get(id);
|
const connectors = this.getElementsByType(
|
||||||
|
'connector'
|
||||||
|
) as unknown[] as ConnectorElementModel[];
|
||||||
|
|
||||||
if (!connectorIds?.size) {
|
return connectors.filter(
|
||||||
return [];
|
connector => connector.source?.id === id || connector.target?.id === id
|
||||||
}
|
);
|
||||||
|
|
||||||
const staleConnectorIds: string[] = [];
|
|
||||||
const connectors: ConnectorElementModel[] = [];
|
|
||||||
|
|
||||||
connectorIds.forEach(connectorId => {
|
|
||||||
const model = this.getElementById(connectorId);
|
|
||||||
|
|
||||||
if (!this._isConnectorModel(model)) {
|
|
||||||
staleConnectorIds.push(connectorId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
connectors.push(model);
|
|
||||||
});
|
|
||||||
|
|
||||||
staleConnectorIds.forEach(connectorId => {
|
|
||||||
this._removeConnectorFromIndex(connectorId);
|
|
||||||
});
|
|
||||||
|
|
||||||
return connectors;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override getElementsByType<K extends keyof SurfaceElementModelMap>(
|
override getElementsByType<K extends keyof SurfaceElementModelMap>(
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
"@blocksuite/sync": "workspace:*",
|
"@blocksuite/sync": "workspace:*",
|
||||||
"@floating-ui/dom": "^1.6.13",
|
"@floating-ui/dom": "^1.6.13",
|
||||||
"@lit/context": "^1.1.2",
|
"@lit/context": "^1.1.2",
|
||||||
"@lottiefiles/dotlottie-wc": "^0.5.0",
|
"@lottiefiles/dotlottie-wc": "^0.8.0",
|
||||||
"@preact/signals-core": "^1.8.0",
|
"@preact/signals-core": "^1.8.0",
|
||||||
"@toeverything/theme": "^1.1.23",
|
"@toeverything/theme": "^1.1.23",
|
||||||
"@types/hast": "^3.0.4",
|
"@types/hast": "^3.0.4",
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export const autoScrollOnBoundary = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
const cancelBoxListen = effect(() => {
|
const cancelBoxListen = effect(() => {
|
||||||
void box.value;
|
box.value;
|
||||||
startUpdate();
|
startUpdate();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -24,12 +24,12 @@ import {
|
|||||||
DataViewUIBase,
|
DataViewUIBase,
|
||||||
DataViewUILogicBase,
|
DataViewUILogicBase,
|
||||||
} from '../../../core/view/data-view-base.js';
|
} from '../../../core/view/data-view-base.js';
|
||||||
import { LEFT_TOOL_BAR_WIDTH } from '../consts.js';
|
|
||||||
import {
|
import {
|
||||||
|
type TableSingleView,
|
||||||
TableViewRowSelection,
|
TableViewRowSelection,
|
||||||
type TableViewSelectionWithType,
|
type TableViewSelectionWithType,
|
||||||
} from '../selection.js';
|
} from '../../index.js';
|
||||||
import type { TableSingleView } from '../table-view-manager.js';
|
import { LEFT_TOOL_BAR_WIDTH } from '../consts.js';
|
||||||
import { TableClipboardController } from './controller/clipboard.js';
|
import { TableClipboardController } from './controller/clipboard.js';
|
||||||
import { TableDragController } from './controller/drag.js';
|
import { TableDragController } from './controller/drag.js';
|
||||||
import { TableHotkeysController } from './controller/hotkeys.js';
|
import { TableHotkeysController } from './controller/hotkeys.js';
|
||||||
|
|||||||
@@ -60,9 +60,10 @@ export class BaseExtensionProvider<
|
|||||||
* @param context - The context object containing scope and registration function
|
* @param context - The context object containing scope and registration function
|
||||||
* @param option - Optional configuration options for the provider
|
* @param option - Optional configuration options for the provider
|
||||||
*/
|
*/
|
||||||
setup(_context: Context<Scope>, option?: Options) {
|
setup(context: Context<Scope>, option?: Options) {
|
||||||
if (option) {
|
if (option) {
|
||||||
this.schema.parse(option);
|
this.schema.parse(option);
|
||||||
}
|
}
|
||||||
|
context;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -884,7 +884,7 @@ export class ConnectionOverlay extends Overlay {
|
|||||||
private _setupThemeListener(): void {
|
private _setupThemeListener(): void {
|
||||||
const themeService = this.gfx.std.get(ThemeProvider);
|
const themeService = this.gfx.std.get(ThemeProvider);
|
||||||
this._themeDisposer = effect(() => {
|
this._themeDisposer = effect(() => {
|
||||||
void themeService.theme$.value;
|
themeService.theme$;
|
||||||
this._emphasisColor = this._getEmphasisColor();
|
this._emphasisColor = this._getEmphasisColor();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,8 +84,6 @@ export const connectorWatcher: SurfaceMiddleware = (
|
|||||||
);
|
);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
pendingFlag = false;
|
|
||||||
pendingList.clear();
|
|
||||||
disposables.forEach(d => d.unsubscribe());
|
disposables.forEach(d => d.unsubscribe());
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -26,7 +26,6 @@
|
|||||||
"@preact/signals-core": "^1.8.0",
|
"@preact/signals-core": "^1.8.0",
|
||||||
"@toeverything/theme": "^1.1.23",
|
"@toeverything/theme": "^1.1.23",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"fractional-indexing": "^3.2.0",
|
|
||||||
"lit": "^3.2.0",
|
"lit": "^3.2.0",
|
||||||
"lodash-es": "^4.17.23",
|
"lodash-es": "^4.17.23",
|
||||||
"minimatch": "^10.1.1",
|
"minimatch": "^10.1.1",
|
||||||
@@ -34,9 +33,6 @@
|
|||||||
"yjs": "^13.6.27",
|
"yjs": "^13.6.27",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
|
||||||
"vitest": "^3.2.4"
|
|
||||||
},
|
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/index.ts",
|
".": "./src/index.ts",
|
||||||
"./view": "./src/view.ts",
|
"./view": "./src/view.ts",
|
||||||
|
|||||||
@@ -1,152 +0,0 @@
|
|||||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
|
||||||
|
|
||||||
vi.mock('fractional-indexing', () => ({
|
|
||||||
generateKeyBetween: vi.fn(),
|
|
||||||
generateNKeysBetween: vi.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
import { generateKeyBetween, generateNKeysBetween } from 'fractional-indexing';
|
|
||||||
|
|
||||||
import { ungroupCommand } from '../command/group-api.js';
|
|
||||||
|
|
||||||
type TestElement = {
|
|
||||||
id: string;
|
|
||||||
index: string;
|
|
||||||
group: TestElement | null;
|
|
||||||
childElements: TestElement[];
|
|
||||||
removeChildren?: (elements: TestElement[]) => void;
|
|
||||||
addChildren?: (elements: TestElement[]) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockedGenerateNKeysBetween = vi.mocked(generateNKeysBetween);
|
|
||||||
const mockedGenerateKeyBetween = vi.mocked(generateKeyBetween);
|
|
||||||
|
|
||||||
const createElement = (
|
|
||||||
id: string,
|
|
||||||
index: string,
|
|
||||||
group: TestElement | null
|
|
||||||
): TestElement => ({
|
|
||||||
id,
|
|
||||||
index,
|
|
||||||
group,
|
|
||||||
childElements: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
const createUngroupFixture = () => {
|
|
||||||
const parent = createElement('parent', 'p0', null);
|
|
||||||
const left = createElement('left', 'a0', parent);
|
|
||||||
const right = createElement('right', 'a0', parent);
|
|
||||||
const group = createElement('group', 'm0', parent);
|
|
||||||
const childA = createElement('child-a', 'c0', group);
|
|
||||||
const childB = createElement('child-b', 'c1', group);
|
|
||||||
|
|
||||||
group.childElements = [childB, childA];
|
|
||||||
parent.childElements = [left, group, right];
|
|
||||||
|
|
||||||
parent.removeChildren = vi.fn();
|
|
||||||
parent.addChildren = vi.fn();
|
|
||||||
group.removeChildren = vi.fn();
|
|
||||||
|
|
||||||
const elementOrder = new Map<TestElement, number>([
|
|
||||||
[left, 0],
|
|
||||||
[group, 1],
|
|
||||||
[right, 2],
|
|
||||||
[childA, 3],
|
|
||||||
[childB, 4],
|
|
||||||
]);
|
|
||||||
|
|
||||||
const selectionSet = vi.fn();
|
|
||||||
const gfx = {
|
|
||||||
layer: {
|
|
||||||
compare: (a: TestElement, b: TestElement) =>
|
|
||||||
(elementOrder.get(a) ?? 0) - (elementOrder.get(b) ?? 0),
|
|
||||||
},
|
|
||||||
selection: {
|
|
||||||
set: selectionSet,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const std = {
|
|
||||||
get: vi.fn(() => gfx),
|
|
||||||
store: {
|
|
||||||
transact: (callback: () => void) => callback(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
childA,
|
|
||||||
childB,
|
|
||||||
group,
|
|
||||||
parent,
|
|
||||||
selectionSet,
|
|
||||||
std,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('ungroupCommand', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
mockedGenerateNKeysBetween.mockReset();
|
|
||||||
mockedGenerateKeyBetween.mockReset();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('falls back to open-ended key generation when sibling interval is invalid', () => {
|
|
||||||
const fixture = createUngroupFixture();
|
|
||||||
mockedGenerateNKeysBetween
|
|
||||||
.mockImplementationOnce(() => {
|
|
||||||
throw new Error('interval reversed');
|
|
||||||
})
|
|
||||||
.mockReturnValueOnce(['n0', 'n1']);
|
|
||||||
|
|
||||||
const next = vi.fn();
|
|
||||||
ungroupCommand(
|
|
||||||
{
|
|
||||||
std: fixture.std,
|
|
||||||
group: fixture.group as any,
|
|
||||||
} as any,
|
|
||||||
next
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(mockedGenerateNKeysBetween).toHaveBeenNthCalledWith(
|
|
||||||
1,
|
|
||||||
'a0',
|
|
||||||
'a0',
|
|
||||||
2
|
|
||||||
);
|
|
||||||
expect(mockedGenerateNKeysBetween).toHaveBeenNthCalledWith(
|
|
||||||
2,
|
|
||||||
'a0',
|
|
||||||
null,
|
|
||||||
2
|
|
||||||
);
|
|
||||||
expect(fixture.childA.index).toBe('n0');
|
|
||||||
expect(fixture.childB.index).toBe('n1');
|
|
||||||
expect(fixture.selectionSet).toHaveBeenCalledWith({
|
|
||||||
editing: false,
|
|
||||||
elements: ['child-a', 'child-b'],
|
|
||||||
});
|
|
||||||
expect(next).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('falls back to key-by-key generation when all batched strategies fail', () => {
|
|
||||||
const fixture = createUngroupFixture();
|
|
||||||
mockedGenerateNKeysBetween.mockImplementation(() => {
|
|
||||||
throw new Error('invalid range');
|
|
||||||
});
|
|
||||||
|
|
||||||
let seq = 0;
|
|
||||||
mockedGenerateKeyBetween.mockImplementation(() => `k${seq++}`);
|
|
||||||
|
|
||||||
ungroupCommand(
|
|
||||||
{
|
|
||||||
std: fixture.std,
|
|
||||||
group: fixture.group as any,
|
|
||||||
} as any,
|
|
||||||
vi.fn()
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(mockedGenerateNKeysBetween).toHaveBeenCalledTimes(4);
|
|
||||||
expect(mockedGenerateKeyBetween).toHaveBeenCalledTimes(2);
|
|
||||||
expect(fixture.childA.index).toBe('k0');
|
|
||||||
expect(fixture.childB.index).toBe('k1');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -4,80 +4,7 @@ import {
|
|||||||
MindmapElementModel,
|
MindmapElementModel,
|
||||||
} from '@blocksuite/affine-model';
|
} from '@blocksuite/affine-model';
|
||||||
import type { Command } from '@blocksuite/std';
|
import type { Command } from '@blocksuite/std';
|
||||||
import {
|
import { GfxControllerIdentifier, type GfxModel } from '@blocksuite/std/gfx';
|
||||||
batchAddChildren,
|
|
||||||
batchRemoveChildren,
|
|
||||||
type GfxController,
|
|
||||||
GfxControllerIdentifier,
|
|
||||||
type GfxModel,
|
|
||||||
measureOperation,
|
|
||||||
} from '@blocksuite/std/gfx';
|
|
||||||
import { generateKeyBetween, generateNKeysBetween } from 'fractional-indexing';
|
|
||||||
|
|
||||||
const getTopLevelOrderedElements = (gfx: GfxController) => {
|
|
||||||
const topLevelElements = gfx.layer.layers.reduce<GfxModel[]>(
|
|
||||||
(elements, layer) => {
|
|
||||||
layer.elements.forEach(element => {
|
|
||||||
if (element.group === null) {
|
|
||||||
elements.push(element as GfxModel);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return elements;
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
topLevelElements.sort((a, b) => gfx.layer.compare(a, b));
|
|
||||||
return topLevelElements;
|
|
||||||
};
|
|
||||||
|
|
||||||
const buildUngroupIndexes = (
|
|
||||||
orderedElements: GfxModel[],
|
|
||||||
afterIndex: string | null,
|
|
||||||
beforeIndex: string | null,
|
|
||||||
fallbackAnchorIndex: string
|
|
||||||
) => {
|
|
||||||
if (orderedElements.length === 0) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const count = orderedElements.length;
|
|
||||||
const tryGenerateN = (left: string | null, right: string | null) => {
|
|
||||||
try {
|
|
||||||
const generated = generateNKeysBetween(left, right, count);
|
|
||||||
return generated.length === count ? generated : null;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const tryGenerateOneByOne = (left: string | null, right: string | null) => {
|
|
||||||
try {
|
|
||||||
let cursor = left;
|
|
||||||
return orderedElements.map(() => {
|
|
||||||
cursor = generateKeyBetween(cursor, right);
|
|
||||||
return cursor;
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Preferred: keep ungrouped children in the original group slot.
|
|
||||||
return (
|
|
||||||
tryGenerateN(afterIndex, beforeIndex) ??
|
|
||||||
// Fallback: ignore the upper bound when legacy/broken data has reversed interval.
|
|
||||||
tryGenerateN(afterIndex, null) ??
|
|
||||||
// Fallback: use group index as anchor when sibling interval is unavailable.
|
|
||||||
tryGenerateN(fallbackAnchorIndex, null) ??
|
|
||||||
// Last resort: always valid.
|
|
||||||
tryGenerateN(null, null) ??
|
|
||||||
// Defensive fallback for unexpected library behavior.
|
|
||||||
tryGenerateOneByOne(null, null) ??
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createGroupCommand: Command<
|
export const createGroupCommand: Command<
|
||||||
{ elements: GfxModel[] | string[] },
|
{ elements: GfxModel[] | string[] },
|
||||||
@@ -112,118 +39,96 @@ export const createGroupFromSelectedCommand: Command<
|
|||||||
{},
|
{},
|
||||||
{ groupId: string }
|
{ groupId: string }
|
||||||
> = (ctx, next) => {
|
> = (ctx, next) => {
|
||||||
measureOperation('edgeless:create-group-from-selected', () => {
|
const { std } = ctx;
|
||||||
const { std } = ctx;
|
const gfx = std.get(GfxControllerIdentifier);
|
||||||
const gfx = std.get(GfxControllerIdentifier);
|
const { selection, surface } = gfx;
|
||||||
const { selection, surface } = gfx;
|
|
||||||
|
|
||||||
if (!surface) {
|
if (!surface) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
selection.selectedElements.length === 0 ||
|
selection.selectedElements.length === 0 ||
|
||||||
!selection.selectedElements.every(
|
!selection.selectedElements.every(
|
||||||
element =>
|
element =>
|
||||||
element.group === selection.firstElement.group &&
|
element.group === selection.firstElement.group &&
|
||||||
!(element.group instanceof MindmapElementModel)
|
!(element.group instanceof MindmapElementModel)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const parent = selection.firstElement.group;
|
const parent = selection.firstElement.group as GroupElementModel;
|
||||||
let groupId: string | undefined;
|
|
||||||
std.store.transact(() => {
|
|
||||||
const [_, result] = std.command.exec(createGroupCommand, {
|
|
||||||
elements: selection.selectedElements,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!result.groupId) {
|
if (parent !== null) {
|
||||||
return;
|
selection.selectedElements.forEach(element => {
|
||||||
}
|
// oxlint-disable-next-line unicorn/prefer-dom-node-remove
|
||||||
|
parent.removeChild(element);
|
||||||
groupId = result.groupId;
|
|
||||||
const group = surface.getElementById(groupId);
|
|
||||||
|
|
||||||
if (parent !== null && group) {
|
|
||||||
batchRemoveChildren(parent, selection.selectedElements);
|
|
||||||
batchAddChildren(parent, [group]);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!groupId) {
|
const [_, result] = std.command.exec(createGroupCommand, {
|
||||||
return;
|
elements: selection.selectedElements,
|
||||||
}
|
|
||||||
|
|
||||||
selection.set({
|
|
||||||
editing: false,
|
|
||||||
elements: [groupId],
|
|
||||||
});
|
|
||||||
|
|
||||||
next({ groupId });
|
|
||||||
});
|
});
|
||||||
|
if (!result.groupId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const group = surface.getElementById(result.groupId);
|
||||||
|
|
||||||
|
if (parent !== null && group) {
|
||||||
|
parent.addChild(group);
|
||||||
|
}
|
||||||
|
|
||||||
|
selection.set({
|
||||||
|
editing: false,
|
||||||
|
elements: [result.groupId],
|
||||||
|
});
|
||||||
|
|
||||||
|
next({ groupId: result.groupId });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ungroupCommand: Command<{ group: GroupElementModel }, {}> = (
|
export const ungroupCommand: Command<{ group: GroupElementModel }, {}> = (
|
||||||
ctx,
|
ctx,
|
||||||
next
|
next
|
||||||
) => {
|
) => {
|
||||||
measureOperation('edgeless:ungroup', () => {
|
const { std, group } = ctx;
|
||||||
const { std, group } = ctx;
|
const gfx = std.get(GfxControllerIdentifier);
|
||||||
const gfx = std.get(GfxControllerIdentifier);
|
const { selection } = gfx;
|
||||||
const { selection } = gfx;
|
const parent = group.group as GroupElementModel;
|
||||||
const parent = group.group;
|
const elements = group.childElements;
|
||||||
const elements = [...group.childElements];
|
|
||||||
|
|
||||||
if (group instanceof MindmapElementModel) {
|
if (group instanceof MindmapElementModel) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const orderedElements = [...elements].sort((a, b) =>
|
if (parent !== null) {
|
||||||
gfx.layer.compare(a, b)
|
// oxlint-disable-next-line unicorn/prefer-dom-node-remove
|
||||||
);
|
parent.removeChild(group);
|
||||||
const siblings = parent
|
}
|
||||||
? [...parent.childElements].sort((a, b) => gfx.layer.compare(a, b))
|
|
||||||
: getTopLevelOrderedElements(gfx);
|
|
||||||
const groupPosition = siblings.indexOf(group);
|
|
||||||
const beforeSiblingIndex =
|
|
||||||
groupPosition > 0 ? (siblings[groupPosition - 1]?.index ?? null) : null;
|
|
||||||
const afterSiblingIndex =
|
|
||||||
groupPosition === -1
|
|
||||||
? null
|
|
||||||
: (siblings[groupPosition + 1]?.index ?? null);
|
|
||||||
const nextIndexes = buildUngroupIndexes(
|
|
||||||
orderedElements,
|
|
||||||
beforeSiblingIndex,
|
|
||||||
afterSiblingIndex,
|
|
||||||
group.index
|
|
||||||
);
|
|
||||||
|
|
||||||
std.store.transact(() => {
|
elements.forEach(element => {
|
||||||
if (parent !== null) {
|
// oxlint-disable-next-line unicorn/prefer-dom-node-remove
|
||||||
batchRemoveChildren(parent, [group]);
|
group.removeChild(element);
|
||||||
}
|
|
||||||
|
|
||||||
batchRemoveChildren(group, elements);
|
|
||||||
|
|
||||||
// keep relative index order of group children after ungroup
|
|
||||||
orderedElements.forEach((element, idx) => {
|
|
||||||
const index = nextIndexes[idx];
|
|
||||||
if (element.index !== index) {
|
|
||||||
element.index = index;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (parent !== null) {
|
|
||||||
batchAddChildren(parent, orderedElements);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
selection.set({
|
|
||||||
editing: false,
|
|
||||||
elements: orderedElements.map(ele => ele.id),
|
|
||||||
});
|
|
||||||
next();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// keep relative index order of group children after ungroup
|
||||||
|
elements
|
||||||
|
.sort((a, b) => gfx.layer.compare(a, b))
|
||||||
|
.forEach(element => {
|
||||||
|
std.store.transact(() => {
|
||||||
|
element.index = gfx.layer.generateIndex();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (parent !== null) {
|
||||||
|
elements.forEach(element => {
|
||||||
|
parent.addChild(element);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
selection.set({
|
||||||
|
editing: false,
|
||||||
|
elements: elements.map(ele => ele.id),
|
||||||
|
});
|
||||||
|
next();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
import { defineConfig } from 'vitest/config';
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
esbuild: {
|
|
||||||
target: 'es2018',
|
|
||||||
},
|
|
||||||
test: {
|
|
||||||
globalSetup: '../../../scripts/vitest-global.js',
|
|
||||||
include: ['src/__tests__/**/*.unit.spec.ts'],
|
|
||||||
testTimeout: 1000,
|
|
||||||
coverage: {
|
|
||||||
provider: 'istanbul',
|
|
||||||
reporter: ['lcov'],
|
|
||||||
reportsDirectory: '../../../.coverage/affine-gfx-group',
|
|
||||||
},
|
|
||||||
onConsoleLog(log, type) {
|
|
||||||
if (log.includes('lit.dev/msg/dev-mode')) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
console.warn(`Unexpected ${type} log`, log);
|
|
||||||
throw new Error(log);
|
|
||||||
},
|
|
||||||
environment: 'happy-dom',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -32,9 +32,6 @@
|
|||||||
"yjs": "^13.6.27",
|
"yjs": "^13.6.27",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
|
||||||
"vitest": "^3.2.4"
|
|
||||||
},
|
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/index.ts",
|
".": "./src/index.ts",
|
||||||
"./view": "./src/view.ts"
|
"./view": "./src/view.ts"
|
||||||
|
|||||||
@@ -1,73 +0,0 @@
|
|||||||
import { describe, expect, test } from 'vitest';
|
|
||||||
|
|
||||||
import {
|
|
||||||
AdaptiveCooldownController,
|
|
||||||
AdaptiveStrideController,
|
|
||||||
} from '../snap/adaptive-load-controller.js';
|
|
||||||
|
|
||||||
describe('AdaptiveStrideController', () => {
|
|
||||||
test('increases stride under heavy cost and respects maxStride', () => {
|
|
||||||
const controller = new AdaptiveStrideController({
|
|
||||||
heavyCostMs: 6,
|
|
||||||
maxStride: 3,
|
|
||||||
recoveryCostMs: 2,
|
|
||||||
});
|
|
||||||
|
|
||||||
controller.reportCost(10);
|
|
||||||
controller.reportCost(12);
|
|
||||||
controller.reportCost(15);
|
|
||||||
|
|
||||||
// stride should be capped at 3, so only every 3rd tick runs.
|
|
||||||
expect(controller.shouldSkip()).toBe(false);
|
|
||||||
expect(controller.shouldSkip()).toBe(true);
|
|
||||||
expect(controller.shouldSkip()).toBe(true);
|
|
||||||
expect(controller.shouldSkip()).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('decreases stride when cost recovers and reset clears state', () => {
|
|
||||||
const controller = new AdaptiveStrideController({
|
|
||||||
heavyCostMs: 8,
|
|
||||||
maxStride: 4,
|
|
||||||
recoveryCostMs: 3,
|
|
||||||
});
|
|
||||||
|
|
||||||
controller.reportCost(12);
|
|
||||||
controller.reportCost(12);
|
|
||||||
controller.reportCost(1);
|
|
||||||
|
|
||||||
// From stride 3 recovered to stride 2: run every other tick.
|
|
||||||
expect(controller.shouldSkip()).toBe(false);
|
|
||||||
expect(controller.shouldSkip()).toBe(true);
|
|
||||||
expect(controller.shouldSkip()).toBe(false);
|
|
||||||
|
|
||||||
controller.reset();
|
|
||||||
expect(controller.shouldSkip()).toBe(false);
|
|
||||||
expect(controller.shouldSkip()).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('AdaptiveCooldownController', () => {
|
|
||||||
test('enters cooldown when cost exceeds threshold', () => {
|
|
||||||
const controller = new AdaptiveCooldownController({
|
|
||||||
cooldownFrames: 2,
|
|
||||||
maxCostMs: 5,
|
|
||||||
});
|
|
||||||
|
|
||||||
controller.reportCost(9);
|
|
||||||
expect(controller.shouldRun()).toBe(false);
|
|
||||||
expect(controller.shouldRun()).toBe(false);
|
|
||||||
expect(controller.shouldRun()).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('reset exits cooldown immediately', () => {
|
|
||||||
const controller = new AdaptiveCooldownController({
|
|
||||||
cooldownFrames: 3,
|
|
||||||
maxCostMs: 5,
|
|
||||||
});
|
|
||||||
|
|
||||||
controller.reportCost(6);
|
|
||||||
expect(controller.shouldRun()).toBe(false);
|
|
||||||
controller.reset();
|
|
||||||
expect(controller.shouldRun()).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,177 +0,0 @@
|
|||||||
import { EdgelessLegacySlotIdentifier } from '@blocksuite/affine-block-surface';
|
|
||||||
import { MouseButton } from '@blocksuite/std/gfx';
|
|
||||||
import { afterEach, describe, expect, test, vi } from 'vitest';
|
|
||||||
|
|
||||||
import { PanTool } from '../tools/pan-tool.js';
|
|
||||||
|
|
||||||
type PointerDownHandler = (event: {
|
|
||||||
raw: {
|
|
||||||
button: number;
|
|
||||||
preventDefault: () => void;
|
|
||||||
};
|
|
||||||
}) => unknown;
|
|
||||||
|
|
||||||
const mockRaf = () => {
|
|
||||||
let callback: FrameRequestCallback | undefined;
|
|
||||||
const requestAnimationFrameMock = vi
|
|
||||||
.fn()
|
|
||||||
.mockImplementation((cb: FrameRequestCallback) => {
|
|
||||||
callback = cb;
|
|
||||||
return 1;
|
|
||||||
});
|
|
||||||
const cancelAnimationFrameMock = vi.fn();
|
|
||||||
|
|
||||||
vi.stubGlobal('requestAnimationFrame', requestAnimationFrameMock);
|
|
||||||
vi.stubGlobal('cancelAnimationFrame', cancelAnimationFrameMock);
|
|
||||||
|
|
||||||
return {
|
|
||||||
getCallback: () => callback,
|
|
||||||
requestAnimationFrameMock,
|
|
||||||
cancelAnimationFrameMock,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const createToolFixture = (options?: {
|
|
||||||
currentToolName?: string;
|
|
||||||
currentToolOptions?: Record<string, unknown>;
|
|
||||||
}) => {
|
|
||||||
const applyDeltaCenter = vi.fn();
|
|
||||||
const selectionSet = vi.fn();
|
|
||||||
const setTool = vi.fn();
|
|
||||||
const navigatorSettingUpdated = {
|
|
||||||
next: vi.fn(),
|
|
||||||
};
|
|
||||||
const currentToolName = options?.currentToolName;
|
|
||||||
const currentToolOption = {
|
|
||||||
toolType: currentToolName
|
|
||||||
? ({
|
|
||||||
toolName: currentToolName,
|
|
||||||
} as any)
|
|
||||||
: undefined,
|
|
||||||
options: options?.currentToolOptions,
|
|
||||||
};
|
|
||||||
|
|
||||||
const gfx = {
|
|
||||||
viewport: {
|
|
||||||
zoom: 2,
|
|
||||||
applyDeltaCenter,
|
|
||||||
},
|
|
||||||
selection: {
|
|
||||||
surfaceSelections: [{ elements: ['shape-1'] }],
|
|
||||||
set: selectionSet,
|
|
||||||
},
|
|
||||||
tool: {
|
|
||||||
currentTool$: {
|
|
||||||
peek: () => null,
|
|
||||||
},
|
|
||||||
currentToolOption$: {
|
|
||||||
peek: () => currentToolOption,
|
|
||||||
},
|
|
||||||
setTool,
|
|
||||||
},
|
|
||||||
std: {
|
|
||||||
get: (identifier: unknown) => {
|
|
||||||
if (identifier === EdgelessLegacySlotIdentifier) {
|
|
||||||
return { navigatorSettingUpdated };
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
doc: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
const tool = new PanTool(gfx as any);
|
|
||||||
|
|
||||||
return {
|
|
||||||
applyDeltaCenter,
|
|
||||||
navigatorSettingUpdated,
|
|
||||||
selectionSet,
|
|
||||||
setTool,
|
|
||||||
tool,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
vi.unstubAllGlobals();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('PanTool', () => {
|
|
||||||
test('flushes accumulated delta on dragEnd', () => {
|
|
||||||
mockRaf();
|
|
||||||
const { tool, applyDeltaCenter } = createToolFixture();
|
|
||||||
|
|
||||||
tool.dragStart({ x: 100, y: 100 } as any);
|
|
||||||
tool.dragMove({ x: 80, y: 60 } as any);
|
|
||||||
tool.dragMove({ x: 70, y: 40 } as any);
|
|
||||||
|
|
||||||
expect(applyDeltaCenter).not.toHaveBeenCalled();
|
|
||||||
tool.dragEnd({} as any);
|
|
||||||
|
|
||||||
expect(applyDeltaCenter).toHaveBeenCalledTimes(1);
|
|
||||||
expect(applyDeltaCenter).toHaveBeenCalledWith(15, 30);
|
|
||||||
expect(tool.panning$.value).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('cancel in unmounted drops pending deltas', () => {
|
|
||||||
mockRaf();
|
|
||||||
const { tool, applyDeltaCenter } = createToolFixture();
|
|
||||||
|
|
||||||
tool.dragStart({ x: 100, y: 100 } as any);
|
|
||||||
tool.dragMove({ x: 80, y: 60 } as any);
|
|
||||||
tool.unmounted();
|
|
||||||
tool.dragEnd({} as any);
|
|
||||||
|
|
||||||
expect(applyDeltaCenter).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('middle click temporary pan restores frameNavigator with restoredAfterPan', () => {
|
|
||||||
const { tool, navigatorSettingUpdated, selectionSet, setTool } =
|
|
||||||
createToolFixture({
|
|
||||||
currentToolName: 'frameNavigator',
|
|
||||||
currentToolOptions: { mode: 'fit' },
|
|
||||||
});
|
|
||||||
|
|
||||||
const hooks: Partial<Record<'pointerDown', PointerDownHandler>> = {};
|
|
||||||
(tool as any).eventTarget = {
|
|
||||||
addHook: (eventName: 'pointerDown', handler: PointerDownHandler) => {
|
|
||||||
hooks[eventName] = handler;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
tool.mounted();
|
|
||||||
|
|
||||||
const preventDefault = vi.fn();
|
|
||||||
const pointerDown = hooks.pointerDown!;
|
|
||||||
const ret = pointerDown({
|
|
||||||
raw: {
|
|
||||||
button: MouseButton.MIDDLE,
|
|
||||||
preventDefault,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(ret).toBe(false);
|
|
||||||
expect(preventDefault).toHaveBeenCalledTimes(1);
|
|
||||||
expect(navigatorSettingUpdated.next).toHaveBeenCalledWith({
|
|
||||||
blackBackground: false,
|
|
||||||
});
|
|
||||||
expect(setTool).toHaveBeenNthCalledWith(1, PanTool, {
|
|
||||||
panning: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
document.dispatchEvent(
|
|
||||||
new PointerEvent('pointerup', { button: MouseButton.MIDDLE })
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(selectionSet).toHaveBeenCalledWith([{ elements: ['shape-1'] }]);
|
|
||||||
expect(setTool).toHaveBeenNthCalledWith(
|
|
||||||
2,
|
|
||||||
expect.objectContaining({
|
|
||||||
toolName: 'frameNavigator',
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
mode: 'fit',
|
|
||||||
restoredAfterPan: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
export class AdaptiveStrideController {
|
|
||||||
private _stride = 1;
|
|
||||||
|
|
||||||
private _ticks = 0;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly _options: {
|
|
||||||
heavyCostMs: number;
|
|
||||||
maxStride: number;
|
|
||||||
recoveryCostMs: number;
|
|
||||||
}
|
|
||||||
) {}
|
|
||||||
|
|
||||||
reportCost(costMs: number) {
|
|
||||||
if (costMs > this._options.heavyCostMs) {
|
|
||||||
this._stride = Math.min(this._options.maxStride, this._stride + 1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (costMs < this._options.recoveryCostMs && this._stride > 1) {
|
|
||||||
this._stride -= 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
reset() {
|
|
||||||
this._stride = 1;
|
|
||||||
this._ticks = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldSkip() {
|
|
||||||
const shouldSkip = this._stride > 1 && this._ticks % this._stride !== 0;
|
|
||||||
this._ticks += 1;
|
|
||||||
return shouldSkip;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class AdaptiveCooldownController {
|
|
||||||
private _remainingFrames = 0;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly _options: {
|
|
||||||
cooldownFrames: number;
|
|
||||||
maxCostMs: number;
|
|
||||||
}
|
|
||||||
) {}
|
|
||||||
|
|
||||||
reportCost(costMs: number) {
|
|
||||||
if (costMs > this._options.maxCostMs) {
|
|
||||||
this._remainingFrames = this._options.cooldownFrames;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
reset() {
|
|
||||||
this._remainingFrames = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldRun() {
|
|
||||||
if (this._remainingFrames <= 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._remainingFrames -= 1;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,18 +8,11 @@ import {
|
|||||||
InteractivityExtension,
|
InteractivityExtension,
|
||||||
} from '@blocksuite/std/gfx';
|
} from '@blocksuite/std/gfx';
|
||||||
|
|
||||||
import { AdaptiveStrideController } from './adaptive-load-controller';
|
|
||||||
import type { SnapOverlay } from './snap-overlay';
|
import type { SnapOverlay } from './snap-overlay';
|
||||||
|
|
||||||
export class SnapExtension extends InteractivityExtension {
|
export class SnapExtension extends InteractivityExtension {
|
||||||
static override key = 'snap-manager';
|
static override key = 'snap-manager';
|
||||||
|
|
||||||
private static readonly MAX_ALIGN_SKIP_STRIDE = 3;
|
|
||||||
|
|
||||||
private static readonly ALIGN_HEAVY_COST_MS = 5;
|
|
||||||
|
|
||||||
private static readonly ALIGN_RECOVERY_COST_MS = 2;
|
|
||||||
|
|
||||||
get snapOverlay() {
|
get snapOverlay() {
|
||||||
return this.std.getOptional(
|
return this.std.getOptional(
|
||||||
OverlayIdentifier('snap-manager')
|
OverlayIdentifier('snap-manager')
|
||||||
@@ -36,11 +29,6 @@ export class SnapExtension extends InteractivityExtension {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let alignBound: Bound | null = null;
|
let alignBound: Bound | null = null;
|
||||||
const alignStride = new AdaptiveStrideController({
|
|
||||||
heavyCostMs: SnapExtension.ALIGN_HEAVY_COST_MS,
|
|
||||||
maxStride: SnapExtension.MAX_ALIGN_SKIP_STRIDE,
|
|
||||||
recoveryCostMs: SnapExtension.ALIGN_RECOVERY_COST_MS,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
onDragStart() {
|
onDragStart() {
|
||||||
@@ -54,7 +42,6 @@ export class SnapExtension extends InteractivityExtension {
|
|||||||
return pre;
|
return pre;
|
||||||
}, [] as GfxModel[])
|
}, [] as GfxModel[])
|
||||||
);
|
);
|
||||||
alignStride.reset();
|
|
||||||
},
|
},
|
||||||
onDragMove(context: ExtensionDragMoveContext) {
|
onDragMove(context: ExtensionDragMoveContext) {
|
||||||
if (
|
if (
|
||||||
@@ -66,22 +53,14 @@ export class SnapExtension extends InteractivityExtension {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (alignStride.shouldSkip()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentBound = alignBound.moveDelta(context.dx, context.dy);
|
const currentBound = alignBound.moveDelta(context.dx, context.dy);
|
||||||
const alignStart = performance.now();
|
|
||||||
const alignRst = snapOverlay.align(currentBound);
|
const alignRst = snapOverlay.align(currentBound);
|
||||||
const alignCost = performance.now() - alignStart;
|
|
||||||
alignStride.reportCost(alignCost);
|
|
||||||
|
|
||||||
context.dx = alignRst.dx + context.dx;
|
context.dx = alignRst.dx + context.dx;
|
||||||
context.dy = alignRst.dy + context.dy;
|
context.dy = alignRst.dy + context.dy;
|
||||||
},
|
},
|
||||||
clear() {
|
clear() {
|
||||||
alignBound = null;
|
alignBound = null;
|
||||||
alignStride.reset();
|
|
||||||
snapOverlay.clear();
|
snapOverlay.clear();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import {
|
|||||||
import { almostEqual, Bound, type IVec, Point } from '@blocksuite/global/gfx';
|
import { almostEqual, Bound, type IVec, Point } from '@blocksuite/global/gfx';
|
||||||
import type { GfxModel } from '@blocksuite/std/gfx';
|
import type { GfxModel } from '@blocksuite/std/gfx';
|
||||||
|
|
||||||
import { AdaptiveCooldownController } from './adaptive-load-controller';
|
|
||||||
|
|
||||||
interface Distance {
|
interface Distance {
|
||||||
horiz?: {
|
horiz?: {
|
||||||
/**
|
/**
|
||||||
@@ -37,9 +35,6 @@ interface Distance {
|
|||||||
const ALIGN_THRESHOLD = 8;
|
const ALIGN_THRESHOLD = 8;
|
||||||
const DISTRIBUTION_LINE_OFFSET = 1;
|
const DISTRIBUTION_LINE_OFFSET = 1;
|
||||||
const STROKE_WIDTH = 2;
|
const STROKE_WIDTH = 2;
|
||||||
const DISTRIBUTE_ALIGN_MAX_CANDIDATES = 160;
|
|
||||||
const DISTRIBUTE_ALIGN_MAX_COST_MS = 5;
|
|
||||||
const DISTRIBUTE_ALIGN_COOLDOWN_FRAMES = 2;
|
|
||||||
|
|
||||||
export class SnapOverlay extends Overlay {
|
export class SnapOverlay extends Overlay {
|
||||||
static override overlayName: string = 'snap-manager';
|
static override overlayName: string = 'snap-manager';
|
||||||
@@ -80,11 +75,6 @@ export class SnapOverlay extends Overlay {
|
|||||||
vertical: [],
|
vertical: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly _distributeCooldown = new AdaptiveCooldownController({
|
|
||||||
cooldownFrames: DISTRIBUTE_ALIGN_COOLDOWN_FRAMES,
|
|
||||||
maxCostMs: DISTRIBUTE_ALIGN_MAX_COST_MS,
|
|
||||||
});
|
|
||||||
|
|
||||||
override clear() {
|
override clear() {
|
||||||
this._referenceBounds = {
|
this._referenceBounds = {
|
||||||
vertical: [],
|
vertical: [],
|
||||||
@@ -97,7 +87,6 @@ export class SnapOverlay extends Overlay {
|
|||||||
};
|
};
|
||||||
this._distributedAlignLines = [];
|
this._distributedAlignLines = [];
|
||||||
this._skippedElements.clear();
|
this._skippedElements.clear();
|
||||||
this._distributeCooldown.reset();
|
|
||||||
|
|
||||||
super.clear();
|
super.clear();
|
||||||
}
|
}
|
||||||
@@ -684,24 +673,13 @@ export class SnapOverlay extends Overlay {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const shouldTryDistribute =
|
// point align priority is higher than distribute align
|
||||||
this._referenceBounds.all.length <= DISTRIBUTE_ALIGN_MAX_CANDIDATES &&
|
if (rst.dx === 0) {
|
||||||
this._distributeCooldown.shouldRun();
|
this._alignDistributeHorizontally(rst, bound, threshold, viewport);
|
||||||
|
}
|
||||||
|
|
||||||
if (shouldTryDistribute) {
|
if (rst.dy === 0) {
|
||||||
const distributeStart = performance.now();
|
this._alignDistributeVertically(rst, bound, threshold, viewport);
|
||||||
|
|
||||||
// point align priority is higher than distribute align
|
|
||||||
if (rst.dx === 0) {
|
|
||||||
this._alignDistributeHorizontally(rst, bound, threshold, viewport);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rst.dy === 0) {
|
|
||||||
this._alignDistributeVertically(rst, bound, threshold, viewport);
|
|
||||||
}
|
|
||||||
|
|
||||||
const distributeCost = performance.now() - distributeStart;
|
|
||||||
this._distributeCooldown.reportCost(distributeCost);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this._renderer?.refresh();
|
this._renderer?.refresh();
|
||||||
@@ -798,26 +776,24 @@ export class SnapOverlay extends Overlay {
|
|||||||
});
|
});
|
||||||
const verticalBounds: Bound[] = [];
|
const verticalBounds: Bound[] = [];
|
||||||
const horizBounds: Bound[] = [];
|
const horizBounds: Bound[] = [];
|
||||||
const allCandidateElements = new Set<GfxModel>();
|
const allBounds: Bound[] = [];
|
||||||
|
|
||||||
vertCandidates.forEach(candidate => {
|
vertCandidates.forEach(candidate => {
|
||||||
if (skipped.has(candidate) || this._isSkippedElement(candidate)) return;
|
if (skipped.has(candidate) || this._isSkippedElement(candidate)) return;
|
||||||
const bound = candidate.elementBound;
|
verticalBounds.push(candidate.elementBound);
|
||||||
verticalBounds.push(bound);
|
allBounds.push(candidate.elementBound);
|
||||||
allCandidateElements.add(candidate);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
horizCandidates.forEach(candidate => {
|
horizCandidates.forEach(candidate => {
|
||||||
if (skipped.has(candidate) || this._isSkippedElement(candidate)) return;
|
if (skipped.has(candidate) || this._isSkippedElement(candidate)) return;
|
||||||
const bound = candidate.elementBound;
|
horizBounds.push(candidate.elementBound);
|
||||||
horizBounds.push(bound);
|
allBounds.push(candidate.elementBound);
|
||||||
allCandidateElements.add(candidate);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this._referenceBounds = {
|
this._referenceBounds = {
|
||||||
horizontal: horizBounds,
|
horizontal: horizBounds,
|
||||||
vertical: verticalBounds,
|
vertical: verticalBounds,
|
||||||
all: [...allCandidateElements].map(element => element.elementBound),
|
all: allBounds,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,7 @@ import {
|
|||||||
} from '@blocksuite/affine-block-surface';
|
} from '@blocksuite/affine-block-surface';
|
||||||
import { on } from '@blocksuite/affine-shared/utils';
|
import { on } from '@blocksuite/affine-shared/utils';
|
||||||
import type { PointerEventState } from '@blocksuite/std';
|
import type { PointerEventState } from '@blocksuite/std';
|
||||||
import {
|
import { BaseTool, MouseButton, type ToolOptions } from '@blocksuite/std/gfx';
|
||||||
BaseTool,
|
|
||||||
createRafCoalescer,
|
|
||||||
MouseButton,
|
|
||||||
type ToolOptions,
|
|
||||||
} from '@blocksuite/std/gfx';
|
|
||||||
import { Signal } from '@preact/signals-core';
|
import { Signal } from '@preact/signals-core';
|
||||||
|
|
||||||
interface RestorablePresentToolOptions {
|
interface RestorablePresentToolOptions {
|
||||||
@@ -26,30 +21,13 @@ export class PanTool extends BaseTool<PanToolOption> {
|
|||||||
|
|
||||||
private _lastPoint: [number, number] | null = null;
|
private _lastPoint: [number, number] | null = null;
|
||||||
|
|
||||||
private _pendingDelta: [number, number] = [0, 0];
|
|
||||||
|
|
||||||
private readonly _deltaFlushCoalescer = createRafCoalescer<void>(() => {
|
|
||||||
this._flushPendingDelta();
|
|
||||||
});
|
|
||||||
|
|
||||||
readonly panning$ = new Signal<boolean>(false);
|
readonly panning$ = new Signal<boolean>(false);
|
||||||
|
|
||||||
private _flushPendingDelta() {
|
|
||||||
if (this._pendingDelta[0] === 0 && this._pendingDelta[1] === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [deltaX, deltaY] = this._pendingDelta;
|
|
||||||
this._pendingDelta = [0, 0];
|
|
||||||
this.gfx.viewport.applyDeltaCenter(deltaX, deltaY);
|
|
||||||
}
|
|
||||||
|
|
||||||
override get allowDragWithRightButton(): boolean {
|
override get allowDragWithRightButton(): boolean {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
override dragEnd(_: PointerEventState): void {
|
override dragEnd(_: PointerEventState): void {
|
||||||
this._deltaFlushCoalescer.flush();
|
|
||||||
this._lastPoint = null;
|
this._lastPoint = null;
|
||||||
this.panning$.value = false;
|
this.panning$.value = false;
|
||||||
}
|
}
|
||||||
@@ -65,14 +43,12 @@ export class PanTool extends BaseTool<PanToolOption> {
|
|||||||
const deltaY = lastY - e.y;
|
const deltaY = lastY - e.y;
|
||||||
|
|
||||||
this._lastPoint = [e.x, e.y];
|
this._lastPoint = [e.x, e.y];
|
||||||
this._pendingDelta[0] += deltaX / zoom;
|
|
||||||
this._pendingDelta[1] += deltaY / zoom;
|
viewport.applyDeltaCenter(deltaX / zoom, deltaY / zoom);
|
||||||
this._deltaFlushCoalescer.schedule(undefined);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override dragStart(e: PointerEventState): void {
|
override dragStart(e: PointerEventState): void {
|
||||||
this._lastPoint = [e.x, e.y];
|
this._lastPoint = [e.x, e.y];
|
||||||
this._pendingDelta = [0, 0];
|
|
||||||
this.panning$.value = true;
|
this.panning$.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,8 +120,4 @@ export class PanTool extends BaseTool<PanToolOption> {
|
|||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
override unmounted(): void {
|
|
||||||
this._deltaFlushCoalescer.cancel();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
import { defineConfig } from 'vitest/config';
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
esbuild: {
|
|
||||||
target: 'es2018',
|
|
||||||
},
|
|
||||||
test: {
|
|
||||||
globalSetup: '../../../scripts/vitest-global.js',
|
|
||||||
include: ['src/__tests__/**/*.unit.spec.ts'],
|
|
||||||
testTimeout: 1000,
|
|
||||||
coverage: {
|
|
||||||
provider: 'istanbul',
|
|
||||||
reporter: ['lcov'],
|
|
||||||
reportsDirectory: '../../../.coverage/affine-gfx-pointer',
|
|
||||||
},
|
|
||||||
onConsoleLog(log, type) {
|
|
||||||
if (log.includes('lit.dev/msg/dev-mode')) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
console.warn(`Unexpected ${type} log`, log);
|
|
||||||
throw new Error(log);
|
|
||||||
},
|
|
||||||
environment: 'happy-dom',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
/* eslint-disable @typescript-eslint/await-thenable */
|
|
||||||
import type {
|
import type {
|
||||||
Template,
|
Template,
|
||||||
TemplateCategory,
|
TemplateCategory,
|
||||||
|
|||||||
@@ -155,22 +155,9 @@ export class FrameBlockModel
|
|||||||
}
|
}
|
||||||
|
|
||||||
removeChild(element: GfxModel): void {
|
removeChild(element: GfxModel): void {
|
||||||
this.removeChildren([element]);
|
|
||||||
}
|
|
||||||
|
|
||||||
removeChildren(elements: GfxModel[]): void {
|
|
||||||
const childIds = [...new Set(elements.map(element => element.id))];
|
|
||||||
if (!this.props.childElementIds || childIds.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.store.transact(() => {
|
this.store.transact(() => {
|
||||||
const childElementIds = this.props.childElementIds;
|
this.props.childElementIds &&
|
||||||
if (!childElementIds) return;
|
delete this.props.childElementIds[element.id];
|
||||||
|
|
||||||
childIds.forEach(childId => {
|
|
||||||
delete childElementIds[childId];
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,21 +54,12 @@ export class GroupElementModel extends GfxGroupLikeElementModel<GroupElementProp
|
|||||||
}
|
}
|
||||||
|
|
||||||
override addChild(element: GfxModel) {
|
override addChild(element: GfxModel) {
|
||||||
this.addChildren([element]);
|
if (!canSafeAddToContainer(this, element)) {
|
||||||
}
|
|
||||||
|
|
||||||
addChildren(elements: GfxModel[]) {
|
|
||||||
elements = [...new Set(elements)].filter(element =>
|
|
||||||
canSafeAddToContainer(this, element)
|
|
||||||
);
|
|
||||||
if (elements.length === 0) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.surface.store.transact(() => {
|
this.surface.store.transact(() => {
|
||||||
elements.forEach(element => {
|
this.children.set(element.id, true);
|
||||||
this.children.set(element.id, true);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,22 +76,11 @@ export class GroupElementModel extends GfxGroupLikeElementModel<GroupElementProp
|
|||||||
}
|
}
|
||||||
|
|
||||||
removeChild(element: GfxModel) {
|
removeChild(element: GfxModel) {
|
||||||
this.removeChildren([element]);
|
|
||||||
}
|
|
||||||
|
|
||||||
removeChildren(elements: GfxModel[]) {
|
|
||||||
if (!this.children) {
|
if (!this.children) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const childIds = [...new Set(elements.map(element => element.id))];
|
|
||||||
if (childIds.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.surface.store.transact(() => {
|
this.surface.store.transact(() => {
|
||||||
childIds.forEach(childId => {
|
this.children.delete(element.id);
|
||||||
this.children.delete(childId);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
"micromark-extension-gfm-task-list-item": "^2.1.0",
|
"micromark-extension-gfm-task-list-item": "^2.1.0",
|
||||||
"micromark-util-combine-extensions": "^2.0.0",
|
"micromark-util-combine-extensions": "^2.0.0",
|
||||||
"minimatch": "^10.1.1",
|
"minimatch": "^10.1.1",
|
||||||
"pdfmake": "^0.2.20",
|
"pdfmake": "^0.3.0",
|
||||||
"quick-lru": "^7.3.0",
|
"quick-lru": "^7.3.0",
|
||||||
"rehype-parse": "^9.0.0",
|
"rehype-parse": "^9.0.0",
|
||||||
"rehype-stringify": "^10.0.0",
|
"rehype-stringify": "^10.0.0",
|
||||||
@@ -74,7 +74,7 @@
|
|||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/pdfmake": "^0.2.12",
|
"@types/pdfmake": "^0.3.0",
|
||||||
"vitest": "^3.2.4"
|
"vitest": "^3.2.4"
|
||||||
},
|
},
|
||||||
"version": "0.26.1"
|
"version": "0.26.1"
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import rehypeParse from 'rehype-parse';
|
|||||||
import { unified } from 'unified';
|
import { unified } from 'unified';
|
||||||
|
|
||||||
import type { AffineTextAttributes } from '../../types/index.js';
|
import type { AffineTextAttributes } from '../../types/index.js';
|
||||||
import type { HtmlDeltaConverter } from '../html/delta-converter.js';
|
import { HtmlDeltaConverter } from '../html/delta-converter.js';
|
||||||
import {
|
import {
|
||||||
rehypeInlineToBlock,
|
rehypeInlineToBlock,
|
||||||
rehypeWrapInlineElements,
|
rehypeWrapInlineElements,
|
||||||
|
|||||||
@@ -873,7 +873,7 @@ export class PdfAdapter extends BaseAdapter<PdfAdapterFile> {
|
|||||||
return {
|
return {
|
||||||
table: {
|
table: {
|
||||||
headerRows: 0,
|
headerRows: 0,
|
||||||
widths: Array.from({ length: sortedColumns.length }, () => '*'),
|
widths: Array(sortedColumns.length).fill('*'),
|
||||||
body: tableBody,
|
body: tableBody,
|
||||||
},
|
},
|
||||||
margin: [0, 5, 0, 5],
|
margin: [0, 5, 0, 5],
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { FontFamily, FontStyle, FontWeight } from '@blocksuite/affine-model';
|
|
||||||
import { createIdentifier } from '@blocksuite/global/di';
|
import { createIdentifier } from '@blocksuite/global/di';
|
||||||
import { IS_FIREFOX } from '@blocksuite/global/env';
|
import { IS_FIREFOX } from '@blocksuite/global/env';
|
||||||
import { LifeCycleWatcher } from '@blocksuite/std';
|
import { LifeCycleWatcher } from '@blocksuite/std';
|
||||||
@@ -21,171 +20,33 @@ const initFontFace = IS_FIREFOX
|
|||||||
export class FontLoaderService extends LifeCycleWatcher {
|
export class FontLoaderService extends LifeCycleWatcher {
|
||||||
static override readonly key = 'font-loader';
|
static override readonly key = 'font-loader';
|
||||||
|
|
||||||
private static readonly DEFERRED_LOAD_DELAY_MS = 5000;
|
|
||||||
|
|
||||||
private static readonly DEFERRED_LOAD_BATCH_SIZE = 4;
|
|
||||||
|
|
||||||
private static readonly DEFERRED_LOAD_BATCH_INTERVAL_MS = 1000;
|
|
||||||
|
|
||||||
private _idleLoadTaskId: number | null = null;
|
|
||||||
|
|
||||||
private _lazyLoadTimeoutId: number | null = null;
|
|
||||||
|
|
||||||
private _deferredFontsQueue: FontConfig[] = [];
|
|
||||||
|
|
||||||
private _deferredFontsCursor = 0;
|
|
||||||
|
|
||||||
private readonly _loadedFontKeys = new Set<string>();
|
|
||||||
|
|
||||||
readonly fontFaces: FontFace[] = [];
|
readonly fontFaces: FontFace[] = [];
|
||||||
|
|
||||||
get ready() {
|
get ready() {
|
||||||
return Promise.all(this.fontFaces.map(fontFace => fontFace.loaded));
|
return Promise.all(this.fontFaces.map(fontFace => fontFace.loaded));
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly _fontKey = ({ font, weight, style, url }: FontConfig) => {
|
|
||||||
return `${font}:${weight}:${style}:${url}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly _isCriticalCanvasFont = ({
|
|
||||||
font,
|
|
||||||
weight,
|
|
||||||
style,
|
|
||||||
}: FontConfig) => {
|
|
||||||
if (style !== FontStyle.Normal) return false;
|
|
||||||
|
|
||||||
if (font === FontFamily.Poppins) {
|
|
||||||
return (
|
|
||||||
weight === FontWeight.Regular ||
|
|
||||||
weight === FontWeight.Medium ||
|
|
||||||
weight === FontWeight.SemiBold
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (font === FontFamily.Inter) {
|
|
||||||
return weight === FontWeight.Regular || weight === FontWeight.SemiBold;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (font === FontFamily.Kalam) {
|
|
||||||
// Mindmap style four uses bold Kalam text.
|
|
||||||
// We map to SemiBold because this is the strongest shipped Kalam weight.
|
|
||||||
return weight === FontWeight.SemiBold;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly _scheduleDeferredLoad = (fonts: FontConfig[]) => {
|
|
||||||
if (fonts.length === 0 || typeof window === 'undefined') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._deferredFontsQueue = fonts;
|
|
||||||
this._deferredFontsCursor = 0;
|
|
||||||
|
|
||||||
const win = window as Window & {
|
|
||||||
requestIdleCallback?: (
|
|
||||||
callback: () => void,
|
|
||||||
options?: { timeout?: number }
|
|
||||||
) => number;
|
|
||||||
cancelIdleCallback?: (handle: number) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const scheduleBatch = (delayMs: number) => {
|
|
||||||
this._lazyLoadTimeoutId = window.setTimeout(() => {
|
|
||||||
this._lazyLoadTimeoutId = null;
|
|
||||||
const runBatch = () => {
|
|
||||||
this._idleLoadTaskId = null;
|
|
||||||
|
|
||||||
const start = this._deferredFontsCursor;
|
|
||||||
const end = Math.min(
|
|
||||||
start + FontLoaderService.DEFERRED_LOAD_BATCH_SIZE,
|
|
||||||
this._deferredFontsQueue.length
|
|
||||||
);
|
|
||||||
const batch = this._deferredFontsQueue.slice(start, end);
|
|
||||||
this._deferredFontsCursor = end;
|
|
||||||
this.load(batch);
|
|
||||||
|
|
||||||
if (this._deferredFontsCursor < this._deferredFontsQueue.length) {
|
|
||||||
scheduleBatch(FontLoaderService.DEFERRED_LOAD_BATCH_INTERVAL_MS);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (typeof win.requestIdleCallback === 'function') {
|
|
||||||
this._idleLoadTaskId = win.requestIdleCallback(runBatch, {
|
|
||||||
timeout: 2000,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
runBatch();
|
|
||||||
}, delayMs);
|
|
||||||
};
|
|
||||||
|
|
||||||
scheduleBatch(FontLoaderService.DEFERRED_LOAD_DELAY_MS);
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly _cancelDeferredLoad = () => {
|
|
||||||
if (typeof window === 'undefined') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const win = window as Window & {
|
|
||||||
cancelIdleCallback?: (handle: number) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (
|
|
||||||
this._idleLoadTaskId !== null &&
|
|
||||||
typeof win.cancelIdleCallback === 'function'
|
|
||||||
) {
|
|
||||||
win.cancelIdleCallback(this._idleLoadTaskId);
|
|
||||||
this._idleLoadTaskId = null;
|
|
||||||
}
|
|
||||||
if (this._lazyLoadTimeoutId !== null) {
|
|
||||||
window.clearTimeout(this._lazyLoadTimeoutId);
|
|
||||||
this._lazyLoadTimeoutId = null;
|
|
||||||
}
|
|
||||||
this._deferredFontsQueue = [];
|
|
||||||
this._deferredFontsCursor = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
load(fonts: FontConfig[]) {
|
load(fonts: FontConfig[]) {
|
||||||
for (const font of fonts) {
|
this.fontFaces.push(
|
||||||
const key = this._fontKey(font);
|
...fonts.map(font => {
|
||||||
if (this._loadedFontKeys.has(key)) {
|
const fontFace = initFontFace(font);
|
||||||
continue;
|
document.fonts.add(fontFace);
|
||||||
}
|
fontFace.load().catch(console.error);
|
||||||
this._loadedFontKeys.add(key);
|
return fontFace;
|
||||||
const fontFace = initFontFace(font);
|
})
|
||||||
document.fonts.add(fontFace);
|
);
|
||||||
fontFace.load().catch(console.error);
|
|
||||||
this.fontFaces.push(fontFace);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override mounted() {
|
override mounted() {
|
||||||
const config = this.std.getOptional(FontConfigIdentifier);
|
const config = this.std.getOptional(FontConfigIdentifier);
|
||||||
if (!config || config.length === 0) {
|
if (config) {
|
||||||
return;
|
this.load(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
const criticalFonts = config.filter(this._isCriticalCanvasFont);
|
|
||||||
const eagerFonts =
|
|
||||||
criticalFonts.length > 0 ? criticalFonts : config.slice(0, 3);
|
|
||||||
const eagerFontKeySet = new Set(eagerFonts.map(this._fontKey));
|
|
||||||
const deferredFonts = config.filter(
|
|
||||||
font => !eagerFontKeySet.has(this._fontKey(font))
|
|
||||||
);
|
|
||||||
|
|
||||||
this.load(eagerFonts);
|
|
||||||
this._scheduleDeferredLoad(deferredFonts);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override unmounted() {
|
override unmounted() {
|
||||||
this._cancelDeferredLoad();
|
this.fontFaces.forEach(fontFace => document.fonts.delete(fontFace));
|
||||||
for (const fontFace of this.fontFaces) {
|
|
||||||
document.fonts.delete(fontFace);
|
|
||||||
}
|
|
||||||
this.fontFaces.splice(0, this.fontFaces.length);
|
this.fontFaces.splice(0, this.fontFaces.length);
|
||||||
this._loadedFontKeys.clear();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -115,9 +115,12 @@ export async function printToPdf(
|
|||||||
) as HTMLDivElement;
|
) as HTMLDivElement;
|
||||||
|
|
||||||
// force light theme in print iframe
|
// force light theme in print iframe
|
||||||
iframe.contentWindow.document.documentElement.dataset.theme = 'light';
|
iframe.contentWindow.document.documentElement.setAttribute(
|
||||||
iframe.contentWindow.document.body.dataset.theme = 'light';
|
'data-theme',
|
||||||
importedRoot.dataset.theme = 'light';
|
'light'
|
||||||
|
);
|
||||||
|
iframe.contentWindow.document.body.setAttribute('data-theme', 'light');
|
||||||
|
importedRoot.setAttribute('data-theme', 'light');
|
||||||
|
|
||||||
// draw saved canvas image to canvas
|
// draw saved canvas image to canvas
|
||||||
const allImportedCanvas = importedRoot.getElementsByTagName('canvas');
|
const allImportedCanvas = importedRoot.getElementsByTagName('canvas');
|
||||||
|
|||||||
@@ -14,17 +14,6 @@ import {
|
|||||||
} from '../config.js';
|
} from '../config.js';
|
||||||
import type { AffineDragHandleWidget } from '../drag-handle.js';
|
import type { AffineDragHandleWidget } from '../drag-handle.js';
|
||||||
|
|
||||||
type HoveredElemArea = {
|
|
||||||
left: number;
|
|
||||||
top: number;
|
|
||||||
right: number;
|
|
||||||
bottom: number;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
padding: number;
|
|
||||||
containerWidth: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used to control the drag handle visibility in edgeless mode
|
* Used to control the drag handle visibility in edgeless mode
|
||||||
*
|
*
|
||||||
@@ -32,52 +21,6 @@ type HoveredElemArea = {
|
|||||||
* 2. Multiple selection is not supported
|
* 2. Multiple selection is not supported
|
||||||
*/
|
*/
|
||||||
export class EdgelessWatcher {
|
export class EdgelessWatcher {
|
||||||
private _pendingHoveredElemArea: HoveredElemArea | null = null;
|
|
||||||
|
|
||||||
private _lastAppliedHoveredElemArea: HoveredElemArea | null = null;
|
|
||||||
|
|
||||||
private _showDragHandleRafId: number | null = null;
|
|
||||||
|
|
||||||
private _surfaceElementUpdatedRafId: number | null = null;
|
|
||||||
|
|
||||||
private readonly _cloneArea = (area: HoveredElemArea): HoveredElemArea => ({
|
|
||||||
left: area.left,
|
|
||||||
top: area.top,
|
|
||||||
right: area.right,
|
|
||||||
bottom: area.bottom,
|
|
||||||
width: area.width,
|
|
||||||
height: area.height,
|
|
||||||
padding: area.padding,
|
|
||||||
containerWidth: area.containerWidth,
|
|
||||||
});
|
|
||||||
|
|
||||||
private readonly _isAreaEqual = (
|
|
||||||
left: HoveredElemArea | null,
|
|
||||||
right: HoveredElemArea | null
|
|
||||||
) => {
|
|
||||||
if (!left || !right) return false;
|
|
||||||
return (
|
|
||||||
left.left === right.left &&
|
|
||||||
left.top === right.top &&
|
|
||||||
left.right === right.right &&
|
|
||||||
left.bottom === right.bottom &&
|
|
||||||
left.width === right.width &&
|
|
||||||
left.height === right.height &&
|
|
||||||
left.padding === right.padding &&
|
|
||||||
left.containerWidth === right.containerWidth
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly _scheduleShowDragHandleFromSurfaceUpdate = () => {
|
|
||||||
if (this._surfaceElementUpdatedRafId !== null) return;
|
|
||||||
|
|
||||||
this._surfaceElementUpdatedRafId = requestAnimationFrame(() => {
|
|
||||||
this._surfaceElementUpdatedRafId = null;
|
|
||||||
if (!this.widget.isGfxDragHandleVisible) return;
|
|
||||||
this._showDragHandle();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly _handleEdgelessToolUpdated = (
|
private readonly _handleEdgelessToolUpdated = (
|
||||||
newTool: ToolOptionWithType
|
newTool: ToolOptionWithType
|
||||||
) => {
|
) => {
|
||||||
@@ -100,123 +43,46 @@ export class EdgelessWatcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.widget.center[0] !== center[0] ||
|
this.widget.center[0] !== center[0] &&
|
||||||
this.widget.center[1] !== center[1]
|
this.widget.center[1] !== center[1]
|
||||||
) {
|
) {
|
||||||
this.widget.center = [...center];
|
this.widget.center = [...center];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.widget.isGfxDragHandleVisible) {
|
if (this.widget.isGfxDragHandleVisible) {
|
||||||
const area = this.hoveredElemArea;
|
this._showDragHandle();
|
||||||
this._showDragHandle(area);
|
this._updateDragHoverRectTopLevelBlock();
|
||||||
this._updateDragHoverRectTopLevelBlock(area);
|
|
||||||
} else if (this.widget.activeDragHandle) {
|
} else if (this.widget.activeDragHandle) {
|
||||||
this.widget.hide();
|
this.widget.hide();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly _flushShowDragHandle = () => {
|
private readonly _showDragHandle = () => {
|
||||||
this._showDragHandleRafId = null;
|
if (!this.widget.anchorBlockId) return;
|
||||||
|
|
||||||
if (!this.widget.anchorBlockId.peek()) return;
|
|
||||||
|
|
||||||
const container = this.widget.dragHandleContainer;
|
const container = this.widget.dragHandleContainer;
|
||||||
const grabber = this.widget.dragHandleGrabber;
|
const grabber = this.widget.dragHandleGrabber;
|
||||||
if (!container || !grabber) return;
|
if (!container || !grabber) return;
|
||||||
|
|
||||||
const area = this._pendingHoveredElemArea ?? this.hoveredElemArea;
|
const area = this.hoveredElemArea;
|
||||||
this._pendingHoveredElemArea = null;
|
|
||||||
if (!area) return;
|
if (!area) return;
|
||||||
|
|
||||||
if (
|
container.style.transition = 'none';
|
||||||
this.widget.isGfxDragHandleVisible &&
|
container.style.paddingTop = `0px`;
|
||||||
this._isAreaEqual(this._lastAppliedHoveredElemArea, area)
|
container.style.paddingBottom = `0px`;
|
||||||
) {
|
container.style.left = `${area.left}px`;
|
||||||
return;
|
container.style.top = `${area.top}px`;
|
||||||
}
|
container.style.display = 'flex';
|
||||||
|
|
||||||
if (container.style.transition !== 'none') {
|
|
||||||
container.style.transition = 'none';
|
|
||||||
}
|
|
||||||
const nextPaddingTop = '0px';
|
|
||||||
if (container.style.paddingTop !== nextPaddingTop) {
|
|
||||||
container.style.paddingTop = nextPaddingTop;
|
|
||||||
}
|
|
||||||
const nextPaddingBottom = '0px';
|
|
||||||
if (container.style.paddingBottom !== nextPaddingBottom) {
|
|
||||||
container.style.paddingBottom = nextPaddingBottom;
|
|
||||||
}
|
|
||||||
const nextLeft = `${area.left}px`;
|
|
||||||
if (container.style.left !== nextLeft) {
|
|
||||||
container.style.left = nextLeft;
|
|
||||||
}
|
|
||||||
const nextTop = `${area.top}px`;
|
|
||||||
if (container.style.top !== nextTop) {
|
|
||||||
container.style.top = nextTop;
|
|
||||||
}
|
|
||||||
if (container.style.display !== 'flex') {
|
|
||||||
container.style.display = 'flex';
|
|
||||||
}
|
|
||||||
|
|
||||||
this.widget.handleAnchorModelDisposables();
|
this.widget.handleAnchorModelDisposables();
|
||||||
|
|
||||||
this.widget.activeDragHandle = 'gfx';
|
this.widget.activeDragHandle = 'gfx';
|
||||||
this._lastAppliedHoveredElemArea = this._cloneArea(area);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly _showDragHandle = (area?: HoveredElemArea | null) => {
|
private readonly _updateDragHoverRectTopLevelBlock = () => {
|
||||||
const nextArea = area ?? this.hoveredElemArea;
|
|
||||||
this._pendingHoveredElemArea = nextArea;
|
|
||||||
if (!this._pendingHoveredElemArea) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
this.widget.isGfxDragHandleVisible &&
|
|
||||||
this._showDragHandleRafId === null &&
|
|
||||||
this._isAreaEqual(
|
|
||||||
this._lastAppliedHoveredElemArea,
|
|
||||||
this._pendingHoveredElemArea
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this._showDragHandleRafId !== null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._showDragHandleRafId = requestAnimationFrame(
|
|
||||||
this._flushShowDragHandle
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly _updateDragHoverRectTopLevelBlock = (
|
|
||||||
area?: HoveredElemArea | null
|
|
||||||
) => {
|
|
||||||
if (!this.widget.dragHoverRect) return;
|
if (!this.widget.dragHoverRect) return;
|
||||||
|
|
||||||
const nextArea = area ?? this.hoveredElemArea;
|
this.widget.dragHoverRect = this.hoveredElemAreaRect;
|
||||||
if (!nextArea) {
|
|
||||||
this.widget.dragHoverRect = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextRect = new Rect(
|
|
||||||
nextArea.left,
|
|
||||||
nextArea.top,
|
|
||||||
nextArea.right,
|
|
||||||
nextArea.bottom
|
|
||||||
);
|
|
||||||
const prevRect = this.widget.dragHoverRect;
|
|
||||||
if (
|
|
||||||
prevRect &&
|
|
||||||
prevRect.left === nextRect.left &&
|
|
||||||
prevRect.top === nextRect.top &&
|
|
||||||
prevRect.width === nextRect.width &&
|
|
||||||
prevRect.height === nextRect.height
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.widget.dragHoverRect = nextRect;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
get gfx() {
|
get gfx() {
|
||||||
@@ -257,7 +123,7 @@ export class EdgelessWatcher {
|
|||||||
return new Rect(area.left, area.top, area.right, area.bottom);
|
return new Rect(area.left, area.top, area.right, area.bottom);
|
||||||
}
|
}
|
||||||
|
|
||||||
get hoveredElemArea(): HoveredElemArea | null {
|
get hoveredElemArea() {
|
||||||
const edgelessElement = this.widget.anchorEdgelessElement.peek();
|
const edgelessElement = this.widget.anchorEdgelessElement.peek();
|
||||||
|
|
||||||
if (!edgelessElement) return null;
|
if (!edgelessElement) return null;
|
||||||
@@ -308,19 +174,6 @@ export class EdgelessWatcher {
|
|||||||
viewport.viewportUpdated.subscribe(this._handleEdgelessViewPortUpdated)
|
viewport.viewportUpdated.subscribe(this._handleEdgelessViewPortUpdated)
|
||||||
);
|
);
|
||||||
|
|
||||||
disposables.add(() => {
|
|
||||||
if (this._showDragHandleRafId !== null) {
|
|
||||||
cancelAnimationFrame(this._showDragHandleRafId);
|
|
||||||
this._showDragHandleRafId = null;
|
|
||||||
}
|
|
||||||
if (this._surfaceElementUpdatedRafId !== null) {
|
|
||||||
cancelAnimationFrame(this._surfaceElementUpdatedRafId);
|
|
||||||
this._surfaceElementUpdatedRafId = null;
|
|
||||||
}
|
|
||||||
this._pendingHoveredElemArea = null;
|
|
||||||
this._lastAppliedHoveredElemArea = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
disposables.add(
|
disposables.add(
|
||||||
selection.slots.updated.subscribe(() => {
|
selection.slots.updated.subscribe(() => {
|
||||||
this.updateAnchorElement();
|
this.updateAnchorElement();
|
||||||
@@ -363,7 +216,7 @@ export class EdgelessWatcher {
|
|||||||
this.widget.hide();
|
this.widget.hide();
|
||||||
}
|
}
|
||||||
if (payload.type === 'update') {
|
if (payload.type === 'update') {
|
||||||
this._scheduleShowDragHandleFromSurfaceUpdate();
|
this._showDragHandle();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -371,10 +224,9 @@ export class EdgelessWatcher {
|
|||||||
|
|
||||||
if (surface) {
|
if (surface) {
|
||||||
disposables.add(
|
disposables.add(
|
||||||
surface.elementUpdated.subscribe(({ id }) => {
|
surface.elementUpdated.subscribe(() => {
|
||||||
if (this.widget.isGfxDragHandleVisible) {
|
if (this.widget.isGfxDragHandleVisible) {
|
||||||
if (id !== this.widget.anchorBlockId.peek()) return;
|
this._showDragHandle();
|
||||||
this._scheduleShowDragHandleFromSurfaceUpdate();
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -153,10 +153,6 @@ export class PointerEventWatcher {
|
|||||||
|
|
||||||
private _lastShowedBlock: { id: string; el: BlockComponent } | null = null;
|
private _lastShowedBlock: { id: string; el: BlockComponent } | null = null;
|
||||||
|
|
||||||
private _lastPointerHitBlockId: string | null = null;
|
|
||||||
|
|
||||||
private _lastPointerHitBlockElement: Element | null = null;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When pointer move on block, should show drag handle
|
* When pointer move on block, should show drag handle
|
||||||
* And update hover block id and path
|
* And update hover block id and path
|
||||||
@@ -173,7 +169,6 @@ export class PointerEventWatcher {
|
|||||||
point
|
point
|
||||||
);
|
);
|
||||||
if (!closestBlock) {
|
if (!closestBlock) {
|
||||||
this._lastPointerHitBlockId = null;
|
|
||||||
this.widget.anchorBlockId.value = null;
|
this.widget.anchorBlockId.value = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -242,38 +237,19 @@ export class PointerEventWatcher {
|
|||||||
|
|
||||||
const state = ctx.get('pointerState');
|
const state = ctx.get('pointerState');
|
||||||
|
|
||||||
|
// When pointer is moving, should do nothing
|
||||||
|
if (state.delta.x !== 0 && state.delta.y !== 0) return;
|
||||||
|
|
||||||
const { target } = state.raw;
|
const { target } = state.raw;
|
||||||
const element = captureEventTarget(target);
|
const element = captureEventTarget(target);
|
||||||
// When pointer not on block or on dragging, should do nothing
|
// When pointer not on block or on dragging, should do nothing
|
||||||
if (!element) {
|
if (!element) return;
|
||||||
this._lastPointerHitBlockId = null;
|
|
||||||
this._lastPointerHitBlockElement = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// When pointer on drag handle, should do nothing
|
// When pointer on drag handle, should do nothing
|
||||||
if (element.closest('.affine-drag-handle-container')) return;
|
if (element.closest('.affine-drag-handle-container')) return;
|
||||||
|
|
||||||
if (!this.widget.rootComponent) return;
|
if (!this.widget.rootComponent) return;
|
||||||
|
|
||||||
const hitBlock = element.closest(`[${BLOCK_ID_ATTR}]`);
|
|
||||||
const hitBlockId = hitBlock?.getAttribute(BLOCK_ID_ATTR) ?? null;
|
|
||||||
|
|
||||||
// Pointer move events are high-frequency. If hovered block identity is
|
|
||||||
// unchanged and the underlying block element is the same, skip the
|
|
||||||
// closest-note lookup.
|
|
||||||
if (
|
|
||||||
hitBlockId &&
|
|
||||||
this.widget.isBlockDragHandleVisible &&
|
|
||||||
hitBlockId === this._lastPointerHitBlockId &&
|
|
||||||
hitBlock === this._lastPointerHitBlockElement &&
|
|
||||||
isBlockIdEqual(this.widget.anchorBlockId.peek(), hitBlockId)
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._lastPointerHitBlockId = hitBlockId;
|
|
||||||
this._lastPointerHitBlockElement = hitBlock;
|
|
||||||
|
|
||||||
// When pointer out of note block hover area or inside database, should hide drag handle
|
// When pointer out of note block hover area or inside database, should hide drag handle
|
||||||
const point = new Point(state.raw.x, state.raw.y);
|
const point = new Point(state.raw.x, state.raw.y);
|
||||||
|
|
||||||
@@ -378,8 +354,6 @@ export class PointerEventWatcher {
|
|||||||
reset() {
|
reset() {
|
||||||
this._lastHoveredBlockId = null;
|
this._lastHoveredBlockId = null;
|
||||||
this._lastShowedBlock = null;
|
this._lastShowedBlock = null;
|
||||||
this._lastPointerHitBlockId = null;
|
|
||||||
this._lastPointerHitBlockElement = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
watch() {
|
watch() {
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ export class EdgelessZoomToolbar extends WithDisposable(LitElement) {
|
|||||||
|
|
||||||
this.disposables.add(
|
this.disposables.add(
|
||||||
effect(() => {
|
effect(() => {
|
||||||
void this.gfx.tool.currentToolName$.value;
|
this.gfx.tool.currentToolName$.value;
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -289,7 +289,7 @@ export class AffineKeyboardToolbar extends SignalWatcher(
|
|||||||
this.disposables.add(
|
this.disposables.add(
|
||||||
effect(() => {
|
effect(() => {
|
||||||
const std = this.rootComponent.std;
|
const std = this.rootComponent.std;
|
||||||
void std.selection.value;
|
std.selection.value;
|
||||||
// wait cursor updated
|
// wait cursor updated
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
this._scrollCurrentBlockIntoView();
|
this._scrollCurrentBlockIntoView();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { ExtensionType, Schema, Workspace } from '@blocksuite/store';
|
import type { ExtensionType, Schema, Workspace } from '@blocksuite/store';
|
||||||
// @ts-expect-error -- mammoth.browser has no compatible type declaration for this subpath.
|
// @ts-ignore
|
||||||
import { convertToHtml } from 'mammoth/mammoth.browser';
|
import { convertToHtml } from 'mammoth/mammoth.browser';
|
||||||
|
|
||||||
import { HtmlTransformer } from './html';
|
import { HtmlTransformer } from './html';
|
||||||
|
|||||||
@@ -10,12 +10,12 @@ import { Container } from '@blocksuite/global/di';
|
|||||||
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
|
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
|
||||||
import { sha } from '@blocksuite/global/utils';
|
import { sha } from '@blocksuite/global/utils';
|
||||||
import type {
|
import type {
|
||||||
DocMeta,
|
|
||||||
ExtensionType,
|
ExtensionType,
|
||||||
Schema,
|
Schema,
|
||||||
Store,
|
Store,
|
||||||
Workspace,
|
Workspace,
|
||||||
} from '@blocksuite/store';
|
} from '@blocksuite/store';
|
||||||
|
import type { DocMeta } from '@blocksuite/store';
|
||||||
import { extMimeMap, Transformer } from '@blocksuite/store';
|
import { extMimeMap, Transformer } from '@blocksuite/store';
|
||||||
|
|
||||||
import type { AssetMap, ImportedFileEntry, PathBlobIdMap } from './type.js';
|
import type { AssetMap, ImportedFileEntry, PathBlobIdMap } from './type.js';
|
||||||
|
|||||||
@@ -171,11 +171,9 @@ export class Unzip {
|
|||||||
const fileExt =
|
const fileExt =
|
||||||
fileName.lastIndexOf('.') === -1 ? '' : fileName.split('.').at(-1);
|
fileName.lastIndexOf('.') === -1 ? '' : fileName.split('.').at(-1);
|
||||||
const mime = extMimeMap.get(fileExt ?? '');
|
const mime = extMimeMap.get(fileExt ?? '');
|
||||||
const content = new File(
|
const content = new File([this.unzipped![path]], fileName, {
|
||||||
[new Uint8Array(this.unzipped![path]).buffer],
|
type: mime ?? '',
|
||||||
fileName,
|
}) as Blob;
|
||||||
mime ? { type: mime } : undefined
|
|
||||||
) as Blob;
|
|
||||||
|
|
||||||
const fixedPath = this.fixFileNameEncoding(path);
|
const fixedPath = this.fixFileNameEncoding(path);
|
||||||
|
|
||||||
|
|||||||
@@ -27,10 +27,10 @@ async function exportDocs(
|
|||||||
titleMiddleware(collection.meta.docMetas),
|
titleMiddleware(collection.meta.docMetas),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
const snapshots = await Promise.all(docs.map(job.docToSnapshot));
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
docs
|
snapshots
|
||||||
.map(job.docToSnapshot)
|
|
||||||
.filter((snapshot): snapshot is DocSnapshot => !!snapshot)
|
.filter((snapshot): snapshot is DocSnapshot => !!snapshot)
|
||||||
.map(async snapshot => {
|
.map(async snapshot => {
|
||||||
// Use the title and id as the snapshot file name
|
// Use the title and id as the snapshot file name
|
||||||
|
|||||||
@@ -34,7 +34,6 @@
|
|||||||
- [canSafeAddToContainer](functions/canSafeAddToContainer.md)
|
- [canSafeAddToContainer](functions/canSafeAddToContainer.md)
|
||||||
- [compareLayer](functions/compareLayer.md)
|
- [compareLayer](functions/compareLayer.md)
|
||||||
- [convert](functions/convert.md)
|
- [convert](functions/convert.md)
|
||||||
- [createRafCoalescer](functions/createRafCoalescer.md)
|
|
||||||
- [derive](functions/derive.md)
|
- [derive](functions/derive.md)
|
||||||
- [generateKeyBetween](functions/generateKeyBetween.md)
|
- [generateKeyBetween](functions/generateKeyBetween.md)
|
||||||
- [generateKeyBetweenV2](functions/generateKeyBetweenV2.md)
|
- [generateKeyBetweenV2](functions/generateKeyBetweenV2.md)
|
||||||
@@ -43,6 +42,5 @@
|
|||||||
- [GfxCompatible](functions/GfxCompatible.md)
|
- [GfxCompatible](functions/GfxCompatible.md)
|
||||||
- [isGfxGroupCompatibleModel](functions/isGfxGroupCompatibleModel.md)
|
- [isGfxGroupCompatibleModel](functions/isGfxGroupCompatibleModel.md)
|
||||||
- [local](functions/local.md)
|
- [local](functions/local.md)
|
||||||
- [measureOperation](functions/measureOperation.md)
|
|
||||||
- [observe](functions/observe.md)
|
- [observe](functions/observe.md)
|
||||||
- [watch](functions/watch.md)
|
- [watch](functions/watch.md)
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
[**BlockSuite API Documentation**](../../../../README.md)
|
|
||||||
|
|
||||||
***
|
|
||||||
|
|
||||||
[BlockSuite API Documentation](../../../../README.md) / [@blocksuite/std](../../README.md) / [gfx](../README.md) / createRafCoalescer
|
|
||||||
|
|
||||||
# Function: createRafCoalescer()
|
|
||||||
|
|
||||||
> **createRafCoalescer**\<`T`\>(`apply`): `RafCoalescer`\<`T`\>
|
|
||||||
|
|
||||||
Coalesce high-frequency updates and only process the latest payload in one frame.
|
|
||||||
|
|
||||||
## Type Parameters
|
|
||||||
|
|
||||||
### T
|
|
||||||
|
|
||||||
`T`
|
|
||||||
|
|
||||||
## Parameters
|
|
||||||
|
|
||||||
### apply
|
|
||||||
|
|
||||||
(`payload`) => `void`
|
|
||||||
|
|
||||||
## Returns
|
|
||||||
|
|
||||||
`RafCoalescer`\<`T`\>
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
[**BlockSuite API Documentation**](../../../../README.md)
|
|
||||||
|
|
||||||
***
|
|
||||||
|
|
||||||
[BlockSuite API Documentation](../../../../README.md) / [@blocksuite/std](../../README.md) / [gfx](../README.md) / measureOperation
|
|
||||||
|
|
||||||
# Function: measureOperation()
|
|
||||||
|
|
||||||
> **measureOperation**\<`T`\>(`name`, `fn`): `T`
|
|
||||||
|
|
||||||
Measure operation cost via Performance API when available.
|
|
||||||
|
|
||||||
Marks are always cleared, while measure entries are intentionally retained
|
|
||||||
so callers can inspect them from Performance tools.
|
|
||||||
|
|
||||||
## Type Parameters
|
|
||||||
|
|
||||||
### T
|
|
||||||
|
|
||||||
`T`
|
|
||||||
|
|
||||||
## Parameters
|
|
||||||
|
|
||||||
### name
|
|
||||||
|
|
||||||
`string`
|
|
||||||
|
|
||||||
### fn
|
|
||||||
|
|
||||||
() => `T`
|
|
||||||
|
|
||||||
## Returns
|
|
||||||
|
|
||||||
`T`
|
|
||||||
@@ -356,63 +356,3 @@ describe('convert decorator', () => {
|
|||||||
expect(elementModel.shapeType).toBe('rect');
|
expect(elementModel.shapeType).toBe('rect');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('surface group index cache', () => {
|
|
||||||
test('syncGroupChildrenIndex should replace outdated parent mappings', () => {
|
|
||||||
const { surfaceModel } = commonSetup();
|
|
||||||
const model = surfaceModel as any;
|
|
||||||
|
|
||||||
model._syncGroupChildrenIndex('group-1', ['a', 'b'], []);
|
|
||||||
expect(model._parentGroupMap.get('a')).toBe('group-1');
|
|
||||||
expect(model._parentGroupMap.get('b')).toBe('group-1');
|
|
||||||
|
|
||||||
model._syncGroupChildrenIndex('group-1', ['b', 'c']);
|
|
||||||
expect(model._parentGroupMap.has('a')).toBe(false);
|
|
||||||
expect(model._parentGroupMap.get('b')).toBe('group-1');
|
|
||||||
expect(model._parentGroupMap.get('c')).toBe('group-1');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('removeGroupFromChildrenIndex should clear both child snapshot and reverse lookup', () => {
|
|
||||||
const { surfaceModel } = commonSetup();
|
|
||||||
const model = surfaceModel as any;
|
|
||||||
|
|
||||||
model._syncGroupChildrenIndex('group-2', ['x', 'y'], []);
|
|
||||||
model._removeGroupFromChildrenIndex('group-2');
|
|
||||||
|
|
||||||
expect(model._groupChildIdsMap.has('group-2')).toBe(false);
|
|
||||||
expect(model._parentGroupMap.has('x')).toBe(false);
|
|
||||||
expect(model._parentGroupMap.has('y')).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('getGroup should recover from stale cache and update reverse lookup', () => {
|
|
||||||
const { surfaceModel } = commonSetup();
|
|
||||||
const model = surfaceModel as any;
|
|
||||||
|
|
||||||
const shapeId = surfaceModel.addElement({
|
|
||||||
type: 'testShape',
|
|
||||||
});
|
|
||||||
const shape = surfaceModel.getElementById(shapeId)!;
|
|
||||||
|
|
||||||
const fakeGroup = {
|
|
||||||
id: 'group-fallback',
|
|
||||||
hasChild: (element: { id: string }) => element.id === shapeId,
|
|
||||||
};
|
|
||||||
|
|
||||||
model._groupLikeModels.set(fakeGroup.id, fakeGroup);
|
|
||||||
model._parentGroupMap.set(shapeId, 'stale-group-id');
|
|
||||||
|
|
||||||
expect(surfaceModel.getGroup(shapeId)).toBe(fakeGroup);
|
|
||||||
expect(model._parentGroupMap.get(shapeId)).toBe(fakeGroup.id);
|
|
||||||
expect(model._parentGroupMap.has('stale-group-id')).toBe(false);
|
|
||||||
|
|
||||||
const otherShapeId = surfaceModel.addElement({
|
|
||||||
type: 'testShape',
|
|
||||||
});
|
|
||||||
model._parentGroupMap.set(otherShapeId, 'another-missing-group');
|
|
||||||
expect(surfaceModel.getGroup(otherShapeId)).toBeNull();
|
|
||||||
expect(model._parentGroupMap.has(otherShapeId)).toBe(false);
|
|
||||||
|
|
||||||
// keep one explicit check on element-based lookup path
|
|
||||||
expect(surfaceModel.getGroup(shape as any)).toBe(fakeGroup);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,165 +0,0 @@
|
|||||||
import { describe, expect, test, vi } from 'vitest';
|
|
||||||
|
|
||||||
import {
|
|
||||||
type GfxGroupCompatibleInterface,
|
|
||||||
gfxGroupCompatibleSymbol,
|
|
||||||
} from '../../gfx/model/base.js';
|
|
||||||
import type { GfxModel } from '../../gfx/model/model.js';
|
|
||||||
import {
|
|
||||||
batchAddChildren,
|
|
||||||
batchRemoveChildren,
|
|
||||||
canSafeAddToContainer,
|
|
||||||
descendantElementsImpl,
|
|
||||||
getTopElements,
|
|
||||||
} from '../../utils/tree.js';
|
|
||||||
|
|
||||||
type TestElement = {
|
|
||||||
id: string;
|
|
||||||
group: TestGroup | null;
|
|
||||||
groups: TestGroup[];
|
|
||||||
};
|
|
||||||
|
|
||||||
type TestGroup = TestElement & {
|
|
||||||
[gfxGroupCompatibleSymbol]: true;
|
|
||||||
childIds: string[];
|
|
||||||
childElements: GfxModel[];
|
|
||||||
addChild: (element: GfxModel) => void;
|
|
||||||
removeChild: (element: GfxModel) => void;
|
|
||||||
hasChild: (element: GfxModel) => boolean;
|
|
||||||
hasDescendant: (element: GfxModel) => boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const createElement = (id: string): TestElement => ({
|
|
||||||
id,
|
|
||||||
group: null,
|
|
||||||
groups: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
const createGroup = (id: string): TestGroup => {
|
|
||||||
const group: TestGroup = {
|
|
||||||
id,
|
|
||||||
[gfxGroupCompatibleSymbol]: true,
|
|
||||||
group: null,
|
|
||||||
groups: [],
|
|
||||||
childIds: [],
|
|
||||||
childElements: [],
|
|
||||||
addChild(element: GfxModel) {
|
|
||||||
const child = element as unknown as TestElement;
|
|
||||||
if (this.childElements.includes(element)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.childElements.push(element);
|
|
||||||
this.childIds.push(child.id);
|
|
||||||
child.group = this;
|
|
||||||
child.groups = [...this.groups, this];
|
|
||||||
},
|
|
||||||
removeChild(element: GfxModel) {
|
|
||||||
const child = element as unknown as TestElement;
|
|
||||||
this.childElements = this.childElements.filter(item => item !== element);
|
|
||||||
this.childIds = this.childIds.filter(id => id !== child.id);
|
|
||||||
if (child.group === this) {
|
|
||||||
child.group = null;
|
|
||||||
child.groups = [];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
hasChild(element: GfxModel) {
|
|
||||||
return this.childElements.includes(element);
|
|
||||||
},
|
|
||||||
hasDescendant(element: GfxModel) {
|
|
||||||
return descendantElementsImpl(
|
|
||||||
this as unknown as GfxGroupCompatibleInterface
|
|
||||||
).includes(element);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return group;
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('tree utils', () => {
|
|
||||||
test('batchAddChildren prefers container.addChildren and deduplicates', () => {
|
|
||||||
const a = createElement('a') as unknown as GfxModel;
|
|
||||||
const b = createElement('b') as unknown as GfxModel;
|
|
||||||
const container = {
|
|
||||||
addChildren: vi.fn(),
|
|
||||||
addChild: vi.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
batchAddChildren(container as any, [a, a, b]);
|
|
||||||
|
|
||||||
expect(container.addChildren).toHaveBeenCalledTimes(1);
|
|
||||||
expect(container.addChildren).toHaveBeenCalledWith([a, b]);
|
|
||||||
expect(container.addChild).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('batchRemoveChildren falls back to container.removeChild and deduplicates', () => {
|
|
||||||
const a = createElement('a') as unknown as GfxModel;
|
|
||||||
const b = createElement('b') as unknown as GfxModel;
|
|
||||||
const container = {
|
|
||||||
removeChild: vi.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
batchRemoveChildren(container as any, [a, a, b]);
|
|
||||||
|
|
||||||
expect(container.removeChild).toHaveBeenCalledTimes(2);
|
|
||||||
expect(container.removeChild).toHaveBeenNthCalledWith(1, a);
|
|
||||||
expect(container.removeChild).toHaveBeenNthCalledWith(2, b);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('getTopElements removes descendants when ancestors are selected', () => {
|
|
||||||
const root = createGroup('root');
|
|
||||||
const nested = createGroup('nested');
|
|
||||||
const leafA = createElement('leaf-a');
|
|
||||||
const leafB = createElement('leaf-b');
|
|
||||||
const leafC = createElement('leaf-c');
|
|
||||||
|
|
||||||
root.addChild(leafA as unknown as GfxModel);
|
|
||||||
root.addChild(nested as unknown as GfxModel);
|
|
||||||
nested.addChild(leafB as unknown as GfxModel);
|
|
||||||
|
|
||||||
const result = getTopElements([
|
|
||||||
root as unknown as GfxModel,
|
|
||||||
nested as unknown as GfxModel,
|
|
||||||
leafA as unknown as GfxModel,
|
|
||||||
leafB as unknown as GfxModel,
|
|
||||||
leafC as unknown as GfxModel,
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(result).toEqual([
|
|
||||||
root as unknown as GfxModel,
|
|
||||||
leafC as unknown as GfxModel,
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('descendantElementsImpl stops on cyclic graph', () => {
|
|
||||||
const groupA = createGroup('group-a');
|
|
||||||
const groupB = createGroup('group-b');
|
|
||||||
groupA.addChild(groupB as unknown as GfxModel);
|
|
||||||
groupB.addChild(groupA as unknown as GfxModel);
|
|
||||||
|
|
||||||
const descendants = descendantElementsImpl(groupA as unknown as any);
|
|
||||||
|
|
||||||
expect(descendants).toHaveLength(2);
|
|
||||||
expect(new Set(descendants).size).toBe(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('canSafeAddToContainer blocks self and circular descendants', () => {
|
|
||||||
const parent = createGroup('parent');
|
|
||||||
const child = createGroup('child');
|
|
||||||
const unrelated = createElement('plain');
|
|
||||||
|
|
||||||
parent.addChild(child as unknown as GfxModel);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
canSafeAddToContainer(parent as unknown as any, parent as unknown as any)
|
|
||||||
).toBe(false);
|
|
||||||
expect(
|
|
||||||
canSafeAddToContainer(child as unknown as any, parent as unknown as any)
|
|
||||||
).toBe(false);
|
|
||||||
expect(
|
|
||||||
canSafeAddToContainer(
|
|
||||||
parent as unknown as any,
|
|
||||||
unrelated as unknown as any
|
|
||||||
)
|
|
||||||
).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -190,7 +190,7 @@ export class Clipboard extends LifeCycleWatcher {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return slice;
|
return slice;
|
||||||
} catch {
|
} catch (error) {
|
||||||
const getDataByType = this._getDataByType(data);
|
const getDataByType = this._getDataByType(data);
|
||||||
const slice = await this._getSnapshotByPriority(
|
const slice = await this._getSnapshotByPriority(
|
||||||
type => getDataByType(type),
|
type => getDataByType(type),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import { LifeCycleWatcher } from '../extension/index.js';
|
||||||
import { BlockServiceIdentifier } from '../identifier.js';
|
import { BlockServiceIdentifier } from '../identifier.js';
|
||||||
import { LifeCycleWatcher } from './lifecycle-watcher.js';
|
|
||||||
|
|
||||||
export class ServiceManager extends LifeCycleWatcher {
|
export class ServiceManager extends LifeCycleWatcher {
|
||||||
static override readonly key = 'serviceManager';
|
static override readonly key = 'serviceManager';
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ export {
|
|||||||
SortOrder,
|
SortOrder,
|
||||||
} from '../utils/layer.js';
|
} from '../utils/layer.js';
|
||||||
export {
|
export {
|
||||||
batchAddChildren,
|
|
||||||
batchRemoveChildren,
|
|
||||||
canSafeAddToContainer,
|
canSafeAddToContainer,
|
||||||
descendantElementsImpl,
|
descendantElementsImpl,
|
||||||
getTopElements,
|
getTopElements,
|
||||||
@@ -96,8 +94,6 @@ export {
|
|||||||
type SurfaceBlockProps,
|
type SurfaceBlockProps,
|
||||||
type SurfaceMiddleware,
|
type SurfaceMiddleware,
|
||||||
} from './model/surface/surface-model.js';
|
} from './model/surface/surface-model.js';
|
||||||
export { measureOperation } from './perf.js';
|
|
||||||
export { createRafCoalescer, type RafCoalescer } from './raf-coalescer.js';
|
|
||||||
export { GfxSelectionManager } from './selection.js';
|
export { GfxSelectionManager } from './selection.js';
|
||||||
export {
|
export {
|
||||||
SurfaceMiddlewareBuilder,
|
SurfaceMiddlewareBuilder,
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import { GfxExtension, GfxExtensionIdentifier } from '../extension.js';
|
|||||||
import { GfxBlockElementModel } from '../model/gfx-block-model.js';
|
import { GfxBlockElementModel } from '../model/gfx-block-model.js';
|
||||||
import type { GfxModel } from '../model/model.js';
|
import type { GfxModel } from '../model/model.js';
|
||||||
import { GfxPrimitiveElementModel } from '../model/surface/element-model.js';
|
import { GfxPrimitiveElementModel } from '../model/surface/element-model.js';
|
||||||
import { createRafCoalescer } from '../raf-coalescer.js';
|
|
||||||
import type { GfxElementModelView } from '../view/view.js';
|
import type { GfxElementModelView } from '../view/view.js';
|
||||||
import { createInteractionContext, type SupportedEvents } from './event.js';
|
import { createInteractionContext, type SupportedEvents } from './event.js';
|
||||||
import {
|
import {
|
||||||
@@ -56,20 +55,6 @@ export const InteractivityIdentifier = GfxExtensionIdentifier(
|
|||||||
'interactivity-manager'
|
'interactivity-manager'
|
||||||
) as ServiceIdentifier<InteractivityManager>;
|
) as ServiceIdentifier<InteractivityManager>;
|
||||||
|
|
||||||
const DRAG_MOVE_RAF_THRESHOLD = 100;
|
|
||||||
const DRAG_MOVE_HEAVY_COST_MS = 4;
|
|
||||||
|
|
||||||
const shouldAllowDragMoveCoalescing = (
|
|
||||||
elements: { model: GfxModel }[]
|
|
||||||
): boolean => {
|
|
||||||
return elements.every(({ model }) => {
|
|
||||||
const isConnector = 'type' in model && model.type === 'connector';
|
|
||||||
const isContainer = 'childIds' in model;
|
|
||||||
|
|
||||||
return !isConnector && !isContainer;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export class InteractivityManager extends GfxExtension {
|
export class InteractivityManager extends GfxExtension {
|
||||||
static override key = 'interactivity-manager';
|
static override key = 'interactivity-manager';
|
||||||
|
|
||||||
@@ -396,18 +381,11 @@ export class InteractivityManager extends GfxExtension {
|
|||||||
};
|
};
|
||||||
let dragLastPos = internal.dragStartPos;
|
let dragLastPos = internal.dragStartPos;
|
||||||
let lastEvent = event;
|
let lastEvent = event;
|
||||||
let lastMoveDelta: [number, number] | null = null;
|
|
||||||
const canCoalesceDragMove = shouldAllowDragMoveCoalescing(
|
|
||||||
internal.elements
|
|
||||||
);
|
|
||||||
let shouldCoalesceDragMove =
|
|
||||||
canCoalesceDragMove &&
|
|
||||||
internal.elements.length >= DRAG_MOVE_RAF_THRESHOLD;
|
|
||||||
|
|
||||||
const applyDragMove = (event: PointerEvent) => {
|
|
||||||
const moveStart = performance.now();
|
|
||||||
lastEvent = event;
|
|
||||||
|
|
||||||
|
const viewportWatcher = this.gfx.viewport.viewportMoved.subscribe(() => {
|
||||||
|
onDragMove(lastEvent as PointerEvent);
|
||||||
|
});
|
||||||
|
const onDragMove = (event: PointerEvent) => {
|
||||||
dragLastPos = Point.from(
|
dragLastPos = Point.from(
|
||||||
this.gfx.viewport.toModelCoordFromClientCoord([event.x, event.y])
|
this.gfx.viewport.toModelCoordFromClientCoord([event.x, event.y])
|
||||||
);
|
);
|
||||||
@@ -429,16 +407,6 @@ export class InteractivityManager extends GfxExtension {
|
|||||||
moveContext[direction] = 0;
|
moveContext[direction] = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
|
||||||
lastMoveDelta &&
|
|
||||||
lastMoveDelta[0] === moveContext.dx &&
|
|
||||||
lastMoveDelta[1] === moveContext.dy
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
lastMoveDelta = [moveContext.dx, moveContext.dy];
|
|
||||||
|
|
||||||
this._safeExecute(() => {
|
this._safeExecute(() => {
|
||||||
activeExtensionHandlers.forEach(handler =>
|
activeExtensionHandlers.forEach(handler =>
|
||||||
handler?.onDragMove?.(moveContext)
|
handler?.onDragMove?.(moveContext)
|
||||||
@@ -455,39 +423,13 @@ export class InteractivityManager extends GfxExtension {
|
|||||||
elements: internal.elements,
|
elements: internal.elements,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
if (
|
|
||||||
canCoalesceDragMove &&
|
|
||||||
!shouldCoalesceDragMove &&
|
|
||||||
performance.now() - moveStart > DRAG_MOVE_HEAVY_COST_MS
|
|
||||||
) {
|
|
||||||
shouldCoalesceDragMove = true;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const dragMoveCoalescer = createRafCoalescer<PointerEvent>(applyDragMove);
|
|
||||||
|
|
||||||
const flushPendingDragMove = () => {
|
|
||||||
dragMoveCoalescer.flush();
|
|
||||||
};
|
|
||||||
const onDragMove = (event: PointerEvent) => {
|
|
||||||
if (!shouldCoalesceDragMove) {
|
|
||||||
applyDragMove(event);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
dragMoveCoalescer.schedule(event);
|
|
||||||
};
|
|
||||||
const viewportWatcher = this.gfx.viewport.viewportMoved.subscribe(() => {
|
|
||||||
onDragMove(lastEvent as PointerEvent);
|
|
||||||
});
|
|
||||||
const onDragEnd = (event: PointerEvent) => {
|
const onDragEnd = (event: PointerEvent) => {
|
||||||
this.activeInteraction$.value = null;
|
this.activeInteraction$.value = null;
|
||||||
|
|
||||||
host.removeEventListener('pointermove', onDragMove, false);
|
host.removeEventListener('pointermove', onDragMove, false);
|
||||||
host.removeEventListener('pointerup', onDragEnd, false);
|
host.removeEventListener('pointerup', onDragEnd, false);
|
||||||
viewportWatcher.unsubscribe();
|
viewportWatcher.unsubscribe();
|
||||||
flushPendingDragMove();
|
|
||||||
|
|
||||||
dragLastPos = Point.from(
|
dragLastPos = Point.from(
|
||||||
this.gfx.viewport.toModelCoordFromClientCoord([event.x, event.y])
|
this.gfx.viewport.toModelCoordFromClientCoord([event.x, event.y])
|
||||||
|
|||||||
@@ -101,8 +101,6 @@ export class LayerManager extends GfxExtension {
|
|||||||
|
|
||||||
layers: Layer[] = [];
|
layers: Layer[] = [];
|
||||||
|
|
||||||
private readonly _groupChildSnapshot = new Map<string, string[]>();
|
|
||||||
|
|
||||||
slots = {
|
slots = {
|
||||||
layerUpdated: new Subject<{
|
layerUpdated: new Subject<{
|
||||||
type: 'delete' | 'add' | 'update';
|
type: 'delete' | 'add' | 'update';
|
||||||
@@ -150,43 +148,6 @@ export class LayerManager extends GfxExtension {
|
|||||||
: 'block';
|
: 'block';
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getModelById(id: string): GfxModel | null {
|
|
||||||
if (!this._surface) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
this._surface.getElementById(id) ??
|
|
||||||
(this._doc.getModelById(id) as GfxModel | undefined) ??
|
|
||||||
null
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _getRelatedGroupElements(
|
|
||||||
group: GfxModel & GfxGroupCompatibleInterface,
|
|
||||||
oldChildIds?: string[]
|
|
||||||
) {
|
|
||||||
const elements = new Set<GfxModel>([group, ...group.descendantElements]);
|
|
||||||
|
|
||||||
oldChildIds?.forEach(id => {
|
|
||||||
const model = this._getModelById(id);
|
|
||||||
if (!model) return;
|
|
||||||
|
|
||||||
elements.add(model);
|
|
||||||
if (isGfxGroupCompatibleModel(model)) {
|
|
||||||
model.descendantElements.forEach(descendant => {
|
|
||||||
elements.add(descendant);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return [...elements];
|
|
||||||
}
|
|
||||||
|
|
||||||
private _syncGroupChildSnapshot(
|
|
||||||
group: GfxModel & GfxGroupCompatibleInterface
|
|
||||||
) {
|
|
||||||
this._groupChildSnapshot.set(group.id, [...group.childIds]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _initLayers() {
|
private _initLayers() {
|
||||||
let blockIdx = 0;
|
let blockIdx = 0;
|
||||||
let canvasIdx = 0;
|
let canvasIdx = 0;
|
||||||
@@ -526,29 +487,6 @@ export class LayerManager extends GfxExtension {
|
|||||||
updateLayersZIndex(layers, index);
|
updateLayersZIndex(layers, index);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _refreshElementsInLayer(elements: GfxModel[]) {
|
|
||||||
const uniqueElements = [...new Set(elements)];
|
|
||||||
|
|
||||||
uniqueElements.forEach(element => {
|
|
||||||
const modelType = this._getModelType(element);
|
|
||||||
if (modelType === 'canvas') {
|
|
||||||
removeFromOrderedArray(this.canvasElements, element);
|
|
||||||
insertToOrderedArray(this.canvasElements, element);
|
|
||||||
} else {
|
|
||||||
removeFromOrderedArray(this.blocks, element);
|
|
||||||
insertToOrderedArray(this.blocks, element);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
uniqueElements.forEach(element => {
|
|
||||||
this._removeFromLayer(element, this._getModelType(element));
|
|
||||||
});
|
|
||||||
|
|
||||||
uniqueElements.sort(compare).forEach(element => {
|
|
||||||
this._insertIntoLayer(element, this._getModelType(element));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private _reset() {
|
private _reset() {
|
||||||
const elements = (
|
const elements = (
|
||||||
this._doc
|
this._doc
|
||||||
@@ -574,17 +512,6 @@ export class LayerManager extends GfxExtension {
|
|||||||
|
|
||||||
this.canvasElements.sort(compare);
|
this.canvasElements.sort(compare);
|
||||||
this.blocks.sort(compare);
|
this.blocks.sort(compare);
|
||||||
this._groupChildSnapshot.clear();
|
|
||||||
this.canvasElements.forEach(element => {
|
|
||||||
if (isGfxGroupCompatibleModel(element)) {
|
|
||||||
this._syncGroupChildSnapshot(element);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.blocks.forEach(element => {
|
|
||||||
if (isGfxGroupCompatibleModel(element)) {
|
|
||||||
this._syncGroupChildSnapshot(element);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this._initLayers();
|
this._initLayers();
|
||||||
this._buildCanvasLayers();
|
this._buildCanvasLayers();
|
||||||
@@ -595,8 +522,7 @@ export class LayerManager extends GfxExtension {
|
|||||||
*/
|
*/
|
||||||
private _updateLayer(
|
private _updateLayer(
|
||||||
element: GfxModel | GfxLocalElementModel,
|
element: GfxModel | GfxLocalElementModel,
|
||||||
props?: Record<string, unknown>,
|
props?: Record<string, unknown>
|
||||||
oldValues?: Record<string, unknown>
|
|
||||||
) {
|
) {
|
||||||
const modelType = this._getModelType(element);
|
const modelType = this._getModelType(element);
|
||||||
const isLocalElem = element instanceof GfxLocalElementModel;
|
const isLocalElem = element instanceof GfxLocalElementModel;
|
||||||
@@ -613,16 +539,7 @@ export class LayerManager extends GfxExtension {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (shouldUpdateGroupChildren) {
|
if (shouldUpdateGroupChildren) {
|
||||||
const group = element as GfxModel & GfxGroupCompatibleInterface;
|
this._reset();
|
||||||
const oldChildIds = childIdsChanged
|
|
||||||
? Array.isArray(oldValues?.['childIds'])
|
|
||||||
? (oldValues['childIds'] as string[])
|
|
||||||
: this._groupChildSnapshot.get(group.id)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const relatedElements = this._getRelatedGroupElements(group, oldChildIds);
|
|
||||||
this._refreshElementsInLayer(relatedElements);
|
|
||||||
this._syncGroupChildSnapshot(group);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -664,13 +581,6 @@ export class LayerManager extends GfxExtension {
|
|||||||
element
|
element
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isContainer) {
|
|
||||||
this._syncGroupChildSnapshot(
|
|
||||||
element as GfxModel & GfxGroupCompatibleInterface
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this._insertIntoLayer(element as GfxModel, modelType);
|
this._insertIntoLayer(element as GfxModel, modelType);
|
||||||
|
|
||||||
if (isContainer) {
|
if (isContainer) {
|
||||||
@@ -738,26 +648,7 @@ export class LayerManager extends GfxExtension {
|
|||||||
const isLocalElem = element instanceof GfxLocalElementModel;
|
const isLocalElem = element instanceof GfxLocalElementModel;
|
||||||
|
|
||||||
if (isGroup) {
|
if (isGroup) {
|
||||||
const groupElements = this._getRelatedGroupElements(
|
this._reset();
|
||||||
element as GfxModel & GfxGroupCompatibleInterface
|
|
||||||
);
|
|
||||||
const descendants = groupElements.filter(model => model !== element);
|
|
||||||
|
|
||||||
if (!isLocalElem) {
|
|
||||||
const groupType = this._getModelType(element);
|
|
||||||
if (groupType === 'canvas') {
|
|
||||||
removeFromOrderedArray(this.canvasElements, element);
|
|
||||||
} else {
|
|
||||||
removeFromOrderedArray(this.blocks, element);
|
|
||||||
}
|
|
||||||
|
|
||||||
this._removeFromLayer(element, groupType);
|
|
||||||
}
|
|
||||||
|
|
||||||
this._groupChildSnapshot.delete(element.id);
|
|
||||||
|
|
||||||
this._refreshElementsInLayer(descendants);
|
|
||||||
this._buildCanvasLayers();
|
|
||||||
this.slots.layerUpdated.next({
|
this.slots.layerUpdated.next({
|
||||||
type: 'delete',
|
type: 'delete',
|
||||||
initiatingElement: element as GfxModel,
|
initiatingElement: element as GfxModel,
|
||||||
@@ -789,7 +680,6 @@ export class LayerManager extends GfxExtension {
|
|||||||
|
|
||||||
override unmounted() {
|
override unmounted() {
|
||||||
this.slots.layerUpdated.complete();
|
this.slots.layerUpdated.complete();
|
||||||
this._groupChildSnapshot.clear();
|
|
||||||
this._disposable.dispose();
|
this._disposable.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -887,10 +777,9 @@ export class LayerManager extends GfxExtension {
|
|||||||
|
|
||||||
update(
|
update(
|
||||||
element: GfxModel | GfxLocalElementModel,
|
element: GfxModel | GfxLocalElementModel,
|
||||||
props?: Record<string, unknown>,
|
props?: Record<string, unknown>
|
||||||
oldValues?: Record<string, unknown>
|
|
||||||
) {
|
) {
|
||||||
if (this._updateLayer(element, props, oldValues)) {
|
if (this._updateLayer(element, props)) {
|
||||||
this._buildCanvasLayers();
|
this._buildCanvasLayers();
|
||||||
this.slots.layerUpdated.next({
|
this.slots.layerUpdated.next({
|
||||||
type: 'update',
|
type: 'update',
|
||||||
@@ -978,11 +867,7 @@ export class LayerManager extends GfxExtension {
|
|||||||
this._disposable.add(
|
this._disposable.add(
|
||||||
surface.elementUpdated.subscribe(payload => {
|
surface.elementUpdated.subscribe(payload => {
|
||||||
if (payload.props['index'] || payload.props['childIds']) {
|
if (payload.props['index'] || payload.props['childIds']) {
|
||||||
this.update(
|
this.update(surface.getElementById(payload.id)!, payload.props);
|
||||||
surface.getElementById(payload.id)!,
|
|
||||||
payload.props,
|
|
||||||
payload.oldValues
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { signal } from '@preact/signals-core';
|
|||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import * as Y from 'yjs';
|
import * as Y from 'yjs';
|
||||||
|
|
||||||
import { measureOperation } from '../../perf.js';
|
|
||||||
import {
|
import {
|
||||||
type GfxGroupCompatibleInterface,
|
type GfxGroupCompatibleInterface,
|
||||||
isGfxGroupCompatibleModel,
|
isGfxGroupCompatibleModel,
|
||||||
@@ -75,10 +74,6 @@ export class SurfaceBlockModel extends BlockModel<SurfaceBlockProps> {
|
|||||||
|
|
||||||
protected _groupLikeModels = new Map<string, GfxGroupModel>();
|
protected _groupLikeModels = new Map<string, GfxGroupModel>();
|
||||||
|
|
||||||
protected _parentGroupMap = new Map<string, string>();
|
|
||||||
|
|
||||||
protected _groupChildIdsMap = new Map<string, string[]>();
|
|
||||||
|
|
||||||
protected _middlewares: SurfaceMiddleware[] = [];
|
protected _middlewares: SurfaceMiddleware[] = [];
|
||||||
|
|
||||||
protected _surfaceBlockModel = true;
|
protected _surfaceBlockModel = true;
|
||||||
@@ -138,44 +133,6 @@ export class SurfaceBlockModel extends BlockModel<SurfaceBlockProps> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _collectElementsToDelete(
|
|
||||||
id: string,
|
|
||||||
deleteElementIds: Set<string>,
|
|
||||||
orderedDeleteIds: string[],
|
|
||||||
deleteBlockIds: Set<string>
|
|
||||||
) {
|
|
||||||
if (deleteElementIds.has(id)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const element = this.getElementById(id);
|
|
||||||
if (!element) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteElementIds.add(id);
|
|
||||||
|
|
||||||
if (element instanceof GfxGroupLikeElementModel) {
|
|
||||||
element.childIds.forEach(childId => {
|
|
||||||
if (this.hasElementById(childId)) {
|
|
||||||
this._collectElementsToDelete(
|
|
||||||
childId,
|
|
||||||
deleteElementIds,
|
|
||||||
orderedDeleteIds,
|
|
||||||
deleteBlockIds
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.store.hasBlock(childId)) {
|
|
||||||
deleteBlockIds.add(childId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
orderedDeleteIds.push(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _createElementFromProps(
|
private _createElementFromProps(
|
||||||
props: Record<string, unknown>,
|
props: Record<string, unknown>,
|
||||||
options: {
|
options: {
|
||||||
@@ -290,26 +247,6 @@ export class SurfaceBlockModel extends BlockModel<SurfaceBlockProps> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private _emitElementUpdated(
|
|
||||||
model: GfxPrimitiveElementModel,
|
|
||||||
payload: ElementUpdatedData
|
|
||||||
) {
|
|
||||||
if (
|
|
||||||
isGfxGroupCompatibleModel(model) &&
|
|
||||||
('childIds' in payload.props || 'childIds' in payload.oldValues)
|
|
||||||
) {
|
|
||||||
const oldChildIds = Array.isArray(payload.oldValues['childIds'])
|
|
||||||
? (payload.oldValues['childIds'] as string[])
|
|
||||||
: undefined;
|
|
||||||
this._syncGroupChildrenIndex(model.id, model.childIds, oldChildIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.elementUpdated.next(payload);
|
|
||||||
Object.keys(payload.props).forEach(key => {
|
|
||||||
model.propsUpdated.next({ key });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private _initElementModels() {
|
private _initElementModels() {
|
||||||
const elementsYMap = this.elements.getValue()!;
|
const elementsYMap = this.elements.getValue()!;
|
||||||
const addToType = (type: string, model: GfxPrimitiveElementModel) => {
|
const addToType = (type: string, model: GfxPrimitiveElementModel) => {
|
||||||
@@ -323,7 +260,6 @@ export class SurfaceBlockModel extends BlockModel<SurfaceBlockProps> {
|
|||||||
|
|
||||||
if (isGfxGroupCompatibleModel(model)) {
|
if (isGfxGroupCompatibleModel(model)) {
|
||||||
this._groupLikeModels.set(model.id, model);
|
this._groupLikeModels.set(model.id, model);
|
||||||
this._syncGroupChildrenIndex(model.id, model.childIds, []);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const removeFromType = (type: string, model: GfxPrimitiveElementModel) => {
|
const removeFromType = (type: string, model: GfxPrimitiveElementModel) => {
|
||||||
@@ -334,10 +270,7 @@ export class SurfaceBlockModel extends BlockModel<SurfaceBlockProps> {
|
|||||||
sameTypeElements.splice(index, 1);
|
sameTypeElements.splice(index, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._parentGroupMap.delete(model.id);
|
if (this._groupLikeModels.has(model.id)) {
|
||||||
|
|
||||||
if (isGfxGroupCompatibleModel(model)) {
|
|
||||||
this._removeGroupFromChildrenIndex(model.id);
|
|
||||||
this._groupLikeModels.delete(model.id);
|
this._groupLikeModels.delete(model.id);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -371,9 +304,9 @@ export class SurfaceBlockModel extends BlockModel<SurfaceBlockProps> {
|
|||||||
element,
|
element,
|
||||||
{
|
{
|
||||||
onChange: payload => {
|
onChange: payload => {
|
||||||
this._emitElementUpdated(model.model, {
|
this.elementUpdated.next(payload);
|
||||||
...payload,
|
Object.keys(payload.props).forEach(key => {
|
||||||
id,
|
model.model.propsUpdated.next({ key });
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
skipFieldInit: true,
|
skipFieldInit: true,
|
||||||
@@ -418,10 +351,10 @@ export class SurfaceBlockModel extends BlockModel<SurfaceBlockProps> {
|
|||||||
val,
|
val,
|
||||||
{
|
{
|
||||||
onChange: payload => {
|
onChange: payload => {
|
||||||
this._emitElementUpdated(model.model, {
|
(this.elementUpdated.next(payload),
|
||||||
...payload,
|
Object.keys(payload.props).forEach(key => {
|
||||||
id: key,
|
model.model.propsUpdated.next({ key });
|
||||||
});
|
}));
|
||||||
},
|
},
|
||||||
skipFieldInit: true,
|
skipFieldInit: true,
|
||||||
}
|
}
|
||||||
@@ -438,12 +371,9 @@ export class SurfaceBlockModel extends BlockModel<SurfaceBlockProps> {
|
|||||||
Object.values(this.store.blocks.peek()).forEach(block => {
|
Object.values(this.store.blocks.peek()).forEach(block => {
|
||||||
if (isGfxGroupCompatibleModel(block.model)) {
|
if (isGfxGroupCompatibleModel(block.model)) {
|
||||||
this._groupLikeModels.set(block.id, block.model);
|
this._groupLikeModels.set(block.id, block.model);
|
||||||
this._syncGroupChildrenIndex(block.id, block.model.childIds, []);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this._rebuildGroupChildrenIndex();
|
|
||||||
|
|
||||||
elementsYMap.observe(onElementsMapChange);
|
elementsYMap.observe(onElementsMapChange);
|
||||||
|
|
||||||
const subscription = this.store.slots.blockUpdated.subscribe(payload => {
|
const subscription = this.store.slots.blockUpdated.subscribe(payload => {
|
||||||
@@ -451,17 +381,11 @@ export class SurfaceBlockModel extends BlockModel<SurfaceBlockProps> {
|
|||||||
case 'add':
|
case 'add':
|
||||||
if (isGfxGroupCompatibleModel(payload.model)) {
|
if (isGfxGroupCompatibleModel(payload.model)) {
|
||||||
this._groupLikeModels.set(payload.id, payload.model);
|
this._groupLikeModels.set(payload.id, payload.model);
|
||||||
this._syncGroupChildrenIndex(
|
|
||||||
payload.id,
|
|
||||||
payload.model.childIds,
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case 'delete':
|
case 'delete':
|
||||||
if (isGfxGroupCompatibleModel(payload.model)) {
|
if (isGfxGroupCompatibleModel(payload.model)) {
|
||||||
this._removeGroupFromChildrenIndex(payload.id);
|
|
||||||
this._groupLikeModels.delete(payload.id);
|
this._groupLikeModels.delete(payload.id);
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
@@ -471,16 +395,6 @@ export class SurfaceBlockModel extends BlockModel<SurfaceBlockProps> {
|
|||||||
group.removeChild(payload.model as GfxModel);
|
group.removeChild(payload.model as GfxModel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this._parentGroupMap.delete(payload.id);
|
|
||||||
|
|
||||||
break;
|
|
||||||
case 'update':
|
|
||||||
if (payload.props.key === 'childElementIds') {
|
|
||||||
const group = this.store.getBlock(payload.id)?.model;
|
|
||||||
if (group && isGfxGroupCompatibleModel(group)) {
|
|
||||||
this._syncGroupChildrenIndex(group.id, group.childIds);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -489,8 +403,6 @@ export class SurfaceBlockModel extends BlockModel<SurfaceBlockProps> {
|
|||||||
this.deleted.subscribe(() => {
|
this.deleted.subscribe(() => {
|
||||||
elementsYMap.unobserve(onElementsMapChange);
|
elementsYMap.unobserve(onElementsMapChange);
|
||||||
subscription.unsubscribe();
|
subscription.unsubscribe();
|
||||||
this._groupChildIdsMap.clear();
|
|
||||||
this._parentGroupMap.clear();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -588,71 +500,6 @@ export class SurfaceBlockModel extends BlockModel<SurfaceBlockProps> {
|
|||||||
return this._elementCtorMap[type];
|
return this._elementCtorMap[type];
|
||||||
}
|
}
|
||||||
|
|
||||||
private _rebuildGroupChildrenIndex() {
|
|
||||||
this._groupChildIdsMap.clear();
|
|
||||||
this._parentGroupMap.clear();
|
|
||||||
|
|
||||||
this._groupLikeModels.forEach(group => {
|
|
||||||
this._syncGroupChildrenIndex(group.id, group.childIds, []);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private _removeFromParentGroupIfNeeded(
|
|
||||||
element: GfxModel,
|
|
||||||
deleteElementIds: Set<string>
|
|
||||||
) {
|
|
||||||
const parentGroupId = this._parentGroupMap.get(element.id);
|
|
||||||
|
|
||||||
if (parentGroupId && deleteElementIds.has(parentGroupId)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let parentGroup: GfxGroupModel | null = null;
|
|
||||||
|
|
||||||
if (parentGroupId) {
|
|
||||||
parentGroup = this._groupLikeModels.get(parentGroupId) ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
parentGroup = parentGroup ?? this.getGroup(element.id);
|
|
||||||
|
|
||||||
if (parentGroup && !deleteElementIds.has(parentGroup.id)) {
|
|
||||||
// oxlint-disable-next-line unicorn/prefer-dom-node-remove
|
|
||||||
parentGroup.removeChild(element);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _removeGroupFromChildrenIndex(groupId: string) {
|
|
||||||
const previousChildIds = this._groupChildIdsMap.get(groupId) ?? [];
|
|
||||||
|
|
||||||
previousChildIds.forEach(childId => {
|
|
||||||
if (this._parentGroupMap.get(childId) === groupId) {
|
|
||||||
this._parentGroupMap.delete(childId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this._groupChildIdsMap.delete(groupId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _syncGroupChildrenIndex(
|
|
||||||
groupId: string,
|
|
||||||
nextChildIds: string[],
|
|
||||||
previousChildIds?: string[]
|
|
||||||
) {
|
|
||||||
const prev = previousChildIds ?? this._groupChildIdsMap.get(groupId) ?? [];
|
|
||||||
|
|
||||||
prev.forEach(childId => {
|
|
||||||
if (this._parentGroupMap.get(childId) === groupId) {
|
|
||||||
this._parentGroupMap.delete(childId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
nextChildIds.forEach(childId => {
|
|
||||||
this._parentGroupMap.set(childId, groupId);
|
|
||||||
});
|
|
||||||
|
|
||||||
this._groupChildIdsMap.set(groupId, [...nextChildIds]);
|
|
||||||
}
|
|
||||||
|
|
||||||
addElement<T extends object = Record<string, unknown>>(
|
addElement<T extends object = Record<string, unknown>>(
|
||||||
props: Partial<T> & { type: string }
|
props: Partial<T> & { type: string }
|
||||||
) {
|
) {
|
||||||
@@ -679,9 +526,9 @@ export class SurfaceBlockModel extends BlockModel<SurfaceBlockProps> {
|
|||||||
|
|
||||||
const elementModel = this._createElementFromProps(props, {
|
const elementModel = this._createElementFromProps(props, {
|
||||||
onChange: payload => {
|
onChange: payload => {
|
||||||
this._emitElementUpdated(elementModel.model, {
|
this.elementUpdated.next(payload);
|
||||||
...payload,
|
Object.keys(payload.props).forEach(key => {
|
||||||
id,
|
elementModel.model.propsUpdated.next({ key });
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -713,48 +560,24 @@ export class SurfaceBlockModel extends BlockModel<SurfaceBlockProps> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
measureOperation('edgeless:delete-element', () => {
|
this.store.transact(() => {
|
||||||
const deleteElementIds = new Set<string>();
|
const element = this.getElementById(id)!;
|
||||||
const orderedDeleteIds: string[] = [];
|
const group = this.getGroup(id);
|
||||||
const deleteBlockIds = new Set<string>();
|
|
||||||
|
|
||||||
this._collectElementsToDelete(
|
if (element instanceof GfxGroupLikeElementModel) {
|
||||||
id,
|
element.childIds.forEach(childId => {
|
||||||
deleteElementIds,
|
if (this.hasElementById(childId)) {
|
||||||
orderedDeleteIds,
|
this.deleteElement(childId);
|
||||||
deleteBlockIds
|
} else if (this.store.hasBlock(childId)) {
|
||||||
);
|
this.store.deleteBlock(this.store.getBlock(childId)!.model);
|
||||||
|
}
|
||||||
if (orderedDeleteIds.length === 0) {
|
});
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.store.transact(() => {
|
// oxlint-disable-next-line unicorn/prefer-dom-node-remove
|
||||||
orderedDeleteIds.forEach(elementId => {
|
group?.removeChild(element as GfxModel);
|
||||||
const element = this.getElementById(elementId);
|
|
||||||
|
|
||||||
if (!element) {
|
this.elements.getValue()!.delete(id);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._removeFromParentGroupIfNeeded(element, deleteElementIds);
|
|
||||||
this.elements.getValue()!.delete(elementId);
|
|
||||||
});
|
|
||||||
|
|
||||||
deleteBlockIds.forEach(blockId => {
|
|
||||||
const block = this.store.getBlock(blockId)?.model;
|
|
||||||
|
|
||||||
if (!block) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._removeFromParentGroupIfNeeded(
|
|
||||||
block as GfxModel,
|
|
||||||
deleteElementIds
|
|
||||||
);
|
|
||||||
this.store.deleteBlock(block);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -784,31 +607,18 @@ export class SurfaceBlockModel extends BlockModel<SurfaceBlockProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getGroup(elem: string | GfxModel): GfxGroupModel | null {
|
getGroup(elem: string | GfxModel): GfxGroupModel | null {
|
||||||
const id = typeof elem === 'string' ? elem : elem.id;
|
elem =
|
||||||
const parentGroupId = this._parentGroupMap.get(id);
|
|
||||||
|
|
||||||
if (parentGroupId) {
|
|
||||||
const group = this._groupLikeModels.get(parentGroupId);
|
|
||||||
if (group) {
|
|
||||||
return group;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._parentGroupMap.delete(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
const model =
|
|
||||||
typeof elem === 'string'
|
typeof elem === 'string'
|
||||||
? ((this.getElementById(elem) ??
|
? ((this.getElementById(elem) ??
|
||||||
this.store.getBlock(elem)?.model) as GfxModel)
|
this.store.getBlock(elem)?.model) as GfxModel)
|
||||||
: elem;
|
: elem;
|
||||||
|
|
||||||
if (!model) return null;
|
if (!elem) return null;
|
||||||
|
|
||||||
assertType<GfxModel>(model);
|
assertType<GfxModel>(elem);
|
||||||
|
|
||||||
for (const group of this._groupLikeModels.values()) {
|
for (const group of this._groupLikeModels.values()) {
|
||||||
if (group.hasChild(model)) {
|
if (group.hasChild(elem)) {
|
||||||
this._parentGroupMap.set(id, group.id);
|
|
||||||
return group;
|
return group;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
let opMeasureSeq = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Measure operation cost via Performance API when available.
|
|
||||||
*
|
|
||||||
* Marks are always cleared, while measure entries are intentionally retained
|
|
||||||
* so callers can inspect them from Performance tools.
|
|
||||||
*/
|
|
||||||
export const measureOperation = <T>(name: string, fn: () => T): T => {
|
|
||||||
if (
|
|
||||||
typeof performance === 'undefined' ||
|
|
||||||
typeof performance.mark !== 'function' ||
|
|
||||||
typeof performance.measure !== 'function'
|
|
||||||
) {
|
|
||||||
return fn();
|
|
||||||
}
|
|
||||||
|
|
||||||
const operationId = opMeasureSeq++;
|
|
||||||
const startMark = `${name}:${operationId}:start`;
|
|
||||||
const endMark = `${name}:${operationId}:end`;
|
|
||||||
performance.mark(startMark);
|
|
||||||
|
|
||||||
try {
|
|
||||||
return fn();
|
|
||||||
} finally {
|
|
||||||
performance.mark(endMark);
|
|
||||||
performance.measure(name, startMark, endMark);
|
|
||||||
performance.clearMarks(startMark);
|
|
||||||
performance.clearMarks(endMark);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
export interface RafCoalescer<T> {
|
|
||||||
cancel: () => void;
|
|
||||||
flush: () => void;
|
|
||||||
schedule: (payload: T) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
type FrameScheduler = (callback: FrameRequestCallback) => number;
|
|
||||||
type FrameCanceller = (id: number) => void;
|
|
||||||
|
|
||||||
const getFrameScheduler = (): FrameScheduler => {
|
|
||||||
if (typeof requestAnimationFrame === 'function') {
|
|
||||||
return requestAnimationFrame;
|
|
||||||
}
|
|
||||||
|
|
||||||
return callback => {
|
|
||||||
return globalThis.setTimeout(() => {
|
|
||||||
callback(
|
|
||||||
typeof performance !== 'undefined' ? performance.now() : Date.now()
|
|
||||||
);
|
|
||||||
}, 16) as unknown as number;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const getFrameCanceller = (): FrameCanceller => {
|
|
||||||
if (typeof cancelAnimationFrame === 'function') {
|
|
||||||
return cancelAnimationFrame;
|
|
||||||
}
|
|
||||||
|
|
||||||
return id => globalThis.clearTimeout(id);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Coalesce high-frequency updates and only process the latest payload in one frame.
|
|
||||||
*/
|
|
||||||
export const createRafCoalescer = <T>(
|
|
||||||
apply: (payload: T) => void
|
|
||||||
): RafCoalescer<T> => {
|
|
||||||
const scheduleFrame = getFrameScheduler();
|
|
||||||
const cancelFrame = getFrameCanceller();
|
|
||||||
|
|
||||||
let pendingPayload: T | undefined;
|
|
||||||
let hasPendingPayload = false;
|
|
||||||
let rafId: number | null = null;
|
|
||||||
|
|
||||||
const run = () => {
|
|
||||||
rafId = null;
|
|
||||||
if (!hasPendingPayload) return;
|
|
||||||
|
|
||||||
const payload = pendingPayload as T;
|
|
||||||
pendingPayload = undefined;
|
|
||||||
hasPendingPayload = false;
|
|
||||||
apply(payload);
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
schedule(payload: T) {
|
|
||||||
pendingPayload = payload;
|
|
||||||
hasPendingPayload = true;
|
|
||||||
|
|
||||||
if (rafId !== null) return;
|
|
||||||
rafId = scheduleFrame(run);
|
|
||||||
},
|
|
||||||
flush() {
|
|
||||||
if (rafId !== null) cancelFrame(rafId);
|
|
||||||
run();
|
|
||||||
},
|
|
||||||
cancel() {
|
|
||||||
if (rafId !== null) {
|
|
||||||
cancelFrame(rafId);
|
|
||||||
rafId = null;
|
|
||||||
}
|
|
||||||
pendingPayload = undefined;
|
|
||||||
hasPendingPayload = false;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -41,10 +41,6 @@ export function requestThrottledConnectedFrame<
|
|||||||
viewport: PropTypes.instanceOf(Viewport),
|
viewport: PropTypes.instanceOf(Viewport),
|
||||||
})
|
})
|
||||||
export class GfxViewportElement extends WithDisposable(ShadowlessElement) {
|
export class GfxViewportElement extends WithDisposable(ShadowlessElement) {
|
||||||
private static readonly VIEWPORT_REFRESH_PIXEL_THRESHOLD = 18;
|
|
||||||
|
|
||||||
private static readonly VIEWPORT_REFRESH_MAX_INTERVAL = 120;
|
|
||||||
|
|
||||||
static override styles = css`
|
static override styles = css`
|
||||||
gfx-viewport {
|
gfx-viewport {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -108,14 +104,6 @@ export class GfxViewportElement extends WithDisposable(ShadowlessElement) {
|
|||||||
|
|
||||||
private _lastVisibleModels?: Set<GfxBlockElementModel>;
|
private _lastVisibleModels?: Set<GfxBlockElementModel>;
|
||||||
|
|
||||||
private _lastViewportUpdate?: { zoom: number; center: [number, number] };
|
|
||||||
|
|
||||||
private _lastViewportRefreshTime = 0;
|
|
||||||
|
|
||||||
private _pendingViewportRefreshTimer: ReturnType<
|
|
||||||
typeof globalThis.setTimeout
|
|
||||||
> | null = null;
|
|
||||||
|
|
||||||
private readonly _pendingChildrenUpdates: {
|
private readonly _pendingChildrenUpdates: {
|
||||||
id: string;
|
id: string;
|
||||||
resolve: () => void;
|
resolve: () => void;
|
||||||
@@ -127,90 +115,26 @@ export class GfxViewportElement extends WithDisposable(ShadowlessElement) {
|
|||||||
|
|
||||||
private _updatingChildrenFlag = false;
|
private _updatingChildrenFlag = false;
|
||||||
|
|
||||||
private _clearPendingViewportRefreshTimer() {
|
|
||||||
if (this._pendingViewportRefreshTimer !== null) {
|
|
||||||
clearTimeout(this._pendingViewportRefreshTimer);
|
|
||||||
this._pendingViewportRefreshTimer = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _scheduleTrailingViewportRefresh() {
|
|
||||||
this._clearPendingViewportRefreshTimer();
|
|
||||||
this._pendingViewportRefreshTimer = globalThis.setTimeout(() => {
|
|
||||||
this._pendingViewportRefreshTimer = null;
|
|
||||||
this._lastViewportRefreshTime = performance.now();
|
|
||||||
this._refreshViewport();
|
|
||||||
}, GfxViewportElement.VIEWPORT_REFRESH_MAX_INTERVAL);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _refreshViewportByViewportUpdate(update: {
|
|
||||||
zoom: number;
|
|
||||||
center: [number, number];
|
|
||||||
}) {
|
|
||||||
const now = performance.now();
|
|
||||||
const previous = this._lastViewportUpdate;
|
|
||||||
this._lastViewportUpdate = {
|
|
||||||
zoom: update.zoom,
|
|
||||||
center: [update.center[0], update.center[1]],
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!previous) {
|
|
||||||
this._lastViewportRefreshTime = now;
|
|
||||||
this._refreshViewport();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const zoomChanged = Math.abs(previous.zoom - update.zoom) > 0.0001;
|
|
||||||
const centerMovedInPixel = Math.hypot(
|
|
||||||
(update.center[0] - previous.center[0]) * update.zoom,
|
|
||||||
(update.center[1] - previous.center[1]) * update.zoom
|
|
||||||
);
|
|
||||||
const timeoutReached =
|
|
||||||
now - this._lastViewportRefreshTime >=
|
|
||||||
GfxViewportElement.VIEWPORT_REFRESH_MAX_INTERVAL;
|
|
||||||
|
|
||||||
if (
|
|
||||||
zoomChanged ||
|
|
||||||
centerMovedInPixel >=
|
|
||||||
GfxViewportElement.VIEWPORT_REFRESH_PIXEL_THRESHOLD ||
|
|
||||||
timeoutReached
|
|
||||||
) {
|
|
||||||
this._clearPendingViewportRefreshTimer();
|
|
||||||
this._lastViewportRefreshTime = now;
|
|
||||||
this._refreshViewport();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._scheduleTrailingViewportRefresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
override connectedCallback(): void {
|
override connectedCallback(): void {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
|
|
||||||
|
const viewportUpdateCallback = () => {
|
||||||
|
this._refreshViewport();
|
||||||
|
};
|
||||||
|
|
||||||
if (!this.enableChildrenSchedule) {
|
if (!this.enableChildrenSchedule) {
|
||||||
delete this.scheduleUpdateChildren;
|
delete this.scheduleUpdateChildren;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._hideOutsideAndNoSelectedBlock();
|
this._hideOutsideAndNoSelectedBlock();
|
||||||
this.disposables.add(
|
this.disposables.add(
|
||||||
this.viewport.viewportUpdated.subscribe(update =>
|
this.viewport.viewportUpdated.subscribe(() => viewportUpdateCallback())
|
||||||
this._refreshViewportByViewportUpdate(update)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
this.disposables.add(
|
this.disposables.add(
|
||||||
this.viewport.sizeUpdated.subscribe(() => {
|
this.viewport.sizeUpdated.subscribe(() => viewportUpdateCallback())
|
||||||
this._clearPendingViewportRefreshTimer();
|
|
||||||
this._lastViewportRefreshTime = performance.now();
|
|
||||||
this._refreshViewport();
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
override disconnectedCallback(): void {
|
|
||||||
this._clearPendingViewportRefreshTimer();
|
|
||||||
super.disconnectedCallback();
|
|
||||||
}
|
|
||||||
|
|
||||||
override render() {
|
override render() {
|
||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,15 +10,25 @@ import type { InlineRange } from '../types.js';
|
|||||||
import { deltaInsertsToChunks } from '../utils/delta-convert.js';
|
import { deltaInsertsToChunks } from '../utils/delta-convert.js';
|
||||||
|
|
||||||
export class RenderService<TextAttributes extends BaseTextAttributes> {
|
export class RenderService<TextAttributes extends BaseTextAttributes> {
|
||||||
private _pendingRemoteInlineRangeSync = false;
|
private readonly _onYTextChange = (
|
||||||
|
_: Y.YTextEvent,
|
||||||
|
transaction: Y.Transaction
|
||||||
|
) => {
|
||||||
|
this.editor.slots.textChange.next();
|
||||||
|
|
||||||
private _carriageReturnValidationCounter = 0;
|
const yText = this.editor.yText;
|
||||||
|
|
||||||
private _renderVersion = 0;
|
if (yText.toString().includes('\r')) {
|
||||||
|
throw new BlockSuiteError(
|
||||||
|
ErrorCode.InlineEditorError,
|
||||||
|
'yText must not contain "\\r" because it will break the range synchronization'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.render();
|
||||||
|
|
||||||
private readonly _syncRemoteInlineRange = () => {
|
|
||||||
const inlineRange = this.editor.inlineRange$.peek();
|
const inlineRange = this.editor.inlineRange$.peek();
|
||||||
if (!inlineRange) return;
|
if (!inlineRange || transaction.local) return;
|
||||||
|
|
||||||
const lastStartRelativePosition = this.editor.lastStartRelativePosition;
|
const lastStartRelativePosition = this.editor.lastStartRelativePosition;
|
||||||
const lastEndRelativePosition = this.editor.lastEndRelativePosition;
|
const lastEndRelativePosition = this.editor.lastEndRelativePosition;
|
||||||
@@ -40,7 +50,7 @@ export class RenderService<TextAttributes extends BaseTextAttributes> {
|
|||||||
|
|
||||||
const startIndex = absoluteStart?.index;
|
const startIndex = absoluteStart?.index;
|
||||||
const endIndex = absoluteEnd?.index;
|
const endIndex = absoluteEnd?.index;
|
||||||
if (startIndex == null || endIndex == null) return;
|
if (!startIndex || !endIndex) return;
|
||||||
|
|
||||||
const newInlineRange: InlineRange = {
|
const newInlineRange: InlineRange = {
|
||||||
index: startIndex,
|
index: startIndex,
|
||||||
@@ -49,31 +59,7 @@ export class RenderService<TextAttributes extends BaseTextAttributes> {
|
|||||||
if (!this.editor.isValidInlineRange(newInlineRange)) return;
|
if (!this.editor.isValidInlineRange(newInlineRange)) return;
|
||||||
|
|
||||||
this.editor.setInlineRange(newInlineRange);
|
this.editor.setInlineRange(newInlineRange);
|
||||||
};
|
this.editor.syncInlineRange();
|
||||||
|
|
||||||
private readonly _onYTextChange = (
|
|
||||||
_: Y.YTextEvent,
|
|
||||||
transaction: Y.Transaction
|
|
||||||
) => {
|
|
||||||
this.editor.slots.textChange.next();
|
|
||||||
|
|
||||||
const yText = this.editor.yText;
|
|
||||||
|
|
||||||
if (
|
|
||||||
(this._carriageReturnValidationCounter++ & 0x3f) === 0 &&
|
|
||||||
yText.toString().includes('\r')
|
|
||||||
) {
|
|
||||||
throw new BlockSuiteError(
|
|
||||||
ErrorCode.InlineEditorError,
|
|
||||||
'yText must not contain "\\r" because it will break the range synchronization'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!transaction.local) {
|
|
||||||
this._pendingRemoteInlineRangeSync = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.render();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
mount = () => {
|
mount = () => {
|
||||||
@@ -84,7 +70,6 @@ export class RenderService<TextAttributes extends BaseTextAttributes> {
|
|||||||
editor.disposables.add({
|
editor.disposables.add({
|
||||||
dispose: () => {
|
dispose: () => {
|
||||||
yText.unobserve(this._onYTextChange);
|
yText.unobserve(this._onYTextChange);
|
||||||
this._pendingRemoteInlineRangeSync = false;
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -97,7 +82,6 @@ export class RenderService<TextAttributes extends BaseTextAttributes> {
|
|||||||
render = () => {
|
render = () => {
|
||||||
if (!this.editor.rootElement) return;
|
if (!this.editor.rootElement) return;
|
||||||
|
|
||||||
const renderVersion = ++this._renderVersion;
|
|
||||||
this._rendering = true;
|
this._rendering = true;
|
||||||
|
|
||||||
const rootElement = this.editor.rootElement;
|
const rootElement = this.editor.rootElement;
|
||||||
@@ -168,21 +152,11 @@ export class RenderService<TextAttributes extends BaseTextAttributes> {
|
|||||||
this.editor
|
this.editor
|
||||||
.waitForUpdate()
|
.waitForUpdate()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (renderVersion !== this._renderVersion) return;
|
|
||||||
if (this._pendingRemoteInlineRangeSync) {
|
|
||||||
this._pendingRemoteInlineRangeSync = false;
|
|
||||||
this._syncRemoteInlineRange();
|
|
||||||
}
|
|
||||||
this._rendering = false;
|
this._rendering = false;
|
||||||
this.editor.slots.renderComplete.next();
|
this.editor.slots.renderComplete.next();
|
||||||
this.editor.syncInlineRange();
|
this.editor.syncInlineRange();
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(console.error);
|
||||||
if (renderVersion === this._renderVersion) {
|
|
||||||
this._rendering = false;
|
|
||||||
}
|
|
||||||
console.error(error);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
rerenderWholeEditor = () => {
|
rerenderWholeEditor = () => {
|
||||||
|
|||||||
@@ -9,12 +9,7 @@ import {
|
|||||||
isVElement,
|
isVElement,
|
||||||
isVLine,
|
isVLine,
|
||||||
} from './guard.js';
|
} from './guard.js';
|
||||||
import {
|
import { calculateTextLength, getTextNodesFromElement } from './text.js';
|
||||||
calculateTextLength,
|
|
||||||
getInlineRootTextCache,
|
|
||||||
getTextNodesFromElement,
|
|
||||||
invalidateInlineRootTextCache,
|
|
||||||
} from './text.js';
|
|
||||||
|
|
||||||
export function nativePointToTextPoint(
|
export function nativePointToTextPoint(
|
||||||
node: unknown,
|
node: unknown,
|
||||||
@@ -72,6 +67,19 @@ export function textPointToDomPoint(
|
|||||||
|
|
||||||
if (!rootElement.contains(text)) return null;
|
if (!rootElement.contains(text)) return null;
|
||||||
|
|
||||||
|
const texts = getTextNodesFromElement(rootElement);
|
||||||
|
if (texts.length === 0) return null;
|
||||||
|
|
||||||
|
const goalIndex = texts.indexOf(text);
|
||||||
|
let index = 0;
|
||||||
|
for (const text of texts.slice(0, goalIndex)) {
|
||||||
|
index += calculateTextLength(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text.wholeText !== ZERO_WIDTH_FOR_EMPTY_LINE) {
|
||||||
|
index += offset;
|
||||||
|
}
|
||||||
|
|
||||||
const textParentElement = text.parentElement;
|
const textParentElement = text.parentElement;
|
||||||
if (!textParentElement) {
|
if (!textParentElement) {
|
||||||
throw new BlockSuiteError(
|
throw new BlockSuiteError(
|
||||||
@@ -89,44 +97,9 @@ export function textPointToDomPoint(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const textOffset = text.wholeText === ZERO_WIDTH_FOR_EMPTY_LINE ? 0 : offset;
|
|
||||||
|
|
||||||
for (let attempt = 0; attempt < 2; attempt++) {
|
|
||||||
const { textNodes, textNodeIndexMap, prefixLengths, lineIndexMap } =
|
|
||||||
getInlineRootTextCache(rootElement);
|
|
||||||
if (textNodes.length === 0) return null;
|
|
||||||
|
|
||||||
const goalIndex = textNodeIndexMap.get(text);
|
|
||||||
const lineIndex = lineIndexMap.get(lineElement);
|
|
||||||
if (goalIndex !== undefined && lineIndex !== undefined) {
|
|
||||||
const index = (prefixLengths[goalIndex] ?? 0) + textOffset;
|
|
||||||
return { text, index: index + lineIndex };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (attempt === 0) {
|
|
||||||
// MutationObserver marks cache dirty asynchronously; force one sync retry
|
|
||||||
// when a newly-added node is queried within the same task.
|
|
||||||
invalidateInlineRootTextCache(rootElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to linear scan when cache still misses. This keeps behavior
|
|
||||||
// stable even if MutationObserver-based invalidation lags behind.
|
|
||||||
const texts = getTextNodesFromElement(rootElement);
|
|
||||||
if (texts.length === 0) return null;
|
|
||||||
|
|
||||||
const goalIndex = texts.indexOf(text);
|
|
||||||
if (goalIndex < 0) return null;
|
|
||||||
|
|
||||||
let index = textOffset;
|
|
||||||
for (const beforeText of texts.slice(0, goalIndex)) {
|
|
||||||
index += calculateTextLength(beforeText);
|
|
||||||
}
|
|
||||||
|
|
||||||
const lineIndex = Array.from(rootElement.querySelectorAll('v-line')).indexOf(
|
const lineIndex = Array.from(rootElement.querySelectorAll('v-line')).indexOf(
|
||||||
lineElement
|
lineElement
|
||||||
);
|
);
|
||||||
if (lineIndex < 0) return null;
|
|
||||||
|
|
||||||
return { text, index: index + lineIndex };
|
return { text, index: index + lineIndex };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,92 +8,6 @@ export function calculateTextLength(text: Text): number {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type InlineRootTextCache = {
|
|
||||||
dirty: boolean;
|
|
||||||
observer: MutationObserver | null;
|
|
||||||
textNodes: Text[];
|
|
||||||
textNodeIndexMap: WeakMap<Text, number>;
|
|
||||||
prefixLengths: number[];
|
|
||||||
lineIndexMap: WeakMap<Element, number>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const inlineRootTextCaches = new WeakMap<HTMLElement, InlineRootTextCache>();
|
|
||||||
|
|
||||||
const buildInlineRootTextCache = (
|
|
||||||
rootElement: HTMLElement,
|
|
||||||
cache: InlineRootTextCache
|
|
||||||
) => {
|
|
||||||
const textSpanElements = Array.from(
|
|
||||||
rootElement.querySelectorAll('[data-v-text="true"]')
|
|
||||||
);
|
|
||||||
const textNodes: Text[] = [];
|
|
||||||
const textNodeIndexMap = new WeakMap<Text, number>();
|
|
||||||
const prefixLengths: number[] = [];
|
|
||||||
let prefixLength = 0;
|
|
||||||
|
|
||||||
for (const textSpanElement of textSpanElements) {
|
|
||||||
const textNode = Array.from(textSpanElement.childNodes).find(
|
|
||||||
(node): node is Text => node instanceof Text
|
|
||||||
);
|
|
||||||
if (!textNode) continue;
|
|
||||||
prefixLengths.push(prefixLength);
|
|
||||||
textNodeIndexMap.set(textNode, textNodes.length);
|
|
||||||
textNodes.push(textNode);
|
|
||||||
prefixLength += calculateTextLength(textNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
const lineIndexMap = new WeakMap<Element, number>();
|
|
||||||
const lineElements = Array.from(rootElement.querySelectorAll('v-line'));
|
|
||||||
for (const [index, line] of lineElements.entries()) {
|
|
||||||
lineIndexMap.set(line, index);
|
|
||||||
}
|
|
||||||
|
|
||||||
cache.textNodes = textNodes;
|
|
||||||
cache.textNodeIndexMap = textNodeIndexMap;
|
|
||||||
cache.prefixLengths = prefixLengths;
|
|
||||||
cache.lineIndexMap = lineIndexMap;
|
|
||||||
cache.dirty = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function invalidateInlineRootTextCache(rootElement: HTMLElement) {
|
|
||||||
const cache = inlineRootTextCaches.get(rootElement);
|
|
||||||
if (cache) {
|
|
||||||
cache.dirty = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getInlineRootTextCache(rootElement: HTMLElement) {
|
|
||||||
let cache = inlineRootTextCaches.get(rootElement);
|
|
||||||
if (!cache) {
|
|
||||||
cache = {
|
|
||||||
dirty: true,
|
|
||||||
observer: null,
|
|
||||||
textNodes: [],
|
|
||||||
textNodeIndexMap: new WeakMap(),
|
|
||||||
prefixLengths: [],
|
|
||||||
lineIndexMap: new WeakMap(),
|
|
||||||
};
|
|
||||||
inlineRootTextCaches.set(rootElement, cache);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!cache.observer && typeof MutationObserver !== 'undefined') {
|
|
||||||
cache.observer = new MutationObserver(() => {
|
|
||||||
cache!.dirty = true;
|
|
||||||
});
|
|
||||||
cache.observer.observe(rootElement, {
|
|
||||||
subtree: true,
|
|
||||||
childList: true,
|
|
||||||
characterData: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cache.dirty) {
|
|
||||||
buildInlineRootTextCache(rootElement, cache);
|
|
||||||
}
|
|
||||||
|
|
||||||
return cache;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getTextNodesFromElement(element: Element): Text[] {
|
export function getTextNodesFromElement(element: Element): Text[] {
|
||||||
const textSpanElements = Array.from(
|
const textSpanElements = Array.from(
|
||||||
element.querySelectorAll('[data-v-text="true"]')
|
element.querySelectorAll('[data-v-text="true"]')
|
||||||
|
|||||||
@@ -7,11 +7,6 @@ import {
|
|||||||
} from '../gfx/model/base.js';
|
} from '../gfx/model/base.js';
|
||||||
import type { GfxGroupModel, GfxModel } from '../gfx/model/model.js';
|
import type { GfxGroupModel, GfxModel } from '../gfx/model/model.js';
|
||||||
|
|
||||||
type BatchGroupContainer = GfxGroupCompatibleInterface & {
|
|
||||||
addChildren?: (elements: GfxModel[]) => void;
|
|
||||||
removeChildren?: (elements: GfxModel[]) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the top elements from the list of elements, which are in some tree structures.
|
* Get the top elements from the list of elements, which are in some tree structures.
|
||||||
*
|
*
|
||||||
@@ -31,65 +26,19 @@ type BatchGroupContainer = GfxGroupCompatibleInterface & {
|
|||||||
* The result should be `[G1, G4, E6]`
|
* The result should be `[G1, G4, E6]`
|
||||||
*/
|
*/
|
||||||
export function getTopElements(elements: GfxModel[]): GfxModel[] {
|
export function getTopElements(elements: GfxModel[]): GfxModel[] {
|
||||||
const uniqueElements = [...new Set(elements)];
|
const results = new Set(elements);
|
||||||
const selected = new Set(uniqueElements);
|
|
||||||
const topElements: GfxModel[] = [];
|
|
||||||
|
|
||||||
for (const element of uniqueElements) {
|
elements = [...new Set(elements)];
|
||||||
let ancestor = element.group;
|
|
||||||
let hasSelectedAncestor = false;
|
|
||||||
|
|
||||||
while (ancestor) {
|
elements.forEach(e1 => {
|
||||||
if (selected.has(ancestor as GfxModel)) {
|
elements.forEach(e2 => {
|
||||||
hasSelectedAncestor = true;
|
if (isGfxGroupCompatibleModel(e1) && e1.hasDescendant(e2)) {
|
||||||
break;
|
results.delete(e2);
|
||||||
}
|
}
|
||||||
ancestor = ancestor.group;
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasSelectedAncestor) {
|
|
||||||
topElements.push(element);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return topElements;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function batchAddChildren(
|
|
||||||
container: GfxGroupCompatibleInterface,
|
|
||||||
elements: GfxModel[]
|
|
||||||
) {
|
|
||||||
const uniqueElements = [...new Set(elements)];
|
|
||||||
if (uniqueElements.length === 0) return;
|
|
||||||
|
|
||||||
const batchContainer = container as BatchGroupContainer;
|
|
||||||
if (batchContainer.addChildren) {
|
|
||||||
batchContainer.addChildren(uniqueElements);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
uniqueElements.forEach(element => {
|
|
||||||
container.addChild(element);
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
export function batchRemoveChildren(
|
return [...results];
|
||||||
container: GfxGroupCompatibleInterface,
|
|
||||||
elements: GfxModel[]
|
|
||||||
) {
|
|
||||||
const uniqueElements = [...new Set(elements)];
|
|
||||||
if (uniqueElements.length === 0) return;
|
|
||||||
|
|
||||||
const batchContainer = container as BatchGroupContainer;
|
|
||||||
if (batchContainer.removeChildren) {
|
|
||||||
batchContainer.removeChildren(uniqueElements);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
uniqueElements.forEach(element => {
|
|
||||||
// oxlint-disable-next-line unicorn/prefer-dom-node-remove
|
|
||||||
container.removeChild(element);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function traverse(
|
function traverse(
|
||||||
@@ -115,9 +64,7 @@ function traverse(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (postCallBack) {
|
postCallBack && postCallBack(element);
|
||||||
postCallBack(element);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
innerTraverse(element);
|
innerTraverse(element);
|
||||||
|
|||||||
@@ -170,10 +170,10 @@ export class EditorHost extends SignalWatcher(
|
|||||||
...Object.values(widgetTags),
|
...Object.values(widgetTags),
|
||||||
];
|
];
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
elementsTags.map(async tag => {
|
elementsTags.map(tag => {
|
||||||
const element = this.renderRoot.querySelector(tag._$litStatic$);
|
const element = this.renderRoot.querySelector(tag._$litStatic$);
|
||||||
if (element instanceof LitElement) {
|
if (element instanceof LitElement) {
|
||||||
return await element.updateComplete;
|
return element.updateComplete;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -382,7 +382,6 @@ describe('addBlock', () => {
|
|||||||
|
|
||||||
const doc0 = collection.createDoc('doc:home');
|
const doc0 = collection.createDoc('doc:home');
|
||||||
const doc1 = collection.createDoc('space:doc1');
|
const doc1 = collection.createDoc('space:doc1');
|
||||||
// eslint-disable-next-line @typescript-eslint/await-thenable
|
|
||||||
await Promise.all([doc0.load(), doc1.load()]);
|
await Promise.all([doc0.load(), doc1.load()]);
|
||||||
assert.equal(collection.docs.size, 2);
|
assert.equal(collection.docs.size, 2);
|
||||||
const store0 = doc0.getStore({
|
const store0 = doc0.getStore({
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { minimatch } from 'minimatch';
|
import { minimatch } from 'minimatch';
|
||||||
|
|
||||||
import { SCHEMA_NOT_FOUND_MESSAGE } from '../consts.js';
|
import { SCHEMA_NOT_FOUND_MESSAGE } from '../consts.js';
|
||||||
import { BlockSchema, type BlockSchemaType } from '../model/block/zod.js';
|
import { BlockSchema, type BlockSchemaType } from '../model/index.js';
|
||||||
import { SchemaValidateError } from './error.js';
|
import { SchemaValidateError } from './error.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { BlockModel } from '../model/block/block-model.js';
|
import {
|
||||||
import { type DraftModel, toDraftModel } from '../model/block/draft.js';
|
BlockModel,
|
||||||
import type { Store } from '../model/store/store.js';
|
type DraftModel,
|
||||||
|
type Store,
|
||||||
|
toDraftModel,
|
||||||
|
} from '../model/index';
|
||||||
|
|
||||||
type SliceData = {
|
type SliceData = {
|
||||||
content: DraftModel[];
|
content: DraftModel[];
|
||||||
|
|||||||
@@ -3,11 +3,14 @@ import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
|
|||||||
import { nextTick } from '@blocksuite/global/utils';
|
import { nextTick } from '@blocksuite/global/utils';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
|
|
||||||
import { BlockModel } from '../model/block/block-model.js';
|
import {
|
||||||
import { type DraftModel, toDraftModel } from '../model/block/draft.js';
|
BlockModel,
|
||||||
import type { BlockSchemaType } from '../model/block/zod.js';
|
type BlockSchemaType,
|
||||||
import type { Store } from '../model/store/store.js';
|
type DraftModel,
|
||||||
import type { Schema } from '../schema/schema.js';
|
type Store,
|
||||||
|
toDraftModel,
|
||||||
|
} from '../model/index.js';
|
||||||
|
import type { Schema } from '../schema/index.js';
|
||||||
import { AssetsManager } from './assets.js';
|
import { AssetsManager } from './assets.js';
|
||||||
import { BaseBlockTransformer } from './base.js';
|
import { BaseBlockTransformer } from './base.js';
|
||||||
import type {
|
import type {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
"@blocksuite/icons": "^2.2.17",
|
"@blocksuite/icons": "^2.2.17",
|
||||||
"@floating-ui/dom": "^1.6.13",
|
"@floating-ui/dom": "^1.6.13",
|
||||||
"@lit/context": "^1.1.3",
|
"@lit/context": "^1.1.3",
|
||||||
"@lottiefiles/dotlottie-wc": "^0.5.0",
|
"@lottiefiles/dotlottie-wc": "^0.8.0",
|
||||||
"@preact/signals-core": "^1.8.0",
|
"@preact/signals-core": "^1.8.0",
|
||||||
"@toeverything/theme": "^1.1.23",
|
"@toeverything/theme": "^1.1.23",
|
||||||
"@vanilla-extract/css": "^1.17.0",
|
"@vanilla-extract/css": "^1.17.0",
|
||||||
|
|||||||
@@ -47,10 +47,7 @@ describe('frame', () => {
|
|||||||
expect(rect!.width).toBeGreaterThan(0);
|
expect(rect!.width).toBeGreaterThan(0);
|
||||||
expect(rect!.height).toBeGreaterThan(0);
|
expect(rect!.height).toBeGreaterThan(0);
|
||||||
|
|
||||||
const [titleX, titleY] = service.viewport.toModelCoordFromClientCoord([
|
const [titleX, titleY] = service.viewport.toModelCoord(rect!.x, rect!.y);
|
||||||
rect!.x,
|
|
||||||
rect!.y,
|
|
||||||
]);
|
|
||||||
expect(titleX).toBeCloseTo(0);
|
expect(titleX).toBeCloseTo(0);
|
||||||
expect(titleY).toBeLessThan(0);
|
expect(titleY).toBeLessThan(0);
|
||||||
|
|
||||||
@@ -69,11 +66,10 @@ describe('frame', () => {
|
|||||||
if (!nestedTitle) return;
|
if (!nestedTitle) return;
|
||||||
|
|
||||||
const nestedTitleRect = nestedTitle.getBoundingClientRect()!;
|
const nestedTitleRect = nestedTitle.getBoundingClientRect()!;
|
||||||
const [nestedTitleX, nestedTitleY] =
|
const [nestedTitleX, nestedTitleY] = service.viewport.toModelCoord(
|
||||||
service.viewport.toModelCoordFromClientCoord([
|
nestedTitleRect.x,
|
||||||
nestedTitleRect.x,
|
nestedTitleRect.y
|
||||||
nestedTitleRect.y,
|
);
|
||||||
]);
|
|
||||||
|
|
||||||
expect(nestedTitleX).toBeGreaterThan(20);
|
expect(nestedTitleX).toBeGreaterThan(20);
|
||||||
expect(nestedTitleY).toBeGreaterThan(20);
|
expect(nestedTitleY).toBeGreaterThan(20);
|
||||||
|
|||||||
@@ -5,14 +5,6 @@ import { wait } from '../utils/common.js';
|
|||||||
import { getSurface } from '../utils/edgeless.js';
|
import { getSurface } from '../utils/edgeless.js';
|
||||||
import { setupEditor } from '../utils/setup.js';
|
import { setupEditor } from '../utils/setup.js';
|
||||||
|
|
||||||
function expectPxCloseTo(
|
|
||||||
value: string,
|
|
||||||
expected: number,
|
|
||||||
precision: number = 2
|
|
||||||
) {
|
|
||||||
expect(Number.parseFloat(value)).toBeCloseTo(expected, precision);
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('Shape rendering with DOM renderer', () => {
|
describe('Shape rendering with DOM renderer', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const cleanup = await setupEditor('edgeless', [], {
|
const cleanup = await setupEditor('edgeless', [], {
|
||||||
@@ -67,8 +59,7 @@ describe('Shape rendering with DOM renderer', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(shapeElement).not.toBeNull();
|
expect(shapeElement).not.toBeNull();
|
||||||
const zoom = surfaceView.renderer.viewport.zoom;
|
expect(shapeElement?.style.borderRadius).toBe('6px');
|
||||||
expectPxCloseTo(shapeElement!.style.borderRadius, 6 * zoom);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should remove shape DOM node when element is deleted', async () => {
|
test('should remove shape DOM node when element is deleted', async () => {
|
||||||
@@ -119,9 +110,8 @@ describe('Shape rendering with DOM renderer', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(shapeElement).not.toBeNull();
|
expect(shapeElement).not.toBeNull();
|
||||||
const zoom = surfaceView.renderer.viewport.zoom;
|
expect(shapeElement?.style.width).toBe('80px');
|
||||||
expectPxCloseTo(shapeElement!.style.width, 80 * zoom);
|
expect(shapeElement?.style.height).toBe('60px');
|
||||||
expectPxCloseTo(shapeElement!.style.height, 60 * zoom);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should correctly render triangle shape', async () => {
|
test('should correctly render triangle shape', async () => {
|
||||||
@@ -142,8 +132,7 @@ describe('Shape rendering with DOM renderer', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(shapeElement).not.toBeNull();
|
expect(shapeElement).not.toBeNull();
|
||||||
const zoom = surfaceView.renderer.viewport.zoom;
|
expect(shapeElement?.style.width).toBe('80px');
|
||||||
expectPxCloseTo(shapeElement!.style.width, 80 * zoom);
|
expect(shapeElement?.style.height).toBe('60px');
|
||||||
expectPxCloseTo(shapeElement!.style.height, 60 * zoom);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -235,69 +235,6 @@ describe('connector', () => {
|
|||||||
expect(model.getConnectors(id2)).toEqual([]);
|
expect(model.getConnectors(id2)).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should update endpoint index when connector retargets', () => {
|
|
||||||
const id = model.addElement({
|
|
||||||
type: 'shape',
|
|
||||||
});
|
|
||||||
const id2 = model.addElement({
|
|
||||||
type: 'shape',
|
|
||||||
});
|
|
||||||
const id3 = model.addElement({
|
|
||||||
type: 'shape',
|
|
||||||
});
|
|
||||||
const connectorId = model.addElement({
|
|
||||||
type: 'connector',
|
|
||||||
source: {
|
|
||||||
id,
|
|
||||||
},
|
|
||||||
target: {
|
|
||||||
id: id2,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const connector = model.getElementById(connectorId)!;
|
|
||||||
|
|
||||||
expect(model.getConnectors(id).map(c => c.id)).toEqual([connector.id]);
|
|
||||||
expect(model.getConnectors(id2).map(c => c.id)).toEqual([connector.id]);
|
|
||||||
|
|
||||||
model.updateElement(connectorId, {
|
|
||||||
source: {
|
|
||||||
id: id3,
|
|
||||||
},
|
|
||||||
target: {
|
|
||||||
id: id2,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(model.getConnectors(id)).toEqual([]);
|
|
||||||
expect(model.getConnectors(id3).map(c => c.id)).toEqual([connector.id]);
|
|
||||||
expect(model.getConnectors(id2).map(c => c.id)).toEqual([connector.id]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('getConnectors should purge stale connector ids from endpoint cache', () => {
|
|
||||||
const shapeId = model.addElement({
|
|
||||||
type: 'shape',
|
|
||||||
});
|
|
||||||
const surfaceModel = model as any;
|
|
||||||
surfaceModel._connectorIdsByEndpoint.set(
|
|
||||||
shapeId,
|
|
||||||
new Set(['missing-connector-id'])
|
|
||||||
);
|
|
||||||
surfaceModel._connectorEndpoints.set('missing-connector-id', {
|
|
||||||
sourceId: shapeId,
|
|
||||||
targetId: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(model.getConnectors(shapeId)).toEqual([]);
|
|
||||||
expect(
|
|
||||||
surfaceModel._connectorIdsByEndpoint
|
|
||||||
.get(shapeId)
|
|
||||||
?.has('missing-connector-id') ?? false
|
|
||||||
).toBe(false);
|
|
||||||
expect(surfaceModel._connectorEndpoints.has('missing-connector-id')).toBe(
|
|
||||||
false
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should return null if connector are deleted', async () => {
|
test('should return null if connector are deleted', async () => {
|
||||||
const id = model.addElement({
|
const id = model.addElement({
|
||||||
type: 'shape',
|
type: 'shape',
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 4.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 8.2 KiB |
@@ -1,363 +0,0 @@
|
|||||||
import { LinkExtension } from '@blocksuite/affine-inline-link';
|
|
||||||
import { textKeymap } from '@blocksuite/affine-inline-preset';
|
|
||||||
import type {
|
|
||||||
ListBlockModel,
|
|
||||||
ParagraphBlockModel,
|
|
||||||
} from '@blocksuite/affine-model';
|
|
||||||
import { insertContent } from '@blocksuite/affine-rich-text';
|
|
||||||
import { REFERENCE_NODE } from '@blocksuite/affine-shared/consts';
|
|
||||||
import { createDefaultDoc } from '@blocksuite/affine-shared/utils';
|
|
||||||
import { TextSelection } from '@blocksuite/std';
|
|
||||||
import type { InlineMarkdownMatch } from '@blocksuite/std/inline';
|
|
||||||
import { Text } from '@blocksuite/store';
|
|
||||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
|
||||||
|
|
||||||
import { defaultSlashMenuConfig } from '../../../../affine/widgets/slash-menu/src/config.js';
|
|
||||||
import type {
|
|
||||||
SlashMenuActionItem,
|
|
||||||
SlashMenuItem,
|
|
||||||
} from '../../../../affine/widgets/slash-menu/src/types.js';
|
|
||||||
import { wait } from '../utils/common.js';
|
|
||||||
import { addNote } from '../utils/edgeless.js';
|
|
||||||
import { setupEditor } from '../utils/setup.js';
|
|
||||||
|
|
||||||
type RichTextElement = HTMLElement & {
|
|
||||||
inlineEditor: {
|
|
||||||
getFormat: (range: {
|
|
||||||
index: number;
|
|
||||||
length: number;
|
|
||||||
}) => Record<string, unknown>;
|
|
||||||
getInlineRange: () => { index: number; length: number } | null;
|
|
||||||
setInlineRange: (range: { index: number; length: number }) => void;
|
|
||||||
yTextString: string;
|
|
||||||
};
|
|
||||||
markdownMatches: InlineMarkdownMatch[];
|
|
||||||
undoManager: {
|
|
||||||
stopCapturing: () => void;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
function findSlashActionItem(
|
|
||||||
items: SlashMenuItem[],
|
|
||||||
name: string
|
|
||||||
): SlashMenuActionItem {
|
|
||||||
const item = items.find(entry => entry.name === name);
|
|
||||||
if (!item || !('action' in item)) {
|
|
||||||
throw new Error(`Cannot find slash-menu action: ${name}`);
|
|
||||||
}
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRichTextByBlockId(blockId: string): RichTextElement {
|
|
||||||
const block = editor.host?.view.getBlock(blockId) as HTMLElement | null;
|
|
||||||
if (!block) {
|
|
||||||
throw new Error(`Cannot find block view: ${blockId}`);
|
|
||||||
}
|
|
||||||
const richText = block.querySelector('rich-text') as RichTextElement | null;
|
|
||||||
if (!richText) {
|
|
||||||
throw new Error(`Cannot find rich-text for block: ${blockId}`);
|
|
||||||
}
|
|
||||||
return richText;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createParagraph(text = '') {
|
|
||||||
const noteId = addNote(doc);
|
|
||||||
const note = doc.getBlock(noteId)?.model;
|
|
||||||
if (!note) {
|
|
||||||
throw new Error('Cannot find note model');
|
|
||||||
}
|
|
||||||
const paragraph = note.children[0] as ParagraphBlockModel | undefined;
|
|
||||||
if (!paragraph) {
|
|
||||||
throw new Error('Cannot find paragraph model');
|
|
||||||
}
|
|
||||||
if (text) {
|
|
||||||
doc.updateBlock(paragraph, {
|
|
||||||
text: new Text(text),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await wait();
|
|
||||||
return {
|
|
||||||
noteId,
|
|
||||||
paragraphId: paragraph.id,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function setTextSelection(blockId: string, index: number, length: number) {
|
|
||||||
const to = length
|
|
||||||
? {
|
|
||||||
blockId,
|
|
||||||
index: index + length,
|
|
||||||
length: 0,
|
|
||||||
}
|
|
||||||
: null;
|
|
||||||
const selection = editor.host?.selection.create(TextSelection, {
|
|
||||||
from: {
|
|
||||||
blockId,
|
|
||||||
index,
|
|
||||||
length: 0,
|
|
||||||
},
|
|
||||||
to,
|
|
||||||
});
|
|
||||||
if (!selection) {
|
|
||||||
throw new Error('Cannot create text selection');
|
|
||||||
}
|
|
||||||
editor.host?.selection.setGroup('note', [selection]);
|
|
||||||
const richText = getRichTextByBlockId(blockId);
|
|
||||||
richText.inlineEditor.setInlineRange({ index, length });
|
|
||||||
}
|
|
||||||
|
|
||||||
async function triggerMarkdown(
|
|
||||||
blockId: string,
|
|
||||||
input: string,
|
|
||||||
matcherName: string
|
|
||||||
) {
|
|
||||||
const model = doc.getBlock(blockId)?.model as ParagraphBlockModel | undefined;
|
|
||||||
if (!model) {
|
|
||||||
throw new Error(`Cannot find paragraph model: ${blockId}`);
|
|
||||||
}
|
|
||||||
doc.updateBlock(model, {
|
|
||||||
text: new Text(input),
|
|
||||||
});
|
|
||||||
await wait();
|
|
||||||
|
|
||||||
const richText = getRichTextByBlockId(blockId);
|
|
||||||
const matcher = richText.markdownMatches.find(
|
|
||||||
item => item.name === matcherName
|
|
||||||
);
|
|
||||||
if (!matcher) {
|
|
||||||
throw new Error(`Cannot find markdown matcher: ${matcherName}`);
|
|
||||||
}
|
|
||||||
const inlineRange = { index: input.length, length: 0 };
|
|
||||||
setTextSelection(blockId, inlineRange.index, 0);
|
|
||||||
|
|
||||||
matcher.action({
|
|
||||||
inlineEditor: richText.inlineEditor as any,
|
|
||||||
prefixText: input,
|
|
||||||
inlineRange,
|
|
||||||
pattern: matcher.pattern,
|
|
||||||
undoManager: richText.undoManager as any,
|
|
||||||
});
|
|
||||||
|
|
||||||
await wait();
|
|
||||||
}
|
|
||||||
|
|
||||||
function mockKeyboardContext() {
|
|
||||||
const preventDefault = vi.fn();
|
|
||||||
const ctx = {
|
|
||||||
get(key: string) {
|
|
||||||
if (key === 'keyboardState') {
|
|
||||||
return { raw: { preventDefault } };
|
|
||||||
}
|
|
||||||
throw new Error(`Unexpected state key: ${key}`);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
return { ctx: ctx as any, preventDefault };
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
const cleanup = await setupEditor('page', [LinkExtension]);
|
|
||||||
return cleanup;
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('markdown/list/paragraph/quote/code/link', () => {
|
|
||||||
test('markdown list shortcut converts to todo list and keeps checked state', async () => {
|
|
||||||
const { noteId, paragraphId } = await createParagraph();
|
|
||||||
await triggerMarkdown(paragraphId, '[x] ', 'list');
|
|
||||||
|
|
||||||
const note = doc.getBlock(noteId)?.model;
|
|
||||||
if (!note) {
|
|
||||||
throw new Error('Cannot find note model');
|
|
||||||
}
|
|
||||||
const model = note.children[0] as ListBlockModel;
|
|
||||||
expect(model.flavour).toBe('affine:list');
|
|
||||||
expect(model.props.type).toBe('todo');
|
|
||||||
expect(model.props.checked).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('markdown heading and quote shortcuts convert paragraph type', async () => {
|
|
||||||
const { noteId: headingNoteId, paragraphId: headingParagraphId } =
|
|
||||||
await createParagraph();
|
|
||||||
await triggerMarkdown(headingParagraphId, '# ', 'heading');
|
|
||||||
const headingNote = doc.getBlock(headingNoteId)?.model;
|
|
||||||
if (!headingNote) {
|
|
||||||
throw new Error('Cannot find heading note model');
|
|
||||||
}
|
|
||||||
const headingModel = headingNote.children[0] as ParagraphBlockModel;
|
|
||||||
expect(headingModel.flavour).toBe('affine:paragraph');
|
|
||||||
expect(headingModel.props.type).toBe('h1');
|
|
||||||
|
|
||||||
const { noteId: quoteNoteId, paragraphId: quoteParagraphId } =
|
|
||||||
await createParagraph();
|
|
||||||
await triggerMarkdown(quoteParagraphId, '> ', 'heading');
|
|
||||||
const quoteNote = doc.getBlock(quoteNoteId)?.model;
|
|
||||||
if (!quoteNote) {
|
|
||||||
throw new Error('Cannot find quote note model');
|
|
||||||
}
|
|
||||||
const quoteModel = quoteNote.children[0] as ParagraphBlockModel;
|
|
||||||
expect(quoteModel.flavour).toBe('affine:paragraph');
|
|
||||||
expect(quoteModel.props.type).toBe('quote');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('markdown code shortcut converts paragraph to code block with language', async () => {
|
|
||||||
const { noteId, paragraphId } = await createParagraph();
|
|
||||||
await triggerMarkdown(paragraphId, '```ts ', 'code-block');
|
|
||||||
|
|
||||||
const note = doc.getBlock(noteId)?.model;
|
|
||||||
if (!note) {
|
|
||||||
throw new Error('Cannot find note model');
|
|
||||||
}
|
|
||||||
const model = note.children[0];
|
|
||||||
expect(model.flavour).toBe('affine:code');
|
|
||||||
expect((model as any).props.language).toBe('typescript');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('inline markdown converts style and link attributes', async () => {
|
|
||||||
const { paragraphId: boldParagraphId } = await createParagraph();
|
|
||||||
await triggerMarkdown(boldParagraphId, '**bold** ', 'bold');
|
|
||||||
const boldRichText = getRichTextByBlockId(boldParagraphId);
|
|
||||||
expect(boldRichText.inlineEditor.yTextString).toBe('bold');
|
|
||||||
expect(
|
|
||||||
boldRichText.inlineEditor.getFormat({ index: 1, length: 0 })
|
|
||||||
).toMatchObject({
|
|
||||||
bold: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { paragraphId: codeParagraphId } = await createParagraph();
|
|
||||||
await triggerMarkdown(codeParagraphId, '`code` ', 'code');
|
|
||||||
const codeRichText = getRichTextByBlockId(codeParagraphId);
|
|
||||||
expect(codeRichText.inlineEditor.yTextString).toBe('code');
|
|
||||||
expect(
|
|
||||||
codeRichText.inlineEditor.getFormat({ index: 1, length: 0 })
|
|
||||||
).toMatchObject({
|
|
||||||
code: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { paragraphId: linkParagraphId } = await createParagraph();
|
|
||||||
await triggerMarkdown(
|
|
||||||
linkParagraphId,
|
|
||||||
'[AFFiNE](https://affine.pro) ',
|
|
||||||
'link'
|
|
||||||
);
|
|
||||||
const linkRichText = getRichTextByBlockId(linkParagraphId);
|
|
||||||
expect(linkRichText.inlineEditor.yTextString).toBe('AFFiNE');
|
|
||||||
expect(
|
|
||||||
linkRichText.inlineEditor.getFormat({ index: 1, length: 0 })
|
|
||||||
).toMatchObject({
|
|
||||||
link: 'https://affine.pro',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('hotkey/bracket/linked-page', () => {
|
|
||||||
test('bracket keymap skips redundant right bracket in code block', async () => {
|
|
||||||
const { noteId, paragraphId } = await createParagraph();
|
|
||||||
await triggerMarkdown(paragraphId, '```ts ', 'code-block');
|
|
||||||
const note = doc.getBlock(noteId)?.model;
|
|
||||||
const codeId = note?.children[0]?.id;
|
|
||||||
if (!codeId) {
|
|
||||||
throw new Error('Cannot find code block id');
|
|
||||||
}
|
|
||||||
const codeModel = doc.getBlock(codeId)?.model;
|
|
||||||
if (!codeModel) {
|
|
||||||
throw new Error('Cannot find code block model');
|
|
||||||
}
|
|
||||||
const keymap = textKeymap(editor.std);
|
|
||||||
const leftHandler = keymap['('];
|
|
||||||
const rightHandler = keymap[')'];
|
|
||||||
expect(leftHandler).toBeDefined();
|
|
||||||
if (!rightHandler) {
|
|
||||||
throw new Error('Cannot find bracket key handlers');
|
|
||||||
}
|
|
||||||
|
|
||||||
doc.updateBlock(codeModel, {
|
|
||||||
text: new Text('()'),
|
|
||||||
});
|
|
||||||
await wait();
|
|
||||||
const codeRichText = getRichTextByBlockId(codeId);
|
|
||||||
setTextSelection(codeId, 1, 0);
|
|
||||||
const rightContext = mockKeyboardContext();
|
|
||||||
rightHandler(rightContext.ctx);
|
|
||||||
expect(rightContext.preventDefault).not.toHaveBeenCalled();
|
|
||||||
expect(codeRichText.inlineEditor.yTextString).toBe('()');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('consecutive linked-page reference nodes render as separate references', async () => {
|
|
||||||
const { paragraphId } = await createParagraph();
|
|
||||||
const paragraphModel = doc.getBlock(paragraphId)?.model as
|
|
||||||
| ParagraphBlockModel
|
|
||||||
| undefined;
|
|
||||||
if (!paragraphModel) {
|
|
||||||
throw new Error('Cannot find paragraph model');
|
|
||||||
}
|
|
||||||
const linkedDoc = createDefaultDoc(collection, {
|
|
||||||
title: 'Linked page',
|
|
||||||
});
|
|
||||||
|
|
||||||
setTextSelection(paragraphId, 0, 0);
|
|
||||||
insertContent(editor.std, paragraphModel, REFERENCE_NODE, {
|
|
||||||
reference: {
|
|
||||||
type: 'LinkedPage',
|
|
||||||
pageId: linkedDoc.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
insertContent(editor.std, paragraphModel, REFERENCE_NODE, {
|
|
||||||
reference: {
|
|
||||||
type: 'LinkedPage',
|
|
||||||
pageId: linkedDoc.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await wait();
|
|
||||||
expect(collection.docs.has(linkedDoc.id)).toBe(true);
|
|
||||||
|
|
||||||
const richText = getRichTextByBlockId(paragraphId);
|
|
||||||
expect(richText.querySelectorAll('affine-reference').length).toBe(2);
|
|
||||||
expect(richText.inlineEditor.yTextString.length).toBe(2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('slash-menu action semantics', () => {
|
|
||||||
test('date and move actions mutate block content/order as expected', async () => {
|
|
||||||
const noteId = addNote(doc);
|
|
||||||
const note = doc.getBlock(noteId)?.model;
|
|
||||||
if (!note) {
|
|
||||||
throw new Error('Cannot find note model');
|
|
||||||
}
|
|
||||||
const first = note.children[0] as ParagraphBlockModel;
|
|
||||||
const secondId = doc.addBlock(
|
|
||||||
'affine:paragraph',
|
|
||||||
{ text: new Text('second') },
|
|
||||||
noteId
|
|
||||||
);
|
|
||||||
const second = doc.getBlock(secondId)?.model as
|
|
||||||
| ParagraphBlockModel
|
|
||||||
| undefined;
|
|
||||||
if (!second) {
|
|
||||||
throw new Error('Cannot find second paragraph model');
|
|
||||||
}
|
|
||||||
doc.updateBlock(first, { text: new Text('first') });
|
|
||||||
await wait();
|
|
||||||
|
|
||||||
const slashItems = defaultSlashMenuConfig.items;
|
|
||||||
const items =
|
|
||||||
typeof slashItems === 'function'
|
|
||||||
? slashItems({ std: editor.std, model: first })
|
|
||||||
: slashItems;
|
|
||||||
const today = findSlashActionItem(items, 'Today');
|
|
||||||
const moveDown = findSlashActionItem(items, 'Move Down');
|
|
||||||
const moveUp = findSlashActionItem(items, 'Move Up');
|
|
||||||
|
|
||||||
moveDown.action({ std: editor.std, model: first });
|
|
||||||
await wait();
|
|
||||||
expect(note.children.map(child => child.id)).toEqual([second.id, first.id]);
|
|
||||||
|
|
||||||
moveUp.action({ std: editor.std, model: first });
|
|
||||||
await wait();
|
|
||||||
expect(note.children.map(child => child.id)).toEqual([first.id, second.id]);
|
|
||||||
|
|
||||||
setTextSelection(first.id, 0, 0);
|
|
||||||
today.action({ std: editor.std, model: first });
|
|
||||||
await wait();
|
|
||||||
const richText = getRichTextByBlockId(first.id);
|
|
||||||
expect(richText.inlineEditor.yTextString).toMatch(/\d{4}-\d{2}-\d{2}/);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -19,11 +19,7 @@ export default defineConfig(_configEnv =>
|
|||||||
browser: {
|
browser: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
headless: process.env.CI === 'true',
|
headless: process.env.CI === 'true',
|
||||||
instances: [
|
instances: [{ browser: 'chromium' }],
|
||||||
{ browser: 'chromium' },
|
|
||||||
{ browser: 'firefox' },
|
|
||||||
{ browser: 'webkit' },
|
|
||||||
],
|
|
||||||
provider: 'playwright',
|
provider: 'playwright',
|
||||||
isolate: false,
|
isolate: false,
|
||||||
viewport: {
|
viewport: {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
"@toeverything/pdfium": "^0.1.1",
|
"@toeverything/pdfium": "^0.1.1",
|
||||||
"@toeverything/y-indexeddb": "0.10.0-canary.9",
|
"@toeverything/y-indexeddb": "0.10.0-canary.9",
|
||||||
"@types/katex": "^0.16.7",
|
"@types/katex": "^0.16.7",
|
||||||
"browser-fs-access": "^0.37.0",
|
"browser-fs-access": "^0.38.0",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
"katex": "^0.16.27",
|
"katex": "^0.16.27",
|
||||||
"lit": "^3.2.0",
|
"lit": "^3.2.0",
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import eslint from '@eslint/js';
|
|||||||
import tsParser from '@typescript-eslint/parser';
|
import tsParser from '@typescript-eslint/parser';
|
||||||
import eslintConfigPrettier from 'eslint-config-prettier';
|
import eslintConfigPrettier from 'eslint-config-prettier';
|
||||||
import importX from 'eslint-plugin-import-x';
|
import importX from 'eslint-plugin-import-x';
|
||||||
import oxlint from 'eslint-plugin-oxlint';
|
|
||||||
import react from 'eslint-plugin-react';
|
import react from 'eslint-plugin-react';
|
||||||
import reactHooks from 'eslint-plugin-react-hooks';
|
import reactHooks from 'eslint-plugin-react-hooks';
|
||||||
import simpleImportSort from 'eslint-plugin-simple-import-sort';
|
import simpleImportSort from 'eslint-plugin-simple-import-sort';
|
||||||
@@ -17,10 +16,7 @@ const __require = createRequire(import.meta.url);
|
|||||||
|
|
||||||
const rxjs = __require('@smarttools/eslint-plugin-rxjs');
|
const rxjs = __require('@smarttools/eslint-plugin-rxjs');
|
||||||
|
|
||||||
const ignoreList = readFileSync(
|
const ignoreList = readFileSync('.prettierignore', 'utf-8')
|
||||||
new URL('.prettierignore', import.meta.url),
|
|
||||||
'utf-8'
|
|
||||||
)
|
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.filter(line => line.trim() && !line.startsWith('#'));
|
.filter(line => line.trim() && !line.startsWith('#'));
|
||||||
|
|
||||||
@@ -64,51 +60,105 @@ export default tseslint.config(
|
|||||||
'simple-import-sort': simpleImportSort,
|
'simple-import-sort': simpleImportSort,
|
||||||
rxjs,
|
rxjs,
|
||||||
unicorn,
|
unicorn,
|
||||||
oxlint,
|
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
...eslint.configs.recommended.rules,
|
...eslint.configs.recommended.rules,
|
||||||
...react.configs.recommended.rules,
|
...react.configs.recommended.rules,
|
||||||
...react.configs['jsx-runtime'].rules,
|
...react.configs['jsx-runtime'].rules,
|
||||||
...reactHooks.configs.recommended.rules,
|
...reactHooks.configs.recommended.rules,
|
||||||
...oxlint.configs.recommended.rules,
|
|
||||||
// covered by TypeScript
|
// covered by TypeScript
|
||||||
'no-dupe-args': 'off',
|
'no-dupe-args': 'off',
|
||||||
// the following rules are disabled because they are covered by oxlint
|
// the following rules are disabled because they are covered by oxlint
|
||||||
'array-callback-return': 'off',
|
'array-callback-return': 'off',
|
||||||
|
'constructor-super': 'off',
|
||||||
eqeqeq: 'off',
|
eqeqeq: 'off',
|
||||||
'getter-return': 'off',
|
'getter-return': 'off',
|
||||||
|
'for-direction': 'off',
|
||||||
|
'require-yield': 'off',
|
||||||
|
'use-isnan': 'off',
|
||||||
|
'valid-typeof': 'off',
|
||||||
'no-self-compare': 'off',
|
'no-self-compare': 'off',
|
||||||
'no-empty': 'off',
|
'no-empty': 'off',
|
||||||
|
'no-constant-binary-expression': 'off',
|
||||||
'no-constructor-return': 'off',
|
'no-constructor-return': 'off',
|
||||||
|
'no-func-assign': 'off',
|
||||||
|
'no-global-assign': 'off',
|
||||||
|
'no-ex-assign': 'off',
|
||||||
'no-fallthrough': 'off',
|
'no-fallthrough': 'off',
|
||||||
|
'no-irregular-whitespace': 'off',
|
||||||
|
'no-control-regex': 'off',
|
||||||
|
'no-with': 'off',
|
||||||
|
'no-debugger': 'off',
|
||||||
|
'no-const-assign': 'off',
|
||||||
|
'no-import-assign': 'off',
|
||||||
|
'no-setter-return': 'off',
|
||||||
|
'no-obj-calls': 'off',
|
||||||
|
'no-unsafe-negation': 'off',
|
||||||
|
'no-dupe-class-members': 'off',
|
||||||
|
'no-dupe-keys': 'off',
|
||||||
|
'no-this-before-super': 'off',
|
||||||
|
'no-empty-character-class': 'off',
|
||||||
|
'no-useless-catch': 'off',
|
||||||
|
'no-async-promise-executor': 'off',
|
||||||
'no-unreachable': 'off',
|
'no-unreachable': 'off',
|
||||||
|
'no-duplicate-case': 'off',
|
||||||
|
'no-empty-pattern': 'off',
|
||||||
|
'no-unused-labels': 'off',
|
||||||
|
'no-sparse-arrays': 'off',
|
||||||
|
'no-delete-var': 'off',
|
||||||
|
'no-compare-neg-zero': 'off',
|
||||||
'no-redeclare': 'off',
|
'no-redeclare': 'off',
|
||||||
'no-case-declarations': 'off',
|
'no-case-declarations': 'off',
|
||||||
|
'no-class-assign': 'off',
|
||||||
'no-var': 'off',
|
'no-var': 'off',
|
||||||
|
'no-self-assign': 'off',
|
||||||
'no-inner-declarations': 'off',
|
'no-inner-declarations': 'off',
|
||||||
|
'no-dupe-else-if': 'off',
|
||||||
|
'no-invalid-regexp': 'off',
|
||||||
|
'no-unsafe-finally': 'off',
|
||||||
'no-prototype-builtins': 'off',
|
'no-prototype-builtins': 'off',
|
||||||
|
'no-shadow-restricted-names': 'off',
|
||||||
|
'no-nonoctal-decimal-escape': 'off',
|
||||||
|
'no-constant-condition': 'off',
|
||||||
|
'no-useless-escape': 'off',
|
||||||
|
'no-unsafe-optional-chaining': 'off',
|
||||||
|
'no-extra-boolean-cast': 'off',
|
||||||
'no-regex-spaces': 'off',
|
'no-regex-spaces': 'off',
|
||||||
'no-unused-vars': 'off',
|
'no-unused-vars': 'off',
|
||||||
'no-undef': 'off',
|
'no-undef': 'off',
|
||||||
|
'no-cond-assign': 'off',
|
||||||
'react/jsx-no-useless-fragment': 'off',
|
'react/jsx-no-useless-fragment': 'off',
|
||||||
'react/no-unknown-property': 'off',
|
'react/no-unknown-property': 'off',
|
||||||
|
'react/no-string-refs': 'off',
|
||||||
|
'react/no-direct-mutation-state': 'off',
|
||||||
'react/require-render-return': 'off',
|
'react/require-render-return': 'off',
|
||||||
|
'react/jsx-no-undef': 'off',
|
||||||
|
'react/jsx-no-duplicate-props': 'off',
|
||||||
|
'react/jsx-key': 'off',
|
||||||
|
'react/no-danger-with-children': 'off',
|
||||||
'react/no-unescaped-entities': 'off',
|
'react/no-unescaped-entities': 'off',
|
||||||
|
'react/no-is-mounted': 'off',
|
||||||
|
'react/no-find-dom-node': 'off',
|
||||||
|
'react/no-children-prop': 'off',
|
||||||
|
'react/no-render-return-value': 'off',
|
||||||
'react/jsx-no-target-blank': 'off',
|
'react/jsx-no-target-blank': 'off',
|
||||||
'react/jsx-no-comment-textnodes': 'off',
|
'react/jsx-no-comment-textnodes': 'off',
|
||||||
'react/prop-types': 'off',
|
'react/prop-types': 'off',
|
||||||
'react-hooks/immutability': 'off',
|
|
||||||
'react-hooks/refs': 'off',
|
|
||||||
'react-hooks/set-state-in-effect': 'off',
|
|
||||||
'react-hooks/static-components': 'off',
|
|
||||||
'react-hooks/use-memo': 'off',
|
|
||||||
'sonarjs/no-useless-catch': 'off',
|
'sonarjs/no-useless-catch': 'off',
|
||||||
'@typescript-eslint/consistent-type-imports': 'off',
|
'@typescript-eslint/consistent-type-imports': 'off',
|
||||||
'@typescript-eslint/no-unused-vars': 'off',
|
'@typescript-eslint/no-unused-vars': 'off',
|
||||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||||
|
'@typescript-eslint/no-loss-of-precision': 'off',
|
||||||
'@typescript-eslint/ban-ts-comment': 'off',
|
'@typescript-eslint/ban-ts-comment': 'off',
|
||||||
|
'@typescript-eslint/triple-slash-reference': 'off',
|
||||||
|
'@typescript-eslint/no-non-null-asserted-optional-chain': 'off',
|
||||||
|
'@typescript-eslint/no-duplicate-enum-values': 'off',
|
||||||
|
'@typescript-eslint/no-extra-non-null-assertion': 'off',
|
||||||
|
'@typescript-eslint/no-misused-new': 'off',
|
||||||
'@typescript-eslint/prefer-for-of': 'error',
|
'@typescript-eslint/prefer-for-of': 'error',
|
||||||
|
'@typescript-eslint/no-unsafe-declaration-merging': 'off',
|
||||||
|
'@typescript-eslint/no-this-alias': 'off',
|
||||||
|
'@typescript-eslint/prefer-as-const': 'off',
|
||||||
'@typescript-eslint/no-var-requires': 'off',
|
'@typescript-eslint/no-var-requires': 'off',
|
||||||
'@typescript-eslint/no-namespace': 'off',
|
'@typescript-eslint/no-namespace': 'off',
|
||||||
'@typescript-eslint/no-unnecessary-type-constraint': 'off',
|
'@typescript-eslint/no-unnecessary-type-constraint': 'off',
|
||||||
@@ -117,13 +167,30 @@ export default tseslint.config(
|
|||||||
'@typescript-eslint/no-empty-function': 'off',
|
'@typescript-eslint/no-empty-function': 'off',
|
||||||
|
|
||||||
// rules that are not supported by oxlint
|
// rules that are not supported by oxlint
|
||||||
'no-unreachable-loop': 'error',
|
|
||||||
'@typescript-eslint/no-unsafe-function-type': 'error',
|
'@typescript-eslint/no-unsafe-function-type': 'error',
|
||||||
|
'@typescript-eslint/no-wrapper-object-types': 'error',
|
||||||
'@typescript-eslint/unified-signatures': 'error',
|
'@typescript-eslint/unified-signatures': 'error',
|
||||||
'@typescript-eslint/return-await': [
|
'@typescript-eslint/return-await': [
|
||||||
'error',
|
'error',
|
||||||
'error-handling-correctness-only',
|
'error-handling-correctness-only',
|
||||||
],
|
],
|
||||||
|
'@typescript-eslint/no-restricted-imports': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
patterns: [
|
||||||
|
{
|
||||||
|
group: ['**/dist'],
|
||||||
|
message: "Don't import from dist",
|
||||||
|
allowTypeImports: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: ['**/src'],
|
||||||
|
message: "Don't import from src",
|
||||||
|
allowTypeImports: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
'sonarjs/no-all-duplicated-branches': 'error',
|
'sonarjs/no-all-duplicated-branches': 'error',
|
||||||
'sonarjs/no-element-overwrite': 'error',
|
'sonarjs/no-element-overwrite': 'error',
|
||||||
'sonarjs/no-empty-collection': 'error',
|
'sonarjs/no-empty-collection': 'error',
|
||||||
@@ -131,6 +198,7 @@ export default tseslint.config(
|
|||||||
'sonarjs/no-identical-conditions': 'error',
|
'sonarjs/no-identical-conditions': 'error',
|
||||||
'sonarjs/no-identical-expressions': 'error',
|
'sonarjs/no-identical-expressions': 'error',
|
||||||
'sonarjs/no-ignored-return': 'error',
|
'sonarjs/no-ignored-return': 'error',
|
||||||
|
'sonarjs/no-one-iteration-loop': 'error',
|
||||||
'sonarjs/no-use-of-empty-return-value': 'error',
|
'sonarjs/no-use-of-empty-return-value': 'error',
|
||||||
'sonarjs/non-existent-operator': 'error',
|
'sonarjs/non-existent-operator': 'error',
|
||||||
'sonarjs/no-collapsible-if': 'error',
|
'sonarjs/no-collapsible-if': 'error',
|
||||||
@@ -166,6 +234,13 @@ export default tseslint.config(
|
|||||||
'error',
|
'error',
|
||||||
{ includeInternal: true },
|
{ includeInternal: true },
|
||||||
],
|
],
|
||||||
|
'react-hooks/exhaustive-deps': [
|
||||||
|
'warn',
|
||||||
|
{
|
||||||
|
additionalHooks:
|
||||||
|
'(useAsyncCallback|useCatchEventCallback|useDraggable|useDropTarget|useRefEffect)',
|
||||||
|
},
|
||||||
|
],
|
||||||
'rxjs/finnish': [
|
'rxjs/finnish': [
|
||||||
'error',
|
'error',
|
||||||
{
|
{
|
||||||
@@ -229,6 +304,7 @@ export default tseslint.config(
|
|||||||
{ ignoreVoid: true },
|
{ ignoreVoid: true },
|
||||||
],
|
],
|
||||||
'@typescript-eslint/no-misused-promises': 0,
|
'@typescript-eslint/no-misused-promises': 0,
|
||||||
|
'@typescript-eslint/no-restricted-imports': 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,10 +5,6 @@
|
|||||||
"correctness": "error",
|
"correctness": "error",
|
||||||
"perf": "error"
|
"perf": "error"
|
||||||
},
|
},
|
||||||
"env": {
|
|
||||||
"builtin": true,
|
|
||||||
"es2026": true
|
|
||||||
},
|
|
||||||
"ignorePatterns": [
|
"ignorePatterns": [
|
||||||
"**/node_modules",
|
"**/node_modules",
|
||||||
".yarn",
|
".yarn",
|
||||||
@@ -48,34 +44,6 @@
|
|||||||
"**/test-blocks.json"
|
"**/test-blocks.json"
|
||||||
],
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
"no-empty-static-block": "error",
|
|
||||||
"no-misleading-character-class": "error",
|
|
||||||
"no-new-native-nonconstructor": "error",
|
|
||||||
"no-unused-private-class-members": "error",
|
|
||||||
"no-useless-backreference": "error",
|
|
||||||
"react/display-name": "error",
|
|
||||||
"react/rules-of-hooks": "error",
|
|
||||||
"react/exhaustive-deps": "warn",
|
|
||||||
"@typescript-eslint/prefer-for-of": "error",
|
|
||||||
"@typescript-eslint/no-unsafe-function-type": "error",
|
|
||||||
"@typescript-eslint/no-wrapper-object-types": "error",
|
|
||||||
"no-restricted-imports": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"patterns": [
|
|
||||||
{
|
|
||||||
"group": ["**/dist"],
|
|
||||||
"message": "Don't import from dist",
|
|
||||||
"allowTypeImports": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"group": ["**/src"],
|
|
||||||
"message": "Don't import from src",
|
|
||||||
"allowTypeImports": false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"no-await-in-loop": "allow",
|
"no-await-in-loop": "allow",
|
||||||
"no-redeclare": "allow",
|
"no-redeclare": "allow",
|
||||||
"promise/no-callback-in-promise": "allow",
|
"promise/no-callback-in-promise": "allow",
|
||||||
@@ -102,14 +70,6 @@
|
|||||||
"no-func-assign": "error",
|
"no-func-assign": "error",
|
||||||
"no-global-assign": "error",
|
"no-global-assign": "error",
|
||||||
"no-unused-vars": "error",
|
"no-unused-vars": "error",
|
||||||
"no-unused-expressions": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"allowShortCircuit": true,
|
|
||||||
"allowTernary": true,
|
|
||||||
"allowTaggedTemplates": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"no-ex-assign": "error",
|
"no-ex-assign": "error",
|
||||||
"no-loss-of-precision": "error",
|
"no-loss-of-precision": "error",
|
||||||
"no-fallthrough": "error",
|
"no-fallthrough": "error",
|
||||||
@@ -166,7 +126,6 @@
|
|||||||
"react/no-render-return-value": "error",
|
"react/no-render-return-value": "error",
|
||||||
"react/jsx-no-target-blank": "error",
|
"react/jsx-no-target-blank": "error",
|
||||||
"react/jsx-no-comment-textnodes": "error",
|
"react/jsx-no-comment-textnodes": "error",
|
||||||
"react/no-array-index-key": "off",
|
|
||||||
"typescript/consistent-type-imports": "error",
|
"typescript/consistent-type-imports": "error",
|
||||||
"typescript/no-non-null-assertion": "error",
|
"typescript/no-non-null-assertion": "error",
|
||||||
"typescript/triple-slash-reference": "error",
|
"typescript/triple-slash-reference": "error",
|
||||||
@@ -282,42 +241,6 @@
|
|||||||
"typescript/consistent-type-imports": "off",
|
"typescript/consistent-type-imports": "off",
|
||||||
"import/no-cycle": "off"
|
"import/no-cycle": "off"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
"files": [
|
|
||||||
"packages/**/*.{ts,tsx}",
|
|
||||||
"tools/**/*.{ts,tsx}",
|
|
||||||
"blocksuite/**/*.{ts,tsx}"
|
|
||||||
],
|
|
||||||
"rules": {
|
|
||||||
"react/exhaustive-deps": [
|
|
||||||
"warn",
|
|
||||||
{
|
|
||||||
"additionalHooks": "(useAsyncCallback|useCatchEventCallback|useDraggable|useDropTarget|useRefEffect)"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"files": [
|
|
||||||
"**/__tests__/**/*",
|
|
||||||
"**/*.stories.tsx",
|
|
||||||
"**/*.spec.ts",
|
|
||||||
"**/tests/**/*",
|
|
||||||
"scripts/**/*",
|
|
||||||
"**/benchmark/**/*",
|
|
||||||
"**/__debug__/**/*",
|
|
||||||
"**/e2e/**/*"
|
|
||||||
],
|
|
||||||
"rules": {
|
|
||||||
"no-restricted-imports": "off"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"files": ["**/*.{ts,js,mjs}"],
|
|
||||||
"rules": {
|
|
||||||
"react/rules-of-hooks": "off"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
34
package.json
34
package.json
@@ -26,10 +26,9 @@
|
|||||||
"lint:eslint:fix": "yarn lint:eslint --fix --fix-type problem,suggestion,layout",
|
"lint:eslint:fix": "yarn lint:eslint --fix --fix-type problem,suggestion,layout",
|
||||||
"lint:prettier": "prettier --ignore-unknown --cache --check .",
|
"lint:prettier": "prettier --ignore-unknown --cache --check .",
|
||||||
"lint:prettier:fix": "prettier --ignore-unknown --cache --write .",
|
"lint:prettier:fix": "prettier --ignore-unknown --cache --write .",
|
||||||
"lint:ox": "oxlint --deny-warnings",
|
"lint:ox": "oxlint -c oxlint.json --deny-warnings",
|
||||||
"lint:ox:fix": "yarn lint:ox --fix",
|
"lint": "yarn lint:eslint && yarn lint:prettier",
|
||||||
"lint": "yarn lint:ox && yarn lint:eslint && yarn lint:prettier",
|
"lint:fix": "yarn lint:eslint:fix && yarn lint:prettier:fix",
|
||||||
"lint:fix": "yarn lint:ox:fix && yarn lint:eslint:fix && yarn lint:prettier:fix",
|
|
||||||
"test": "vitest --run",
|
"test": "vitest --run",
|
||||||
"test:ui": "vitest --ui",
|
"test:ui": "vitest --ui",
|
||||||
"test:coverage": "vitest run --coverage",
|
"test:coverage": "vitest run --coverage",
|
||||||
@@ -52,43 +51,42 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@affine-tools/cli": "workspace:*",
|
"@affine-tools/cli": "workspace:*",
|
||||||
"@capacitor/cli": "^7.0.0",
|
"@capacitor/cli": "^7.0.0",
|
||||||
"@eslint/js": "^9.39.2",
|
"@eslint/js": "^9.16.0",
|
||||||
"@faker-js/faker": "^10.1.0",
|
"@faker-js/faker": "^10.1.0",
|
||||||
"@istanbuljs/schema": "^0.1.3",
|
"@istanbuljs/schema": "^0.1.3",
|
||||||
"@magic-works/i18n-codegen": "^0.6.1",
|
"@magic-works/i18n-codegen": "^0.6.1",
|
||||||
"@playwright/test": "=1.52.0",
|
"@playwright/test": "=1.58.2",
|
||||||
"@smarttools/eslint-plugin-rxjs": "^1.0.8",
|
"@smarttools/eslint-plugin-rxjs": "^1.0.8",
|
||||||
"@taplo/cli": "^0.7.0",
|
"@taplo/cli": "^0.7.0",
|
||||||
"@toeverything/infra": "workspace:*",
|
"@toeverything/infra": "workspace:*",
|
||||||
"@types/eslint": "^9.6.1",
|
"@types/eslint": "^9.6.1",
|
||||||
"@types/node": "^22.0.0",
|
"@types/node": "^22.0.0",
|
||||||
"@typescript-eslint/parser": "^8.55.0",
|
"@typescript-eslint/parser": "^8.18.0",
|
||||||
"@vanilla-extract/vite-plugin": "^5.0.0",
|
"@vanilla-extract/vite-plugin": "^5.0.0",
|
||||||
"@vitest/browser": "^3.2.4",
|
"@vitest/browser": "^3.2.4",
|
||||||
"@vitest/coverage-istanbul": "^3.2.4",
|
"@vitest/coverage-istanbul": "^3.2.4",
|
||||||
"@vitest/ui": "^3.2.4",
|
"@vitest/ui": "^3.2.4",
|
||||||
"cross-env": "^10.1.0",
|
"cross-env": "^10.1.0",
|
||||||
"electron": "^39.0.0",
|
"electron": "^39.0.0",
|
||||||
"eslint": "^9.39.2",
|
"eslint": "^9.16.0",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.0.0",
|
||||||
"eslint-import-resolver-typescript": "^4.4.4",
|
"eslint-import-resolver-typescript": "^4.0.0",
|
||||||
"eslint-plugin-import-x": "^4.16.1",
|
"eslint-plugin-import-x": "^4.5.0",
|
||||||
"eslint-plugin-oxlint": "^1.46.0",
|
"eslint-plugin-react": "^7.37.2",
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react-hooks": "^5.1.0",
|
||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
|
||||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||||
"eslint-plugin-sonarjs": "^3.0.7",
|
"eslint-plugin-sonarjs": "^3.0.1",
|
||||||
"eslint-plugin-unicorn": "^63.0.0",
|
"eslint-plugin-unicorn": "^59.0.0",
|
||||||
"happy-dom": "^20.0.0",
|
"happy-dom": "^20.0.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"lint-staged": "^16.0.0",
|
"lint-staged": "^16.0.0",
|
||||||
"msw": "^2.12.4",
|
"msw": "^2.12.4",
|
||||||
"oxlint": "^1.47.0",
|
"oxlint": "~1.18.0",
|
||||||
"prettier": "^3.7.4",
|
"prettier": "^3.7.4",
|
||||||
"semver": "^7.7.3",
|
"semver": "^7.7.3",
|
||||||
"serve": "^14.2.4",
|
"serve": "^14.2.4",
|
||||||
"typescript": "^5.7.2",
|
"typescript": "^5.7.2",
|
||||||
"typescript-eslint": "^8.55.0",
|
"typescript-eslint": "^8.18.0",
|
||||||
"unplugin-swc": "^1.5.9",
|
"unplugin-swc": "^1.5.9",
|
||||||
"vite": "^7.2.7",
|
"vite": "^7.2.7",
|
||||||
"vitest": "^3.2.4"
|
"vitest": "^3.2.4"
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ assert.strictEqual(
|
|||||||
bench
|
bench
|
||||||
.add('tiktoken', () => {
|
.add('tiktoken', () => {
|
||||||
const encoder = encoding_for_model('gpt-4o');
|
const encoder = encoding_for_model('gpt-4o');
|
||||||
void encoder.encode_ordinary(FIXTURE).length;
|
encoder.encode_ordinary(FIXTURE).length;
|
||||||
})
|
})
|
||||||
.add('native', () => {
|
.add('native', () => {
|
||||||
fromModelName('gpt-4o').count(FIXTURE);
|
fromModelName('gpt-4o').count(FIXTURE);
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user