mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-19 07:17:00 +08:00
Compare commits
33 Commits
v0.26.0
...
v0.26.3-be
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
850e646ab9 | ||
|
|
728e02cab7 | ||
|
|
792164edd1 | ||
|
|
e3177e6837 | ||
|
|
42f2d2b337 | ||
|
|
9d7f4acaf1 | ||
|
|
9a1f600fc9 | ||
|
|
0f906ad623 | ||
|
|
09aa65c52a | ||
|
|
25227a09f7 | ||
|
|
c0694c589b | ||
|
|
819402d9f1 | ||
|
|
33bc3e2fe9 | ||
|
|
2b71b3f345 | ||
|
|
3bc28ba78c | ||
|
|
72df9cb457 | ||
|
|
98e5747fdc | ||
|
|
4460604dd3 | ||
|
|
b4be9118ad | ||
|
|
b46bf91575 | ||
|
|
3ad482351b | ||
|
|
03b1d15a8f | ||
|
|
52c7b04a01 | ||
|
|
1c0f873c9d | ||
|
|
8b68574820 | ||
|
|
bb01bb1aef | ||
|
|
8192a492d9 | ||
|
|
31e11b2563 | ||
|
|
5a36acea7b | ||
|
|
8ce620e2e6 | ||
|
|
7655c2b73e | ||
|
|
a33b4ad73d | ||
|
|
1a2410f541 |
99
.github/actions/deploy/deploy.mjs
vendored
99
.github/actions/deploy/deploy.mjs
vendored
@@ -25,47 +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: {
|
||||||
web: 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,
|
||||||
sync: Number(process.env.PRODUCTION_SYNC_REPLICA) || 2,
|
|
||||||
renderer: Number(process.env.PRODUCTION_RENDERER_REPLICA) || 2,
|
|
||||||
doc: Number(process.env.PRODUCTION_DOC_REPLICA) || 2,
|
|
||||||
},
|
},
|
||||||
beta: {
|
beta: {
|
||||||
web: 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,
|
||||||
sync: Number(process.env.BETA_SYNC_REPLICA) || 1,
|
|
||||||
renderer: Number(process.env.BETA_RENDERER_REPLICA) || 1,
|
|
||||||
doc: Number(process.env.BETA_DOC_REPLICA) || 1,
|
|
||||||
},
|
|
||||||
canary: {
|
|
||||||
web: 1,
|
|
||||||
graphql: 1,
|
|
||||||
sync: 1,
|
|
||||||
renderer: 1,
|
|
||||||
doc: 1,
|
|
||||||
},
|
},
|
||||||
|
canary: { front: 1, graphql: 1 },
|
||||||
};
|
};
|
||||||
|
|
||||||
const cpuConfig = {
|
const cpuConfig = {
|
||||||
beta: {
|
beta: { front: '1', graphql: '1' },
|
||||||
web: '300m',
|
canary: { front: '500m', graphql: '1' },
|
||||||
graphql: '1',
|
};
|
||||||
sync: '1',
|
|
||||||
doc: '1',
|
const memoryConfig = {
|
||||||
renderer: '300m',
|
beta: { front: '2Gi', graphql: '1Gi' },
|
||||||
},
|
canary: { front: '512Mi', graphql: '512Mi' },
|
||||||
canary: {
|
|
||||||
web: '300m',
|
|
||||||
graphql: '1',
|
|
||||||
sync: '1',
|
|
||||||
doc: '1',
|
|
||||||
renderer: '300m',
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const createHelmCommand = ({ isDryRun }) => {
|
const createHelmCommand = ({ isDryRun }) => {
|
||||||
@@ -89,33 +72,48 @@ 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 web.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 sync.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
|
||||||
? [
|
? [
|
||||||
`--set-json web.service.annotations="{ \\"cloud.google.com/neg\\": \\"{\\\\\\"ingress\\\\\\": true}\\" }"`,
|
`--set-json front.services.web.annotations="{ \\"cloud.google.com/neg\\": \\"{\\\\\\"ingress\\\\\\": true}\\" }"`,
|
||||||
|
`--set-json front.services.sync.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 sync.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="{ \\"iam.gke.io/gke-metadata-server-enabled\\": \\"true\\" }"`,
|
`--set-json cloud-sql-proxy.nodeSelector="${cloudSqlNodeSelector}"`,
|
||||||
]
|
]
|
||||||
: []
|
: []
|
||||||
);
|
);
|
||||||
|
const spotNodeSelector = `{ \\"cloud.google.com/gke-spot\\": \\"true\\" }`;
|
||||||
const cpu = cpuConfig[buildType];
|
const spotScheduling = isSpotEnabled
|
||||||
const resources = cpu
|
|
||||||
? [
|
? [
|
||||||
`--set web.resources.requests.cpu="${cpu.web}"`,
|
`--set-json front.nodeSelector="${spotNodeSelector}"`,
|
||||||
`--set graphql.resources.requests.cpu="${cpu.graphql}"`,
|
`--set-json graphql.nodeSelector="${spotNodeSelector}"`,
|
||||||
`--set sync.resources.requests.cpu="${cpu.sync}"`,
|
|
||||||
`--set doc.resources.requests.cpu="${cpu.doc}"`,
|
|
||||||
]
|
]
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
|
const cpu = cpuConfig[buildType];
|
||||||
|
const memory = memoryConfig[buildType];
|
||||||
|
let resources = [];
|
||||||
|
if (cpu) {
|
||||||
|
resources = resources.concat([
|
||||||
|
`--set front.resources.requests.cpu="${cpu.front}"`,
|
||||||
|
`--set graphql.resources.requests.cpu="${cpu.graphql}"`,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
if (memory) {
|
||||||
|
resources = resources.concat([
|
||||||
|
`--set front.resources.requests.memory="${memory.front}"`,
|
||||||
|
`--set graphql.resources.requests.memory="${memory.graphql}"`,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
const replica = replicaConfig[buildType] || replicaConfig.canary;
|
const replica = replicaConfig[buildType] || replicaConfig.canary;
|
||||||
|
|
||||||
const namespace = isProduction
|
const namespace = isProduction
|
||||||
@@ -130,6 +128,7 @@ const createHelmCommand = ({ isDryRun }) => {
|
|||||||
.split(',')
|
.split(',')
|
||||||
.map(host => host.trim())
|
.map(host => host.trim())
|
||||||
.filter(host => host);
|
.filter(host => host);
|
||||||
|
const primaryHost = hosts[0] || '0.0.0.0';
|
||||||
const deployCommand = [
|
const deployCommand = [
|
||||||
`helm upgrade --install affine .github/helm/affine`,
|
`helm upgrade --install affine .github/helm/affine`,
|
||||||
`--namespace ${namespace}`,
|
`--namespace ${namespace}`,
|
||||||
@@ -144,20 +143,14 @@ const createHelmCommand = ({ isDryRun }) => {
|
|||||||
`--set-string global.version="${APP_VERSION}"`,
|
`--set-string global.version="${APP_VERSION}"`,
|
||||||
...redisAndPostgres,
|
...redisAndPostgres,
|
||||||
...indexerOptions,
|
...indexerOptions,
|
||||||
`--set web.replicaCount=${replica.web}`,
|
`--set front.replicaCount=${replica.front}`,
|
||||||
`--set-string web.image.tag="${imageTag}"`,
|
`--set-string front.image.tag="${imageTag}"`,
|
||||||
|
`--set-string front.app.host="${primaryHost}"`,
|
||||||
`--set graphql.replicaCount=${replica.graphql}`,
|
`--set graphql.replicaCount=${replica.graphql}`,
|
||||||
`--set-string graphql.image.tag="${imageTag}"`,
|
`--set-string graphql.image.tag="${imageTag}"`,
|
||||||
`--set graphql.app.host=${hosts[0]}`,
|
`--set-string graphql.app.host="${primaryHost}"`,
|
||||||
`--set sync.replicaCount=${replica.sync}`,
|
|
||||||
`--set-string sync.image.tag="${imageTag}"`,
|
|
||||||
`--set-string renderer.image.tag="${imageTag}"`,
|
|
||||||
`--set renderer.app.host=${hosts[0]}`,
|
|
||||||
`--set renderer.replicaCount=${replica.renderer}`,
|
|
||||||
`--set-string doc.image.tag="${imageTag}"`,
|
|
||||||
`--set doc.app.host=${hosts[0]}`,
|
|
||||||
`--set doc.replicaCount=${replica.doc}`,
|
|
||||||
...serviceAnnotations,
|
...serviceAnnotations,
|
||||||
|
...spotScheduling,
|
||||||
...resources,
|
...resources,
|
||||||
`--timeout 10m`,
|
`--timeout 10m`,
|
||||||
flag,
|
flag,
|
||||||
|
|||||||
13
.github/deployment/front/Dockerfile
vendored
13
.github/deployment/front/Dockerfile
vendored
@@ -1,13 +0,0 @@
|
|||||||
FROM openresty/openresty:1.27.1.1-0-buster
|
|
||||||
WORKDIR /app
|
|
||||||
COPY ./packages/frontend/apps/web/dist ./dist
|
|
||||||
COPY ./packages/frontend/admin/dist ./admin
|
|
||||||
COPY ./packages/frontend/apps/mobile/dist ./mobile
|
|
||||||
COPY ./.github/deployment/front/nginx.conf /usr/local/openresty/nginx/conf/nginx.conf
|
|
||||||
COPY ./.github/deployment/front/affine.nginx.conf /etc/nginx/conf.d/affine.nginx.conf
|
|
||||||
|
|
||||||
RUN mkdir -p /var/log/nginx && \
|
|
||||||
rm /etc/nginx/conf.d/default.conf
|
|
||||||
|
|
||||||
EXPOSE 8080
|
|
||||||
CMD ["/usr/local/openresty/bin/openresty", "-g", "daemon off;"]
|
|
||||||
42
.github/deployment/front/affine.nginx.conf
vendored
42
.github/deployment/front/affine.nginx.conf
vendored
@@ -1,42 +0,0 @@
|
|||||||
server {
|
|
||||||
listen 8080;
|
|
||||||
location /admin {
|
|
||||||
root /app/;
|
|
||||||
index index.html;
|
|
||||||
try_files $uri/index.html $uri/ $uri /admin/index.html;
|
|
||||||
}
|
|
||||||
|
|
||||||
set $app_root_path /app/dist/;
|
|
||||||
set $mobile_root /app/dist/;
|
|
||||||
set_by_lua $affine_env 'return os.getenv("AFFINE_ENV")';
|
|
||||||
|
|
||||||
if ($affine_env = "dev") {
|
|
||||||
set $mobile_root /app/mobile/;
|
|
||||||
}
|
|
||||||
|
|
||||||
# https://gist.github.com/mariusom/6683dc52b1cad1a1f372e908bdb209d0
|
|
||||||
if ($http_user_agent ~* "(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino") {
|
|
||||||
set $app_root_path $mobile_root;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($http_user_agent ~* "^(1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-)") {
|
|
||||||
set $app_root_path $mobile_root;
|
|
||||||
}
|
|
||||||
|
|
||||||
location ~ ^/(_plugin|assets|imgs|js|plugins|static)/ {
|
|
||||||
root $app_root_path;
|
|
||||||
try_files $uri $uri/ =404;
|
|
||||||
}
|
|
||||||
|
|
||||||
location / {
|
|
||||||
root $app_root_path;
|
|
||||||
index index.html;
|
|
||||||
try_files $uri $uri/ /index.html;
|
|
||||||
add_header Cache-Control "private, no-cache, no-store, max-age=0, must-revalidate";
|
|
||||||
}
|
|
||||||
|
|
||||||
error_page 404 /404.html;
|
|
||||||
location = /404.html {
|
|
||||||
internal;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
15
.github/deployment/front/nginx.conf
vendored
15
.github/deployment/front/nginx.conf
vendored
@@ -1,15 +0,0 @@
|
|||||||
worker_processes 4;
|
|
||||||
error_log /var/log/nginx/error.log warn;
|
|
||||||
pcre_jit on;
|
|
||||||
env AFFINE_ENV;
|
|
||||||
events {
|
|
||||||
worker_connections 1024;
|
|
||||||
}
|
|
||||||
http {
|
|
||||||
include mime.types;
|
|
||||||
log_format main '$remote_addr [$time_local] "$request" '
|
|
||||||
'$status $body_bytes_sent "$http_referer" '
|
|
||||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
|
||||||
access_log /var/log/nginx/access.log main;
|
|
||||||
include /etc/nginx/conf.d/*.conf;
|
|
||||||
}
|
|
||||||
2
.github/helm/affine/Chart.yaml
vendored
2
.github/helm/affine/Chart.yaml
vendored
@@ -3,4 +3,4 @@ name: affine
|
|||||||
description: AFFiNE cloud chart
|
description: AFFiNE cloud chart
|
||||||
type: application
|
type: application
|
||||||
version: 0.0.0
|
version: 0.0.0
|
||||||
appVersion: "0.26.0"
|
appVersion: "0.26.1"
|
||||||
|
|||||||
2
.github/helm/affine/charts/doc/Chart.yaml
vendored
2
.github/helm/affine/charts/doc/Chart.yaml
vendored
@@ -3,7 +3,7 @@ name: doc
|
|||||||
description: AFFiNE doc server
|
description: AFFiNE doc server
|
||||||
type: application
|
type: application
|
||||||
version: 0.0.0
|
version: 0.0.0
|
||||||
appVersion: "0.26.0"
|
appVersion: "0.26.1"
|
||||||
dependencies:
|
dependencies:
|
||||||
- name: gcloud-sql-proxy
|
- name: gcloud-sql-proxy
|
||||||
version: 0.0.0
|
version: 0.0.0
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
1. Get the application URL by running these commands:
|
|
||||||
{{- if contains "NodePort" .Values.service.type }}
|
|
||||||
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "doc.fullname" . }})
|
|
||||||
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
|
|
||||||
echo http://$NODE_IP:$NODE_PORT
|
|
||||||
{{- else if contains "LoadBalancer" .Values.service.type }}
|
|
||||||
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
|
|
||||||
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "doc.fullname" . }}'
|
|
||||||
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "doc.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
|
|
||||||
echo http://$SERVICE_IP:{{ .Values.service.port }}
|
|
||||||
{{- else if contains "ClusterIP" .Values.service.type }}
|
|
||||||
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "doc.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
|
|
||||||
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
|
|
||||||
echo "Visit http://127.0.0.1:8080 to use your application"
|
|
||||||
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
|
|
||||||
{{- end }}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
{{/*
|
|
||||||
Expand the name of the chart.
|
|
||||||
*/}}
|
|
||||||
{{- define "doc.name" -}}
|
|
||||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
{{/*
|
|
||||||
Create a default fully qualified app name.
|
|
||||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
|
||||||
If release name contains chart name it will be used as a full name.
|
|
||||||
*/}}
|
|
||||||
{{- define "doc.fullname" -}}
|
|
||||||
{{- if .Values.fullnameOverride }}
|
|
||||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
|
||||||
{{- else }}
|
|
||||||
{{- $name := default .Chart.Name .Values.nameOverride }}
|
|
||||||
{{- if contains $name .Release.Name }}
|
|
||||||
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
|
||||||
{{- else }}
|
|
||||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
|
||||||
{{- end }}
|
|
||||||
{{- end }}
|
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
{{/*
|
|
||||||
Create chart name and version as used by the chart label.
|
|
||||||
*/}}
|
|
||||||
{{- define "doc.chart" -}}
|
|
||||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
{{/*
|
|
||||||
Common labels
|
|
||||||
*/}}
|
|
||||||
{{- define "doc.labels" -}}
|
|
||||||
helm.sh/chart: {{ include "doc.chart" . }}
|
|
||||||
{{ include "doc.selectorLabels" . }}
|
|
||||||
{{- if .Chart.AppVersion }}
|
|
||||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
|
||||||
{{- end }}
|
|
||||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
|
||||||
monitoring: enabled
|
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
{{/*
|
|
||||||
Selector labels
|
|
||||||
*/}}
|
|
||||||
{{- define "doc.selectorLabels" -}}
|
|
||||||
app.kubernetes.io/name: {{ include "doc.name" . }}
|
|
||||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
{{/*
|
|
||||||
Create the name of the service account to use
|
|
||||||
*/}}
|
|
||||||
{{- define "doc.serviceAccountName" -}}
|
|
||||||
{{- if .Values.serviceAccount.create }}
|
|
||||||
{{- default (include "doc.fullname" .) .Values.global.docService.name }}
|
|
||||||
{{- else }}
|
|
||||||
{{- default "default" .Values.global.docService.name }}
|
|
||||||
{{- end }}
|
|
||||||
{{- end }}
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: {{ include "doc.fullname" . }}
|
|
||||||
labels:
|
|
||||||
{{- include "doc.labels" . | nindent 4 }}
|
|
||||||
spec:
|
|
||||||
replicas: {{ .Values.replicaCount }}
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
{{- include "doc.selectorLabels" . | nindent 6 }}
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
{{- with .Values.podAnnotations }}
|
|
||||||
annotations:
|
|
||||||
{{- toYaml . | nindent 8 }}
|
|
||||||
{{- end }}
|
|
||||||
labels:
|
|
||||||
{{- include "doc.selectorLabels" . | nindent 8 }}
|
|
||||||
spec:
|
|
||||||
{{- with .Values.imagePullSecrets }}
|
|
||||||
imagePullSecrets:
|
|
||||||
{{- toYaml . | nindent 8 }}
|
|
||||||
{{- end }}
|
|
||||||
serviceAccountName: {{ include "doc.serviceAccountName" . }}
|
|
||||||
containers:
|
|
||||||
- name: {{ .Chart.Name }}
|
|
||||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
|
||||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
|
||||||
env:
|
|
||||||
- name: AFFINE_PRIVATE_KEY
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: "{{ .Values.global.secret.secretName }}"
|
|
||||||
key: key
|
|
||||||
- name: NODE_ENV
|
|
||||||
value: "{{ .Values.env }}"
|
|
||||||
- name: NODE_OPTIONS
|
|
||||||
value: "--max-old-space-size=4096"
|
|
||||||
- name: NO_COLOR
|
|
||||||
value: "1"
|
|
||||||
- name: DEPLOYMENT_TYPE
|
|
||||||
value: "{{ .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,12 +0,0 @@
|
|||||||
{{- if .Values.serviceAccount.create -}}
|
|
||||||
apiVersion: v1
|
|
||||||
kind: ServiceAccount
|
|
||||||
metadata:
|
|
||||||
name: {{ include "doc.serviceAccountName" . }}
|
|
||||||
labels:
|
|
||||||
{{- include "doc.labels" . | nindent 4 }}
|
|
||||||
{{- with .Values.serviceAccount.annotations }}
|
|
||||||
annotations:
|
|
||||||
{{- toYaml . | nindent 4 }}
|
|
||||||
{{- end }}
|
|
||||||
{{- end }}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: Pod
|
|
||||||
metadata:
|
|
||||||
name: "{{ include "doc.fullname" . }}-test-connection"
|
|
||||||
labels:
|
|
||||||
{{- include "doc.labels" . | nindent 4 }}
|
|
||||||
annotations:
|
|
||||||
"helm.sh/hook": test
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: wget
|
|
||||||
image: busybox
|
|
||||||
command: ['wget']
|
|
||||||
args: ['{{ include "doc.fullname" . }}:{{ .Values.global.docService.port }}']
|
|
||||||
restartPolicy: Never
|
|
||||||
5
.github/helm/affine/charts/doc/values.yaml
vendored
5
.github/helm/affine/charts/doc/values.yaml
vendored
@@ -30,9 +30,12 @@ podSecurityContext:
|
|||||||
fsGroup: 2000
|
fsGroup: 2000
|
||||||
|
|
||||||
resources:
|
resources:
|
||||||
requests:
|
limits:
|
||||||
cpu: '1'
|
cpu: '1'
|
||||||
memory: 4Gi
|
memory: 4Gi
|
||||||
|
requests:
|
||||||
|
cpu: '1'
|
||||||
|
memory: 2Gi
|
||||||
|
|
||||||
probe:
|
probe:
|
||||||
initialDelaySeconds: 20
|
initialDelaySeconds: 20
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
apiVersion: v2
|
apiVersion: v2
|
||||||
name: sync
|
name: front
|
||||||
description: AFFiNE Sync Server
|
description: AFFiNE front server
|
||||||
type: application
|
type: application
|
||||||
version: 0.0.0
|
version: 0.0.0
|
||||||
appVersion: "0.26.0"
|
appVersion: "0.26.1"
|
||||||
dependencies:
|
dependencies:
|
||||||
- name: gcloud-sql-proxy
|
- name: gcloud-sql-proxy
|
||||||
version: 0.0.0
|
version: 0.0.0
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
1. Get the application URL by running these commands:
|
1. Get the application URL by running these commands:
|
||||||
{{- if contains "NodePort" .Values.service.type }}
|
{{- if contains "NodePort" .Values.services.sync.type }}
|
||||||
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "web.fullname" . }})
|
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ .Values.services.sync.name }})
|
||||||
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
|
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
|
||||||
echo http://$NODE_IP:$NODE_PORT
|
echo http://$NODE_IP:$NODE_PORT
|
||||||
{{- else if contains "LoadBalancer" .Values.service.type }}
|
{{- else if contains "LoadBalancer" .Values.services.sync.type }}
|
||||||
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
|
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 "web.fullname" . }}'
|
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ .Values.services.sync.name }}'
|
||||||
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "web.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
|
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ .Values.services.sync.name }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
|
||||||
echo http://$SERVICE_IP:{{ .Values.service.port }}
|
echo http://$SERVICE_IP:{{ .Values.services.sync.port }}
|
||||||
{{- else if contains "ClusterIP" .Values.service.type }}
|
{{- else if contains "ClusterIP" .Values.services.sync.type }}
|
||||||
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "web.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
|
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "front.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}")
|
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"
|
echo "Visit http://127.0.0.1:8080 to use your application"
|
||||||
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
|
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{{/*
|
{{/*
|
||||||
Expand the name of the chart.
|
Expand the name of the chart.
|
||||||
*/}}
|
*/}}
|
||||||
{{- define "web.name" -}}
|
{{- define "front.name" -}}
|
||||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ 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).
|
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.
|
If release name contains chart name it will be used as a full name.
|
||||||
*/}}
|
*/}}
|
||||||
{{- define "web.fullname" -}}
|
{{- define "front.fullname" -}}
|
||||||
{{- if .Values.fullnameOverride }}
|
{{- if .Values.fullnameOverride }}
|
||||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||||
{{- else }}
|
{{- else }}
|
||||||
@@ -26,16 +26,16 @@ If release name contains chart name it will be used as a full name.
|
|||||||
{{/*
|
{{/*
|
||||||
Create chart name and version as used by the chart label.
|
Create chart name and version as used by the chart label.
|
||||||
*/}}
|
*/}}
|
||||||
{{- define "web.chart" -}}
|
{{- define "front.chart" -}}
|
||||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
||||||
{{/*
|
{{/*
|
||||||
Common labels
|
Common labels
|
||||||
*/}}
|
*/}}
|
||||||
{{- define "web.labels" -}}
|
{{- define "front.labels" -}}
|
||||||
helm.sh/chart: {{ include "web.chart" . }}
|
helm.sh/chart: {{ include "front.chart" . }}
|
||||||
{{ include "web.selectorLabels" . }}
|
{{ include "front.selectorLabels" . }}
|
||||||
{{- if .Chart.AppVersion }}
|
{{- if .Chart.AppVersion }}
|
||||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
@@ -46,17 +46,17 @@ monitoring: enabled
|
|||||||
{{/*
|
{{/*
|
||||||
Selector labels
|
Selector labels
|
||||||
*/}}
|
*/}}
|
||||||
{{- define "web.selectorLabels" -}}
|
{{- define "front.selectorLabels" -}}
|
||||||
app.kubernetes.io/name: {{ include "web.name" . }}
|
app.kubernetes.io/name: {{ include "front.name" . }}
|
||||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
||||||
{{/*
|
{{/*
|
||||||
Create the name of the service account to use
|
Create the name of the service account to use
|
||||||
*/}}
|
*/}}
|
||||||
{{- define "web.serviceAccountName" -}}
|
{{- define "front.serviceAccountName" -}}
|
||||||
{{- if .Values.serviceAccount.create }}
|
{{- if .Values.serviceAccount.create }}
|
||||||
{{- default (include "web.fullname" .) .Values.serviceAccount.name }}
|
{{- default (include "front.fullname" .) .Values.serviceAccount.name }}
|
||||||
{{- else }}
|
{{- else }}
|
||||||
{{- default "default" .Values.serviceAccount.name }}
|
{{- default "default" .Values.serviceAccount.name }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
118
.github/helm/affine/charts/front/templates/deployment.yaml
vendored
Normal file
118
.github/helm/affine/charts/front/templates/deployment.yaml
vendored
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: {{ include "front.fullname" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "front.labels" . | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
replicas: {{ .Values.replicaCount }}
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
{{- include "front.selectorLabels" . | nindent 6 }}
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
{{- with .Values.podAnnotations }}
|
||||||
|
annotations:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
labels:
|
||||||
|
{{- include "front.selectorLabels" . | nindent 8 }}
|
||||||
|
spec:
|
||||||
|
{{- with .Values.imagePullSecrets }}
|
||||||
|
imagePullSecrets:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
serviceAccountName: {{ include "front.serviceAccountName" . }}
|
||||||
|
securityContext:
|
||||||
|
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||||
|
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: "{{ .Values.nodeOptions }}"
|
||||||
|
- 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: "front"
|
||||||
|
- 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.app.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.app.port }}
|
||||||
|
protocol: TCP
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /info
|
||||||
|
port: http
|
||||||
|
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /info
|
||||||
|
port: http
|
||||||
|
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
|
||||||
|
resources:
|
||||||
|
{{- toYaml .Values.resources | nindent 12 }}
|
||||||
|
{{- with .Values.nodeSelector }}
|
||||||
|
nodeSelector:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.affinity }}
|
||||||
|
affinity:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.tolerations }}
|
||||||
|
tolerations:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
@@ -1,19 +1,19 @@
|
|||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: Service
|
kind: Service
|
||||||
metadata:
|
metadata:
|
||||||
name: {{ include "doc.fullname" . }}
|
name: {{ .Values.global.docService.name }}
|
||||||
labels:
|
labels:
|
||||||
{{- include "doc.labels" . | nindent 4 }}
|
{{- include "front.labels" . | nindent 4 }}
|
||||||
{{- with .Values.service.annotations }}
|
{{- with .Values.services.doc.annotations }}
|
||||||
annotations:
|
annotations:
|
||||||
{{- toYaml . | nindent 4 }}
|
{{- toYaml . | nindent 4 }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
spec:
|
spec:
|
||||||
type: {{ .Values.service.type }}
|
type: {{ .Values.services.doc.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 "doc.selectorLabels" . | nindent 4 }}
|
{{- include "front.selectorLabels" . | nindent 4 }}
|
||||||
19
.github/helm/affine/charts/front/templates/service-renderer.yaml
vendored
Normal file
19
.github/helm/affine/charts/front/templates/service-renderer.yaml
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: {{ .Values.services.renderer.name }}
|
||||||
|
labels:
|
||||||
|
{{- include "front.labels" . | nindent 4 }}
|
||||||
|
{{- with .Values.services.renderer.annotations }}
|
||||||
|
annotations:
|
||||||
|
{{- toYaml . | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
spec:
|
||||||
|
type: {{ .Values.services.renderer.type }}
|
||||||
|
ports:
|
||||||
|
- port: {{ .Values.services.renderer.port }}
|
||||||
|
targetPort: http
|
||||||
|
protocol: TCP
|
||||||
|
name: http
|
||||||
|
selector:
|
||||||
|
{{- include "front.selectorLabels" . | nindent 4 }}
|
||||||
19
.github/helm/affine/charts/front/templates/service-sync.yaml
vendored
Normal file
19
.github/helm/affine/charts/front/templates/service-sync.yaml
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: {{ .Values.services.sync.name }}
|
||||||
|
labels:
|
||||||
|
{{- include "front.labels" . | nindent 4 }}
|
||||||
|
{{- with .Values.services.sync.annotations }}
|
||||||
|
annotations:
|
||||||
|
{{- toYaml . | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
spec:
|
||||||
|
type: {{ .Values.services.sync.type }}
|
||||||
|
ports:
|
||||||
|
- port: {{ .Values.services.sync.port }}
|
||||||
|
targetPort: http
|
||||||
|
protocol: TCP
|
||||||
|
name: http
|
||||||
|
selector:
|
||||||
|
{{- include "front.selectorLabels" . | nindent 4 }}
|
||||||
19
.github/helm/affine/charts/front/templates/service-web.yaml
vendored
Normal file
19
.github/helm/affine/charts/front/templates/service-web.yaml
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: {{ .Values.services.web.name }}
|
||||||
|
labels:
|
||||||
|
{{- include "front.labels" . | nindent 4 }}
|
||||||
|
{{- with .Values.services.web.annotations }}
|
||||||
|
annotations:
|
||||||
|
{{- toYaml . | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
spec:
|
||||||
|
type: {{ .Values.services.web.type }}
|
||||||
|
ports:
|
||||||
|
- port: {{ .Values.services.web.port }}
|
||||||
|
targetPort: http
|
||||||
|
protocol: TCP
|
||||||
|
name: http
|
||||||
|
selector:
|
||||||
|
{{- include "front.selectorLabels" . | nindent 4 }}
|
||||||
@@ -2,9 +2,9 @@
|
|||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: ServiceAccount
|
kind: ServiceAccount
|
||||||
metadata:
|
metadata:
|
||||||
name: {{ include "sync.serviceAccountName" . }}
|
name: {{ include "front.serviceAccountName" . }}
|
||||||
labels:
|
labels:
|
||||||
{{- include "sync.labels" . | nindent 4 }}
|
{{- include "front.labels" . | nindent 4 }}
|
||||||
{{- with .Values.serviceAccount.annotations }}
|
{{- with .Values.serviceAccount.annotations }}
|
||||||
annotations:
|
annotations:
|
||||||
{{- toYaml . | nindent 4 }}
|
{{- toYaml . | nindent 4 }}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: Pod
|
kind: Pod
|
||||||
metadata:
|
metadata:
|
||||||
name: "{{ include "sync.fullname" . }}-test-connection"
|
name: "{{ include "front.fullname" . }}-test-connection"
|
||||||
labels:
|
labels:
|
||||||
{{- include "sync.labels" . | nindent 4 }}
|
{{- include "front.labels" . | nindent 4 }}
|
||||||
annotations:
|
annotations:
|
||||||
"helm.sh/hook": test
|
"helm.sh/hook": test
|
||||||
spec:
|
spec:
|
||||||
@@ -11,5 +11,5 @@ spec:
|
|||||||
- name: wget
|
- name: wget
|
||||||
image: busybox
|
image: busybox
|
||||||
command: ['wget']
|
command: ['wget']
|
||||||
args: ['{{ include "sync.fullname" . }}:{{ .Values.service.port }}']
|
args: ['{{ .Values.services.sync.name }}:{{ .Values.services.sync.port }}']
|
||||||
restartPolicy: Never
|
restartPolicy: Never
|
||||||
66
.github/helm/affine/charts/front/values.yaml
vendored
Normal file
66
.github/helm/affine/charts/front/values.yaml
vendored
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
replicaCount: 1
|
||||||
|
image:
|
||||||
|
repository: ghcr.io/toeverything/affine
|
||||||
|
pullPolicy: IfNotPresent
|
||||||
|
tag: ''
|
||||||
|
|
||||||
|
imagePullSecrets: []
|
||||||
|
nameOverride: ''
|
||||||
|
fullnameOverride: ''
|
||||||
|
# map to NODE_ENV environment variable
|
||||||
|
env: 'production'
|
||||||
|
nodeOptions: '--max-old-space-size=3072'
|
||||||
|
app:
|
||||||
|
# AFFINE_SERVER_PORT
|
||||||
|
port: 3010
|
||||||
|
# AFFINE_SERVER_SUB_PATH
|
||||||
|
path: ''
|
||||||
|
# AFFINE_SERVER_HOST
|
||||||
|
host: '0.0.0.0'
|
||||||
|
https: true
|
||||||
|
serviceAccount:
|
||||||
|
create: true
|
||||||
|
annotations: {}
|
||||||
|
name: 'affine-front'
|
||||||
|
|
||||||
|
podAnnotations: {}
|
||||||
|
|
||||||
|
podSecurityContext:
|
||||||
|
fsGroup: 2000
|
||||||
|
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpu: '1'
|
||||||
|
memory: 2Gi
|
||||||
|
requests:
|
||||||
|
cpu: '1'
|
||||||
|
memory: 2Gi
|
||||||
|
|
||||||
|
probe:
|
||||||
|
initialDelaySeconds: 20
|
||||||
|
|
||||||
|
services:
|
||||||
|
sync:
|
||||||
|
name: affine-sync
|
||||||
|
type: ClusterIP
|
||||||
|
port: 3010
|
||||||
|
annotations:
|
||||||
|
cloud.google.com/backend-config: '{"default": "affine-api-backendconfig"}'
|
||||||
|
renderer:
|
||||||
|
name: affine-renderer
|
||||||
|
type: ClusterIP
|
||||||
|
port: 3000
|
||||||
|
annotations:
|
||||||
|
cloud.google.com/backend-config: '{"default": "affine-api-backendconfig"}'
|
||||||
|
web:
|
||||||
|
name: affine-web
|
||||||
|
type: ClusterIP
|
||||||
|
port: 8080
|
||||||
|
annotations: {}
|
||||||
|
doc:
|
||||||
|
type: ClusterIP
|
||||||
|
annotations: {}
|
||||||
|
|
||||||
|
nodeSelector: {}
|
||||||
|
tolerations: []
|
||||||
|
affinity: {}
|
||||||
@@ -3,7 +3,7 @@ name: graphql
|
|||||||
description: AFFiNE GraphQL server
|
description: AFFiNE GraphQL server
|
||||||
type: application
|
type: application
|
||||||
version: 0.0.0
|
version: 0.0.0
|
||||||
appVersion: "0.26.0"
|
appVersion: "0.26.1"
|
||||||
dependencies:
|
dependencies:
|
||||||
- name: gcloud-sql-proxy
|
- name: gcloud-sql-proxy
|
||||||
version: 0.0.0
|
version: 0.0.0
|
||||||
|
|||||||
@@ -27,8 +27,11 @@ podSecurityContext:
|
|||||||
fsGroup: 2000
|
fsGroup: 2000
|
||||||
|
|
||||||
resources:
|
resources:
|
||||||
|
limits:
|
||||||
|
cpu: '1'
|
||||||
|
memory: 4Gi
|
||||||
requests:
|
requests:
|
||||||
cpu: '2'
|
cpu: '1'
|
||||||
memory: 2Gi
|
memory: 2Gi
|
||||||
|
|
||||||
probe:
|
probe:
|
||||||
|
|||||||
11
.github/helm/affine/charts/renderer/Chart.yaml
vendored
11
.github/helm/affine/charts/renderer/Chart.yaml
vendored
@@ -1,11 +0,0 @@
|
|||||||
apiVersion: v2
|
|
||||||
name: renderer
|
|
||||||
description: AFFiNE renderer server
|
|
||||||
type: application
|
|
||||||
version: 0.0.0
|
|
||||||
appVersion: "0.26.0"
|
|
||||||
dependencies:
|
|
||||||
- name: gcloud-sql-proxy
|
|
||||||
version: 0.0.0
|
|
||||||
repository: "file://../gcloud-sql-proxy"
|
|
||||||
condition: .global.database.gcloud.enabled
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
1. Get the application URL by running these commands:
|
|
||||||
{{- if contains "NodePort" .Values.service.type }}
|
|
||||||
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "renderer.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 "renderer.fullname" . }}'
|
|
||||||
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "renderer.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 "renderer.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
|
|
||||||
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
|
|
||||||
echo "Visit http://127.0.0.1:8080 to use your application"
|
|
||||||
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
|
|
||||||
{{- end }}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
{{/*
|
|
||||||
Expand the name of the chart.
|
|
||||||
*/}}
|
|
||||||
{{- define "renderer.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 "renderer.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 "renderer.chart" -}}
|
|
||||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
{{/*
|
|
||||||
Common labels
|
|
||||||
*/}}
|
|
||||||
{{- define "renderer.labels" -}}
|
|
||||||
helm.sh/chart: {{ include "renderer.chart" . }}
|
|
||||||
{{ include "renderer.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 "renderer.selectorLabels" -}}
|
|
||||||
app.kubernetes.io/name: {{ include "renderer.name" . }}
|
|
||||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
{{/*
|
|
||||||
Create the name of the service account to use
|
|
||||||
*/}}
|
|
||||||
{{- define "renderer.serviceAccountName" -}}
|
|
||||||
{{- if .Values.serviceAccount.create }}
|
|
||||||
{{- default (include "renderer.fullname" .) .Values.serviceAccount.name }}
|
|
||||||
{{- else }}
|
|
||||||
{{- default "default" .Values.serviceAccount.name }}
|
|
||||||
{{- end }}
|
|
||||||
{{- end }}
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: {{ include "renderer.fullname" . }}
|
|
||||||
labels:
|
|
||||||
{{- include "renderer.labels" . | nindent 4 }}
|
|
||||||
spec:
|
|
||||||
replicas: {{ .Values.replicaCount }}
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
{{- include "renderer.selectorLabels" . | nindent 6 }}
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
{{- with .Values.podAnnotations }}
|
|
||||||
annotations:
|
|
||||||
{{- toYaml . | nindent 8 }}
|
|
||||||
{{- end }}
|
|
||||||
labels:
|
|
||||||
{{- include "renderer.selectorLabels" . | nindent 8 }}
|
|
||||||
spec:
|
|
||||||
{{- with .Values.imagePullSecrets }}
|
|
||||||
imagePullSecrets:
|
|
||||||
{{- toYaml . | nindent 8 }}
|
|
||||||
{{- end }}
|
|
||||||
serviceAccountName: {{ include "renderer.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=2048"
|
|
||||||
- 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: "renderer"
|
|
||||||
- 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.service.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 }}"
|
|
||||||
- name: DOC_SERVICE_ENDPOINT
|
|
||||||
value: "http://{{ .Values.global.docService.name }}:{{ .Values.global.docService.port }}"
|
|
||||||
ports:
|
|
||||||
- name: http
|
|
||||||
containerPort: {{ .Values.service.port }}
|
|
||||||
protocol: TCP
|
|
||||||
livenessProbe:
|
|
||||||
httpGet:
|
|
||||||
path: /info
|
|
||||||
port: http
|
|
||||||
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
|
|
||||||
readinessProbe:
|
|
||||||
httpGet:
|
|
||||||
path: /info
|
|
||||||
port: http
|
|
||||||
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
|
|
||||||
resources:
|
|
||||||
{{- toYaml .Values.resources | nindent 12 }}
|
|
||||||
{{- with .Values.nodeSelector }}
|
|
||||||
nodeSelector:
|
|
||||||
{{- toYaml . | nindent 8 }}
|
|
||||||
{{- end }}
|
|
||||||
{{- with .Values.affinity }}
|
|
||||||
affinity:
|
|
||||||
{{- toYaml . | nindent 8 }}
|
|
||||||
{{- end }}
|
|
||||||
{{- with .Values.tolerations }}
|
|
||||||
tolerations:
|
|
||||||
{{- toYaml . | nindent 8 }}
|
|
||||||
{{- end }}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: {{ include "graphql.fullname" . }}
|
|
||||||
labels:
|
|
||||||
{{- include "graphql.labels" . | nindent 4 }}
|
|
||||||
{{- with .Values.service.annotations }}
|
|
||||||
annotations:
|
|
||||||
{{- toYaml . | nindent 4 }}
|
|
||||||
{{- end }}
|
|
||||||
spec:
|
|
||||||
type: {{ .Values.service.type }}
|
|
||||||
ports:
|
|
||||||
- port: {{ .Values.service.port }}
|
|
||||||
targetPort: http
|
|
||||||
protocol: TCP
|
|
||||||
name: http
|
|
||||||
selector:
|
|
||||||
{{- include "graphql.selectorLabels" . | nindent 4 }}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
{{- if .Values.serviceAccount.create -}}
|
|
||||||
apiVersion: v1
|
|
||||||
kind: ServiceAccount
|
|
||||||
metadata:
|
|
||||||
name: {{ include "graphql.serviceAccountName" . }}
|
|
||||||
labels:
|
|
||||||
{{- include "graphql.labels" . | nindent 4 }}
|
|
||||||
{{- with .Values.serviceAccount.annotations }}
|
|
||||||
annotations:
|
|
||||||
{{- toYaml . | nindent 4 }}
|
|
||||||
{{- end }}
|
|
||||||
{{- end }}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: Pod
|
|
||||||
metadata:
|
|
||||||
name: "{{ include "renderer.fullname" . }}-test-connection"
|
|
||||||
labels:
|
|
||||||
{{- include "renderer.labels" . | nindent 4 }}
|
|
||||||
annotations:
|
|
||||||
"helm.sh/hook": test
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: wget
|
|
||||||
image: busybox
|
|
||||||
command: ['wget']
|
|
||||||
args: ['{{ include "renderer.fullname" . }}:{{ .Values.service.port }}']
|
|
||||||
restartPolicy: Never
|
|
||||||
38
.github/helm/affine/charts/renderer/values.yaml
vendored
38
.github/helm/affine/charts/renderer/values.yaml
vendored
@@ -1,38 +0,0 @@
|
|||||||
replicaCount: 1
|
|
||||||
image:
|
|
||||||
repository: ghcr.io/toeverything/affine
|
|
||||||
pullPolicy: IfNotPresent
|
|
||||||
tag: ''
|
|
||||||
|
|
||||||
imagePullSecrets: []
|
|
||||||
nameOverride: ''
|
|
||||||
fullnameOverride: ''
|
|
||||||
# map to NODE_ENV environment variable
|
|
||||||
env: 'production'
|
|
||||||
app:
|
|
||||||
# AFFINE_SERVER_SUB_PATH
|
|
||||||
path: ''
|
|
||||||
# AFFINE_SERVER_HOST
|
|
||||||
host: '0.0.0.0'
|
|
||||||
https: true
|
|
||||||
serviceAccount:
|
|
||||||
create: true
|
|
||||||
annotations: {}
|
|
||||||
name: 'affine-renderer'
|
|
||||||
|
|
||||||
podAnnotations: {}
|
|
||||||
|
|
||||||
podSecurityContext:
|
|
||||||
fsGroup: 2000
|
|
||||||
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
cpu: '1'
|
|
||||||
memory: 2Gi
|
|
||||||
|
|
||||||
probe:
|
|
||||||
initialDelaySeconds: 20
|
|
||||||
|
|
||||||
nodeSelector: {}
|
|
||||||
tolerations: []
|
|
||||||
affinity: {}
|
|
||||||
23
.github/helm/affine/charts/sync/.helmignore
vendored
23
.github/helm/affine/charts/sync/.helmignore
vendored
@@ -1,23 +0,0 @@
|
|||||||
# Patterns to ignore when building packages.
|
|
||||||
# This supports shell glob matching, relative path matching, and
|
|
||||||
# negation (prefixed with !). Only one pattern per line.
|
|
||||||
.DS_Store
|
|
||||||
# Common VCS dirs
|
|
||||||
.git/
|
|
||||||
.gitignore
|
|
||||||
.bzr/
|
|
||||||
.bzrignore
|
|
||||||
.hg/
|
|
||||||
.hgignore
|
|
||||||
.svn/
|
|
||||||
# Common backup files
|
|
||||||
*.swp
|
|
||||||
*.bak
|
|
||||||
*.tmp
|
|
||||||
*.orig
|
|
||||||
*~
|
|
||||||
# Various IDEs
|
|
||||||
.project
|
|
||||||
.idea/
|
|
||||||
*.tmproj
|
|
||||||
.vscode/
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
1. Get the application URL by running these commands:
|
|
||||||
{{- if contains "NodePort" .Values.service.type }}
|
|
||||||
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "sync.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 "sync.fullname" . }}'
|
|
||||||
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "sync.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 "sync.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
|
|
||||||
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
|
|
||||||
echo "Visit http://127.0.0.1:8080 to use your application"
|
|
||||||
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
|
|
||||||
{{- end }}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
{{/*
|
|
||||||
Expand the name of the chart.
|
|
||||||
*/}}
|
|
||||||
{{- define "sync.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 "sync.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 "sync.chart" -}}
|
|
||||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
{{/*
|
|
||||||
Common labels
|
|
||||||
*/}}
|
|
||||||
{{- define "sync.labels" -}}
|
|
||||||
helm.sh/chart: {{ include "sync.chart" . }}
|
|
||||||
{{ include "sync.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 "sync.selectorLabels" -}}
|
|
||||||
app.kubernetes.io/name: {{ include "sync.name" . }}
|
|
||||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
{{/*
|
|
||||||
Create the name of the service account to use
|
|
||||||
*/}}
|
|
||||||
{{- define "sync.serviceAccountName" -}}
|
|
||||||
{{- if .Values.serviceAccount.create }}
|
|
||||||
{{- default (include "sync.fullname" .) .Values.serviceAccount.name }}
|
|
||||||
{{- else }}
|
|
||||||
{{- default "default" .Values.serviceAccount.name }}
|
|
||||||
{{- end }}
|
|
||||||
{{- end }}
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: {{ include "sync.fullname" . }}
|
|
||||||
labels:
|
|
||||||
{{- include "sync.labels" . | nindent 4 }}
|
|
||||||
spec:
|
|
||||||
replicas: {{ .Values.replicaCount }}
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
{{- include "sync.selectorLabels" . | nindent 6 }}
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
{{- with .Values.podAnnotations }}
|
|
||||||
annotations:
|
|
||||||
{{- toYaml . | nindent 8 }}
|
|
||||||
{{- end }}
|
|
||||||
labels:
|
|
||||||
{{- include "sync.selectorLabels" . | nindent 8 }}
|
|
||||||
spec:
|
|
||||||
{{- with .Values.imagePullSecrets }}
|
|
||||||
imagePullSecrets:
|
|
||||||
{{- toYaml . | nindent 8 }}
|
|
||||||
{{- end }}
|
|
||||||
serviceAccountName: {{ include "sync.serviceAccountName" . }}
|
|
||||||
securityContext:
|
|
||||||
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
|
||||||
containers:
|
|
||||||
- name: {{ .Chart.Name }}
|
|
||||||
securityContext:
|
|
||||||
{{- toYaml .Values.securityContext | nindent 12 }}
|
|
||||||
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: NO_COLOR
|
|
||||||
value: "1"
|
|
||||||
- name: DEPLOYMENT_TYPE
|
|
||||||
value: "{{ .Values.global.deployment.type }}"
|
|
||||||
- name: DEPLOYMENT_PLATFORM
|
|
||||||
value: "{{ .Values.global.deployment.platform }}"
|
|
||||||
- name: SERVER_FLAVOR
|
|
||||||
value: "sync"
|
|
||||||
- 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_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.service.port }}"
|
|
||||||
- name: AFFINE_SERVER_HOST
|
|
||||||
value: "{{ .Values.app.host }}"
|
|
||||||
- name: DOC_SERVICE_ENDPOINT
|
|
||||||
value: "http://{{ .Values.global.docService.name }}:{{ .Values.global.docService.port }}"
|
|
||||||
ports:
|
|
||||||
- name: http
|
|
||||||
containerPort: {{ .Values.service.port }}
|
|
||||||
protocol: TCP
|
|
||||||
livenessProbe:
|
|
||||||
tcpSocket:
|
|
||||||
port: http
|
|
||||||
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
|
|
||||||
readinessProbe:
|
|
||||||
tcpSocket:
|
|
||||||
port: http
|
|
||||||
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
|
|
||||||
resources:
|
|
||||||
{{- toYaml .Values.resources | nindent 12 }}
|
|
||||||
{{- with .Values.nodeSelector }}
|
|
||||||
nodeSelector:
|
|
||||||
{{- toYaml . | nindent 8 }}
|
|
||||||
{{- end }}
|
|
||||||
{{- with .Values.affinity }}
|
|
||||||
affinity:
|
|
||||||
{{- toYaml . | nindent 8 }}
|
|
||||||
{{- end }}
|
|
||||||
{{- with .Values.tolerations }}
|
|
||||||
tolerations:
|
|
||||||
{{- toYaml . | nindent 8 }}
|
|
||||||
{{- end }}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: {{ include "sync.fullname" . }}
|
|
||||||
labels:
|
|
||||||
{{- include "sync.labels" . | nindent 4 }}
|
|
||||||
{{- with .Values.service.annotations }}
|
|
||||||
annotations:
|
|
||||||
{{- toYaml . | nindent 4 }}
|
|
||||||
{{- end }}
|
|
||||||
spec:
|
|
||||||
type: {{ .Values.service.type }}
|
|
||||||
ports:
|
|
||||||
- port: {{ .Values.service.port }}
|
|
||||||
targetPort: http
|
|
||||||
protocol: TCP
|
|
||||||
name: http
|
|
||||||
selector:
|
|
||||||
{{- include "sync.selectorLabels" . | nindent 4 }}
|
|
||||||
38
.github/helm/affine/charts/sync/values.yaml
vendored
38
.github/helm/affine/charts/sync/values.yaml
vendored
@@ -1,38 +0,0 @@
|
|||||||
replicaCount: 1
|
|
||||||
image:
|
|
||||||
repository: ghcr.io/toeverything/affine
|
|
||||||
pullPolicy: IfNotPresent
|
|
||||||
tag: ''
|
|
||||||
|
|
||||||
imagePullSecrets: []
|
|
||||||
nameOverride: ''
|
|
||||||
fullnameOverride: ''
|
|
||||||
# map to NODE_ENV environment variable
|
|
||||||
env: 'production'
|
|
||||||
app:
|
|
||||||
# AFFINE_SERVER_HOST
|
|
||||||
host: '0.0.0.0'
|
|
||||||
serviceAccount:
|
|
||||||
create: true
|
|
||||||
annotations: {}
|
|
||||||
name: 'affine-sync'
|
|
||||||
|
|
||||||
podAnnotations: {}
|
|
||||||
|
|
||||||
podSecurityContext:
|
|
||||||
fsGroup: 2000
|
|
||||||
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
cpu: '2'
|
|
||||||
memory: 4Gi
|
|
||||||
requests:
|
|
||||||
cpu: '1'
|
|
||||||
memory: 2Gi
|
|
||||||
|
|
||||||
probe:
|
|
||||||
initialDelaySeconds: 20
|
|
||||||
|
|
||||||
nodeSelector: {}
|
|
||||||
tolerations: []
|
|
||||||
affinity: {}
|
|
||||||
23
.github/helm/affine/charts/web/.helmignore
vendored
23
.github/helm/affine/charts/web/.helmignore
vendored
@@ -1,23 +0,0 @@
|
|||||||
# Patterns to ignore when building packages.
|
|
||||||
# This supports shell glob matching, relative path matching, and
|
|
||||||
# negation (prefixed with !). Only one pattern per line.
|
|
||||||
.DS_Store
|
|
||||||
# Common VCS dirs
|
|
||||||
.git/
|
|
||||||
.gitignore
|
|
||||||
.bzr/
|
|
||||||
.bzrignore
|
|
||||||
.hg/
|
|
||||||
.hgignore
|
|
||||||
.svn/
|
|
||||||
# Common backup files
|
|
||||||
*.swp
|
|
||||||
*.bak
|
|
||||||
*.tmp
|
|
||||||
*.orig
|
|
||||||
*~
|
|
||||||
# Various IDEs
|
|
||||||
.project
|
|
||||||
.idea/
|
|
||||||
*.tmproj
|
|
||||||
.vscode/
|
|
||||||
6
.github/helm/affine/charts/web/Chart.yaml
vendored
6
.github/helm/affine/charts/web/Chart.yaml
vendored
@@ -1,6 +0,0 @@
|
|||||||
apiVersion: v2
|
|
||||||
name: web
|
|
||||||
description: A Helm chart for Kubernetes
|
|
||||||
type: application
|
|
||||||
version: 0.0.0
|
|
||||||
appVersion: "0.7.0-canary.18"
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: {{ include "web.fullname" . }}
|
|
||||||
labels:
|
|
||||||
{{- include "web.labels" . | nindent 4 }}
|
|
||||||
spec:
|
|
||||||
replicas: {{ .Values.replicaCount }}
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
{{- include "web.selectorLabels" . | nindent 6 }}
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
{{- with .Values.podAnnotations }}
|
|
||||||
annotations:
|
|
||||||
{{- toYaml . | nindent 8 }}
|
|
||||||
{{- end }}
|
|
||||||
labels:
|
|
||||||
{{- include "web.selectorLabels" . | nindent 8 }}
|
|
||||||
spec:
|
|
||||||
{{- with .Values.imagePullSecrets }}
|
|
||||||
imagePullSecrets:
|
|
||||||
{{- toYaml . | nindent 8 }}
|
|
||||||
{{- end }}
|
|
||||||
serviceAccountName: {{ include "web.serviceAccountName" . }}
|
|
||||||
containers:
|
|
||||||
- name: {{ .Chart.Name }}
|
|
||||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
|
||||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
|
||||||
env:
|
|
||||||
- name: AFFINE_ENV
|
|
||||||
value: "{{ .Release.Namespace }}"
|
|
||||||
ports:
|
|
||||||
- name: http
|
|
||||||
containerPort: {{ .Values.service.port }}
|
|
||||||
protocol: TCP
|
|
||||||
livenessProbe:
|
|
||||||
httpGet:
|
|
||||||
path: /
|
|
||||||
port: http
|
|
||||||
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
|
|
||||||
readinessProbe:
|
|
||||||
httpGet:
|
|
||||||
path: /
|
|
||||||
port: http
|
|
||||||
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
|
|
||||||
resources:
|
|
||||||
{{- toYaml .Values.resources | nindent 12 }}
|
|
||||||
{{- with .Values.nodeSelector }}
|
|
||||||
nodeSelector:
|
|
||||||
{{- toYaml . | nindent 8 }}
|
|
||||||
{{- end }}
|
|
||||||
{{- with .Values.affinity }}
|
|
||||||
affinity:
|
|
||||||
{{- toYaml . | nindent 8 }}
|
|
||||||
{{- end }}
|
|
||||||
{{- with .Values.tolerations }}
|
|
||||||
tolerations:
|
|
||||||
{{- toYaml . | nindent 8 }}
|
|
||||||
{{- end }}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: {{ include "web.fullname" . }}
|
|
||||||
labels:
|
|
||||||
{{- include "web.labels" . | nindent 4 }}
|
|
||||||
spec:
|
|
||||||
type: {{ .Values.service.type }}
|
|
||||||
ports:
|
|
||||||
- port: {{ .Values.service.port }}
|
|
||||||
targetPort: http
|
|
||||||
protocol: TCP
|
|
||||||
name: http
|
|
||||||
selector:
|
|
||||||
{{- include "web.selectorLabels" . | nindent 4 }}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
{{- if .Values.serviceAccount.create -}}
|
|
||||||
apiVersion: v1
|
|
||||||
kind: ServiceAccount
|
|
||||||
metadata:
|
|
||||||
name: {{ include "web.serviceAccountName" . }}
|
|
||||||
labels:
|
|
||||||
{{- include "web.labels" . | nindent 4 }}
|
|
||||||
{{- with .Values.serviceAccount.annotations }}
|
|
||||||
annotations:
|
|
||||||
{{- toYaml . | nindent 4 }}
|
|
||||||
{{- end }}
|
|
||||||
{{- end }}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: Pod
|
|
||||||
metadata:
|
|
||||||
name: "{{ include "web.fullname" . }}-test-connection"
|
|
||||||
labels:
|
|
||||||
{{- include "web.labels" . | nindent 4 }}
|
|
||||||
annotations:
|
|
||||||
"helm.sh/hook": test
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: wget
|
|
||||||
image: busybox
|
|
||||||
command: ['wget']
|
|
||||||
args: ['{{ include "web.fullname" . }}:{{ .Values.service.port }}']
|
|
||||||
restartPolicy: Never
|
|
||||||
37
.github/helm/affine/charts/web/values.yaml
vendored
37
.github/helm/affine/charts/web/values.yaml
vendored
@@ -1,37 +0,0 @@
|
|||||||
replicaCount: 1
|
|
||||||
|
|
||||||
image:
|
|
||||||
repository: ghcr.io/toeverything/affine-front
|
|
||||||
pullPolicy: IfNotPresent
|
|
||||||
tag: ""
|
|
||||||
|
|
||||||
imagePullSecrets: []
|
|
||||||
nameOverride: ""
|
|
||||||
fullnameOverride: ""
|
|
||||||
|
|
||||||
serviceAccount:
|
|
||||||
create: true
|
|
||||||
annotations: {}
|
|
||||||
name: "affine-web"
|
|
||||||
|
|
||||||
podAnnotations: {}
|
|
||||||
|
|
||||||
podSecurityContext:
|
|
||||||
fsGroup: 2000
|
|
||||||
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
cpu: '500m'
|
|
||||||
memory: 2Gi
|
|
||||||
requests:
|
|
||||||
cpu: '500m'
|
|
||||||
memory: 2Gi
|
|
||||||
|
|
||||||
nodeSelector: {}
|
|
||||||
|
|
||||||
tolerations: []
|
|
||||||
|
|
||||||
affinity: {}
|
|
||||||
|
|
||||||
probe:
|
|
||||||
initialDelaySeconds: 1
|
|
||||||
12
.github/helm/affine/templates/ingress.yaml
vendored
12
.github/helm/affine/templates/ingress.yaml
vendored
@@ -44,9 +44,9 @@ spec:
|
|||||||
pathType: Prefix
|
pathType: Prefix
|
||||||
backend:
|
backend:
|
||||||
service:
|
service:
|
||||||
name: affine-sync
|
name: {{ $.Values.front.services.sync.name }}
|
||||||
port:
|
port:
|
||||||
number: {{ $.Values.sync.service.port }}
|
number: {{ $.Values.front.services.sync.port }}
|
||||||
- path: /graphql
|
- path: /graphql
|
||||||
pathType: Prefix
|
pathType: Prefix
|
||||||
backend:
|
backend:
|
||||||
@@ -65,15 +65,15 @@ spec:
|
|||||||
pathType: Prefix
|
pathType: Prefix
|
||||||
backend:
|
backend:
|
||||||
service:
|
service:
|
||||||
name: affine-renderer
|
name: {{ $.Values.front.services.renderer.name }}
|
||||||
port:
|
port:
|
||||||
number: {{ $.Values.renderer.service.port }}
|
number: {{ $.Values.front.services.renderer.port }}
|
||||||
- path: /
|
- path: /
|
||||||
pathType: Prefix
|
pathType: Prefix
|
||||||
backend:
|
backend:
|
||||||
service:
|
service:
|
||||||
name: affine-web
|
name: {{ $.Values.front.services.web.name }}
|
||||||
port:
|
port:
|
||||||
number: {{ $.Values.web.service.port }}
|
number: {{ $.Values.front.services.web.port }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|||||||
46
.github/helm/affine/values.yaml
vendored
46
.github/helm/affine/values.yaml
vendored
@@ -47,27 +47,25 @@ graphql:
|
|||||||
annotations:
|
annotations:
|
||||||
cloud.google.com/backend-config: '{"default": "affine-api-backendconfig"}'
|
cloud.google.com/backend-config: '{"default": "affine-api-backendconfig"}'
|
||||||
|
|
||||||
sync:
|
front:
|
||||||
service:
|
services:
|
||||||
type: ClusterIP
|
sync:
|
||||||
port: 3010
|
name: affine-sync
|
||||||
annotations:
|
type: ClusterIP
|
||||||
cloud.google.com/backend-config: '{"default": "affine-api-backendconfig"}'
|
port: 3010
|
||||||
|
annotations:
|
||||||
renderer:
|
cloud.google.com/backend-config: '{"default": "affine-api-backendconfig"}'
|
||||||
service:
|
renderer:
|
||||||
type: ClusterIP
|
name: affine-renderer
|
||||||
port: 3000
|
type: ClusterIP
|
||||||
annotations:
|
port: 3000
|
||||||
cloud.google.com/backend-config: '{"default": "affine-api-backendconfig"}'
|
annotations:
|
||||||
|
cloud.google.com/backend-config: '{"default": "affine-api-backendconfig"}'
|
||||||
doc:
|
web:
|
||||||
service:
|
name: affine-web
|
||||||
type: ClusterIP
|
type: ClusterIP
|
||||||
annotations:
|
port: 8080
|
||||||
cloud.google.com/backend-config: '{"default": "affine-api-backendconfig"}'
|
doc:
|
||||||
|
type: ClusterIP
|
||||||
web:
|
annotations:
|
||||||
service:
|
cloud.google.com/backend-config: '{"default": "affine-api-backendconfig"}'
|
||||||
type: ClusterIP
|
|
||||||
port: 8080
|
|
||||||
|
|||||||
6
.github/workflows/auto-labeler.yml
vendored
6
.github/workflows/auto-labeler.yml
vendored
@@ -1,6 +1,10 @@
|
|||||||
name: 'Pull Request Labeler'
|
name: 'Pull Request Labeler'
|
||||||
on:
|
on:
|
||||||
- pull_request_target
|
pull_request_target:
|
||||||
|
types:
|
||||||
|
- opened
|
||||||
|
- reopened
|
||||||
|
- synchronize
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
triage:
|
triage:
|
||||||
|
|||||||
13
.github/workflows/build-images.yml
vendored
13
.github/workflows/build-images.yml
vendored
@@ -263,18 +263,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
app-version: ${{ inputs.app-version }}
|
app-version: ${{ inputs.app-version }}
|
||||||
|
|
||||||
- name: Build front Dockerfile
|
- name: Build backend Dockerfile
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
push: true
|
|
||||||
pull: true
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
provenance: true
|
|
||||||
file: .github/deployment/front/Dockerfile
|
|
||||||
tags: ghcr.io/toeverything/affine-front:${{inputs.build-type}}-${{ inputs.git-short-hash }}
|
|
||||||
|
|
||||||
- name: Build graphql Dockerfile
|
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
|
|||||||
296
.github/workflows/build-test.yml
vendored
296
.github/workflows/build-test.yml
vendored
@@ -210,18 +210,13 @@ 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: ${{ matrix.browser }}
|
playwright-platform: 'chromium,firefox,webkit'
|
||||||
electron-install: false
|
electron-install: false
|
||||||
full-cache: true
|
full-cache: true
|
||||||
|
|
||||||
@@ -229,18 +224,64 @@ jobs:
|
|||||||
run: yarn workspace @blocksuite/playground build
|
run: yarn workspace @blocksuite/playground build
|
||||||
|
|
||||||
- name: Run playwright tests
|
- name: Run playwright tests
|
||||||
env:
|
run: |
|
||||||
BROWSER: ${{ matrix.browser }}
|
yarn workspace @blocksuite/integration-test test:unit
|
||||||
run: yarn workspace @affine-test/blocksuite test "cross-platform/" --forbid-only --shard=${{ matrix.shard }}/${{ strategy.job-total }}
|
yarn workspace @affine-test/blocksuite test "cross-platform/" --forbid-only
|
||||||
|
|
||||||
- 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-${{ matrix.browser }}-${{ matrix.shard }}
|
name: test-results-e2e-bs-cross-browser
|
||||||
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
|
||||||
@@ -307,7 +348,7 @@ jobs:
|
|||||||
name: Unit Test
|
name: Unit Test
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
- build-native
|
- build-native-linux
|
||||||
env:
|
env:
|
||||||
DISTRIBUTION: web
|
DISTRIBUTION: web
|
||||||
strategy:
|
strategy:
|
||||||
@@ -321,6 +362,7 @@ 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
|
||||||
@@ -341,7 +383,39 @@ jobs:
|
|||||||
name: affine
|
name: affine
|
||||||
fail_ci_if_error: false
|
fail_ci_if_error: false
|
||||||
|
|
||||||
build-native:
|
build-native-linux:
|
||||||
|
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:
|
||||||
@@ -350,7 +424,6 @@ 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 }
|
||||||
|
|
||||||
@@ -383,7 +456,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-windows-native:
|
build-native-windows:
|
||||||
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:
|
||||||
@@ -483,7 +556,7 @@ jobs:
|
|||||||
name: Native Unit Test
|
name: Native Unit Test
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
- build-native
|
- build-native-linux
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
@@ -577,8 +650,6 @@ 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
|
||||||
@@ -819,11 +890,51 @@ 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
|
||||||
@@ -857,53 +968,29 @@ 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 }}
|
||||||
@@ -914,6 +1001,7 @@ 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
|
||||||
@@ -928,6 +1016,7 @@ 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
|
||||||
@@ -951,30 +1040,7 @@ 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
|
||||||
@@ -983,20 +1049,17 @@ 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 }}
|
||||||
@@ -1006,7 +1069,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
- build-server-native
|
- build-server-native
|
||||||
- build-native
|
- build-native-linux
|
||||||
env:
|
env:
|
||||||
DISTRIBUTION: web
|
DISTRIBUTION: web
|
||||||
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
|
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
|
||||||
@@ -1099,7 +1162,9 @@ jobs:
|
|||||||
runs-on: ${{ matrix.spec.os }}
|
runs-on: ${{ matrix.spec.os }}
|
||||||
needs:
|
needs:
|
||||||
- build-electron-renderer
|
- build-electron-renderer
|
||||||
- build-native
|
- build-native-linux
|
||||||
|
- build-native-macos
|
||||||
|
- build-native-windows
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -1182,84 +1247,6 @@ 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:
|
||||||
@@ -1299,6 +1286,14 @@ 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
|
||||||
@@ -1312,8 +1307,9 @@ jobs:
|
|||||||
- e2e-blocksuite-cross-browser-test
|
- e2e-blocksuite-cross-browser-test
|
||||||
- e2e-mobile-test
|
- e2e-mobile-test
|
||||||
- unit-test
|
- unit-test
|
||||||
- build-native
|
- build-native-linux
|
||||||
- build-windows-native
|
- build-native-macos
|
||||||
|
- build-native-windows
|
||||||
- build-server-native
|
- build-server-native
|
||||||
- build-electron-renderer
|
- build-electron-renderer
|
||||||
- native-unit-test
|
- native-unit-test
|
||||||
@@ -1323,10 +1319,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,6 +16,7 @@ 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,13 +201,44 @@ jobs:
|
|||||||
nmHoistingLimits: workspaces
|
nmHoistingLimits: workspaces
|
||||||
env:
|
env:
|
||||||
npm_config_arch: ${{ matrix.spec.arch }}
|
npm_config_arch: ${{ matrix.spec.arch }}
|
||||||
- name: Download and overwrite packaged artifacts
|
- name: Download 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: .
|
path: signed-packaged-diff
|
||||||
- name: unzip file
|
- name: Apply signed packaged file diff
|
||||||
run: Expand-Archive -Path signed.zip -DestinationPath packages/frontend/apps/electron/out
|
shell: pwsh
|
||||||
|
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 }}
|
||||||
@@ -267,13 +298,44 @@ jobs:
|
|||||||
arch: arm64
|
arch: arm64
|
||||||
runs-on: ${{ matrix.spec.runner }}
|
runs-on: ${{ matrix.spec.runner }}
|
||||||
steps:
|
steps:
|
||||||
- name: Download and overwrite installer artifacts
|
- name: Download 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: .
|
path: signed-installer-diff
|
||||||
- name: unzip file
|
- name: Apply signed installer file diff
|
||||||
run: Expand-Archive -Path signed.zip -DestinationPath packages/frontend/apps/electron/out/${{ env.BUILD_TYPE }}/make
|
shell: pwsh
|
||||||
|
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: |
|
||||||
|
|||||||
72
.github/workflows/sync-i18n.yml
vendored
72
.github/workflows/sync-i18n.yml
vendored
@@ -1,72 +0,0 @@
|
|||||||
name: Sync I18n with Crowdin
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- canary
|
|
||||||
paths:
|
|
||||||
- 'packages/frontend/i18n/**'
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
synchronize-with-crowdin:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Crowdin action
|
|
||||||
id: crowdin
|
|
||||||
uses: crowdin/github-action@v2
|
|
||||||
with:
|
|
||||||
upload_sources: true
|
|
||||||
upload_translations: false
|
|
||||||
download_translations: true
|
|
||||||
auto_approve_imported: true
|
|
||||||
import_eq_suggestions: true
|
|
||||||
export_only_approved: true
|
|
||||||
skip_untranslated_strings: true
|
|
||||||
localization_branch_name: l10n_crowdin_translations
|
|
||||||
create_pull_request: true
|
|
||||||
pull_request_title: 'chore(i18n): sync translations'
|
|
||||||
pull_request_body: 'New Crowdin translations by [Crowdin GH Action](https://github.com/crowdin/github-action)'
|
|
||||||
pull_request_base_branch_name: 'canary'
|
|
||||||
config: packages/frontend/i18n/crowdin.yml
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
|
||||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
|
||||||
i18n-codegen:
|
|
||||||
needs: synchronize-with-crowdin
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
ref: l10n_crowdin_translations
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: ./.github/actions/setup-node
|
|
||||||
with:
|
|
||||||
electron-install: false
|
|
||||||
full-cache: true
|
|
||||||
|
|
||||||
- name: Run i18n codegen
|
|
||||||
run: yarn affine @affine/i18n build
|
|
||||||
|
|
||||||
- name: Commit changes
|
|
||||||
run: |
|
|
||||||
git config user.name "github-actions[bot]"
|
|
||||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
||||||
git add .
|
|
||||||
git commit -m "chore(i18n): i18n codegen"
|
|
||||||
git push origin l10n_crowdin_translations
|
|
||||||
40
.github/workflows/windows-signer.yml
vendored
40
.github/workflows/windows-signer.yml
vendored
@@ -30,13 +30,43 @@ 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: zip file
|
- name: collect signed file diff
|
||||||
shell: cmd
|
shell: powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File {0}
|
||||||
run: |
|
run: |
|
||||||
cd ${{ env.ARCHIVE_DIR }}
|
$OutDir = Join-Path '${{ env.ARCHIVE_DIR }}' 'out'
|
||||||
7za a signed.zip .\out\*
|
$DiffDir = Join-Path '${{ env.ARCHIVE_DIR }}' 'signed-diff'
|
||||||
|
$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.zip
|
path: ${{ env.ARCHIVE_DIR }}/signed-diff
|
||||||
|
|||||||
@@ -5,6 +5,10 @@
|
|||||||
"correctness": "error",
|
"correctness": "error",
|
||||||
"perf": "error"
|
"perf": "error"
|
||||||
},
|
},
|
||||||
|
"env": {
|
||||||
|
"builtin": true,
|
||||||
|
"es2026": true
|
||||||
|
},
|
||||||
"ignorePatterns": [
|
"ignorePatterns": [
|
||||||
"**/node_modules",
|
"**/node_modules",
|
||||||
".yarn",
|
".yarn",
|
||||||
@@ -44,6 +48,34 @@
|
|||||||
"**/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",
|
||||||
@@ -70,6 +102,14 @@
|
|||||||
"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",
|
||||||
@@ -126,6 +166,7 @@
|
|||||||
"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",
|
||||||
@@ -241,6 +282,42 @@
|
|||||||
"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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
13
.vscode/settings.template.json
vendored
13
.vscode/settings.template.json
vendored
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"eslint.packageManager": "yarn",
|
"prisma.pinToPrisma6": true,
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"editor.formatOnSaveMode": "file",
|
"editor.formatOnSaveMode": "file",
|
||||||
@@ -14,11 +14,13 @@
|
|||||||
"testid",
|
"testid",
|
||||||
"schemars"
|
"schemars"
|
||||||
],
|
],
|
||||||
|
"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*, .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*, crowdin*, cypress.*, 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.config.*, webpack*, workspace.json, xo.config.*, yarn*, babel.*, .babelrc, project.json",
|
"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.*",
|
||||||
"Cargo.toml": "Cargo.lock",
|
"Cargo.toml": "Cargo.lock, rust-toolchain*, rustfmt.toml, .taplo.toml",
|
||||||
"README.md": "LICENSE, CHANGELOG.md, CODE_OF_CONDUCT.md, CONTRIBUTING.md"
|
"README.md": "LICENSE*, CHANGELOG.md, CODE_OF_CONDUCT.md, CONTRIBUTING.md, SECURITY.md, README.*",
|
||||||
|
".gitignore": ".gitattributes, .dockerignore, .eslintignore, .prettierignore, .stylelintignore, .tslintignore, .yarnignore"
|
||||||
},
|
},
|
||||||
"[rust]": {
|
"[rust]": {
|
||||||
"editor.defaultFormatter": "rust-lang.rust-analyzer"
|
"editor.defaultFormatter": "rust-lang.rust-analyzer"
|
||||||
@@ -32,5 +34,6 @@
|
|||||||
"vitest.include": ["packages/**/*.spec.ts", "packages/**/*.spec.tsx"],
|
"vitest.include": ["packages/**/*.spec.ts", "packages/**/*.spec.tsx"],
|
||||||
"rust-analyzer.check.extraEnv": {
|
"rust-analyzer.check.extraEnv": {
|
||||||
"DATABASE_URL": "sqlite:affine.db"
|
"DATABASE_URL": "sqlite:affine.db"
|
||||||
}
|
},
|
||||||
|
"typescript.tsdk": "node_modules/typescript/lib"
|
||||||
}
|
}
|
||||||
|
|||||||
35
README.md
35
README.md
@@ -21,23 +21,6 @@
|
|||||||
<br/>
|
<br/>
|
||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
<div align="left" valign="middle">
|
|
||||||
<a href="https://runblaze.dev">
|
|
||||||
<picture>
|
|
||||||
<source media="(prefers-color-scheme: dark)" srcset="https://www.runblaze.dev/logo_dark.png">
|
|
||||||
<img align="right" src="https://www.runblaze.dev/logo_light.png" height="102px"/>
|
|
||||||
</picture>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<br style="display: none;"/>
|
|
||||||
|
|
||||||
_Special thanks to [Blaze](https://runblaze.dev) for their support of this project. They provide high-performance Apple Silicon macOS and Linux (AMD64 & ARM64) runners for GitHub Actions, greatly reducing our automated build times._
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<br/>
|
|
||||||
<br/>
|
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<a href="https://affine.pro">Home Page</a> |
|
<a href="https://affine.pro">Home Page</a> |
|
||||||
<a href="https://affine.pro/redirect/discord">Discord</a> |
|
<a href="https://affine.pro/redirect/discord">Discord</a> |
|
||||||
@@ -107,10 +90,10 @@ Thanks for checking us out, we appreciate your interest and sincerely hope that
|
|||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
| Bug Reports | Feature Requests | Questions/Discussions | AFFiNE Community |
|
| Bug Reports | Feature Requests | Questions/Discussions | AFFiNE Community |
|
||||||
| --------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | --------------------------------------------------------- |
|
| --------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | ---------------------------------------------------------- |
|
||||||
| [Create a bug report](https://github.com/toeverything/AFFiNE/issues/new?assignees=&labels=bug%2Cproduct-review&template=BUG-REPORT.yml&title=TITLE) | [Submit a feature request](https://github.com/toeverything/AFFiNE/issues/new?assignees=&labels=feat%2Cproduct-review&template=FEATURE-REQUEST.yml&title=TITLE) | [Check GitHub Discussion](https://github.com/toeverything/AFFiNE/discussions) | [Vist the AFFiNE Community](https://community.affine.pro) |
|
| [Create a bug report](https://github.com/toeverything/AFFiNE/issues/new?assignees=&labels=bug%2Cproduct-review&template=BUG-REPORT.yml&title=TITLE) | [Submit a feature request](https://github.com/toeverything/AFFiNE/issues/new?assignees=&labels=feat%2Cproduct-review&template=FEATURE-REQUEST.yml&title=TITLE) | [Check GitHub Discussion](https://github.com/toeverything/AFFiNE/discussions) | [Visit the AFFiNE Community](https://community.affine.pro) |
|
||||||
| Something isn't working as expected | An idea for a new feature, or improvements | Discuss and ask questions | A place to ask, learn and engage with others |
|
| Something isn't working as expected | An idea for a new feature, or improvements | Discuss and ask questions | A place to ask, learn and engage with others |
|
||||||
|
|
||||||
Calling all developers, testers, tech writers and more! Contributions of all types are more than welcome, you can read more in [docs/types-of-contributions.md](docs/types-of-contributions.md). If you are interested in contributing code, read our [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md) and feel free to check out our GitHub issues to get stuck in to show us what you’re made of.
|
Calling all developers, testers, tech writers and more! Contributions of all types are more than welcome, you can read more in [docs/types-of-contributions.md](docs/types-of-contributions.md). If you are interested in contributing code, read our [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md) and feel free to check out our GitHub issues to get stuck in to show us what you’re made of.
|
||||||
|
|
||||||
@@ -169,8 +152,10 @@ Welcome to the AFFiNE blog section! Here, you’ll find the latest insights, tip
|
|||||||
We would also like to give thanks to open-source projects that make AFFiNE possible:
|
We would also like to give thanks to open-source projects that make AFFiNE possible:
|
||||||
|
|
||||||
- [Blocksuite](https://github.com/toeverything/BlockSuite) - 💠 BlockSuite is the open-source collaborative editor project behind AFFiNE.
|
- [Blocksuite](https://github.com/toeverything/BlockSuite) - 💠 BlockSuite is the open-source collaborative editor project behind AFFiNE.
|
||||||
|
- [y-octo](https://github.com/y-crdt/y-octo) - 🐙 y-octo is a native, high-performance, thread-safe YJS CRDT implementation, serving as the core engine enabling the AFFiNE Client/Server to achieve "local-first" functionality.
|
||||||
- [OctoBase](https://github.com/toeverything/OctoBase) - 🐙 OctoBase is the open-source database behind AFFiNE, local-first, yet collaborative. A light-weight, scalable, data engine written in Rust.
|
- [OctoBase](https://github.com/toeverything/OctoBase) - 🐙 OctoBase is the open-source database behind AFFiNE, local-first, yet collaborative. A light-weight, scalable, data engine written in Rust.
|
||||||
- [yjs](https://github.com/yjs/yjs) - Fundamental support of CRDTs for our implementation on state management and data sync.
|
|
||||||
|
- [yjs](https://github.com/yjs/yjs) - Fundamental support of CRDTs for our implementation on state management and data sync on web.
|
||||||
- [electron](https://github.com/electron/electron) - Build cross-platform desktop apps with JavaScript, HTML, and CSS.
|
- [electron](https://github.com/electron/electron) - Build cross-platform desktop apps with JavaScript, HTML, and CSS.
|
||||||
- [React](https://github.com/facebook/react) - The library for web and native user interfaces.
|
- [React](https://github.com/facebook/react) - The library for web and native user interfaces.
|
||||||
- [napi-rs](https://github.com/napi-rs/napi-rs) - A framework for building compiled Node.js add-ons in Rust via Node-API.
|
- [napi-rs](https://github.com/napi-rs/napi-rs) - A framework for building compiled Node.js add-ons in Rust via Node-API.
|
||||||
@@ -221,12 +206,6 @@ See [BUILDING.md] for instructions on how to build AFFiNE from source code.
|
|||||||
We welcome contributions from everyone.
|
We welcome contributions from everyone.
|
||||||
See [docs/contributing/tutorial.md](./docs/contributing/tutorial.md) for details.
|
See [docs/contributing/tutorial.md](./docs/contributing/tutorial.md) for details.
|
||||||
|
|
||||||
## Thanks
|
|
||||||
|
|
||||||
<a href="https://www.chromatic.com/"><img src="https://user-images.githubusercontent.com/321738/84662277-e3db4f80-af1b-11ea-88f5-91d67a5e59f6.png" width="153" height="30" alt="Chromatic" /></a>
|
|
||||||
|
|
||||||
Thanks to [Chromatic](https://www.chromatic.com/) for providing the visual testing platform that helps us review UI changes and catch visual regressions.
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
### Editions
|
### Editions
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ We recommend users to always use the latest major version. Security updates will
|
|||||||
|
|
||||||
| Version | Supported |
|
| Version | Supported |
|
||||||
| --------------- | ------------------ |
|
| --------------- | ------------------ |
|
||||||
| 0.25.x (stable) | :white_check_mark: |
|
| 0.26.x (stable) | :white_check_mark: |
|
||||||
| < 0.25.x | :x: |
|
| < 0.26.x | :x: |
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
|||||||
@@ -296,7 +296,7 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.0",
|
"version": "0.26.1",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vanilla-extract/vite-plugin": "^5.0.0",
|
"@vanilla-extract/vite-plugin": "^5.0.0",
|
||||||
"msw": "^2.12.4",
|
"msw": "^2.12.4",
|
||||||
|
|||||||
@@ -2101,6 +2101,157 @@ 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>`);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import type { AffineTextAttributes } from '@blocksuite/affine-shared/types';
|
||||||
|
import { describe, expect, test } from 'vitest';
|
||||||
|
|
||||||
|
import { insertUrlTextSegments } from '../../../../blocks/database/src/properties/paste-url.js';
|
||||||
|
|
||||||
|
type InsertCall = {
|
||||||
|
range: {
|
||||||
|
index: number;
|
||||||
|
length: number;
|
||||||
|
};
|
||||||
|
text: string;
|
||||||
|
attributes?: AffineTextAttributes;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('insertUrlTextSegments', () => {
|
||||||
|
test('should replace selected text on first insert and append remaining segments', () => {
|
||||||
|
const insertCalls: InsertCall[] = [];
|
||||||
|
const selectionCalls: Array<{ index: number; length: number } | null> = [];
|
||||||
|
const inlineEditor = {
|
||||||
|
insertText: (
|
||||||
|
range: { index: number; length: number },
|
||||||
|
text: string,
|
||||||
|
attributes?: AffineTextAttributes
|
||||||
|
) => {
|
||||||
|
insertCalls.push({ range, text, attributes });
|
||||||
|
},
|
||||||
|
setInlineRange: (range: { index: number; length: number } | null) => {
|
||||||
|
selectionCalls.push(range);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const inlineRange = { index: 4, length: 6 };
|
||||||
|
const segments = [
|
||||||
|
{ text: 'hi - ' },
|
||||||
|
{ text: 'https://google.com', link: 'https://google.com' },
|
||||||
|
];
|
||||||
|
|
||||||
|
insertUrlTextSegments(inlineEditor, inlineRange, segments);
|
||||||
|
|
||||||
|
expect(insertCalls).toEqual([
|
||||||
|
{
|
||||||
|
range: { index: 4, length: 6 },
|
||||||
|
text: 'hi - ',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
range: { index: 9, length: 0 },
|
||||||
|
text: 'https://google.com',
|
||||||
|
attributes: {
|
||||||
|
link: 'https://google.com',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(selectionCalls).toEqual([{ index: 27, length: 0 }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should keep insertion range length zero when there is no selected text', () => {
|
||||||
|
const insertCalls: InsertCall[] = [];
|
||||||
|
const selectionCalls: Array<{ index: number; length: number } | null> = [];
|
||||||
|
const inlineEditor = {
|
||||||
|
insertText: (
|
||||||
|
range: { index: number; length: number },
|
||||||
|
text: string,
|
||||||
|
attributes?: AffineTextAttributes
|
||||||
|
) => {
|
||||||
|
insertCalls.push({ range, text, attributes });
|
||||||
|
},
|
||||||
|
setInlineRange: (range: { index: number; length: number } | null) => {
|
||||||
|
selectionCalls.push(range);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const inlineRange = { index: 2, length: 0 };
|
||||||
|
const segments = [
|
||||||
|
{ text: 'prefix ' },
|
||||||
|
{ text: 'https://a.com', link: 'https://a.com' },
|
||||||
|
];
|
||||||
|
|
||||||
|
insertUrlTextSegments(inlineEditor, inlineRange, segments);
|
||||||
|
|
||||||
|
expect(insertCalls).toEqual([
|
||||||
|
{
|
||||||
|
range: { index: 2, length: 0 },
|
||||||
|
text: 'prefix ',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
range: { index: 9, length: 0 },
|
||||||
|
text: 'https://a.com',
|
||||||
|
attributes: {
|
||||||
|
link: 'https://a.com',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(selectionCalls).toEqual([{ index: 22, length: 0 }]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -41,5 +41,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.0"
|
"version": "0.26.1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,5 +45,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.0"
|
"version": "0.26.1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,5 +45,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.0"
|
"version": "0.26.1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,5 +48,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.0"
|
"version": "0.26.1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,5 +42,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.0"
|
"version": "0.26.1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,5 +48,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.0"
|
"version": "0.26.1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,14 +135,10 @@ export class DatabaseBlockDataSource extends DataSourceBase {
|
|||||||
|
|
||||||
override featureFlags$: ReadonlySignal<DatabaseFlags> = computed(() => {
|
override featureFlags$: ReadonlySignal<DatabaseFlags> = computed(() => {
|
||||||
const featureFlagService = this.doc.get(FeatureFlagService);
|
const featureFlagService = this.doc.get(FeatureFlagService);
|
||||||
const enableNumberFormat = featureFlagService.getFlag(
|
|
||||||
'enable_database_number_formatting'
|
|
||||||
);
|
|
||||||
const enableTableVirtualScroll = featureFlagService.getFlag(
|
const enableTableVirtualScroll = featureFlagService.getFlag(
|
||||||
'enable_table_virtual_scroll'
|
'enable_table_virtual_scroll'
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
enable_number_formatting: enableNumberFormat ?? false,
|
|
||||||
enable_table_virtual_scroll: enableTableVirtualScroll ?? false,
|
enable_table_virtual_scroll: enableTableVirtualScroll ?? false,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import type {
|
||||||
|
AffineInlineEditor,
|
||||||
|
AffineTextAttributes,
|
||||||
|
} from '@blocksuite/affine-shared/types';
|
||||||
|
import {
|
||||||
|
splitTextByUrl,
|
||||||
|
type UrlTextSegment,
|
||||||
|
} from '@blocksuite/affine-shared/utils';
|
||||||
|
import type { InlineRange } from '@blocksuite/std/inline';
|
||||||
|
|
||||||
|
type UrlPasteInlineEditor = Pick<
|
||||||
|
AffineInlineEditor,
|
||||||
|
'insertText' | 'setInlineRange'
|
||||||
|
>;
|
||||||
|
|
||||||
|
export function analyzeTextForUrlPaste(text: string) {
|
||||||
|
const segments = splitTextByUrl(text);
|
||||||
|
const firstSegment = segments[0];
|
||||||
|
const singleUrl =
|
||||||
|
segments.length === 1 && firstSegment?.link && firstSegment.text === text
|
||||||
|
? firstSegment.link
|
||||||
|
: undefined;
|
||||||
|
return {
|
||||||
|
segments,
|
||||||
|
singleUrl,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function insertUrlTextSegments(
|
||||||
|
inlineEditor: UrlPasteInlineEditor,
|
||||||
|
inlineRange: InlineRange,
|
||||||
|
segments: UrlTextSegment[]
|
||||||
|
) {
|
||||||
|
let index = inlineRange.index;
|
||||||
|
let replacedSelection = false;
|
||||||
|
segments.forEach(segment => {
|
||||||
|
if (!segment.text) return;
|
||||||
|
const attributes: AffineTextAttributes | undefined = segment.link
|
||||||
|
? { link: segment.link }
|
||||||
|
: undefined;
|
||||||
|
inlineEditor.insertText(
|
||||||
|
{
|
||||||
|
index,
|
||||||
|
length: replacedSelection ? 0 : inlineRange.length,
|
||||||
|
},
|
||||||
|
segment.text,
|
||||||
|
attributes
|
||||||
|
);
|
||||||
|
replacedSelection = true;
|
||||||
|
index += segment.text.length;
|
||||||
|
});
|
||||||
|
inlineEditor.setInlineRange({
|
||||||
|
index,
|
||||||
|
length: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -8,10 +8,7 @@ import type {
|
|||||||
AffineInlineEditor,
|
AffineInlineEditor,
|
||||||
AffineTextAttributes,
|
AffineTextAttributes,
|
||||||
} from '@blocksuite/affine-shared/types';
|
} from '@blocksuite/affine-shared/types';
|
||||||
import {
|
import { getViewportElement } from '@blocksuite/affine-shared/utils';
|
||||||
getViewportElement,
|
|
||||||
isValidUrl,
|
|
||||||
} from '@blocksuite/affine-shared/utils';
|
|
||||||
import {
|
import {
|
||||||
BaseCellRenderer,
|
BaseCellRenderer,
|
||||||
createFromBaseCellRenderer,
|
createFromBaseCellRenderer,
|
||||||
@@ -26,6 +23,7 @@ import { html } from 'lit/static-html.js';
|
|||||||
|
|
||||||
import { EditorHostKey } from '../../context/host-context.js';
|
import { EditorHostKey } from '../../context/host-context.js';
|
||||||
import type { DatabaseBlockComponent } from '../../database-block.js';
|
import type { DatabaseBlockComponent } from '../../database-block.js';
|
||||||
|
import { analyzeTextForUrlPaste, insertUrlTextSegments } from '../paste-url.js';
|
||||||
import {
|
import {
|
||||||
richTextCellStyle,
|
richTextCellStyle,
|
||||||
richTextContainerStyle,
|
richTextContainerStyle,
|
||||||
@@ -271,10 +269,13 @@ export class RichTextCell extends BaseCellRenderer<Text, string> {
|
|||||||
?.getData('text/plain')
|
?.getData('text/plain')
|
||||||
?.replace(/\r?\n|\r/g, '\n');
|
?.replace(/\r?\n|\r/g, '\n');
|
||||||
if (!text) return;
|
if (!text) return;
|
||||||
|
const { segments, singleUrl } = analyzeTextForUrlPaste(text);
|
||||||
|
|
||||||
if (isValidUrl(text)) {
|
if (singleUrl) {
|
||||||
const std = this.std;
|
const std = this.std;
|
||||||
const result = std?.getOptional(ParseDocUrlProvider)?.parseDocUrl(text);
|
const result = std
|
||||||
|
?.getOptional(ParseDocUrlProvider)
|
||||||
|
?.parseDocUrl(singleUrl);
|
||||||
if (result) {
|
if (result) {
|
||||||
const text = ' ';
|
const text = ' ';
|
||||||
inlineEditor.insertText(inlineRange, text, {
|
inlineEditor.insertText(inlineRange, text, {
|
||||||
@@ -300,22 +301,10 @@ export class RichTextCell extends BaseCellRenderer<Text, string> {
|
|||||||
segment: 'database',
|
segment: 'database',
|
||||||
parentFlavour: 'affine:database',
|
parentFlavour: 'affine:database',
|
||||||
});
|
});
|
||||||
} else {
|
return;
|
||||||
inlineEditor.insertText(inlineRange, text, {
|
|
||||||
link: text,
|
|
||||||
});
|
|
||||||
inlineEditor.setInlineRange({
|
|
||||||
index: inlineRange.index + text.length,
|
|
||||||
length: 0,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
inlineEditor.insertText(inlineRange, text);
|
|
||||||
inlineEditor.setInlineRange({
|
|
||||||
index: inlineRange.index + text.length,
|
|
||||||
length: 0,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
insertUrlTextSegments(inlineEditor, inlineRange, segments);
|
||||||
};
|
};
|
||||||
|
|
||||||
override connectedCallback() {
|
override connectedCallback() {
|
||||||
|
|||||||
@@ -4,10 +4,7 @@ import {
|
|||||||
ParseDocUrlProvider,
|
ParseDocUrlProvider,
|
||||||
TelemetryProvider,
|
TelemetryProvider,
|
||||||
} from '@blocksuite/affine-shared/services';
|
} from '@blocksuite/affine-shared/services';
|
||||||
import {
|
import { getViewportElement } from '@blocksuite/affine-shared/utils';
|
||||||
getViewportElement,
|
|
||||||
isValidUrl,
|
|
||||||
} from '@blocksuite/affine-shared/utils';
|
|
||||||
import { BaseCellRenderer } from '@blocksuite/data-view';
|
import { BaseCellRenderer } from '@blocksuite/data-view';
|
||||||
import { IS_MAC } from '@blocksuite/global/env';
|
import { IS_MAC } from '@blocksuite/global/env';
|
||||||
import { LinkedPageIcon } from '@blocksuite/icons/lit';
|
import { LinkedPageIcon } from '@blocksuite/icons/lit';
|
||||||
@@ -20,6 +17,7 @@ import { html } from 'lit/static-html.js';
|
|||||||
import { EditorHostKey } from '../../context/host-context.js';
|
import { EditorHostKey } from '../../context/host-context.js';
|
||||||
import type { DatabaseBlockComponent } from '../../database-block.js';
|
import type { DatabaseBlockComponent } from '../../database-block.js';
|
||||||
import { getSingleDocIdFromText } from '../../utils/title-doc.js';
|
import { getSingleDocIdFromText } from '../../utils/title-doc.js';
|
||||||
|
import { analyzeTextForUrlPaste, insertUrlTextSegments } from '../paste-url.js';
|
||||||
import {
|
import {
|
||||||
headerAreaIconStyle,
|
headerAreaIconStyle,
|
||||||
titleCellStyle,
|
titleCellStyle,
|
||||||
@@ -95,7 +93,9 @@ export class HeaderAreaTextCell extends BaseCellRenderer<Text, string> {
|
|||||||
private readonly _onPaste = (e: ClipboardEvent) => {
|
private readonly _onPaste = (e: ClipboardEvent) => {
|
||||||
const inlineEditor = this.inlineEditor;
|
const inlineEditor = this.inlineEditor;
|
||||||
const inlineRange = inlineEditor?.getInlineRange();
|
const inlineRange = inlineEditor?.getInlineRange();
|
||||||
if (!inlineRange) return;
|
if (!inlineEditor || !inlineRange) return;
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
if (e.clipboardData) {
|
if (e.clipboardData) {
|
||||||
try {
|
try {
|
||||||
const getDeltas = (snapshot: BlockSnapshot): DeltaInsert[] => {
|
const getDeltas = (snapshot: BlockSnapshot): DeltaInsert[] => {
|
||||||
@@ -121,14 +121,15 @@ export class HeaderAreaTextCell extends BaseCellRenderer<Text, string> {
|
|||||||
?.getData('text/plain')
|
?.getData('text/plain')
|
||||||
?.replace(/\r?\n|\r/g, '\n');
|
?.replace(/\r?\n|\r/g, '\n');
|
||||||
if (!text) return;
|
if (!text) return;
|
||||||
e.preventDefault();
|
const { segments, singleUrl } = analyzeTextForUrlPaste(text);
|
||||||
e.stopPropagation();
|
if (singleUrl) {
|
||||||
if (isValidUrl(text)) {
|
|
||||||
const std = this.std;
|
const std = this.std;
|
||||||
const result = std?.getOptional(ParseDocUrlProvider)?.parseDocUrl(text);
|
const result = std
|
||||||
|
?.getOptional(ParseDocUrlProvider)
|
||||||
|
?.parseDocUrl(singleUrl);
|
||||||
if (result) {
|
if (result) {
|
||||||
const text = ' ';
|
const text = ' ';
|
||||||
inlineEditor?.insertText(inlineRange, text, {
|
inlineEditor.insertText(inlineRange, text, {
|
||||||
reference: {
|
reference: {
|
||||||
type: 'LinkedPage',
|
type: 'LinkedPage',
|
||||||
pageId: result.docId,
|
pageId: result.docId,
|
||||||
@@ -139,7 +140,7 @@ export class HeaderAreaTextCell extends BaseCellRenderer<Text, string> {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
inlineEditor?.setInlineRange({
|
inlineEditor.setInlineRange({
|
||||||
index: inlineRange.index + text.length,
|
index: inlineRange.index + text.length,
|
||||||
length: 0,
|
length: 0,
|
||||||
});
|
});
|
||||||
@@ -151,22 +152,10 @@ export class HeaderAreaTextCell extends BaseCellRenderer<Text, string> {
|
|||||||
segment: 'database',
|
segment: 'database',
|
||||||
parentFlavour: 'affine:database',
|
parentFlavour: 'affine:database',
|
||||||
});
|
});
|
||||||
} else {
|
return;
|
||||||
inlineEditor?.insertText(inlineRange, text, {
|
|
||||||
link: text,
|
|
||||||
});
|
|
||||||
inlineEditor?.setInlineRange({
|
|
||||||
index: inlineRange.index + text.length,
|
|
||||||
length: 0,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
inlineEditor?.insertText(inlineRange, text);
|
|
||||||
inlineEditor?.setInlineRange({
|
|
||||||
index: inlineRange.index + text.length,
|
|
||||||
length: 0,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
insertUrlTextSegments(inlineEditor, inlineRange, segments);
|
||||||
};
|
};
|
||||||
|
|
||||||
insertDelta = (delta: DeltaInsert) => {
|
insertDelta = (delta: DeltaInsert) => {
|
||||||
@@ -240,7 +229,8 @@ export class HeaderAreaTextCell extends BaseCellRenderer<Text, string> {
|
|||||||
this.disposables.addFromEvent(
|
this.disposables.addFromEvent(
|
||||||
this.richText.value,
|
this.richText.value,
|
||||||
'paste',
|
'paste',
|
||||||
this._onPaste
|
this._onPaste,
|
||||||
|
true
|
||||||
);
|
);
|
||||||
const inlineEditor = this.inlineEditor;
|
const inlineEditor = this.inlineEditor;
|
||||||
if (inlineEditor) {
|
if (inlineEditor) {
|
||||||
|
|||||||
@@ -39,5 +39,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.0"
|
"version": "0.26.1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,5 +43,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.0"
|
"version": "0.26.1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,5 +49,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.0"
|
"version": "0.26.1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,5 +49,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.0"
|
"version": "0.26.1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,5 +44,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.0"
|
"version": "0.26.1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,5 +44,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.0"
|
"version": "0.26.1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,11 @@ 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;
|
||||||
@@ -63,6 +68,11 @@ 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(
|
||||||
@@ -70,6 +80,12 @@ 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;
|
||||||
}
|
}
|
||||||
@@ -96,6 +112,134 @@ 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();
|
||||||
|
|
||||||
@@ -108,14 +252,32 @@ 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',
|
||||||
@@ -138,6 +300,13 @@ 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}>
|
||||||
@@ -149,7 +318,7 @@ export class ImageEdgelessBlockComponent extends GfxBlockComponent<ImageBlockMod
|
|||||||
class="drag-target"
|
class="drag-target"
|
||||||
draggable="false"
|
draggable="false"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
src=${blobUrl}
|
src=${imageUrl ?? ''}
|
||||||
alt=${caption}
|
alt=${caption}
|
||||||
@error=${this._handleError}
|
@error=${this._handleError}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -46,5 +46,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.0"
|
"version": "0.26.1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,5 +46,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.0"
|
"version": "0.26.1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,5 +49,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.0"
|
"version": "0.26.1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,5 +42,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.0"
|
"version": "0.26.1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,126 @@ 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 =>
|
||||||
@@ -88,41 +208,37 @@ export const paragraphBlockHtmlAdapterMatcher: BlockHtmlAdapterMatcher = {
|
|||||||
!tagsInAncestor(o, ['p', 'li']) &&
|
!tagsInAncestor(o, ['p', 'li']) &&
|
||||||
HastUtils.isParagraphLike(o.node)
|
HastUtils.isParagraphLike(o.node)
|
||||||
) {
|
) {
|
||||||
walkerContext
|
const delta = deltaConverter.astToDelta(o.node);
|
||||||
.openNode(
|
const deltas = getParagraphDeltas(o.node, delta);
|
||||||
{
|
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: walkerContext.getGlobalContext('hast:blockquote')
|
type,
|
||||||
? 'quote'
|
|
||||||
: 'text',
|
|
||||||
text: {
|
text: {
|
||||||
'$blocksuite:internal:text$': true,
|
'$blocksuite:internal:text$': true,
|
||||||
delta: deltaConverter.astToDelta(o.node),
|
delta,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
children: [],
|
children: [],
|
||||||
@@ -192,6 +308,9 @@ 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' &&
|
||||||
|
|||||||
@@ -67,5 +67,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.0"
|
"version": "0.26.1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ 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 => [
|
||||||
@@ -144,18 +145,39 @@ 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 ? ctx.blockIndex + 1 : 1
|
ctx.blockIndex !== undefined ? 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,7 +33,11 @@ import {
|
|||||||
ReleaseFromGroupIcon,
|
ReleaseFromGroupIcon,
|
||||||
UnlockIcon,
|
UnlockIcon,
|
||||||
} from '@blocksuite/icons/lit';
|
} from '@blocksuite/icons/lit';
|
||||||
import type { GfxModel } from '@blocksuite/std/gfx';
|
import {
|
||||||
|
batchAddChildren,
|
||||||
|
batchRemoveChildren,
|
||||||
|
type GfxModel,
|
||||||
|
} from '@blocksuite/std/gfx';
|
||||||
import { html } from 'lit';
|
import { html } from 'lit';
|
||||||
|
|
||||||
import { renderAlignmentMenu } from './alignment';
|
import { renderAlignmentMenu } from './alignment';
|
||||||
@@ -61,14 +65,13 @@ export const builtinMiscToolbarConfig = {
|
|||||||
|
|
||||||
const group = firstModel.group;
|
const group = firstModel.group;
|
||||||
|
|
||||||
// oxlint-disable-next-line unicorn/prefer-dom-node-remove
|
batchRemoveChildren(group, [firstModel]);
|
||||||
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) {
|
||||||
parent.addChild(firstModel);
|
batchAddChildren(parent, [firstModel]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -255,9 +258,12 @@ 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 => {
|
||||||
// oxlint-disable-next-line unicorn/prefer-dom-node-remove
|
if (element.group) {
|
||||||
element.group?.removeChild(element);
|
batchRemoveChildren(element.group, [element]);
|
||||||
topElement.group?.addChild(element);
|
}
|
||||||
|
if (topElement.group) {
|
||||||
|
batchAddChildren(topElement.group, [element]);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (otherElements.length === 0) {
|
if (otherElements.length === 0) {
|
||||||
|
|||||||
@@ -45,5 +45,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.0"
|
"version": "0.26.1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,5 +46,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.0"
|
"version": "0.26.1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,10 +40,146 @@ 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 }) => {
|
||||||
@@ -52,13 +188,31 @@ export class SurfaceBlockModel extends BaseSurfaceModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getConnectors(id: string) {
|
getConnectors(id: string) {
|
||||||
const connectors = this.getElementsByType(
|
const connectorIds = this._connectorIdsByEndpoint.get(id);
|
||||||
'connector'
|
|
||||||
) as unknown[] as ConnectorElementModel[];
|
|
||||||
|
|
||||||
return connectors.filter(
|
if (!connectorIds?.size) {
|
||||||
connector => connector.source?.id === id || connector.target?.id === id
|
return [];
|
||||||
);
|
}
|
||||||
|
|
||||||
|
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>(
|
||||||
|
|||||||
@@ -42,5 +42,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.0"
|
"version": "0.26.1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,5 +82,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.0"
|
"version": "0.26.1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,5 +48,5 @@
|
|||||||
"!src/__tests__",
|
"!src/__tests__",
|
||||||
"!dist/__tests__"
|
"!dist/__tests__"
|
||||||
],
|
],
|
||||||
"version": "0.26.0"
|
"version": "0.26.1"
|
||||||
}
|
}
|
||||||
|
|||||||
517
blocksuite/affine/data-view/src/__tests__/kanban.unit.spec.ts
Normal file
517
blocksuite/affine/data-view/src/__tests__/kanban.unit.spec.ts
Normal file
@@ -0,0 +1,517 @@
|
|||||||
|
import { signal } from '@preact/signals-core';
|
||||||
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import type { GroupBy } from '../core/common/types.js';
|
||||||
|
import type { DataSource } from '../core/data-source/base.js';
|
||||||
|
import { DetailSelection } from '../core/detail/selection.js';
|
||||||
|
import { groupByMatchers } from '../core/group-by/define.js';
|
||||||
|
import { t } from '../core/logical/type-presets.js';
|
||||||
|
import type { DataViewCellLifeCycle } from '../core/property/index.js';
|
||||||
|
import { checkboxPropertyModelConfig } from '../property-presets/checkbox/define.js';
|
||||||
|
import { multiSelectPropertyModelConfig } from '../property-presets/multi-select/define.js';
|
||||||
|
import { selectPropertyModelConfig } from '../property-presets/select/define.js';
|
||||||
|
import { textPropertyModelConfig } from '../property-presets/text/define.js';
|
||||||
|
import {
|
||||||
|
canGroupable,
|
||||||
|
ensureKanbanGroupColumn,
|
||||||
|
pickKanbanGroupColumn,
|
||||||
|
resolveKanbanGroupBy,
|
||||||
|
} from '../view-presets/kanban/group-by-utils.js';
|
||||||
|
import { materializeKanbanColumns } from '../view-presets/kanban/kanban-view-manager.js';
|
||||||
|
import type { KanbanCard } from '../view-presets/kanban/pc/card.js';
|
||||||
|
import { KanbanDragController } from '../view-presets/kanban/pc/controller/drag.js';
|
||||||
|
import type { KanbanGroup } from '../view-presets/kanban/pc/group.js';
|
||||||
|
|
||||||
|
type Column = {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
data?: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TestPropertyMeta = {
|
||||||
|
type: string;
|
||||||
|
config: {
|
||||||
|
kanbanGroup?: {
|
||||||
|
enabled: boolean;
|
||||||
|
mutable?: boolean;
|
||||||
|
};
|
||||||
|
propertyData: {
|
||||||
|
default: () => Record<string, unknown>;
|
||||||
|
};
|
||||||
|
jsonValue: {
|
||||||
|
type: (options: {
|
||||||
|
data: Record<string, unknown>;
|
||||||
|
dataSource: DataSource;
|
||||||
|
}) => unknown;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type MockDataSource = {
|
||||||
|
properties$: ReturnType<typeof signal<string[]>>;
|
||||||
|
provider: {
|
||||||
|
getAll: () => Map<unknown, unknown>;
|
||||||
|
};
|
||||||
|
serviceGetOrCreate: (key: unknown, create: () => unknown) => unknown;
|
||||||
|
propertyTypeGet: (propertyId: string) => string | undefined;
|
||||||
|
propertyMetaGet: (type: string) => TestPropertyMeta | undefined;
|
||||||
|
propertyDataGet: (propertyId: string) => Record<string, unknown>;
|
||||||
|
propertyDataTypeGet: (propertyId: string) => unknown;
|
||||||
|
propertyAdd: (
|
||||||
|
_position: unknown,
|
||||||
|
ops?: {
|
||||||
|
type?: string;
|
||||||
|
}
|
||||||
|
) => string;
|
||||||
|
propertyDataSet: (propertyId: string, data: Record<string, unknown>) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const asDataSource = (dataSource: object): DataSource =>
|
||||||
|
dataSource as DataSource;
|
||||||
|
|
||||||
|
const toTestMeta = <TData extends Record<string, unknown>>(
|
||||||
|
type: string,
|
||||||
|
config: {
|
||||||
|
kanbanGroup?: {
|
||||||
|
enabled: boolean;
|
||||||
|
mutable?: boolean;
|
||||||
|
};
|
||||||
|
propertyData: {
|
||||||
|
default: () => TData;
|
||||||
|
};
|
||||||
|
jsonValue: {
|
||||||
|
type: (options: { data: TData; dataSource: DataSource }) => unknown;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
): TestPropertyMeta => ({
|
||||||
|
type,
|
||||||
|
config: {
|
||||||
|
kanbanGroup: config.kanbanGroup,
|
||||||
|
propertyData: {
|
||||||
|
default: () => config.propertyData.default(),
|
||||||
|
},
|
||||||
|
jsonValue: {
|
||||||
|
type: ({ data, dataSource }) =>
|
||||||
|
config.jsonValue.type({
|
||||||
|
data: data as TData,
|
||||||
|
dataSource,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const immutableBooleanMeta = toTestMeta('immutable-boolean', {
|
||||||
|
...checkboxPropertyModelConfig.config,
|
||||||
|
kanbanGroup: {
|
||||||
|
enabled: true,
|
||||||
|
mutable: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const createMockDataSource = (columns: Column[]): MockDataSource => {
|
||||||
|
const properties$ = signal(columns.map(column => column.id));
|
||||||
|
const typeById = new Map(columns.map(column => [column.id, column.type]));
|
||||||
|
const dataById = new Map(
|
||||||
|
columns.map(column => [column.id, column.data ?? {}])
|
||||||
|
);
|
||||||
|
const services = new Map<unknown, unknown>();
|
||||||
|
|
||||||
|
const metaEntries: Array<[string, TestPropertyMeta]> = [
|
||||||
|
[
|
||||||
|
checkboxPropertyModelConfig.type,
|
||||||
|
toTestMeta(
|
||||||
|
checkboxPropertyModelConfig.type,
|
||||||
|
checkboxPropertyModelConfig.config
|
||||||
|
),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
selectPropertyModelConfig.type,
|
||||||
|
toTestMeta(
|
||||||
|
selectPropertyModelConfig.type,
|
||||||
|
selectPropertyModelConfig.config
|
||||||
|
),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
multiSelectPropertyModelConfig.type,
|
||||||
|
toTestMeta(
|
||||||
|
multiSelectPropertyModelConfig.type,
|
||||||
|
multiSelectPropertyModelConfig.config
|
||||||
|
),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
textPropertyModelConfig.type,
|
||||||
|
toTestMeta(textPropertyModelConfig.type, textPropertyModelConfig.config),
|
||||||
|
],
|
||||||
|
[immutableBooleanMeta.type, immutableBooleanMeta],
|
||||||
|
];
|
||||||
|
const metaByType = new Map(metaEntries);
|
||||||
|
|
||||||
|
const asRecord = (value: unknown): Record<string, unknown> =>
|
||||||
|
typeof value === 'object' && value != null
|
||||||
|
? (value as Record<string, unknown>)
|
||||||
|
: {};
|
||||||
|
|
||||||
|
let autoColumnId = 0;
|
||||||
|
|
||||||
|
const dataSource = {
|
||||||
|
properties$,
|
||||||
|
provider: {
|
||||||
|
getAll: () => new Map<unknown, unknown>(),
|
||||||
|
},
|
||||||
|
serviceGetOrCreate: (key: unknown, create: () => unknown) => {
|
||||||
|
if (!services.has(key)) {
|
||||||
|
services.set(key, create());
|
||||||
|
}
|
||||||
|
return services.get(key);
|
||||||
|
},
|
||||||
|
propertyTypeGet: (propertyId: string) => typeById.get(propertyId),
|
||||||
|
propertyMetaGet: (type: string) => metaByType.get(type),
|
||||||
|
propertyDataGet: (propertyId: string) => asRecord(dataById.get(propertyId)),
|
||||||
|
propertyDataTypeGet: (propertyId: string) => {
|
||||||
|
const type = typeById.get(propertyId);
|
||||||
|
if (!type) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const meta = metaByType.get(type);
|
||||||
|
if (!meta) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return meta.config.jsonValue.type({
|
||||||
|
data: asRecord(dataById.get(propertyId)),
|
||||||
|
dataSource: asDataSource(dataSource),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
propertyAdd: (
|
||||||
|
_position: unknown,
|
||||||
|
ops?: {
|
||||||
|
type?: string;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
const type = ops?.type ?? selectPropertyModelConfig.type;
|
||||||
|
const id = `auto-${++autoColumnId}`;
|
||||||
|
const meta = metaByType.get(type);
|
||||||
|
const data = meta?.config.propertyData.default() ?? {};
|
||||||
|
|
||||||
|
typeById.set(id, type);
|
||||||
|
dataById.set(id, data);
|
||||||
|
properties$.value = [...properties$.value, id];
|
||||||
|
return id;
|
||||||
|
},
|
||||||
|
propertyDataSet: (propertyId: string, data: Record<string, unknown>) => {
|
||||||
|
dataById.set(propertyId, data);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return dataSource;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createDragController = () => {
|
||||||
|
type DragLogic = ConstructorParameters<typeof KanbanDragController>[0];
|
||||||
|
return new KanbanDragController({} as DragLogic);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('kanban', () => {
|
||||||
|
describe('group-by define', () => {
|
||||||
|
it('boolean group should not include ungroup bucket', () => {
|
||||||
|
const booleanGroup = groupByMatchers.find(
|
||||||
|
group => group.name === 'boolean'
|
||||||
|
);
|
||||||
|
expect(booleanGroup).toBeDefined();
|
||||||
|
|
||||||
|
const keys = booleanGroup!
|
||||||
|
.defaultKeys(t.boolean.instance())
|
||||||
|
.map(group => group.key);
|
||||||
|
|
||||||
|
expect(keys).toEqual(['true', 'false']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('boolean group should fallback invalid values to false bucket', () => {
|
||||||
|
const booleanGroup = groupByMatchers.find(
|
||||||
|
group => group.name === 'boolean'
|
||||||
|
);
|
||||||
|
expect(booleanGroup).toBeDefined();
|
||||||
|
|
||||||
|
const groups = booleanGroup!.valuesGroup(undefined, t.boolean.instance());
|
||||||
|
expect(groups).toEqual([{ key: 'false', value: false }]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('columns materialization', () => {
|
||||||
|
it('appends missing properties while preserving existing order and state', () => {
|
||||||
|
const columns = [{ id: 'status', hide: true }, { id: 'title' }];
|
||||||
|
|
||||||
|
const next = materializeKanbanColumns(columns, [
|
||||||
|
'title',
|
||||||
|
'status',
|
||||||
|
'date',
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(next).toEqual([
|
||||||
|
{ id: 'status', hide: true },
|
||||||
|
{ id: 'title' },
|
||||||
|
{ id: 'date' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('drops stale columns that no longer exist in data source', () => {
|
||||||
|
const columns = [{ id: 'title' }, { id: 'removed', hide: true }];
|
||||||
|
|
||||||
|
const next = materializeKanbanColumns(columns, ['title']);
|
||||||
|
|
||||||
|
expect(next).toEqual([{ id: 'title' }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns original reference when columns are already materialized', () => {
|
||||||
|
const columns = [{ id: 'title' }, { id: 'status', hide: true }];
|
||||||
|
|
||||||
|
const next = materializeKanbanColumns(columns, ['title', 'status']);
|
||||||
|
|
||||||
|
expect(next).toBe(columns);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('drag indicator', () => {
|
||||||
|
it('shows drop preview when insert position exists', () => {
|
||||||
|
const controller = createDragController();
|
||||||
|
const position = {
|
||||||
|
group: {} as KanbanGroup,
|
||||||
|
position: 'end' as const,
|
||||||
|
};
|
||||||
|
controller.getInsertPosition = vi.fn().mockReturnValue(position);
|
||||||
|
|
||||||
|
const displaySpy = vi.spyOn(controller.dropPreview, 'display');
|
||||||
|
const removeSpy = vi.spyOn(controller.dropPreview, 'remove');
|
||||||
|
|
||||||
|
const result = controller.showIndicator({} as MouseEvent, undefined);
|
||||||
|
|
||||||
|
expect(result).toBe(position);
|
||||||
|
expect(displaySpy).toHaveBeenCalledWith(
|
||||||
|
position.group,
|
||||||
|
undefined,
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
expect(removeSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes drop preview when insert position does not exist', () => {
|
||||||
|
const controller = createDragController();
|
||||||
|
controller.getInsertPosition = vi.fn().mockReturnValue(undefined);
|
||||||
|
|
||||||
|
const displaySpy = vi.spyOn(controller.dropPreview, 'display');
|
||||||
|
const removeSpy = vi.spyOn(controller.dropPreview, 'remove');
|
||||||
|
|
||||||
|
const result = controller.showIndicator({} as MouseEvent, undefined);
|
||||||
|
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
expect(displaySpy).not.toHaveBeenCalled();
|
||||||
|
expect(removeSpy).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('forwards hovered card to drop preview for precise insertion cursor', () => {
|
||||||
|
const controller = createDragController();
|
||||||
|
const hoveredCard = document.createElement(
|
||||||
|
'affine-data-view-kanban-card'
|
||||||
|
) as KanbanCard;
|
||||||
|
const positionCard = document.createElement(
|
||||||
|
'affine-data-view-kanban-card'
|
||||||
|
) as KanbanCard;
|
||||||
|
const position = {
|
||||||
|
group: {} as KanbanGroup,
|
||||||
|
card: positionCard,
|
||||||
|
position: { before: true, id: 'card-id' } as const,
|
||||||
|
};
|
||||||
|
controller.getInsertPosition = vi.fn().mockReturnValue(position);
|
||||||
|
|
||||||
|
const displaySpy = vi.spyOn(controller.dropPreview, 'display');
|
||||||
|
|
||||||
|
controller.showIndicator({} as MouseEvent, hoveredCard);
|
||||||
|
|
||||||
|
expect(displaySpy).toHaveBeenCalledWith(
|
||||||
|
position.group,
|
||||||
|
hoveredCard,
|
||||||
|
position.card
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('group-by utils', () => {
|
||||||
|
it('allows only kanban-enabled property types to group', () => {
|
||||||
|
const dataSource = createMockDataSource([
|
||||||
|
{ id: 'text', type: textPropertyModelConfig.type },
|
||||||
|
{ id: 'select', type: selectPropertyModelConfig.type },
|
||||||
|
{ id: 'multi-select', type: multiSelectPropertyModelConfig.type },
|
||||||
|
{ id: 'checkbox', type: checkboxPropertyModelConfig.type },
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(canGroupable(asDataSource(dataSource), 'text')).toBe(false);
|
||||||
|
expect(canGroupable(asDataSource(dataSource), 'select')).toBe(true);
|
||||||
|
expect(canGroupable(asDataSource(dataSource), 'multi-select')).toBe(true);
|
||||||
|
expect(canGroupable(asDataSource(dataSource), 'checkbox')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('prefers mutable group column over immutable ones', () => {
|
||||||
|
const dataSource = createMockDataSource([
|
||||||
|
{
|
||||||
|
id: 'immutable-bool',
|
||||||
|
type: 'immutable-boolean',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'checkbox',
|
||||||
|
type: checkboxPropertyModelConfig.type,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(pickKanbanGroupColumn(asDataSource(dataSource))).toBe('checkbox');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates default status select column when no groupable column exists', () => {
|
||||||
|
const dataSource = createMockDataSource([
|
||||||
|
{
|
||||||
|
id: 'text',
|
||||||
|
type: textPropertyModelConfig.type,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const statusColumnId = ensureKanbanGroupColumn(asDataSource(dataSource));
|
||||||
|
|
||||||
|
expect(statusColumnId).toBeTruthy();
|
||||||
|
expect(dataSource.propertyTypeGet(statusColumnId!)).toBe(
|
||||||
|
selectPropertyModelConfig.type
|
||||||
|
);
|
||||||
|
const options =
|
||||||
|
(
|
||||||
|
dataSource.propertyDataGet(statusColumnId!) as {
|
||||||
|
options?: { value: string }[];
|
||||||
|
}
|
||||||
|
).options ?? [];
|
||||||
|
expect(options.map(option => option.value)).toEqual([
|
||||||
|
'Todo',
|
||||||
|
'In Progress',
|
||||||
|
'Done',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('defaults hideEmpty to true for non-option groups', () => {
|
||||||
|
const dataSource = createMockDataSource([
|
||||||
|
{
|
||||||
|
id: 'checkbox',
|
||||||
|
type: checkboxPropertyModelConfig.type,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const next = resolveKanbanGroupBy(asDataSource(dataSource));
|
||||||
|
expect(next?.columnId).toBe('checkbox');
|
||||||
|
expect(next?.hideEmpty).toBe(true);
|
||||||
|
expect(next?.name).toBe('boolean');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('defaults hideEmpty to false for select grouping', () => {
|
||||||
|
const dataSource = createMockDataSource([
|
||||||
|
{
|
||||||
|
id: 'select',
|
||||||
|
type: selectPropertyModelConfig.type,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const next = resolveKanbanGroupBy(asDataSource(dataSource));
|
||||||
|
expect(next?.columnId).toBe('select');
|
||||||
|
expect(next?.hideEmpty).toBe(false);
|
||||||
|
expect(next?.name).toBe('select');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preserves sort and explicit hideEmpty when resolving groupBy', () => {
|
||||||
|
const dataSource = createMockDataSource([
|
||||||
|
{
|
||||||
|
id: 'checkbox',
|
||||||
|
type: checkboxPropertyModelConfig.type,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const current: GroupBy = {
|
||||||
|
type: 'groupBy',
|
||||||
|
columnId: 'checkbox',
|
||||||
|
name: 'boolean',
|
||||||
|
sort: { desc: true },
|
||||||
|
hideEmpty: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const next = resolveKanbanGroupBy(asDataSource(dataSource), current);
|
||||||
|
|
||||||
|
expect(next?.columnId).toBe('checkbox');
|
||||||
|
expect(next?.sort).toEqual({ desc: true });
|
||||||
|
expect(next?.hideEmpty).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('replaces current non-groupable column with a valid kanban column', () => {
|
||||||
|
const dataSource = createMockDataSource([
|
||||||
|
{ id: 'text', type: textPropertyModelConfig.type },
|
||||||
|
{ id: 'checkbox', type: checkboxPropertyModelConfig.type },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const next = resolveKanbanGroupBy(asDataSource(dataSource), {
|
||||||
|
type: 'groupBy',
|
||||||
|
columnId: 'text',
|
||||||
|
name: 'text',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(next?.columnId).toBe('checkbox');
|
||||||
|
expect(next?.name).toBe('boolean');
|
||||||
|
expect(next?.hideEmpty).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('detail selection', () => {
|
||||||
|
it('should avoid recursive selection update when exiting select edit mode', () => {
|
||||||
|
vi.stubGlobal('requestAnimationFrame', ((cb: FrameRequestCallback) => {
|
||||||
|
cb(0);
|
||||||
|
return 0;
|
||||||
|
}) as typeof requestAnimationFrame);
|
||||||
|
try {
|
||||||
|
let selection: DetailSelection;
|
||||||
|
let beforeExitCalls = 0;
|
||||||
|
|
||||||
|
const cell = {
|
||||||
|
beforeEnterEditMode: () => true,
|
||||||
|
beforeExitEditingMode: () => {
|
||||||
|
beforeExitCalls += 1;
|
||||||
|
selection.selection = {
|
||||||
|
propertyId: 'status',
|
||||||
|
isEditing: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
afterEnterEditingMode: () => {},
|
||||||
|
focusCell: () => true,
|
||||||
|
blurCell: () => true,
|
||||||
|
forceUpdate: () => {},
|
||||||
|
} satisfies DataViewCellLifeCycle;
|
||||||
|
|
||||||
|
const field = {
|
||||||
|
isFocus$: signal(false),
|
||||||
|
isEditing$: signal(false),
|
||||||
|
cell,
|
||||||
|
focus: () => {},
|
||||||
|
blur: () => {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const detail = {
|
||||||
|
querySelector: () => field,
|
||||||
|
};
|
||||||
|
|
||||||
|
selection = new DetailSelection(detail);
|
||||||
|
selection.selection = {
|
||||||
|
propertyId: 'status',
|
||||||
|
isEditing: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
selection.selection = {
|
||||||
|
propertyId: 'status',
|
||||||
|
isEditing: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(beforeExitCalls).toBe(1);
|
||||||
|
expect(field.isEditing$.value).toBe(false);
|
||||||
|
} finally {
|
||||||
|
vi.unstubAllGlobals();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import { describe, expect, test } from 'vitest';
|
|
||||||
|
|
||||||
import { mobileEffects } from '../view-presets/table/mobile/effect.js';
|
|
||||||
import type { MobileTableGroup } from '../view-presets/table/mobile/group.js';
|
|
||||||
import { pcEffects } from '../view-presets/table/pc/effect.js';
|
|
||||||
import type { TableGroup } from '../view-presets/table/pc/group.js';
|
|
||||||
|
|
||||||
/** @vitest-environment happy-dom */
|
|
||||||
|
|
||||||
describe('TableGroup', () => {
|
|
||||||
test('toggle collapse on pc', () => {
|
|
||||||
pcEffects();
|
|
||||||
const group = document.createElement(
|
|
||||||
'affine-data-view-table-group'
|
|
||||||
) as TableGroup;
|
|
||||||
|
|
||||||
expect(group.collapsed$.value).toBe(false);
|
|
||||||
(group as any)._toggleCollapse();
|
|
||||||
expect(group.collapsed$.value).toBe(true);
|
|
||||||
(group as any)._toggleCollapse();
|
|
||||||
expect(group.collapsed$.value).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('toggle collapse on mobile', () => {
|
|
||||||
mobileEffects();
|
|
||||||
const group = document.createElement(
|
|
||||||
'mobile-table-group'
|
|
||||||
) as MobileTableGroup;
|
|
||||||
|
|
||||||
expect(group.collapsed$.value).toBe(false);
|
|
||||||
(group as any)._toggleCollapse();
|
|
||||||
expect(group.collapsed$.value).toBe(true);
|
|
||||||
(group as any)._toggleCollapse();
|
|
||||||
expect(group.collapsed$.value).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
101
blocksuite/affine/data-view/src/__tests__/table.unit.spec.ts
Normal file
101
blocksuite/affine/data-view/src/__tests__/table.unit.spec.ts
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import { describe, expect, test } from 'vitest';
|
||||||
|
|
||||||
|
import { numberFormats } from '../property-presets/number/utils/formats.js';
|
||||||
|
import {
|
||||||
|
formatNumber,
|
||||||
|
NumberFormatSchema,
|
||||||
|
parseNumber,
|
||||||
|
} from '../property-presets/number/utils/formatter.js';
|
||||||
|
import { mobileEffects } from '../view-presets/table/mobile/effect.js';
|
||||||
|
import type { MobileTableGroup } from '../view-presets/table/mobile/group.js';
|
||||||
|
import { pcEffects } from '../view-presets/table/pc/effect.js';
|
||||||
|
import type { TableGroup } from '../view-presets/table/pc/group.js';
|
||||||
|
|
||||||
|
/** @vitest-environment happy-dom */
|
||||||
|
|
||||||
|
describe('TableGroup', () => {
|
||||||
|
test('toggle collapse on pc', () => {
|
||||||
|
pcEffects();
|
||||||
|
const group = document.createElement(
|
||||||
|
'affine-data-view-table-group'
|
||||||
|
) as TableGroup;
|
||||||
|
|
||||||
|
expect(group.collapsed$.value).toBe(false);
|
||||||
|
(group as any)._toggleCollapse();
|
||||||
|
expect(group.collapsed$.value).toBe(true);
|
||||||
|
(group as any)._toggleCollapse();
|
||||||
|
expect(group.collapsed$.value).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('toggle collapse on mobile', () => {
|
||||||
|
mobileEffects();
|
||||||
|
const group = document.createElement(
|
||||||
|
'mobile-table-group'
|
||||||
|
) as MobileTableGroup;
|
||||||
|
|
||||||
|
expect(group.collapsed$.value).toBe(false);
|
||||||
|
(group as any)._toggleCollapse();
|
||||||
|
expect(group.collapsed$.value).toBe(true);
|
||||||
|
(group as any)._toggleCollapse();
|
||||||
|
expect(group.collapsed$.value).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('number formatter', () => {
|
||||||
|
test('number format menu should expose all schema formats', () => {
|
||||||
|
const menuFormats = numberFormats.map(format => format.type);
|
||||||
|
const schemaFormats = NumberFormatSchema.options;
|
||||||
|
|
||||||
|
expect(new Set(menuFormats)).toEqual(new Set(schemaFormats));
|
||||||
|
expect(menuFormats).toHaveLength(schemaFormats.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('formats grouped decimal numbers with Intl grouping rules', () => {
|
||||||
|
const value = 11451.4;
|
||||||
|
const decimals = 1;
|
||||||
|
const expected = new Intl.NumberFormat(navigator.language, {
|
||||||
|
style: 'decimal',
|
||||||
|
useGrouping: true,
|
||||||
|
minimumFractionDigits: decimals,
|
||||||
|
maximumFractionDigits: decimals,
|
||||||
|
}).format(value);
|
||||||
|
|
||||||
|
expect(formatNumber(value, 'numberWithCommas', decimals)).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('formats percent values with Intl percent rules', () => {
|
||||||
|
const value = 0.1234;
|
||||||
|
const decimals = 2;
|
||||||
|
const expected = new Intl.NumberFormat(navigator.language, {
|
||||||
|
style: 'percent',
|
||||||
|
useGrouping: false,
|
||||||
|
minimumFractionDigits: decimals,
|
||||||
|
maximumFractionDigits: decimals,
|
||||||
|
}).format(value);
|
||||||
|
|
||||||
|
expect(formatNumber(value, 'percent', decimals)).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('formats currency values with Intl currency rules', () => {
|
||||||
|
const value = 11451.4;
|
||||||
|
const expected = new Intl.NumberFormat(navigator.language, {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'USD',
|
||||||
|
currencyDisplay: 'symbol',
|
||||||
|
}).format(value);
|
||||||
|
|
||||||
|
expect(formatNumber(value, 'currencyUSD')).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('parses grouped number string pasted from clipboard', () => {
|
||||||
|
expect(parseNumber('11,451.4')).toBe(11451.4);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('keeps regular decimal parsing', () => {
|
||||||
|
expect(parseNumber('123.45')).toBe(123.45);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('supports comma as decimal separator in locale-specific input', () => {
|
||||||
|
expect(parseNumber('11451,4', ',')).toBe(11451.4);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -22,7 +22,6 @@ import { html } from 'lit/static-html.js';
|
|||||||
import { dataViewCommonStyle } from './common/css-variable.js';
|
import { dataViewCommonStyle } from './common/css-variable.js';
|
||||||
import type { DataSource } from './data-source/index.js';
|
import type { DataSource } from './data-source/index.js';
|
||||||
import type { DataViewSelection } from './types.js';
|
import type { DataViewSelection } from './types.js';
|
||||||
import { cacheComputed } from './utils/cache.js';
|
|
||||||
import { renderUniLit } from './utils/uni-component/index.js';
|
import { renderUniLit } from './utils/uni-component/index.js';
|
||||||
import type { DataViewUILogicBase } from './view/data-view-base.js';
|
import type { DataViewUILogicBase } from './view/data-view-base.js';
|
||||||
import type { SingleView } from './view-manager/single-view.js';
|
import type { SingleView } from './view-manager/single-view.js';
|
||||||
@@ -75,12 +74,38 @@ export class DataViewRootUILogic {
|
|||||||
|
|
||||||
return new (logic(view))(this, view);
|
return new (logic(view))(this, view);
|
||||||
}
|
}
|
||||||
private readonly views$ = cacheComputed(this.viewManager.views$, viewId =>
|
private readonly _viewsCache = new Map<
|
||||||
this.createDataViewUILogic(viewId)
|
string,
|
||||||
);
|
{ mode: string; logic: DataViewUILogicBase }
|
||||||
|
>();
|
||||||
|
|
||||||
|
private readonly views$ = computed(() => {
|
||||||
|
const viewDataList = this.dataSource.viewDataList$.value;
|
||||||
|
const validIds = new Set(viewDataList.map(viewData => viewData.id));
|
||||||
|
|
||||||
|
for (const cachedId of this._viewsCache.keys()) {
|
||||||
|
if (!validIds.has(cachedId)) {
|
||||||
|
this._viewsCache.delete(cachedId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return viewDataList.map(viewData => {
|
||||||
|
const cached = this._viewsCache.get(viewData.id);
|
||||||
|
if (cached && cached.mode === viewData.mode) {
|
||||||
|
return cached.logic;
|
||||||
|
}
|
||||||
|
const logic = this.createDataViewUILogic(viewData.id);
|
||||||
|
this._viewsCache.set(viewData.id, {
|
||||||
|
mode: viewData.mode,
|
||||||
|
logic,
|
||||||
|
});
|
||||||
|
return logic;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
private readonly viewsMap$ = computed(() => {
|
private readonly viewsMap$ = computed(() => {
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
this.views$.list.value.map(logic => [logic.view.id, logic])
|
this.views$.value.map(logic => [logic.view.id, logic])
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
private readonly _uiRef = signal<DataViewRootUI>();
|
private readonly _uiRef = signal<DataViewRootUI>();
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import type { KanbanCardSelection } from '../../view-presets';
|
import type { KanbanCardSelection } from '../../view-presets';
|
||||||
import type { KanbanCard } from '../../view-presets/kanban/pc/card.js';
|
import type { KanbanCard } from '../../view-presets/kanban/pc/card.js';
|
||||||
import { KanbanCell } from '../../view-presets/kanban/pc/cell.js';
|
import { KanbanCell } from '../../view-presets/kanban/pc/cell.js';
|
||||||
import type { RecordDetail } from './detail.js';
|
|
||||||
import { RecordField } from './field.js';
|
import { RecordField } from './field.js';
|
||||||
|
|
||||||
type DetailViewSelection = {
|
type DetailViewSelection = {
|
||||||
@@ -9,16 +8,39 @@ type DetailViewSelection = {
|
|||||||
isEditing: boolean;
|
isEditing: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type DetailSelectionHost = {
|
||||||
|
querySelector: (selector: string) => unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isSameDetailSelection = (
|
||||||
|
current?: DetailViewSelection,
|
||||||
|
next?: DetailViewSelection
|
||||||
|
) => {
|
||||||
|
if (!current && !next) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!current || !next) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
current.propertyId === next.propertyId &&
|
||||||
|
current.isEditing === next.isEditing
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export class DetailSelection {
|
export class DetailSelection {
|
||||||
_selection?: DetailViewSelection;
|
_selection?: DetailViewSelection;
|
||||||
|
|
||||||
onSelect = (selection?: DetailViewSelection) => {
|
onSelect = (selection?: DetailViewSelection) => {
|
||||||
|
if (isSameDetailSelection(this._selection, selection)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const old = this._selection;
|
const old = this._selection;
|
||||||
|
this._selection = selection;
|
||||||
if (old) {
|
if (old) {
|
||||||
this.blur(old);
|
this.blur(old);
|
||||||
}
|
}
|
||||||
this._selection = selection;
|
if (selection && isSameDetailSelection(this._selection, selection)) {
|
||||||
if (selection) {
|
|
||||||
this.focus(selection);
|
this.focus(selection);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -49,7 +71,7 @@ export class DetailSelection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(private readonly viewEle: RecordDetail) {}
|
constructor(private readonly viewEle: DetailSelectionHost) {}
|
||||||
|
|
||||||
blur(selection: DetailViewSelection) {
|
blur(selection: DetailViewSelection) {
|
||||||
const container = this.getFocusCellContainer(selection);
|
const container = this.getFocusCellContainer(selection);
|
||||||
@@ -111,8 +133,10 @@ export class DetailSelection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
focusFirstCell() {
|
focusFirstCell() {
|
||||||
const firstId = this.viewEle.querySelector('affine-data-view-record-field')
|
const firstField = this.viewEle.querySelector(
|
||||||
?.column.id;
|
'affine-data-view-record-field'
|
||||||
|
) as RecordField | undefined;
|
||||||
|
const firstId = firstField?.column.id;
|
||||||
if (firstId) {
|
if (firstId) {
|
||||||
this.selection = {
|
this.selection = {
|
||||||
propertyId: firstId,
|
propertyId: firstId,
|
||||||
@@ -144,11 +168,12 @@ export class DetailSelection {
|
|||||||
|
|
||||||
getSelectCard(selection: KanbanCardSelection) {
|
getSelectCard(selection: KanbanCardSelection) {
|
||||||
const { groupKey, cardId } = selection.cards[0];
|
const { groupKey, cardId } = selection.cards[0];
|
||||||
|
const group = this.viewEle.querySelector(
|
||||||
|
`affine-data-view-kanban-group[data-key="${groupKey}"]`
|
||||||
|
) as HTMLElement | undefined;
|
||||||
|
|
||||||
return this.viewEle
|
return group?.querySelector(
|
||||||
.querySelector(`affine-data-view-kanban-group[data-key="${groupKey}"]`)
|
`affine-data-view-kanban-card[data-card-id="${cardId}"]`
|
||||||
?.querySelector(
|
) as KanbanCard | undefined;
|
||||||
`affine-data-view-kanban-card[data-card-id="${cardId}"]`
|
|
||||||
) as KanbanCard | undefined;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -247,12 +247,13 @@ export const groupByMatchers: GroupByConfig[] = [
|
|||||||
matchType: t.boolean.instance(),
|
matchType: t.boolean.instance(),
|
||||||
groupName: (_t, v) => `${v?.toString() ?? ''}`,
|
groupName: (_t, v) => `${v?.toString() ?? ''}`,
|
||||||
defaultKeys: _t => [
|
defaultKeys: _t => [
|
||||||
ungroups,
|
|
||||||
{ key: 'true', value: true },
|
{ key: 'true', value: true },
|
||||||
{ key: 'false', value: false },
|
{ key: 'false', value: false },
|
||||||
],
|
],
|
||||||
valuesGroup: (v, _t) =>
|
valuesGroup: (v, _t) =>
|
||||||
typeof v !== 'boolean' ? [ungroups] : [{ key: v.toString(), value: v }],
|
typeof v !== 'boolean'
|
||||||
|
? [{ key: 'false', value: false }]
|
||||||
|
: [{ key: v.toString(), value: v }],
|
||||||
addToGroup: (v: boolean | null, _old: boolean | null) => v,
|
addToGroup: (v: boolean | null, _old: boolean | null) => v,
|
||||||
view: createUniComponentFromWebComponent(BooleanGroupView),
|
view: createUniComponentFromWebComponent(BooleanGroupView),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { css, html, unsafeCSS } from 'lit';
|
|||||||
import { property, query } from 'lit/decorators.js';
|
import { property, query } from 'lit/decorators.js';
|
||||||
import { repeat } from 'lit/directives/repeat.js';
|
import { repeat } from 'lit/directives/repeat.js';
|
||||||
|
|
||||||
|
import { canGroupable } from '../../view-presets/kanban/group-by-utils.js';
|
||||||
import { KanbanSingleView } from '../../view-presets/kanban/kanban-view-manager.js';
|
import { KanbanSingleView } from '../../view-presets/kanban/kanban-view-manager.js';
|
||||||
import { TableSingleView } from '../../view-presets/table/table-view-manager.js';
|
import { TableSingleView } from '../../view-presets/table/table-view-manager.js';
|
||||||
import { dataViewCssVariable } from '../common/css-variable.js';
|
import { dataViewCssVariable } from '../common/css-variable.js';
|
||||||
@@ -278,6 +279,9 @@ export const selectGroupByProperty = (
|
|||||||
if (property.type$.value === 'title') {
|
if (property.type$.value === 'title') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (view instanceof KanbanSingleView) {
|
||||||
|
return canGroupable(view.manager.dataSource, property.id);
|
||||||
|
}
|
||||||
const dataType = property.dataType$.value;
|
const dataType = property.dataType$.value;
|
||||||
if (!dataType) {
|
if (!dataType) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user