Compare commits

..

44 Commits

Author SHA1 Message Date
Alex Yang
a52fc54d80 v0.7.0-canary.25 2023-06-30 16:27:59 +08:00
Alex Yang
524c342b5e chore: bump blocksuite to '0.0.0-20230630081054-55a25248-nightly' (#2944) 2023-06-30 16:27:38 +08:00
regischen
f4fc084a0a fix(web): migrate connector (#2941)
Co-authored-by: Alex Yang <himself65@outlook.com>
2023-06-30 16:11:04 +08:00
Alex Yang
38a2aa9d17 build(electron): use nx (#2942) 2023-06-30 16:10:35 +08:00
Peng Xiao
9e90242ddb fix: disable sqlite blob storage (#2943) 2023-06-30 16:09:43 +08:00
Alex Yang
fd0c1da608 fix(cli): run dev-web crash 2023-06-30 15:58:13 +08:00
Alex Yang
68c4fccf98 ci: cancel previous build (#2794) 2023-06-30 07:39:27 +00:00
Alex Yang
3c93f4162d build: remove unused package (#2937) 2023-06-30 07:09:12 +00:00
Alex Yang
b6c314e180 refactor(cli): use typescript (#2938) 2023-06-30 06:58:57 +00:00
Alex Yang
62b465a889 ci: build infra code before build layers 2023-06-30 15:20:36 +08:00
3720
9d0db78f64 feat: support for view management (#2892) 2023-06-30 05:40:00 +00:00
Fangdun Tsai
d3393cb0fc feat: expose clipboard apis (#2932) 2023-06-30 04:47:30 +00:00
Alex Yang
79cded302f chore: bump blocksuite to 0.0.0-20230629103121-76e6587d-nightly (#2931) 2023-06-30 04:36:56 +00:00
Alex Yang
53d90a11de chore: tag deprecated files (#2936) 2023-06-30 04:01:14 +00:00
Alex Yang
271ad57160 feat: special ip address 'localhost' (#2935) 2023-06-30 03:54:24 +00:00
Alex Yang
4adbe64a54 fix(web): disable notification center (#2934) 2023-06-30 03:29:05 +00:00
DarkSky
50a8a147fd ci: make helm release only on bump version (#2928) 2023-06-30 02:02:46 +00:00
Ikko Eltociear Ashimine
eaea8e9368 refactor: fix typo in notification-center/index.tsx (#2929) 2023-06-30 09:55:09 +08:00
Hyden Liu
9873baae9f fix: z-index on app sidebar (#2761)
Co-authored-by: Himself65 <himself65@outlook.com>
2023-06-30 01:14:44 +00:00
xiaodong zuo
bc3ce7395e feat: export page as file (#2923) 2023-06-29 21:58:02 +00:00
Alex Yang
8a7908c692 fix(electron): window only ui (#2926) 2023-06-29 16:15:44 +00:00
LongYinan
8021efd81a build: affine Node.js server charts (#2895) 2023-06-29 14:02:46 +00:00
Qi
d7fcad2d0d feat: add and modify test case for new settings modal (#2925) 2023-06-29 12:54:45 +00:00
Alex Yang
b1d2d77263 docs: set nodejs version to 18.16.1 2023-06-29 20:00:57 +08:00
Alex Yang
2c772bd81b v0.7.0-canary.24 2023-06-29 18:50:48 +08:00
JimmFly
7f00011542 chore: update changelog link and remove obsolete changelog components (#2918) 2023-06-29 10:19:26 +00:00
Alex Yang
f76d8b8818 chore: bump blocksuite to 0.0.0-20230629084521-542de4e8-nightly (#2921) 2023-06-29 09:42:47 +00:00
Alex Yang
1d6b39dec9 ci: allow codecov upload failure (#2922) 2023-06-29 09:39:16 +00:00
Qi
5cfdf6c7e2 fix: a serise of ui issues of new setting (#2920)
Co-authored-by: Alex Yang <himself65@outlook.com>
2023-06-29 09:25:42 +00:00
Alex Yang
8410d83744 refactor: rootWorkspacesMetadataAtom loading logic (#2882) 2023-06-29 08:48:12 +00:00
DarkSky
8a2dac9718 fix: incorrect formatting (#2917) 2023-06-29 08:25:43 +00:00
JimmFly
5ad2908760 chore: update translation (#2916)
Co-authored-by: zuozijian3720 <zuozijian1994@gmail.com>
2023-06-29 08:20:25 +00:00
Alex Yang
5b8771485e docs: add apps/README.md 2023-06-29 16:07:30 +08:00
Alex Yang
ed8480caf0 ci: split migration test 2023-06-29 15:11:16 +08:00
Alex Yang
42ef3c0fc2 test: migration test in real world (#2885) 2023-06-29 06:50:26 +00:00
Alex Yang
e08ee9b7ff ci: add prettier format check (#2908) 2023-06-29 04:13:35 +00:00
liuyi
2c95bfcc3d feat(storage): binding jwst storage to node (#2808) 2023-06-29 01:45:45 +00:00
Alex Yang
86616e152d build: disable sqlite provider in canary 2023-06-29 10:00:41 +08:00
Peng Xiao
b1f478ee5e fix: updater color updates (#2913) 2023-06-28 17:21:07 +00:00
DarkSky
6b0f9fbdad feat: add deployment guide & fix pod label (#2912) 2023-06-28 17:12:23 +00:00
Alex Yang
da3f2b784a ci: fix output variable 2023-06-29 01:20:35 +08:00
Alex Yang
acb140ab78 v0.7.0-canary.23 2023-06-29 00:40:50 +08:00
Alex Yang
0b74bd9bfe ci: use production environment 2023-06-29 00:40:50 +08:00
Alex Yang
acfc030d16 ci: fix package version output 2023-06-29 00:40:50 +08:00
250 changed files with 11178 additions and 2386 deletions

View File

@@ -21,7 +21,8 @@
"native",
"templates",
"y-indexeddb",
"debug"
"debug",
"storage"
]
]
}

View File

@@ -65,6 +65,7 @@ const config = {
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'plugin:@typescript-eslint/recommended',
'prettier',
],
parser: '@typescript-eslint/parser',
parserOptions: {

View File

@@ -27,11 +27,11 @@ runs:
.cargo-cache
target/${{ inputs.target }}
key: stable-${{ inputs.target }}-cargo-cache
- name: Build
if: ${{ inputs.target != 'x86_64-unknown-linux-gnu' && inputs.target != 'aarch64-unknown-linux-gnu' }}
shell: bash
run: yarn nx build @affine/native --target ${{ inputs.target }}
run: |
yarn nx build @affine/native --target ${{ inputs.target }}
env:
NX_CLOUD_ACCESS_TOKEN: ${{ inputs.nx_token }}
@@ -41,10 +41,10 @@ runs:
with:
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian
options: --user 0:0 -v ${{ github.workspace }}/.cargo-cache/git/db:/usr/local/cargo/git/db -v ${{ github.workspace }}/.cargo/registry/cache:/usr/local/cargo/registry/cache -v ${{ github.workspace }}/.cargo/registry/index:/usr/local/cargo/registry/index -v ${{ github.workspace }}:/build -w /build -e NX_CLOUD_ACCESS_TOKEN=${{ inputs.nx_token }}
run: >-
export CC=x86_64-unknown-linux-gnu-gcc &&
export CC_x86_64_unknown_linux_gnu=x86_64-unknown-linux-gnu-gcc &&
yarn nx build @affine/native --target ${{ inputs.target }} &&
run: |
export CC=x86_64-unknown-linux-gnu-gcc
export CC_x86_64_unknown_linux_gnu=x86_64-unknown-linux-gnu-gcc
yarn nx build @affine/native --target ${{ inputs.target }}
chmod -R 777 node_modules/.cache
- name: Build
@@ -53,6 +53,6 @@ runs:
with:
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64
options: --user 0:0 -v ${{ github.workspace }}/.cargo-cache/git/db:/usr/local/cargo/git/db -v ${{ github.workspace }}/.cargo/registry/cache:/usr/local/cargo/registry/cache -v ${{ github.workspace }}/.cargo/registry/index:/usr/local/cargo/registry/index -v ${{ github.workspace }}:/build -w /build -e NX_CLOUD_ACCESS_TOKEN=${{ inputs.nx_token }}
run: >-
run: |
yarn nx build @affine/native --target ${{ inputs.target }}
chmod -R 777 node_modules/.cache

31
.github/actions/setup-rust/action.yml vendored Normal file
View File

@@ -0,0 +1,31 @@
name: 'AFFiNE Rust setup'
description: 'Rust setup, including cache configuration'
inputs:
target:
description: 'Cargo target'
required: true
toolchain:
description: 'Rustup toolchain'
required: false
default: 'stable'
runs:
using: 'composite'
steps:
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ inputs.toolchain }}
targets: ${{ inputs.target }}
- name: Cache cargo
uses: actions/cache@v3
with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: cargo-cache-${{ runner.os }}-${{ inputs.toolchain }}-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
cargo-cache-${{ runner.os }}-${{ inputs.toolchain }}-

View File

@@ -3,7 +3,7 @@ server {
root /app/dist;
location / {
try_files $uri $uri/index.html $uri.html =404;;
try_files $uri $uri/index.html $uri.html =404;
}
error_page 404 /404.html;

View File

@@ -3,8 +3,8 @@ name: affine-cloud
description: A Helm chart for AFFiNE Cloud
type: application
version: 0.6.0
appVersion: '0.6.0'
version: 0.6.1
appVersion: '0.6.1'
dependencies:
- name: postgresql

View File

@@ -8,7 +8,7 @@ spec:
replicas: 1
selector:
matchLabels:
app: affine-cloud
{{- include "affine-cloud.selectorLabels" . | nindent 6 }}
strategy:
type: RollingUpdate
rollingUpdate:
@@ -16,7 +16,7 @@ spec:
template:
metadata:
labels:
app: affine-cloud
{{- include "affine-cloud.selectorLabels" . | nindent 8 }}
spec:
restartPolicy: Always
containers:
@@ -30,7 +30,7 @@ spec:
- name: PG_DATABASE
value: "{{ .Values.postgresql.auth.database }}"
- name: PG_HOST
value: "{{ .Release.Name }}-postgresql"
value: "{{ .Values.postgresql.fullnameOverride | default (printf "%s-postgresql" .Release.Name) }}"
- name: DATABASE_URL
value: "{{ .Values.affineCloud.databaseUrl | default "postgresql://$(PG_USER):$(PG_PASS)@$(PG_HOST)/$(PG_DATABASE)" }}"
envFrom:

View File

@@ -1,5 +1,5 @@
affineCloud:
tag: 'nightly-latest'
tag: 'canary-5e0d5e0cc65ea46f326fdde12658bfac59b38c9f-0949'
# databaseUrl: 'postgresql://affine:password@affine-cloud-postgresql:5432/affine'
signKey: TUFtdFdzQTJhdGJuem01TA==
mail:
@@ -12,6 +12,7 @@ affineCloud:
cpu: '250m'
memory: 0.5Gi
postgresql:
fullnameOverride: tcp-postgresql
auth:
# only for demo, please modify it at prod env
username: affine

23
.github/helm/affine/.helmignore vendored Normal file
View File

@@ -0,0 +1,23 @@
# 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/Chart.yaml vendored Normal file
View File

@@ -0,0 +1,6 @@
apiVersion: v2
name: affine
description: AFFiNE cloud chart
type: application
version: 0.0.0
appVersion: '0.7.0-canary.18'

View File

@@ -0,0 +1,6 @@
apiVersion: v2
name: graphql
description: AFFiNE GraphQL server
type: application
version: 0.0.0
appVersion: '0.7.0-canary.18'

View File

@@ -0,0 +1,16 @@
1. Get the application URL by running these commands:
{{- if contains "NodePort" .Values.service.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "graphql.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 "graphql.fullname" . }}'
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "graphql.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 "graphql.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 }}

View File

@@ -0,0 +1,132 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "graphql.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 "graphql.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 "graphql.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "graphql.labels" -}}
helm.sh/chart: {{ include "graphql.chart" . }}
{{ include "graphql.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "graphql.selectorLabels" -}}
app.kubernetes.io/name: {{ include "graphql.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "graphql.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "graphql.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}
{{- define "jwt.key" -}}
{{- $secret := lookup "v1" "Secret" .Release.Namespace .Values.app.jwt.secretName -}}
{{- if and $secret $secret.data.private -}}
{{/*
Reusing existing secret data
*/}}
key: {{ $secret.data.private }}
{{- else -}}
{{/*
Generate new data
*/}}
key: {{ genPrivateKey "ecdsa" | b64enc }}
{{- end -}}
{{- end -}}
{{- define "objectStorage.r2" -}}
{{- $secret := lookup "v1" "Secret" .Release.Namespace .Values.app.objectStorage.r2.secretName -}}
{{- if $secret -}}
{{/*
Reusing existing secret data
*/}}
accountId: {{ $secret.data.accountId }}
accessKeyId: {{ $secret.data.accessKeyId }}
secretAccessKey: {{ $secret.data.secretAccessKey }}
bucket: {{ $secret.data.bucket }}
{{- else -}}
{{/*
Generate new data
*/}}
accountId: {{ .Values.app.objectStorage.r2.accountId | b64enc }}
accessKeyId: {{ .Values.app.objectStorage.r2.accessKeyId | b64enc }}
secretAccessKey: {{ .Values.app.objectStorage.r2.secretAccessKey | b64enc }}
bucket: {{ .Values.app.objectStorage.r2.bucket | b64enc }}
{{- end -}}
{{- end -}}
{{- define "objectStorage.oauth.google" -}}
{{- $secret := lookup "v1" "Secret" .Release.Namespace .Values.app.oauth.google.secretName -}}
{{- if $secret -}}
{{/*
Reusing existing secret data
*/}}
clientId: {{ $secret.data.clientId }}
clientSecret: {{ $secret.data.clientSecret }}
{{- else -}}
{{/*
Generate new data
*/}}
clientId: "{{ .Values.app.oauth.google.clientId | b64enc }}"
clientSecret: "{{ .Values.app.oauth.google.clientSecret | b64enc }}"
{{- end -}}
{{- end -}}
{{- define "objectStorage.oauth.github" -}}
{{- $secret := lookup "v1" "Secret" .Release.Namespace .Values.app.oauth.github.secretName -}}
{{- if $secret -}}
{{/*
Reusing existing secret data
*/}}
clientId: {{ $secret.data.clientId }}
clientSecret: {{ $secret.data.clientSecret }}
{{- else -}}
{{/*
Generate new data
*/}}
clientId: "{{ .Values.app.oauth.github.clientId | b64enc }}"
clientSecret: "{{ .Values.app.oauth.github.clientSecret | b64enc }}"
{{- end -}}
{{- end -}}

View File

@@ -0,0 +1,126 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "graphql.fullname" . }}
labels:
{{- include "graphql.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "graphql.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "graphql.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "graphql.serviceAccountName" . }}
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
env:
- name: AUTH_PRIVATE_KEY
valueFrom:
secretKeyRef:
name: "{{ .Values.app.jwt.secretName }}"
key: key
- name: NODE_ENV
value: "{{ .Values.env }}"
- name: DATABSE_PASSWORD
valueFrom:
secretKeyRef:
name: pg-postgresql
key: postgres-password
- name: DATABASE_URL
value: postgres://{{ .Values.database.user }}:$(DATABSE_PASSWORD)@{{ .Values.database.url }}:{{ .Values.database.port }}/{{ .Values.database.name }}
- 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: ENABLE_R2_OBJECT_STORAGE
value: "{{ .Values.app.objectStorage.r2.enabled }}"
{{ if .Values.app.objectStorage.r2.enabled }}
- name: R2_OBJECT_STORAGE_ACCOUNT_ID
valueFrom:
secretKeyRef:
name: "{{ .Values.app.objectStorage.r2.secretName }}"
key: accountId
- name: R2_OBJECT_STORAGE_ACCESS_KEY_ID
valueFrom:
secretKeyRef:
name: "{{ .Values.app.objectStorage.r2.secretName }}"
key: accessKeyId
- name: R2_OBJECT_STORAGE_SECRET_ACCESS_KEY
valueFrom:
secretKeyRef:
name: "{{ .Values.app.objectStorage.r2.secretName }}"
key: secretAccessKey
- name: R2_OBJECT_STORAGE_BUCKET
valueFrom:
secretKeyRef:
name: "{{ .Values.app.objectStorage.r2.secretName }}"
key: bucket
{{ end }}
{{ if .Values.app.oauth.google.enabled }}
- name: OAUTH_GOOGLE_CLIENT_ID
valueFrom:
secretKeyRef:
name: "{{ .Values.app.oauth.google.secretName }}"
key: clientId
- name: OAUTH_GOOGLE_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: "{{ .Values.app.oauth.google.secretName }}"
key: clientSecret
{{ end }}
{{ if .Values.app.oauth.github.enabled }}
- name: OAUTH_GITHUB_CLIENT_ID
valueFrom:
secretKeyRef:
name: "{{ .Values.app.oauth.github.secretName }}"
key: clientId
- name: OAUTH_GITHUB_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: "{{ .Values.app.oauth.github.secretName }}"
key: clientSecret
{{ end }}
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 }}

View File

@@ -0,0 +1,7 @@
apiVersion: v1
kind: Secret
metadata:
name: "{{ .Values.app.jwt.secretName }}"
type: Opaque
data:
{{- ( include "jwt.key" . ) | indent 2 -}}

View File

@@ -0,0 +1,34 @@
apiVersion: batch/v1
kind: Job
metadata:
name: {{ include "graphql.fullname" . }}-database-migration
labels:
{{- include "graphql.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": pre-install,pre-upgrade
"helm.sh/hook-weight": "-1"
"helm.sh/hook-delete-policy": before-hook-creation
spec:
template:
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
command: ["yarn", "prisma", "migrate", "deploy"]
env:
- name: NODE_ENV
value: "{{ .Values.env }}"
- name: DATABSE_PASSWORD
valueFrom:
secretKeyRef:
name: pg-postgresql
key: postgres-password
- name: DATABASE_URL
value: postgres://{{ .Values.database.user }}:$(DATABSE_PASSWORD)@{{ .Values.database.url }}:{{ .Values.database.port }}/{{ .Values.database.name }}
resources:
requests:
cpu: '100m'
memory: '200Mi'
restartPolicy: Never
backoffLimit: 1

View File

@@ -0,0 +1,10 @@
{{- if .Values.app.oauth.github.enabled -}}
apiVersion: v1
kind: Secret
metadata:
name: "{{ .Values.app.oauth.github.secretName }}"
type: Opaque
data:
{{- ( include "objectStorage.oauth.github" . ) | indent 2 -}}
{{- end }}

View File

@@ -0,0 +1,10 @@
{{- if .Values.app.oauth.google.enabled -}}
apiVersion: v1
kind: Secret
metadata:
name: "{{ .Values.app.oauth.google.secretName }}"
type: Opaque
data:
{{- ( include "objectStorage.oauth.google" . ) | indent 2 -}}
{{- end }}

View File

@@ -0,0 +1,9 @@
{{- if .Values.app.objectStorage.r2.enabled -}}
apiVersion: v1
kind: Secret
metadata:
name: "{{ .Values.app.objectStorage.r2.secretName }}"
type: Opaque
data:
{{- ( include "objectStorage.r2" . ) | indent 2 -}}
{{- end }}

View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "graphql.fullname" . }}
labels:
{{- include "graphql.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "graphql.selectorLabels" . | nindent 4 }}

View File

@@ -0,0 +1,12 @@
{{- 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 }}

View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: Pod
metadata:
name: "{{ include "graphql.fullname" . }}-test-connection"
labels:
{{- include "graphql.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": test
spec:
containers:
- name: wget
image: busybox
command: ['wget']
args: ['{{ include "graphql.fullname" . }}:{{ .Values.service.port }}']
restartPolicy: Never

View File

@@ -0,0 +1,69 @@
replicaCount: 1
image:
repository: ghcr.io/toeverything/affine-graphql
pullPolicy: IfNotPresent
tag: ''
imagePullSecrets: []
nameOverride: ''
fullnameOverride: ''
# map to NODE_ENV environment variable
env: 'production'
database:
user: 'postgres'
url: 'pg-postgresql'
port: '5432'
name: 'affine'
app:
# AFFINE_SERVER_SUB_PATH
path: ''
# AFFINE_SERVER_HOST
host: '0.0.0.0'
jwt:
secretName: jwt-private-key
# base64 encoded ecdsa private key
privateKey: ''
objectStorage:
r2:
enabled: false
secretName: r2
accountId: ''
accessKeyId: ''
secretAccessKey: ''
bucket: ''
oauth:
google:
enabled: false
secretName: oauth-google
clientId: ''
clientSecret: ''
github:
enabled: false
secretName: oauth-github
clientId: ''
clientSecret: ''
serviceAccount:
create: true
annotations: {}
name: 'affine-graphql'
podAnnotations: {}
podSecurityContext:
fsGroup: 2000
resources:
limits:
cpu: '2000m'
memory: 4Gi
requests:
cpu: '1000m'
memory: 2Gi
probe:
initialDelaySeconds: 20
nodeSelector: {}
tolerations: []
affinity: {}

View File

@@ -0,0 +1,23 @@
# 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/

View File

@@ -0,0 +1,6 @@
apiVersion: v2
name: web
description: A Helm chart for Kubernetes
type: application
version: 0.0.0
appVersion: "0.7.0-canary.18"

View File

@@ -0,0 +1,16 @@
1. Get the application URL by running these commands:
{{- if contains "NodePort" .Values.service.type }}
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "web.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 "web.fullname" . }}'
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "web.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 "web.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 }}

View File

@@ -0,0 +1,62 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "web.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 "web.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 "web.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "web.labels" -}}
helm.sh/chart: {{ include "web.chart" . }}
{{ include "web.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "web.selectorLabels" -}}
app.kubernetes.io/name: {{ include "web.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "web.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "web.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,57 @@
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 }}
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 }}

View File

@@ -0,0 +1,15 @@
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 }}

View File

@@ -0,0 +1,12 @@
{{- 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 }}

View File

@@ -0,0 +1,15 @@
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

View File

@@ -0,0 +1,37 @@
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

View File

@@ -0,0 +1,62 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "affine.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 "affine.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 "affine.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "affine.labels" -}}
helm.sh/chart: {{ include "affine.chart" . }}
{{ include "affine.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "affine.selectorLabels" -}}
app.kubernetes.io/name: {{ include "affine.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "affine.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "affine.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,64 @@
{{- if .Values.ingress.enabled -}}
{{- $fullName := include "affine.fullname" . -}}
{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
{{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }}
{{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}}
{{- end }}
{{- end }}
{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
apiVersion: networking.k8s.io/v1
{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
apiVersion: networking.k8s.io/v1beta1
{{- else -}}
apiVersion: extensions/v1beta1
{{- end }}
kind: Ingress
metadata:
name: {{ $fullName }}
labels:
{{- include "affine.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
- host: "{{ .Values.ingress.host }}"
http:
paths:
- path: /graphql
pathType: Prefix
backend:
service:
name: affine-graphql
port:
number: {{ .Values.graphql.service.port }}
- path: /api
pathType: Prefix
backend:
service:
name: affine-graphql
port:
number: {{ .Values.graphql.service.port }}
- path: /
pathType: Prefix
backend:
service:
name: affine-web
port:
number: {{ .Values.web.service.port }}
{{- end }}

17
.github/helm/affine/values.yaml vendored Normal file
View File

@@ -0,0 +1,17 @@
ingress:
enabled: false
className: ''
annotations:
kubernetes.io/ingress.class: nginx
host: affine.pro
tls: []
graphql:
service:
type: ClusterIP
port: 3000
web:
service:
type: ClusterIP
port: 8080

60
.github/helm/deployment_guide.md vendored Normal file
View File

@@ -0,0 +1,60 @@
# Cluster Deployment Guide
This document provides a step-by-step guide for developers on how to deploy services in a Kubernetes cluster. The following content assumes that the reader already has a basic understanding of Kubernetes concepts and operations.
### 1. Configure Service Mesh (Optional)
In the Kubernetes cluster, we optionally use Service Mesh (like Istio and Anthos Service Mesh) to manage the network interactions of microservices. If Service Mesh is already deployed on your cluster or do not need to use the service network, you can skip this step. In this step, we assume that you are using Google Kubernetes Engine (GKE) and have already installed Anthos Service Mesh on your cluster, if you wish to use another Ingress Controller, please refer to the relevant documentation.
To configure your kubectl context to interact with your Kubernetes cluster using the gcloud tool, you need to execute the following commands:
```sh
export CLUSTER_NAME=your_cluster_name
export REGION=your_cluster_region
export PROJECT=your_project_id
gcloud container clusters get-credentials $CLUSTER_NAME --region $REGION --project $PROJECT
```
In this command, you should replace `CLUSTER_NAME`, `REGION` and `PROJECT` with the actual name, region and project id of your Kubernetes cluster. This command retrieves the access credentials for your Kubernetes cluster and automatically configures kubectl to use these credentials.
Now, to inject Service Mesh for a specific Namespace, first, set the environment variable `NAMESPACE` that should correspond to your target Kubernetes Namespace. In this example, we use `prod` as the target Namespace:
```sh
export NAMESPACE=prod
```
Then, we label the Namespace which will enable Istio to automatically inject the sidecar container for all new Pods under this Namespace:
```sh
kubectl label namespace $NAMESPACE istio-injection- istio.io/rev=asm-managed --overwrite
```
Finally, we trigger the Kubernetes Deployment restart mechanism to allow existing Pods to also obtain sidecar container injection:
```sh
kubectl rollout restart deployment -n $NAMESPACE
```
### 2. Deploying the Application
Next, we will deploy our application in the Kubernetes cluster through Helm. First, set relevant environment variables:
```sh
export NAMESPACE=prod
export RELEASE=affine-cloud-prod
export PATH=.github/helm/affine-cloud
```
- `NAMESPACE` should be consistent with the first step, indicating your target Kubernetes Namespace.
- `RELEASE` is the name of your Helm release.
- `PATH` is the location of your Helm chart in your file system.
Finally, use the `helm upgrade --install` command to deploy or upgrade your application:
```sh
helm upgrade --namespace $NAMESPACE --create-namespace --install $RELEASE $PATH
```
This command creates (if it doesn't already exist) and deploys your Helm chart in the specified Namespace. If the release already exists, it will be upgraded.
The above are the complete steps for deploying an application in a Kubernetes cluster. Make sure all prerequisites are met before deploying, and also ensure that you have the correct permissions for operations in Kubernetes.

View File

@@ -27,6 +27,7 @@ on:
env:
DEBUG: napi:*
BUILD_TYPE: canary
APP_NAME: affine
COVERAGE: true
MACOSX_DEPLOYMENT_TARGET: '10.13'
@@ -42,12 +43,22 @@ jobs:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Run checks
run: |
yarn i18n-codegen gen
yarn typecheck
yarn lint --max-warnings=0
yarn circular
- name: Run i18n codegen
run: yarn i18n-codegen gen
- name: Run Type Check
run: yarn typecheck
- name: Run ESLint
run: yarn lint --max-warnings=0 --cache
- name: Run Prettier
run: yarn prettier . --ignore-unknown --cache --check
- name: Run circular
run: yarn circular
- name: Upload server dist
uses: actions/upload-artifact@v3
with:
name: server-dist
path: ./apps/server/dist
if-no-files-found: error
build-docs:
name: Build Docs
@@ -85,8 +96,6 @@ jobs:
name: Build @affine/web
runs-on: ubuntu-latest
environment: development
env:
RELEASE_VERSION: canary
steps:
- uses: actions/checkout@v3
@@ -105,9 +114,6 @@ jobs:
name: Build @affine/web (Desktop)
runs-on: ubuntu-latest
environment: development
env:
ENABLE_BOOKMARK_OPERATION: true
RELEASE_VERSION: canary
steps:
- uses: actions/checkout@v3
@@ -163,10 +169,21 @@ jobs:
working-directory: apps/server
env:
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
- name: Setup Rust
uses: ./.github/actions/setup-rust
with:
target: 'x86_64-unknown-linux-gnu'
- name: Run server tests
run: yarn nx test:coverage @affine/server
env:
CARGO_TARGET_DIR: '${{ github.workspace }}/target'
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
- name: Upload storage.node
uses: actions/upload-artifact@v3
with:
name: storage.node
path: ./packages/storage/storage.node
if-no-files-found: error
- name: Upload server test coverage results
uses: codecov/codecov-action@v3
with:
@@ -174,7 +191,7 @@ jobs:
files: ./apps/server/.coverage/lcov.info
flags: server-test
name: affine
fail_ci_if_error: true
fail_ci_if_error: false
storybook-test:
name: Storybook Test
@@ -253,7 +270,7 @@ jobs:
files: ./.coverage/lcov.info
flags: e2etest
name: affine
fail_ci_if_error: true
fail_ci_if_error: false
- name: Upload test results
if: ${{ failure() }}
@@ -267,6 +284,7 @@ jobs:
name: E2E Migration Test
runs-on: ubuntu-latest
environment: development
needs: [build-web]
steps:
- uses: actions/checkout@v3
@@ -275,6 +293,12 @@ jobs:
with:
playwright-install: true
- name: Download next static
uses: actions/download-artifact@v3
with:
name: next-js-static
path: ./apps/web/out
- name: Unzip
run: yarn unzip
working-directory: ./tests/affine-legacy/0.7.0-canary.18
@@ -283,6 +307,18 @@ jobs:
run: yarn e2e --forbid-only
working-directory: ./tests/affine-legacy/0.7.0-canary.18
- name: Run vitest
run: yarn test
working-directory: ./tests/affine-legacy/0.7.0-canary.18
- name: Upload test results
if: ${{ failure() }}
uses: actions/upload-artifact@v3
with:
name: test-results-e2e-migration
path: ./tests/affine-legacy/0.7.0-canary.18/test-results
if-no-files-found: ignore
desktop-test:
name: Desktop Test
runs-on: ${{ matrix.spec.os }}
@@ -338,8 +374,8 @@ jobs:
run: yarn nx test @affine/monorepo
env:
NATIVE_TEST: 'true'
- name: Build layers
run: yarn workspace @affine/electron build
- name: Build AFFiNE Desktop
run: yarn nx build @affine/electron
- name: Download static resource artifact
uses: actions/download-artifact@v3
with:
@@ -348,13 +384,13 @@ jobs:
- name: Run desktop tests
if: ${{ matrix.spec.test && matrix.spec.os == 'ubuntu-latest' }}
run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn workspace @affine/electron test
run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- yarn nx test @affine/electron
env:
COVERAGE: true
- name: Run desktop tests
if: ${{ matrix.spec.test && matrix.spec.os != 'ubuntu-latest' }}
run: yarn workspace @affine/electron test
run: yarn nx test @affine/electron
env:
COVERAGE: true
@@ -370,7 +406,7 @@ jobs:
files: ./.coverage/lcov.info
flags: e2etest-${{ matrix.spec.os }}-${{ matrix.spec.arch }}
name: affine
fail_ci_if_error: true
fail_ci_if_error: false
- name: Upload test results
if: ${{ failure() }}
@@ -409,13 +445,15 @@ jobs:
files: ./.coverage/store/lcov.info
flags: unittest
name: affine
fail_ci_if_error: true
fail_ci_if_error: false
build-docker:
if: github.ref == 'refs/heads/master'
name: Build Docker
needs:
- build-web-desktop
- lint
- desktop-test
- server-test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
@@ -424,6 +462,16 @@ jobs:
with:
name: next-js-static
path: ./apps/web/out
- name: Download server dist
uses: actions/download-artifact@v3
with:
name: server-dist
path: ./apps/server/dist
- name: Download storage.node
uses: actions/download-artifact@v3
with:
name: storage.node
path: ./apps/server
- name: Setup Git short hash
run: |
echo "GIT_SHORT_HASH=$(git rev-parse --short HEAD)" >> "$GITHUB_ENV"
@@ -449,14 +497,21 @@ jobs:
file: .github/deployment/front/Dockerfile
tags: ghcr.io/toeverything/affine-front:${{ env.GIT_SHORT_HASH }},ghcr.io/toeverything/affine-front:latest
# setup node without cache configuration
# Prisma cache is not compatible with docker build cache
- name: Setup Node.js
uses: ./.github/actions/setup-node
uses: actions/setup-node@v3
with:
package-install: false
node-version-file: '.nvmrc'
registry-url: https://npm.pkg.github.com
scope: '@toeverything'
- name: Install Node.js dependencies
run: yarn workspaces focus @affine/server --production
- name: Generate Prisma client
run: yarn workspace @affine/server prisma generate
- name: Build graphql Dockerfile
uses: docker/build-push-action@v4
with:

17
.github/workflows/cancel.yml vendored Normal file
View File

@@ -0,0 +1,17 @@
name: Cancel
on:
pull_request_target:
types:
- edited
- synchronize
jobs:
cancel:
name: 'Cancel Previous Runs'
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
- uses: styfle/cancel-workflow-action@0.11.0
with:
workflow_id: 44038251
access_token: ${{ github.token }}

View File

@@ -3,8 +3,8 @@ name: Release Charts
on:
push:
branches: [master]
pull_request:
branches: [master]
paths:
- '.github/helm/**/Chart.yml'
jobs:
release:
@@ -43,11 +43,12 @@ jobs:
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
helm dependencies build ../.github/helm/affine
helm dependencies build ../.github/helm/affine-cloud
cr package ../.github/helm/affine
cr package ../.github/helm/affine-cloud
- name: Package charts
if: github.ref == 'refs/heads/master'
- name: Publish charts
working-directory: .helm-chart-repo
run: |
set -ex

View File

@@ -58,10 +58,6 @@ jobs:
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
NEXT_PUBLIC_SENTRY_DSN: ${{ secrets.NEXT_PUBLIC_SENTRY_DSN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
API_SERVER_PROFILE: prod
ENABLE_TEST_PROPERTIES: false
ENABLE_BOOKMARK_OPERATION: true
ENABLE_SQLITE_PROVIDER: false
RELEASE_VERSION: ${{ needs.set-build-version.outputs.version }}
- name: Upload Artifact (web-static)
@@ -125,8 +121,11 @@ jobs:
name: before-make-web-static
path: apps/electron/resources/web-static
- name: Build Infra
run: yarn nx run-many -t build -p plugin-infra infra
- name: Build layers
run: yarn workspace @affine/electron build
run: yarn nx build @affine/electron
- name: Signing By Apple Developer ID
if: ${{ matrix.spec.platform == 'darwin' }}

View File

@@ -40,7 +40,7 @@ env:
jobs:
before-make:
runs-on: ubuntu-latest
environment: ${{ github.ref_name == 'master' && 'production' || 'development' }}
environment: production
outputs:
RELEASE_VERSION: ${{ steps.get-canary-version.outputs.RELEASE_VERSION }}
steps:
@@ -52,12 +52,12 @@ jobs:
if: ${{ github.ref_type == 'tag' }}
run: |
TAG_VERSION=${GITHUB_REF#refs/tags/v}
PACKAGE_VERSION=$(node -p "require('./package.json').version")
PACKAGE_VERSION=$(node -p "require('./apps/electron/package.json').version")
if [ "$TAG_VERSION" != "$PACKAGE_VERSION" ]; then
echo "Tag version ($TAG_VERSION) does not match package.json version ($PACKAGE_VERSION)"
exit 1
fi
echo ::set-env name=RELEASE_VERSION::${TAG_VERSION}
echo "RELEASE_VERSION=$(node -p "require('./apps/electron/package.json').version")" >> $GITHUB_OUTPUT
- name: generate-assets
run: yarn workspace @affine/electron generate-assets
env:
@@ -73,7 +73,7 @@ jobs:
path: apps/electron/resources/web-static
make-distribution:
environment: ${{ github.ref_name == 'master' && 'production' || 'development' }}
environment: production
strategy:
# all combinations: macos-latest x64, macos-latest arm64, windows-latest x64, ubuntu-latest x64
matrix:
@@ -123,8 +123,11 @@ jobs:
name: before-make-web-static
path: apps/electron/resources/web-static
- name: Build layers
run: yarn workspace @affine/electron build
- name: Build Infra
run: yarn nx run-many -t build -p plugin-infra infra
- name: Build AFFiNE Desktop
run: yarn nx build @affine/electron
- name: Signing By Apple Developer ID
if: ${{ matrix.spec.platform == 'darwin' }}
@@ -197,15 +200,15 @@ jobs:
cp ./apps/electron/scripts/generate-yml.js .
node generate-yml.js
env:
RELEASE_VERSION: ${{ github.event.inputs.version || needs.before-make.outputs.version }}
RELEASE_VERSION: ${{ github.event.inputs.version || needs.before-make.outputs.RELEASE_VERSION }}
- name: Create Release Draft
uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
with:
name: ${{ github.event.inputs.version || needs.before-make.outputs.version }}
name: ${{ github.event.inputs.version || needs.before-make.outputs.RELEASE_VERSION }}
body: ''
draft: ${{ github.event.inputs.is-draft || false }}
draft: ${{ github.event.inputs.is-draft || true }}
prerelease: ${{ github.event.inputs.is-pre-release || needs.before-make.outputs.version }}
files: |
./VERSION

View File

@@ -1,5 +1,12 @@
pnpm-lock.yaml
yarn.lock
target
lib
test-results
packages/i18n/src/i18n-generated.ts
packages/graphql/src/graphql/index.ts
.next
out
dist
.yarn
tests/affine-legacy/0.7.0-canary.18/static
.github/helm

View File

@@ -197,7 +197,7 @@ See [LICENSE] for details.
[rust-version-icon]: https://img.shields.io/badge/Rust-1.70.0-dea584
[stars-icon]: https://img.shields.io/github/stars/toeverything/AFFiNE.svg?style=flat&logo=github&colorB=red&label=stars
[codecov]: https://codecov.io/gh/toeverything/affine/branch/master/graphs/badge.svg?branch=master
[node-version-icon]: https://img.shields.io/badge/node-%3E=18.16.0-success
[node-version-icon]: https://img.shields.io/badge/node-%3E=18.16.1-success
[typescript-version-icon]: https://img.shields.io/github/package-json/dependency-version/toeverything/affine/dev/typescript
[react-version-icon]: https://img.shields.io/github/package-json/dependency-version/toeverything/affine/dev/react?color=rgb%2897%2C%20218%2C%20251%29
[blocksuite-icon]: https://img.shields.io/github/package-json/dependency-version/toeverything/AFFiNE/@blocksuite/store?color=6880ff&filename=apps%2Fweb%2Fpackage.json&label=blocksuite

25
apps/README.md Normal file
View File

@@ -0,0 +1,25 @@
# Apps structure
> This is the structure of the `apps` directory.
## docs
AFFiNE Developer Documentation using [waku](https://github.com/dai-shi/waku).
## electron
> `web` needs to be built before electron.
AFFiNE Desktop (macOS, Linux and Windows Distribution) using [Electron](https://www.electronjs.org/).
## server
Server using [Nest.js](https://nestjs.com/).
## storybook
Storybook using [Storybook](https://storybook.js.org/).
## web
AFFiNE Core Application using [React.js](https://reactjs.org/).

View File

@@ -1,6 +1,6 @@
{
"name": "@affine/docs",
"version": "0.7.0-canary.22",
"version": "0.7.0-canary.25",
"type": "module",
"private": true,
"scripts": {
@@ -10,12 +10,12 @@
},
"dependencies": {
"@affine/component": "workspace:*",
"@blocksuite/block-std": "0.0.0-20230627165830-836e6fd1-nightly",
"@blocksuite/blocks": "0.0.0-20230627165830-836e6fd1-nightly",
"@blocksuite/editor": "0.0.0-20230627165830-836e6fd1-nightly",
"@blocksuite/global": "0.0.0-20230627165830-836e6fd1-nightly",
"@blocksuite/lit": "0.0.0-20230627165830-836e6fd1-nightly",
"@blocksuite/store": "0.0.0-20230627165830-836e6fd1-nightly",
"@blocksuite/block-std": "0.0.0-20230630081054-55a25248-nightly",
"@blocksuite/blocks": "0.0.0-20230630081054-55a25248-nightly",
"@blocksuite/editor": "0.0.0-20230630081054-55a25248-nightly",
"@blocksuite/global": "0.0.0-20230630081054-55a25248-nightly",
"@blocksuite/lit": "0.0.0-20230630081054-55a25248-nightly",
"@blocksuite/store": "0.0.0-20230630081054-55a25248-nightly",
"express": "^4.18.2",
"jotai": "^2.2.1",
"react": "18.3.0-canary-8ec962d82-20230623",

View File

@@ -1,7 +1,7 @@
{
"name": "@affine/electron",
"private": true,
"version": "0.7.0-canary.22",
"version": "0.7.0-canary.25",
"author": "affine",
"repository": {
"url": "https://github.com/toeverything/AFFiNE",
@@ -11,8 +11,7 @@
"homepage": "https://github.com/toeverything/AFFiNE",
"scripts": {
"dev": "yarn cross-env DEV_SERVER_URL=http://localhost:8080 node scripts/dev.mjs",
"watch": "yarn cross-env DEV_SERVER_URL=http://localhost:8080 node scripts/dev.mjs --watch",
"prod": "yarn node scripts/dev.mjs",
"dev:prod": "yarn node scripts/dev.mjs",
"build": "zx scripts/build-layers.mjs",
"generate-assets": "zx scripts/generate-assets.mjs",
"package": "electron-forge package",
@@ -23,16 +22,14 @@
"forge": "./forge.config.js"
},
"main": "./dist/main.js",
"exports": {
"./scripts/plugins/build-plugins.mjs": "./scripts/plugins/build-plugins.mjs"
},
"devDependencies": {
"@affine-test/kit": "workspace:*",
"@affine/bookmark-block": "workspace:*",
"@affine/native": "workspace:*",
"@blocksuite/blocks": "0.0.0-20230627165830-836e6fd1-nightly",
"@blocksuite/editor": "0.0.0-20230627165830-836e6fd1-nightly",
"@blocksuite/lit": "0.0.0-20230627165830-836e6fd1-nightly",
"@blocksuite/store": "0.0.0-20230627165830-836e6fd1-nightly",
"@blocksuite/blocks": "0.0.0-20230630081054-55a25248-nightly",
"@blocksuite/editor": "0.0.0-20230630081054-55a25248-nightly",
"@blocksuite/lit": "0.0.0-20230630081054-55a25248-nightly",
"@blocksuite/store": "0.0.0-20230630081054-55a25248-nightly",
"@electron-forge/cli": "^6.2.1",
"@electron-forge/core": "^6.2.1",
"@electron-forge/core-utils": "^6.2.1",
@@ -50,7 +47,7 @@
"electron-log": "^5.0.0-beta.24",
"electron-squirrel-startup": "1.0.0",
"electron-window-state": "^5.0.3",
"esbuild": "^0.18.9",
"esbuild": "^0.18.10",
"fs-extra": "^11.1.1",
"jotai": "^2.2.1",
"playwright": "=1.33.0",

View File

@@ -1,12 +1,9 @@
#!/usr/bin/env zx
import 'zx/globals';
import { resolve } from 'node:path';
import { spawnSync } from 'child_process';
import * as esbuild from 'esbuild';
import { config, rootDir } from './common.mjs';
import { config } from './common.mjs';
const NODE_ENV =
process.env.NODE_ENV === 'development' ? 'development' : 'production';
@@ -18,20 +15,10 @@ if (process.platform === 'win32') {
async function buildLayers() {
const common = config();
console.log('Build plugin infra');
spawnSync('yarn', ['build'], {
stdio: 'inherit',
cwd: resolve(rootDir, './packages/plugin-infra'),
});
console.log('Build plugins');
await import('./plugins/build-plugins.mjs');
await esbuild.build(common.workers);
await esbuild.build({
...common.layers,
define: {
...common.define,
'process.env.NODE_ENV': `"${NODE_ENV}"`,
'process.env.BUILD_TYPE': `"${process.env.BUILD_TYPE || 'stable'}"`,
},

View File

@@ -15,16 +15,9 @@ const DEV_SERVER_URL = process.env.DEV_SERVER_URL;
/** @type 'production' | 'development'' */
const mode = (process.env.NODE_ENV = process.env.NODE_ENV || 'development');
// List of env that will be replaced by esbuild
const ENV_MACROS = ['AFFINE_GOOGLE_CLIENT_ID', 'AFFINE_GOOGLE_CLIENT_SECRET'];
/** @return {{layers: import('esbuild').BuildOptions, workers: import('esbuild').BuildOptions}} */
export const config = () => {
const define = Object.fromEntries([
...ENV_MACROS.map(key => [
'process.env.' + key,
JSON.stringify(process.env[key] ?? ''),
]),
['process.env.NODE_ENV', `"${mode}"`],
['process.env.USE_WORKER', '"true"'],
]);

View File

@@ -1,13 +1,12 @@
/* eslint-disable no-async-promise-executor */
import { spawn } from 'node:child_process';
import { readFileSync } from 'node:fs';
import path, { resolve } from 'node:path';
import path from 'node:path';
import electronPath from 'electron';
import * as esbuild from 'esbuild';
import which from 'which';
import { config, electronDir, rootDir } from './common.mjs';
import { config, electronDir } from './common.mjs';
// this means we don't spawn electron windows, mainly for testing
const watchMode = process.argv.includes('--watch');
@@ -68,14 +67,6 @@ function spawnOrReloadElectron() {
}
const common = config();
const yarnPath = which.sync('yarn');
async function watchPlugins() {
spawn(yarnPath, ['dev'], {
stdio: 'inherit',
cwd: resolve(rootDir, './packages/plugin-infra'),
});
await import('./plugins/dev-plugins.mjs');
}
async function watchLayers() {
return new Promise(async resolve => {
@@ -134,7 +125,6 @@ async function watchWorkers() {
}
async function main() {
await watchPlugins();
await watchLayers();
await watchWorkers();

View File

@@ -0,0 +1,9 @@
import { clipboard, nativeImage } from 'electron';
import type { NamespaceHandlers } from '../type';
export const clipboardHandlers = {
copyAsPng: async (_, dataURL: string) => {
clipboard.writeImage(nativeImage.createFromDataURL(dataURL));
},
} satisfies NamespaceHandlers;

View File

@@ -1,4 +1,4 @@
import { BrowserWindow, dialog, shell } from 'electron';
import { BrowserWindow, dialog } from 'electron';
import fs from 'fs-extra';
import { logger } from '../logger';
@@ -49,8 +49,6 @@ export async function savePDFFileAs(
logger.log(`Wrote PDF successfully to ${filePath}`);
});
});
await shell.openPath(filePath);
return { filePath };
} catch (err) {
logger.error('savePDFFileAs', err);

View File

@@ -1,4 +1,5 @@
import type {
ClipboardHandlerManager,
DebugHandlerManager,
ExportHandlerManager,
UIHandlerManager,
@@ -7,6 +8,7 @@ import type {
} from '@toeverything/infra';
import { ipcMain } from 'electron';
import { clipboardHandlers } from './clipboard';
import { exportHandlers } from './export';
import { getLogFilePath, logger, revealLogFile } from './logger';
import { uiHandlers } from './ui';
@@ -26,6 +28,10 @@ type AllHandlers = {
Electron.IpcMainInvokeEvent,
DebugHandlerManager
>;
clipboard: UnwrapManagerHandlerToServerSide<
Electron.IpcMainInvokeEvent,
ClipboardHandlerManager
>;
export: UnwrapManagerHandlerToServerSide<
Electron.IpcMainInvokeEvent,
ExportHandlerManager
@@ -44,6 +50,7 @@ type AllHandlers = {
export const allHandlers = {
debug: debugHandlers,
ui: uiHandlers,
clipboard: clipboardHandlers,
export: exportHandlers,
updater: updaterHandlers,
} satisfies AllHandlers;

View File

@@ -7,6 +7,7 @@ import { contextBridge, ipcRenderer } from 'electron';
contextBridge.exposeInMainWorld('appInfo', appInfo);
contextBridge.exposeInMainWorld('apis', apis);
contextBridge.exposeInMainWorld('events', events);
contextBridge.exposeInMainWorld('platform', process.platform);
// Credit to microsoft/vscode
const globals = {

View File

@@ -140,3 +140,12 @@ test('affine onboarding button', async ({ page }) => {
expect(await onboardingModal.isVisible()).toEqual(false);
});
test('windows only check', async ({ page }) => {
const windowOnlyUI = page.locator('[data-platform-target=win32]');
if (process.platform === 'win32') {
await expect(windowOnlyUI).toBeVisible();
} else {
await expect(windowOnlyUI).not.toBeVisible();
}
});

View File

@@ -9,5 +9,5 @@
"allowSyntheticDefaultImports": true,
"noEmit": false
},
"include": ["./scripts"]
"include": ["./scripts", "esbuild.main.config.ts", "esbuild.plugin.config.ts"]
}

View File

@@ -1,3 +1,23 @@
# Server
The latest server code of AFFiNE is at https://github.com/toeverything/OctoBase/tree/master/apps/cloud
## Get started
### Install dependencies
```bash
yarn
```
### Build Native binding
```bash
yarn workspace @affine/storage build
```
### Run server
```bash
yarn dev
```
now you can access the server GraphQL endpoint at http://localhost:3000/graphql

View File

@@ -0,0 +1,52 @@
-- CreateTable
CREATE TABLE "blobs" (
"hash" VARCHAR NOT NULL,
"workspace_id" VARCHAR NOT NULL,
"blob" BYTEA NOT NULL,
"length" INTEGER NOT NULL,
"created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "blobs_pkey" PRIMARY KEY ("hash")
);
-- CreateTable
CREATE TABLE "optimized_blobs" (
"hash" VARCHAR NOT NULL,
"workspace_id" VARCHAR NOT NULL,
"params" VARCHAR NOT NULL,
"blob" BYTEA NOT NULL,
"length" INTEGER NOT NULL,
"created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "optimized_blobs_pkey" PRIMARY KEY ("hash")
);
-- CreateTable
CREATE TABLE "docs" (
"id" SERIAL NOT NULL,
"workspace_id" VARCHAR NOT NULL,
"guid" VARCHAR NOT NULL,
"is_workspace" BOOLEAN NOT NULL DEFAULT true,
"blob" BYTEA NOT NULL,
"created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "docs_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "blobs_workspace_id_hash_key" ON "blobs"("workspace_id", "hash");
-- CreateIndex
CREATE UNIQUE INDEX "optimized_blobs_workspace_id_hash_params_key" ON "optimized_blobs"("workspace_id", "hash", "params");
-- CreateIndex
CREATE INDEX "docs_workspace_id_guid_idx" ON "docs"("workspace_id", "guid");
-- AddForeignKey
ALTER TABLE "blobs" ADD CONSTRAINT "blobs_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "optimized_blobs" ADD CONSTRAINT "optimized_blobs_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "docs" ADD CONSTRAINT "docs_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -1,7 +1,7 @@
{
"name": "@affine/server",
"private": true,
"version": "0.7.0-canary.22",
"version": "0.7.0-canary.25",
"description": "Affine Node.js server",
"type": "module",
"bin": {
@@ -40,6 +40,7 @@
"rxjs": "^7.8.1"
},
"devDependencies": {
"@affine/storage": "workspace:*",
"@napi-rs/image": "^1.6.1",
"@nestjs/testing": "^10.0.3",
"@types/express": "^4.17.17",

View File

@@ -1,5 +1,6 @@
generator client {
provider = "prisma-client-js"
provider = "prisma-client-js"
binaryTargets = ["native", "debian-openssl-3.0.x"]
}
datasource db {
@@ -8,10 +9,13 @@ datasource db {
}
model Workspace {
id String @id @default(uuid()) @db.VarChar
public Boolean
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
users UserWorkspacePermission[]
id String @id @default(uuid()) @db.VarChar
public Boolean
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
users UserWorkspacePermission[]
blobs Blob[]
docs Doc[]
optimizedBlobs OptimizedBlob[]
@@map("workspaces")
}
@@ -86,3 +90,44 @@ model VerificationToken {
@@unique([identifier, token])
@@map("verificationtokens")
}
model Blob {
hash String @id @default(uuid()) @db.VarChar
workspaceId String @map("workspace_id") @db.VarChar
blob Bytes @db.ByteA
length Int
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
@@unique([workspaceId, hash])
@@map("blobs")
}
model OptimizedBlob {
hash String @id @default(uuid()) @db.VarChar
workspaceId String @map("workspace_id") @db.VarChar
params String @db.VarChar
blob Bytes @db.ByteA
length Int
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
@@unique([workspaceId, hash, params])
@@map("optimized_blobs")
}
model Doc {
id Int @id @default(autoincrement()) @db.Integer
workspaceId String @map("workspace_id") @db.VarChar
guid String @db.VarChar
is_workspace Boolean @default(true) @db.Boolean
blob Bytes @db.ByteA
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
@@index([workspaceId, guid])
@@map("docs")
}

View File

@@ -0,0 +1,13 @@
import { Controller, Get } from '@nestjs/common';
import pkg from '../package.json' assert { type: 'json' };
@Controller('/')
export class AppController {
@Get()
hello() {
return {
message: `AFFiNE GraphQL server: ${pkg.version}`,
};
}
}

View File

@@ -1,17 +1,20 @@
/// <reference types="./global.d.ts" />
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { ConfigModule } from './config';
import { GqlModule } from './graphql.module';
import { BusinessModules } from './modules';
import { PrismaModule } from './prisma';
import { StorageModule } from './storage';
@Module({
imports: [
PrismaModule,
GqlModule,
ConfigModule.forRoot(),
StorageModule.forRoot(),
...BusinessModules,
],
controllers: [AppController],
})
export class AppModule {}

View File

@@ -106,7 +106,7 @@ export interface AFFiNEConfig {
/**
* which port the server will listen on
*
* @default 3000
* @default 3010
* @env AFFINE_SERVER_PORT
*/
port: number;
@@ -132,6 +132,13 @@ export interface AFFiNEConfig {
*/
get origin(): string;
/**
* the database config
*/
db: {
url: string;
};
/**
* the apollo driver config
*/
@@ -146,23 +153,13 @@ export interface AFFiNEConfig {
/**
* whether use remote object storage
*/
enable: boolean;
/**
* used to store all uploaded builds and analysis reports
*
* the concrete type definition is not given here because different storage providers introduce
* significant differences in configuration
*
* @example
* {
* provider: 'aws',
* region: 'eu-west-1',
* aws_access_key_id: '',
* aws_secret_access_key: '',
* // other aws storage config...
* }
*/
config: Record<string, string>;
r2: {
enabled: boolean;
accountId: string;
bucket: string;
accessKeyId: string;
secretAccessKey: string;
};
/**
* Only used when `enable` is `false`
*/
@@ -217,6 +214,7 @@ export interface AFFiNEConfig {
Record<
ExternalAccount,
{
enabled: boolean;
clientId: string;
clientSecret: string;
/**

View File

@@ -1,5 +1,6 @@
/// <reference types="../global.d.ts" />
import { createPrivateKey, createPublicKey } from 'node:crypto';
import { homedir } from 'node:os';
import { join } from 'node:path';
@@ -7,77 +8,130 @@ import parse from 'parse-duration';
import pkg from '../../package.json' assert { type: 'json' };
import type { AFFiNEConfig } from './def';
import { applyEnvToConfig } from './env';
// Don't use this in production
export const examplePublicKey = `-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEnxM+GhB6eNKPmTP6uH5Gpr+bmQ87
hHGeOiCsay0w/aPwMqzAOKkZGqX+HZ9BNGy/yiXmnscey5b2vOTzxtRvxA==
-----END PUBLIC KEY-----`;
export const examplePrivateKey = `-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIEtyAJLIULkphVhqXqxk4Nr8Ggty3XLwUJWBxzAWCWTMoAoGCCqGSM49
AwEHoUQDQgAEF3U/0wIeJ3jRKXeFKqQyBKlr9F7xaAUScRrAuSP33rajm3cdfihI
3JvMxVNsS2lE8PSGQrvDrJZaDo0L+Lq9Gg==
-----END EC PRIVATE KEY-----`;
// Don't use this in production
export const examplePrivateKey = `-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgWOog5SFXs1Vjh/WP
QCYPQKgf/jsNmWsvD+jYSn6mi3yhRANCAASfEz4aEHp40o+ZM/q4fkamv5uZDzuE
cZ46IKxrLTD9o/AyrMA4qRkapf4dn0E0bL/KJeaexx7Llva85PPG1G/E
-----END PRIVATE KEY-----`;
const jwtKeyPair = (function () {
const AUTH_PRIVATE_KEY = process.env.AUTH_PRIVATE_KEY ?? examplePrivateKey;
const privateKey = createPrivateKey({
key: Buffer.from(AUTH_PRIVATE_KEY),
format: 'pem',
type: 'sec1',
})
.export({
format: 'pem',
type: 'pkcs8',
})
.toString('utf8');
const publicKey = createPublicKey({
key: Buffer.from(AUTH_PRIVATE_KEY),
format: 'pem',
type: 'spki',
})
.export({
format: 'pem',
type: 'spki',
})
.toString('utf8');
export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => ({
serverId: 'affine-nestjs-server',
version: pkg.version,
ENV_MAP: {},
env: process.env.NODE_ENV ?? 'development',
get prod() {
return this.env === 'production';
},
get dev() {
return this.env === 'development';
},
get test() {
return this.env === 'test';
},
get deploy() {
return !this.dev && !this.test;
},
https: false,
host: 'localhost',
port: 3010,
path: '',
get origin() {
return this.dev
? 'http://localhost:8080'
: `${this.https ? 'https' : 'http'}://${this.host}${
this.host === 'localhost' ? `:${this.port}` : ''
}`;
},
get baseUrl() {
return `${this.origin}${this.path}`;
},
graphql: {
buildSchemaOptions: {
numberScalarMode: 'integer',
return {
publicKey,
privateKey,
};
})();
export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => {
const defaultConfig = {
serverId: 'affine-nestjs-server',
version: pkg.version,
ENV_MAP: {
AFFINE_SERVER_PORT: 'port',
AFFINE_SERVER_HOST: 'host',
AFFINE_SERVER_SUB_PATH: 'path',
DATABASE_URL: 'db.url',
AUTH_PRIVATE_KEY: 'auth.privateKey',
ENABLE_R2_OBJECT_STORAGE: 'objectStorage.r2.enabled',
R2_OBJECT_STORAGE_ACCOUNT_ID: 'objectStorage.r2.accountId',
R2_OBJECT_STORAGE_ACCESS_KEY_ID: 'objectStorage.r2.accessKeyId',
R2_OBJECT_STORAGE_SECRET_ACCESS_KEY: 'objectStorage.r2.secretAccessKey',
R2_OBJECT_STORAGE_BUCKET: 'objectStorage.r2.bucket',
OAUTH_GOOGLE_CLIENT_ID: 'auth.oauthProviders.google.clientId',
OAUTH_GOOGLE_CLIENT_SECRET: 'auth.oauthProviders.google.clientSecret',
OAUTH_GITHUB_CLIENT_ID: 'auth.oauthProviders.github.clientId',
OAUTH_GITHUB_CLIENT_SECRET: 'auth.oauthProviders.github.clientSecret',
} satisfies AFFiNEConfig['ENV_MAP'],
env: process.env.NODE_ENV ?? 'development',
get prod() {
return this.env === 'production';
},
introspection: true,
playground: true,
debug: true,
},
auth: {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
accessTokenExpiresIn: parse('1h')! / 1000,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
refreshTokenExpiresIn: parse('7d')! / 1000,
leeway: 60,
publicKey: examplePublicKey,
privateKey: examplePrivateKey,
enableSignup: true,
enableOauth: false,
nextAuthSecret: '',
oauthProviders: {},
},
objectStorage: {
enable: false,
config: {},
fs: {
path: join(homedir(), '.affine-storage'),
get dev() {
return this.env === 'development';
},
},
});
get test() {
return this.env === 'test';
},
get deploy() {
return !this.dev && !this.test;
},
https: false,
host: 'localhost',
port: 3010,
path: '',
db: {
url: '',
},
get origin() {
return this.dev
? 'http://localhost:8080'
: `${this.https ? 'https' : 'http'}://${this.host}${
this.host === 'localhost' ? `:${this.port}` : ''
}`;
},
get baseUrl() {
return `${this.origin}${this.path}`;
},
graphql: {
buildSchemaOptions: {
numberScalarMode: 'integer',
},
introspection: true,
playground: true,
debug: true,
},
auth: {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
accessTokenExpiresIn: parse('1h')! / 1000,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
refreshTokenExpiresIn: parse('7d')! / 1000,
leeway: 60,
privateKey: jwtKeyPair.privateKey,
publicKey: jwtKeyPair.publicKey,
enableSignup: true,
enableOauth: false,
nextAuthSecret: '',
oauthProviders: {},
},
objectStorage: {
r2: {
enabled: false,
bucket: '',
accountId: '',
accessKeyId: '',
secretAccessKey: '',
},
fs: {
path: join(homedir(), '.affine-storage'),
},
},
} as const;
applyEnvToConfig(defaultConfig);
return defaultConfig;
};

View File

@@ -1,15 +1,17 @@
import { set } from 'lodash-es';
import { parseEnvValue } from './def';
import { type AFFiNEConfig, parseEnvValue } from './def';
for (const env in AFFiNE.ENV_MAP) {
const config = AFFiNE.ENV_MAP[env];
const [path, value] =
typeof config === 'string'
? [config, process.env[env]]
: [config[0], parseEnvValue(process.env[env], config[1])];
export function applyEnvToConfig(rawConfig: AFFiNEConfig) {
for (const env in rawConfig.ENV_MAP) {
const config = rawConfig.ENV_MAP[env];
const [path, value] =
typeof config === 'string'
? [config, process.env[env]]
: [config[0], parseEnvValue(process.env[env], config[1])];
if (typeof value !== 'undefined') {
set(globalThis.AFFiNE, path, process.env[env]);
if (typeof value !== 'undefined') {
set(rawConfig, path, value);
}
}
}

View File

@@ -1,9 +1,12 @@
// eslint-disable-next-line simple-import-sort/imports
import type { DynamicModule, FactoryProvider } from '@nestjs/common';
import { merge } from 'lodash-es';
import type { DeepPartial } from '../utils/types';
import type { AFFiNEConfig } from './def';
import '../prelude';
type ConstructorOf<T> = {
new (): T;
};
@@ -37,11 +40,14 @@ function createConfigProvider(
provide: Config,
useFactory: () => {
const wrapper = new Config();
const config = merge({}, AFFiNE, override);
const config = merge({}, globalThis.AFFiNE, override);
const proxy: Config = new Proxy(wrapper, {
get: (_target, property: keyof Config) => {
const desc = Object.getOwnPropertyDescriptor(AFFiNE, property);
const desc = Object.getOwnPropertyDescriptor(
globalThis.AFFiNE,
property
);
if (desc?.get) {
return desc.get.call(proxy);
}

View File

@@ -1,5 +1,4 @@
import './prelude';
/// <reference types="./global.d.ts" />
import { NestFactory } from '@nestjs/core';
import type { NestExpressApplication } from '@nestjs/platform-express';
import { static as staticMiddleware } from 'express';
@@ -28,12 +27,12 @@ app.use(
})
);
const host = process.env.HOST ?? 'localhost';
const port = process.env.PORT ?? 3010;
const config = app.get(Config);
if (!config.objectStorage.enable) {
const host = config.host ?? 'localhost';
const port = config.port ?? 3010;
if (!config.objectStorage.r2.enabled) {
app.use('/assets', staticMiddleware(config.objectStorage.fs.path));
}

View File

@@ -8,7 +8,14 @@ export const S3_SERVICE = Symbol('S3_SERVICE');
export const S3: FactoryProvider<S3Client> = {
provide: S3_SERVICE,
useFactory: (config: Config) => {
const s3 = new S3Client(config.objectStorage.config);
const s3 = new S3Client({
region: 'auto',
endpoint: `https://${config.objectStorage.r2.accountId}.r2.cloudflarestorage.com`,
credentials: {
accessKeyId: config.objectStorage.r2.accessKeyId,
secretAccessKey: config.objectStorage.r2.secretAccessKey,
},
});
return s3;
},
inject: [Config],

View File

@@ -15,11 +15,11 @@ export class StorageService {
) {}
async uploadFile(key: string, file: FileUpload) {
if (this.config.objectStorage.enable) {
if (this.config.objectStorage.r2.enabled) {
await this.s3.send(
new PutObjectCommand({
Body: file.createReadStream(),
Bucket: this.config.objectStorage.config.bucket,
Bucket: this.config.objectStorage.r2.bucket,
Key: key,
})
);

View File

@@ -0,0 +1,36 @@
import type { Storage } from '@affine/storage';
import {
Controller,
Get,
Inject,
NotFoundException,
Param,
Res,
} from '@nestjs/common';
import type { Response } from 'express';
import { StorageProvide } from '../../storage';
@Controller('/api/workspaces')
export class WorkspacesController {
constructor(@Inject(StorageProvide) private readonly storage: Storage) {}
@Get('/:id/blobs/:name')
async blob(
@Param('id') workspaceId: string,
@Param('name') name: string,
@Res() res: Response
) {
const blob = await this.storage.blob(workspaceId, name);
if (!blob) {
throw new NotFoundException('Blob not found');
}
res.setHeader('content-type', blob.contentType);
res.setHeader('last-modified', blob.lastModified);
res.setHeader('content-length', blob.size);
res.send(blob.data);
}
}

View File

@@ -1,10 +1,11 @@
import { Module } from '@nestjs/common';
import { WorkspacesController } from './controller';
import { PermissionService } from './permission';
import { WorkspaceResolver } from './resolver';
@Module({
providers: [WorkspaceResolver, PermissionService],
providers: [WorkspaceResolver, PermissionService, WorkspacesController],
exports: [PermissionService],
})
export class WorkspaceModule {}

View File

@@ -1,4 +1,5 @@
import { ForbiddenException, NotFoundException } from '@nestjs/common';
import type { Storage } from '@affine/storage';
import { ForbiddenException, Inject, NotFoundException } from '@nestjs/common';
import {
Args,
Field,
@@ -16,8 +17,12 @@ import {
Resolver,
} from '@nestjs/graphql';
import type { User, Workspace } from '@prisma/client';
// @ts-expect-error graphql-upload is not typed
import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs';
import { PrismaService } from '../../prisma';
import { StorageProvide } from '../../storage';
import type { FileUpload } from '../../types';
import { Auth, CurrentUser } from '../auth';
import { UserType } from '../users/resolver';
import { PermissionService } from './permission';
@@ -55,7 +60,8 @@ export class UpdateWorkspaceInput extends PickType(
export class WorkspaceResolver {
constructor(
private readonly prisma: PrismaService,
private readonly permissionProvider: PermissionService
private readonly permissionProvider: PermissionService,
@Inject(StorageProvide) private readonly storage: Storage
) {}
@ResolveField(() => Permission, {
@@ -174,8 +180,25 @@ export class WorkspaceResolver {
@Mutation(() => WorkspaceType, {
description: 'Create a new workspace',
})
async createWorkspace(@CurrentUser() user: User) {
return this.prisma.workspace.create({
async createWorkspace(
@CurrentUser() user: User,
@Args({ name: 'init', type: () => GraphQLUpload })
update: FileUpload
) {
// convert stream to buffer
const buffer = await new Promise<Buffer>((resolve, reject) => {
const stream = update.createReadStream();
const chunks: Uint8Array[] = [];
stream.on('data', chunk => {
chunks.push(chunk);
});
stream.on('error', reject);
stream.on('end', () => {
resolve(Buffer.concat(chunks));
});
});
const workspace = await this.prisma.workspace.create({
data: {
public: false,
users: {
@@ -191,6 +214,10 @@ export class WorkspaceResolver {
},
},
});
await this.storage.createWorkspace(workspace.id, buffer);
return workspace;
}
@Mutation(() => WorkspaceType, {
@@ -221,8 +248,15 @@ export class WorkspaceResolver {
},
});
await this.prisma.userWorkspacePermission.deleteMany({
where: {
workspaceId: id,
},
});
// TODO:
// delete all related data, like websocket connections, blobs, etc.
await this.storage.deleteWorkspace(id);
return true;
}
@@ -283,4 +317,28 @@ export class WorkspaceResolver {
return this.permissionProvider.revoke(workspaceId, user.id);
}
@Mutation(() => String)
async uploadBlob(
@CurrentUser() user: User,
@Args('workspaceId') workspaceId: string,
@Args({ name: 'blob', type: () => GraphQLUpload })
blob: FileUpload
) {
await this.permissionProvider.check(workspaceId, user.id);
const buffer = await new Promise<Buffer>((resolve, reject) => {
const stream = blob.createReadStream();
const chunks: Uint8Array[] = [];
stream.on('data', chunk => {
chunks.push(chunk);
});
stream.on('error', reject);
stream.on('end', () => {
resolve(Buffer.concat(chunks));
});
});
return this.storage.uploadBlob(workspaceId, buffer);
}
}

View File

@@ -106,7 +106,7 @@ type Mutation {
"""
Create a new workspace
"""
createWorkspace: WorkspaceType!
createWorkspace(init: Upload!): WorkspaceType!
"""
Update workspace
@@ -121,6 +121,7 @@ type Mutation {
revoke(workspaceId: String!, userId: String!): Boolean!
acceptInvite(workspaceId: String!): Boolean!
leaveWorkspace(workspaceId: String!): Boolean!
uploadBlob(workspaceId: String!, blob: Upload!): String!
"""
Upload user avatar
@@ -128,6 +129,11 @@ type Mutation {
uploadAvatar(id: String!, avatar: Upload!): UserType!
}
"""
The `Upload` scalar type represents a file upload.
"""
scalar Upload
input UpdateWorkspaceInput {
"""
is Public workspace
@@ -135,8 +141,3 @@ input UpdateWorkspaceInput {
public: Boolean
id: ID!
}
"""
The `Upload` scalar type represents a file upload.
"""
scalar Upload

View File

@@ -0,0 +1,37 @@
import { createRequire } from 'node:module';
import type { Storage } from '@affine/storage';
import { type DynamicModule, type FactoryProvider } from '@nestjs/common';
import { Config } from '../config';
export const StorageProvide = Symbol('Storage');
const require = createRequire(import.meta.url);
export class StorageModule {
static forRoot(): DynamicModule {
const storageProvider: FactoryProvider = {
provide: StorageProvide,
useFactory: async (config: Config) => {
let StorageFactory: typeof Storage;
try {
// dev mode
StorageFactory = (await import('@affine/storage')).Storage;
} catch {
// In docker
StorageFactory = require('../../storage.node').Storage;
}
return StorageFactory.connect(config.db.url);
},
inject: [Config],
};
return {
global: true,
module: StorageModule,
providers: [storageProvider],
exports: [storageProvider],
};
}
}

View File

@@ -12,12 +12,9 @@ import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs';
import request from 'supertest';
import { AppModule } from '../app';
import { getDefaultAFFiNEConfig } from '../config/default';
const gql = '/graphql';
globalThis.AFFiNE = getDefaultAFFiNEConfig();
describe('AppModule', () => {
let app: INestApplication;
@@ -76,33 +73,14 @@ describe('AppModule', () => {
.auth(token, { type: 'bearer' })
.send({
query: `
mutation {
createWorkspace {
id
public
createdAt
}
query {
__typename
}
`,
})
.expect(200)
.expect(res => {
ok(
typeof res.body.data.createWorkspace === 'object',
'res.body.data.createWorkspace is not an object'
);
ok(
typeof res.body.data.createWorkspace.id === 'string',
'res.body.data.createWorkspace.id is not a string'
);
ok(
typeof res.body.data.createWorkspace.public === 'boolean',
'res.body.data.createWorkspace.public is not a boolean'
);
ok(
typeof res.body.data.createWorkspace.createdAt === 'string',
'res.body.data.createWorkspace.created_at is not a string'
);
ok(res.body.data.__typename === 'Query');
});
});

View File

@@ -1,3 +1,4 @@
/// <reference types="../global.d.ts" />
import { ok } from 'node:assert';
import { beforeEach, test } from 'node:test';
@@ -5,14 +6,11 @@ import { Test } from '@nestjs/testing';
import { PrismaClient } from '@prisma/client';
import { ConfigModule } from '../config';
import { getDefaultAFFiNEConfig } from '../config/default';
import { GqlModule } from '../graphql.module';
import { AuthModule } from '../modules/auth';
import { AuthService } from '../modules/auth/service';
import { PrismaModule } from '../prisma';
globalThis.AFFiNE = getDefaultAFFiNEConfig();
let auth: AuthService;
// cleanup database before each test

View File

@@ -4,9 +4,6 @@ import { beforeEach, test } from 'node:test';
import { Test } from '@nestjs/testing';
import { Config, ConfigModule } from '../config';
import { getDefaultAFFiNEConfig } from '../config/default';
globalThis.AFFiNE = getDefaultAFFiNEConfig();
let config: Config;
beforeEach(async () => {

View File

@@ -1,22 +1,21 @@
import { ok } from 'node:assert';
import { afterEach, beforeEach, describe, test } from 'node:test';
import { afterEach, beforeEach, describe, it } from 'node:test';
import type { INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { PrismaClient } from '@prisma/client';
// @ts-expect-error graphql-upload is not typed
import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs';
import request from 'supertest';
import { AppModule } from '../app';
import { getDefaultAFFiNEConfig } from '../config/default';
import type { TokenType } from '../modules/auth';
import type { UserType } from '../modules/users';
import type { WorkspaceType } from '../modules/workspaces';
const gql = '/graphql';
globalThis.AFFiNE = getDefaultAFFiNEConfig();
describe('AppModule', () => {
describe('Workspace Module', () => {
let app: INestApplication;
// cleanup database before each test
@@ -32,6 +31,12 @@ describe('AppModule', () => {
imports: [AppModule],
}).compile();
app = module.createNestApplication();
app.use(
graphqlUploadExpress({
maxFileSize: 10 * 1024 * 1024,
maxFiles: 5,
})
);
await app.init();
});
@@ -63,15 +68,20 @@ describe('AppModule', () => {
const res = await request(app.getHttpServer())
.post(gql)
.auth(token, { type: 'bearer' })
.send({
query: `
mutation {
createWorkspace {
.field(
'operations',
JSON.stringify({
name: 'createWorkspace',
query: `mutation createWorkspace($init: Upload!) {
createWorkspace(init: $init) {
id
}
}
`,
})
}`,
variables: { init: null },
})
)
.field('map', JSON.stringify({ '0': ['variables.init'] }))
.attach('0', Buffer.from([0, 0]), 'init.data')
.expect(200);
return res.body.data.createWorkspace;
}
@@ -151,21 +161,21 @@ describe('AppModule', () => {
return res.body.data.revoke;
}
test('should register a user', async () => {
it('should register a user', async () => {
const user = await registerUser('u1', 'u1@affine.pro', '123456');
ok(typeof user.id === 'string', 'user.id is not a string');
ok(user.name === 'u1', 'user.name is not valid');
ok(user.email === 'u1@affine.pro', 'user.email is not valid');
});
test('should create a workspace', async () => {
it('should create a workspace', async () => {
const user = await registerUser('u1', 'u1@affine.pro', '1');
const workspace = await createWorkspace(user.token.token);
ok(typeof workspace.id === 'string', 'workspace.id is not a string');
});
test('should invite a user', async () => {
it('should invite a user', async () => {
const u1 = await registerUser('u1', 'u1@affine.pro', '1');
const u2 = await registerUser('u2', 'u2@affine.pro', '1');
@@ -180,7 +190,7 @@ describe('AppModule', () => {
ok(invite === true, 'failed to invite user');
});
test('should accept an invite', async () => {
it('should accept an invite', async () => {
const u1 = await registerUser('u1', 'u1@affine.pro', '1');
const u2 = await registerUser('u2', 'u2@affine.pro', '1');
@@ -191,7 +201,7 @@ describe('AppModule', () => {
ok(accept === true, 'failed to accept invite');
});
test('should leave a workspace', async () => {
it('should leave a workspace', async () => {
const u1 = await registerUser('u1', 'u1@affine.pro', '1');
const u2 = await registerUser('u2', 'u2@affine.pro', '1');
@@ -203,7 +213,7 @@ describe('AppModule', () => {
ok(leave === true, 'failed to leave workspace');
});
test('should revoke a user', async () => {
it('should revoke a user', async () => {
const u1 = await registerUser('u1', 'u1@affine.pro', '1');
const u2 = await registerUser('u2', 'u2@affine.pro', '1');

View File

@@ -20,6 +20,9 @@
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "../../packages/storage/tsconfig.json"
}
],
"ts-node": {

View File

@@ -30,13 +30,13 @@
"wait-on": "^7.0.1"
},
"devDependencies": {
"@blocksuite/block-std": "0.0.0-20230627165830-836e6fd1-nightly",
"@blocksuite/blocks": "0.0.0-20230627165830-836e6fd1-nightly",
"@blocksuite/editor": "0.0.0-20230627165830-836e6fd1-nightly",
"@blocksuite/global": "0.0.0-20230627165830-836e6fd1-nightly",
"@blocksuite/icons": "^2.1.21",
"@blocksuite/lit": "0.0.0-20230627165830-836e6fd1-nightly",
"@blocksuite/store": "0.0.0-20230627165830-836e6fd1-nightly",
"@blocksuite/block-std": "0.0.0-20230630081054-55a25248-nightly",
"@blocksuite/blocks": "0.0.0-20230630081054-55a25248-nightly",
"@blocksuite/editor": "0.0.0-20230630081054-55a25248-nightly",
"@blocksuite/global": "0.0.0-20230630081054-55a25248-nightly",
"@blocksuite/icons": "^2.1.23",
"@blocksuite/lit": "0.0.0-20230630081054-55a25248-nightly",
"@blocksuite/store": "0.0.0-20230630081054-55a25248-nightly",
"react": "18.3.0-canary-8ec962d82-20230623",
"react-dom": "18.3.0-canary-8ec962d82-20230623"
},
@@ -48,5 +48,5 @@
"@blocksuite/lit": "*",
"@blocksuite/store": "*"
},
"version": "0.7.0-canary.22"
"version": "0.7.0-canary.25"
}

View File

@@ -1,47 +0,0 @@
import { ChangeLog } from '@affine/component/changeLog';
import type { StoryFn } from '@storybook/react';
import { within } from '@storybook/testing-library';
import { useState } from 'react';
export default {
title: 'AFFiNE/ChangeLog',
component: ChangeLog,
};
export const Default: StoryFn = () => (
<div
style={{
width: '256px',
height: '100vh',
}}
>
<ChangeLog onCloseWhatsNew={() => {}} />
</div>
);
export const Close: StoryFn = () => {
const [closed, setIsClosed] = useState(false);
return (
<>
<div>Closed: {closed ? 'true' : 'false'}</div>
<div
style={{
width: '256px',
height: '100vh',
}}
>
<ChangeLog
onCloseWhatsNew={() => {
setIsClosed(true);
}}
/>
</div>
</>
);
};
Close.play = async ({ canvasElement }) => {
const element = within(canvasElement);
await new Promise(resolve => setTimeout(resolve, 2000));
element.getByTestId('change-log-close-button').click();
};

View File

@@ -1,7 +1,7 @@
{
"name": "@affine/web",
"private": true,
"version": "0.7.0-canary.22",
"version": "0.7.0-canary.25",
"scripts": {
"dev": "next dev",
"build": "next build",
@@ -19,13 +19,13 @@
"@affine/jotai": "workspace:*",
"@affine/templates": "workspace:*",
"@affine/workspace": "workspace:*",
"@blocksuite/block-std": "0.0.0-20230627165830-836e6fd1-nightly",
"@blocksuite/blocks": "0.0.0-20230627165830-836e6fd1-nightly",
"@blocksuite/editor": "0.0.0-20230627165830-836e6fd1-nightly",
"@blocksuite/global": "0.0.0-20230627165830-836e6fd1-nightly",
"@blocksuite/icons": "^2.1.21",
"@blocksuite/lit": "0.0.0-20230627165830-836e6fd1-nightly",
"@blocksuite/store": "0.0.0-20230627165830-836e6fd1-nightly",
"@blocksuite/block-std": "0.0.0-20230630081054-55a25248-nightly",
"@blocksuite/blocks": "0.0.0-20230630081054-55a25248-nightly",
"@blocksuite/editor": "0.0.0-20230630081054-55a25248-nightly",
"@blocksuite/global": "0.0.0-20230630081054-55a25248-nightly",
"@blocksuite/icons": "^2.1.23",
"@blocksuite/lit": "0.0.0-20230630081054-55a25248-nightly",
"@blocksuite/store": "0.0.0-20230630081054-55a25248-nightly",
"@dnd-kit/core": "^6.0.8",
"@dnd-kit/sortable": "^7.0.2",
"@emotion/cache": "^11.11.0",

View File

@@ -19,31 +19,35 @@ export const blockSuiteFeatureFlags = {
*/
const buildPreset = {
stable: {
enableAllPageFilter: true,
enableAllPageSaving: false,
enablePlugin: false,
enableTestProperties: false,
enableBroadcastChannelProvider: true,
enableDebugPage: true,
enableLegacyCloud: false,
changelogUrl: 'https://affine.pro/blog/what-is-new-affine-0620',
changelogUrl: 'https://affine.pro/blog/whats-new-affine-0630',
enablePreloading: true,
enableNewSettingModal: false,
enableNewSettingUnstableApi: false,
enableSQLiteProvider: false,
enableNotificationCenter: false,
},
beta: {},
internal: {},
// canary will be aggressive and enable all features
canary: {
enableAllPageFilter: true,
enableAllPageSaving: true,
enablePlugin: true,
enableTestProperties: true,
enableBroadcastChannelProvider: true,
enableDebugPage: true,
enableLegacyCloud: false,
changelogUrl: 'https://github.com/toeverything/AFFiNE/releases',
changelogUrl: 'https://affine.pro/blog/whats-new-affine-0630',
enablePreloading: true,
enableNewSettingModal: true,
enableSQLiteProvider: true,
enableNewSettingUnstableApi: false,
enableSQLiteProvider: false,
enableNotificationCenter: true,
},
};
@@ -51,36 +55,46 @@ const buildPreset = {
buildPreset.beta = buildPreset.stable;
buildPreset.internal = buildPreset.stable;
const currentBuild = process.env.BUILD_ENV || 'stable';
const currentBuild = process.env.BUILD_TYPE || 'stable';
if (process.env.CI && !process.env.BUILD_TYPE) {
throw new Error('BUILD_ENV is required in CI');
}
const currentBuildPreset = buildPreset[currentBuild];
const environmentPreset = {
enablePlugin: process.env.ENABLE_PLUGIN
? process.env.ENABLE_PLUGIN === 'true'
: buildPreset.canary.enablePlugin,
enableAllPageFilter: process.env.ENABLE_ALL_PAGE_FILTER
: currentBuildPreset.enablePlugin,
enableAllPageSaving: process.env.ENABLE_ALL_PAGE_SAVING
? process.env.ENABLE_ALL_PAGE_FILTER === 'true'
: buildPreset.canary.enableAllPageFilter,
: currentBuildPreset.enableAllPageSaving,
enableTestProperties: process.env.ENABLE_TEST_PROPERTIES
? process.env.ENABLE_TEST_PROPERTIES === 'true'
: buildPreset.canary.enableTestProperties,
: currentBuildPreset.enableTestProperties,
enableLegacyCloud: process.env.ENABLE_LEGACY_PROVIDER
? process.env.ENABLE_LEGACY_PROVIDER === 'true'
: buildPreset.canary.enableLegacyCloud,
: currentBuildPreset.enableLegacyCloud,
enableBroadcastChannelProvider: process.env.ENABLE_BC_PROVIDER
? process.env.ENABLE_BC_PROVIDER !== 'false'
: buildPreset.canary.enableBroadcastChannelProvider,
changelogUrl: process.env.CHANGELOG_URL ?? buildPreset.canary.changelogUrl,
: currentBuildPreset.enableBroadcastChannelProvider,
changelogUrl: process.env.CHANGELOG_URL ?? currentBuildPreset.changelogUrl,
enablePreloading: process.env.ENABLE_PRELOADING
? process.env.ENABLE_PRELOADING === 'true'
: buildPreset.canary.enablePreloading,
: currentBuildPreset.enablePreloading,
enableNewSettingModal: process.env.ENABLE_NEW_SETTING_MODAL
? process.env.ENABLE_NEW_SETTING_MODAL === 'true'
: buildPreset.canary.enableNewSettingModal,
: currentBuildPreset.enableNewSettingModal,
enableSQLiteProvider: process.env.ENABLE_SQLITE_PROVIDER
? process.env.ENABLE_SQLITE_PROVIDER === 'true'
: buildPreset.canary.enableSQLiteProvider,
: currentBuildPreset.enableSQLiteProvider,
enableNewSettingUnstableApi: process.env.ENABLE_NEW_SETTING_UNSTABLE_API
? process.env.ENABLE_NEW_SETTING_UNSTABLE_API === 'true'
: currentBuildPreset.enableNewSettingUnstableApi,
enableNotificationCenter: process.env.ENABLE_NOTIFICATION_CENTER
? process.env.ENABLE_NOTIFICATION_CENTER === 'true'
: currentBuildPreset.enableNotificationCenter,
};
/**
@@ -91,7 +105,7 @@ const buildFlags = {
// environment preset will overwrite current build preset
// this environment variable is for debug proposes only
// do not put them into CI
...environmentPreset,
...(process.env.CI ? {} : environmentPreset),
};
export { buildFlags };

View File

@@ -9,6 +9,7 @@ import type {
AffineLegacyCloudWorkspace,
LocalIndexedDBDownloadProvider,
} from '@affine/env/workspace';
import type { WorkspaceAdapter } from '@affine/env/workspace';
import {
LoadPriority,
ReleaseType,
@@ -49,7 +50,6 @@ import {
WorkspaceHeader,
WorkspaceSettingDetail,
} from '../shared';
import type { WorkspaceAdapter } from '../type';
import { QueryKey } from './fetcher';
const storage = createJSONStorage(() => localStorage);
@@ -126,7 +126,7 @@ export const AffineAdapter: WorkspaceAdapter<WorkspaceFlavour.AFFINE> = {
console.warn('Legacy cloud is disabled');
return;
}
rootStore.set(rootWorkspacesMetadataAtom, workspaces =>
await rootStore.set(rootWorkspacesMetadataAtom, workspaces =>
workspaces.filter(
workspace => workspace.flavour !== WorkspaceFlavour.AFFINE
)
@@ -336,10 +336,10 @@ export const AffineAdapter: WorkspaceAdapter<WorkspaceFlavour.AFFINE> = {
</>
);
},
PageList: ({ blockSuiteWorkspace, onOpenPage, view }) => {
PageList: ({ blockSuiteWorkspace, onOpenPage, collection }) => {
return (
<BlockSuitePageList
view={view}
collection={collection}
listType="all"
onOpenPage={onOpenPage}
blockSuiteWorkspace={blockSuiteWorkspace}

View File

@@ -6,6 +6,7 @@ import {
PageNotFoundError,
} from '@affine/env/constant';
import type { LocalIndexedDBDownloadProvider } from '@affine/env/workspace';
import type { WorkspaceAdapter } from '@affine/env/workspace';
import {
LoadPriority,
ReleaseType,
@@ -26,7 +27,6 @@ import {
WorkspaceHeader,
WorkspaceSettingDetail,
} from '../shared';
import type { WorkspaceAdapter } from '../type';
const logger = new DebugLogger('use-create-first-workspace');
@@ -95,11 +95,11 @@ export const LocalAdapter: WorkspaceAdapter<WorkspaceFlavour.LOCAL> = {
</>
);
},
PageList: ({ blockSuiteWorkspace, onOpenPage, view }) => {
PageList: ({ blockSuiteWorkspace, onOpenPage, collection }) => {
return (
<BlockSuitePageList
listType="all"
view={view}
collection={collection}
onOpenPage={onOpenPage}
blockSuiteWorkspace={blockSuiteWorkspace}
/>

View File

@@ -1,21 +0,0 @@
import type {
AppEvents,
WorkspaceCRUD,
WorkspaceUISchema,
} from '@affine/env/workspace';
import type {
LoadPriority,
ReleaseType,
WorkspaceFlavour,
} from '@affine/env/workspace';
export interface WorkspaceAdapter<Flavour extends WorkspaceFlavour> {
releaseType: ReleaseType;
flavour: Flavour;
// Plugin will be loaded according to the priority
loadPriority: LoadPriority;
Events: Partial<AppEvents>;
// Fetch necessary data for the first render
CRUD: WorkspaceCRUD<Flavour>;
UI: WorkspaceUISchema<Flavour>;
}

View File

@@ -1,5 +1,9 @@
import { Unreachable } from '@affine/env/constant';
import type { AppEvents, WorkspaceUISchema } from '@affine/env/workspace';
import type {
AppEvents,
WorkspaceAdapter,
WorkspaceUISchema,
} from '@affine/env/workspace';
import {
LoadPriority,
ReleaseType,
@@ -8,7 +12,6 @@ import {
import { AffineAdapter } from './affine';
import { LocalAdapter } from './local';
import type { WorkspaceAdapter } from './type';
const unimplemented = () => {
throw new Error('Not implemented');

View File

@@ -4,11 +4,15 @@
import 'fake-indexeddb/auto';
import { initEmptyPage } from '@affine/env/blocksuite';
import type { LocalIndexedDBBackgroundProvider } from '@affine/env/workspace';
import type {
LocalIndexedDBBackgroundProvider,
WorkspaceAdapter,
} from '@affine/env/workspace';
import { WorkspaceFlavour, WorkspaceVersion } from '@affine/env/workspace';
import {
rootCurrentWorkspaceIdAtom,
rootWorkspacesMetadataAtom,
workspaceAdaptersAtom,
} from '@affine/workspace/atom';
import { createIndexedDBBackgroundProvider } from '@affine/workspace/providers';
import {
@@ -63,6 +67,13 @@ describe('page mode atom', () => {
describe('currentWorkspace atom', () => {
test('should be defined', async () => {
const store = createStore();
store.set(
workspaceAdaptersAtom,
WorkspaceAdapters as Record<
WorkspaceFlavour,
WorkspaceAdapter<WorkspaceFlavour>
>
);
let id: string;
{
const workspace = createEmptyBlockSuiteWorkspace(
@@ -92,7 +103,7 @@ describe('currentWorkspace atom', () => {
const workspaceId = await WorkspaceAdapters[
WorkspaceFlavour.LOCAL
].CRUD.create(workspace);
store.set(rootWorkspacesMetadataAtom, [
await store.set(rootWorkspacesMetadataAtom, [
{
id: workspaceId,
flavour: WorkspaceFlavour.LOCAL,
@@ -103,7 +114,7 @@ describe('currentWorkspace atom', () => {
}
store.set(
rootCurrentWorkspaceIdAtom,
store.get(rootWorkspacesMetadataAtom)[0].id
(await store.get(rootWorkspacesMetadataAtom))[0].id
);
const workspace = await store.get(rootCurrentWorkspaceAtom);
expect(workspace).toBeDefined();

View File

@@ -1,84 +1,8 @@
import { DebugLogger } from '@affine/debug';
import { WorkspaceFlavour, WorkspaceVersion } from '@affine/env/workspace';
import type { RootWorkspaceMetadataV2 } from '@affine/workspace/atom';
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import { atom } from 'jotai';
import { atomFamily, atomWithStorage } from 'jotai/utils';
import { WorkspaceAdapters } from '../adapters/workspace';
import type { CreateWorkspaceMode } from '../components/affine/create-workspace-modal';
const logger = new DebugLogger('web:atoms');
// workspace necessary atoms
// todo(himself65): move this to the workspace package
rootWorkspacesMetadataAtom.onMount = setAtom => {
function createFirst(): RootWorkspaceMetadataV2[] {
const Plugins = Object.values(WorkspaceAdapters).sort(
(a, b) => a.loadPriority - b.loadPriority
);
return Plugins.flatMap(Plugin => {
return Plugin.Events['app:init']?.().map(
id =>
({
id,
flavour: Plugin.flavour,
// new workspace should all support sub-doc feature
version: WorkspaceVersion.SubDoc,
} satisfies RootWorkspaceMetadataV2)
);
}).filter((ids): ids is RootWorkspaceMetadataV2 => !!ids);
}
const abortController = new AbortController();
if (!environment.isServer) {
// next tick to make sure the hydration is correct
setTimeout(() => {
setAtom(metadata => {
if (abortController.signal.aborted) return metadata;
if (
metadata.length === 0 &&
localStorage.getItem('is-first-open') === null
) {
localStorage.setItem('is-first-open', 'false');
const newMetadata = createFirst();
logger.info('create first workspace', newMetadata);
return newMetadata;
}
return metadata;
});
}, 0);
}
if (environment.isDesktop && runtimeConfig.enableSQLiteProvider) {
window.apis?.workspace
.list()
.then(workspaceIDs => {
if (abortController.signal.aborted) return;
const newMetadata = workspaceIDs.map(w => ({
id: w[0],
flavour: WorkspaceFlavour.LOCAL,
version: undefined,
}));
setAtom(metadata => {
return [
...metadata,
...newMetadata.filter(m => !metadata.find(m2 => m2.id === m.id)),
];
});
})
.catch(err => {
console.error(err);
});
}
return () => {
abortController.abort();
};
};
// modal atoms
export const openWorkspacesModalAtom = atom(false);
export const openCreateWorkspaceModalAtom = atom<CreateWorkspaceMode>(false);

View File

@@ -1,6 +1,9 @@
//#region async atoms that to load the real workspace data
import { DebugLogger } from '@affine/debug';
import type { WorkspaceRegistry } from '@affine/env/workspace';
import type {
WorkspaceAdapter,
WorkspaceRegistry,
} from '@affine/env/workspace';
import { WorkspaceFlavour } from '@affine/env/workspace';
import {
rootCurrentWorkspaceIdAtom,
@@ -23,7 +26,7 @@ export const workspacesAtom = atom<Promise<AllWorkspace[]>>(
const flavours: string[] = Object.values(WorkspaceAdapters).map(
plugin => plugin.flavour
);
const jotaiWorkspaces = get(rootWorkspacesMetadataAtom)
const jotaiWorkspaces = (await get(rootWorkspacesMetadataAtom))
.filter(
workspace => flavours.includes(workspace.flavour)
// TODO: remove this when we remove the legacy cloud
@@ -33,7 +36,7 @@ export const workspacesAtom = atom<Promise<AllWorkspace[]>>(
? workspace.flavour !== WorkspaceFlavour.AFFINE
: true
);
if (jotaiWorkspaces.some(meta => meta.version === undefined)) {
if (jotaiWorkspaces.some(meta => !('version' in meta))) {
// wait until all workspaces have migrated to v2
await new Promise((resolve, reject) => {
signal.addEventListener('abort', reject);
@@ -44,12 +47,11 @@ export const workspacesAtom = atom<Promise<AllWorkspace[]>>(
}
const workspaces = await Promise.all(
jotaiWorkspaces.map(workspace => {
const plugin =
WorkspaceAdapters[
workspace.flavour as keyof typeof WorkspaceAdapters
];
assertExists(plugin);
const { CRUD } = plugin;
const adapter = WorkspaceAdapters[
workspace.flavour
] as WorkspaceAdapter<WorkspaceFlavour>;
assertExists(adapter);
const { CRUD } = adapter;
return CRUD.get(workspace.id).then(workspace => {
if (workspace === null) {
console.warn(
@@ -93,7 +95,7 @@ export const workspacesAtom = atom<Promise<AllWorkspace[]>>(
export const rootCurrentWorkspaceAtom = atom<Promise<AllWorkspace>>(
async (get, { signal }) => {
const { WorkspaceAdapters } = await import('../adapters/workspace');
const metadata = get(rootWorkspacesMetadataAtom);
const metadata = await get(rootWorkspacesMetadataAtom);
const targetId = get(rootCurrentWorkspaceIdAtom);
if (targetId === null) {
throw new Error(
@@ -105,7 +107,7 @@ export const rootCurrentWorkspaceAtom = atom<Promise<AllWorkspace>>(
throw new Error(`cannot find the workspace with id ${targetId}.`);
}
if (!targetWorkspace.version) {
if (!('version' in targetWorkspace)) {
// wait until the workspace has migrated to v2
await new Promise((resolve, reject) => {
signal.addEventListener('abort', reject);
@@ -115,9 +117,12 @@ export const rootCurrentWorkspaceAtom = atom<Promise<AllWorkspace>>(
});
}
const workspace = await WorkspaceAdapters[targetWorkspace.flavour].CRUD.get(
targetWorkspace.id
);
const adapter = WorkspaceAdapters[
targetWorkspace.flavour
] as WorkspaceAdapter<WorkspaceFlavour>;
assertExists(adapter);
const workspace = await adapter.CRUD.get(targetWorkspace.id);
if (!workspace) {
throw new Error(
`cannot find the workspace with id ${targetId} in the plugin ${targetWorkspace.flavour}.`

View File

@@ -1,10 +1,12 @@
import { migrateToSubdoc } from '@affine/env/blocksuite';
import { isDesktop, isServer } from '@affine/env/constant';
import { setupGlobal } from '@affine/env/global';
import type { LocalIndexedDBDownloadProvider } from '@affine/env/workspace';
import { WorkspaceFlavour, WorkspaceVersion } from '@affine/env/workspace';
import { platformSchema, setupGlobal } from '@affine/env/global';
import type {
LocalIndexedDBDownloadProvider,
WorkspaceAdapter,
} from '@affine/env/workspace';
import { WorkspaceFlavour } from '@affine/env/workspace';
import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import { workspaceAdaptersAtom } from '@affine/workspace/atom';
import {
migrateLocalBlobStorage,
upgradeV1ToV2,
@@ -17,19 +19,34 @@ import { WorkspaceAdapters } from '../adapters/workspace';
setupGlobal();
rootStore.set(
workspaceAdaptersAtom,
WorkspaceAdapters as Record<
WorkspaceFlavour,
WorkspaceAdapter<WorkspaceFlavour>
>
);
if (process.env.NODE_ENV === 'development') {
console.log('Runtime Preset', runtimeConfig);
}
if (runtimeConfig.enablePlugin && !isServer) {
if (runtimeConfig.enablePlugin && !environment.isServer) {
import('@affine/copilot');
}
if (!isServer) {
if (!environment.isServer) {
import('@affine/bookmark-block');
}
if (!isDesktop && !isServer) {
// platform check
{
if (globalThis.platform) {
platformSchema.parse(globalThis.platform);
}
}
if (!environment.isDesktop && !environment.isServer) {
// Polyfill Electron
const unimplemented = () => {
throw new Error('AFFiNE Plugin Web will be supported in the future');
@@ -52,65 +69,76 @@ if (!isDesktop && !isServer) {
});
}
rootStore.sub(rootWorkspacesMetadataAtom, () => {
const metadata = rootStore.get(rootWorkspacesMetadataAtom);
metadata.forEach(oldMeta => {
if (!oldMeta.version) {
const adapter = WorkspaceAdapters[oldMeta.flavour];
assertExists(adapter);
const upgrade = async () => {
const workspace = await adapter.CRUD.get(oldMeta.id);
if (!workspace) {
console.warn('cannot find workspace', oldMeta.id);
return;
}
if (workspace.flavour !== WorkspaceFlavour.LOCAL) {
console.warn('not supported');
return;
}
const doc = workspace.blockSuiteWorkspace.doc;
const provider = createIndexedDBDownloadProvider(workspace.id, doc, {
awareness: workspace.blockSuiteWorkspace.awarenessStore.awareness,
}) as LocalIndexedDBDownloadProvider;
provider.sync();
await provider.whenReady;
const newDoc = migrateToSubdoc(doc);
if (doc === newDoc) {
console.log('doc not changed');
rootStore.set(rootWorkspacesMetadataAtom, metadata =>
metadata.map(newMeta =>
newMeta.id === oldMeta.id
? {
...newMeta,
version: WorkspaceVersion.SubDoc,
}
: newMeta
)
);
return;
}
const newWorkspace = upgradeV1ToV2(workspace);
if (environment.isBrowser) {
const value = localStorage.getItem('jotai-workspaces');
if (value) {
try {
const metadata = JSON.parse(value) as RootWorkspaceMetadata[];
const promises: Promise<void>[] = [];
metadata.forEach(oldMeta => {
if (!('version' in oldMeta)) {
const adapter = WorkspaceAdapters[oldMeta.flavour];
assertExists(adapter);
const upgrade = async () => {
const workspace = await adapter.CRUD.get(oldMeta.id);
if (!workspace) {
console.warn('cannot find workspace', oldMeta.id);
return;
}
if (workspace.flavour !== WorkspaceFlavour.LOCAL) {
console.warn('not supported');
return;
}
const doc = workspace.blockSuiteWorkspace.doc;
const provider = createIndexedDBDownloadProvider(
workspace.id,
doc,
{
awareness:
workspace.blockSuiteWorkspace.awarenessStore.awareness,
}
) as LocalIndexedDBDownloadProvider;
provider.sync();
await provider.whenReady;
const newDoc = migrateToSubdoc(doc);
if (doc === newDoc) {
console.log('doc not changed');
return;
}
const newWorkspace = upgradeV1ToV2(workspace);
const newId = await adapter.CRUD.create(
newWorkspace.blockSuiteWorkspace
);
const newId = await adapter.CRUD.create(
newWorkspace.blockSuiteWorkspace
);
await adapter.CRUD.delete(workspace as any);
await migrateLocalBlobStorage(workspace.id, newId);
rootStore.set(rootWorkspacesMetadataAtom, metadata => [
...metadata
.map(newMeta => (newMeta.id === oldMeta.id ? null : newMeta))
.filter((meta): meta is RootWorkspaceMetadata => !!meta),
{
id: newId,
flavour: oldMeta.flavour,
version: WorkspaceVersion.SubDoc,
},
]);
};
await adapter.CRUD.delete(workspace as any);
await migrateLocalBlobStorage(workspace.id, newId);
};
// create a new workspace and push it to metadata
upgrade().catch(console.error);
// create a new workspace and push it to metadata
promises.push(upgrade());
}
});
Promise.all(promises)
.then(() => {
console.log('migration done');
})
.catch(() => {
console.error('migration failed');
})
.finally(() => {
window.dispatchEvent(new CustomEvent('migration-done'));
});
} catch (e) {
console.error('error when migrating data', e);
}
});
});
}
}
declare global {
// global Events
interface WindowEventMap {
'migration-done': CustomEvent;
}
}

View File

@@ -1,6 +1,4 @@
import {
SettingRow,
} from '@affine/component/setting-components';
import { SettingRow } from '@affine/component/setting-components';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { ArrowRightSmallIcon } from '@blocksuite/icons';
import { type FC, useState } from 'react';
@@ -33,6 +31,7 @@ export const DeleteLeaveWorkspace: FC<{
onClick={() => {
setShowDelete(true);
}}
testId="delete-workspace-button"
>
<ArrowRightSmallIcon />
</SettingRow>

View File

@@ -137,9 +137,7 @@ const PublishPanelLocal: FC<PublishPanelLocalProps> = ({
}}
style={{ marginTop: '12px' }}
>
{runtimeConfig.enableLegacyCloud
? t['Enable AFFiNE Cloud']()
: 'Disable AFFiNE Cloud'}
{t['Enable AFFiNE Cloud']()}
</Button>
</SettingRow>
<FakePublishPanelAffine workspace={workspace} />

View File

@@ -1,2 +0,0 @@
// Some settings are not implemented yet, but need to show in the setting modal when boss is watching.
export const IS_EXHIBITION = true;

View File

@@ -8,7 +8,6 @@ import { ArrowRightSmallIcon, OpenInNewIcon } from '@blocksuite/icons';
import { useCallback } from 'react';
import { type AppSetting, useAppSetting } from '../../../../../atoms/settings';
import { IS_EXHIBITION } from '../../config';
import { communityItem, communityWrapper, link } from './style.css';
export const AboutAffine = () => {
@@ -22,8 +21,12 @@ export const AboutAffine = () => {
);
return (
<>
<SettingHeader title={t['About AFFiNE']()} subtitle={t['None yet']()} />
{IS_EXHIBITION && environment.isDesktop ? (
<SettingHeader
title={t['About AFFiNE']()}
subtitle={t['None yet']()}
data-testid="about-title"
/>
{runtimeConfig.enableNewSettingUnstableApi && environment.isDesktop ? (
<SettingWrapper title={t['Version']()}>
<SettingRow
name={t['Check for updates']()}
@@ -57,7 +60,7 @@ export const AboutAffine = () => {
style={{ cursor: 'pointer' }}
onClick={() => {
window.open(
'https://github.com/toeverything/AFFiNE/releases',
'https://affine.pro/blog/whats-new-affine-0630',
'_blank'
);
}}

View File

@@ -12,7 +12,6 @@ import {
windowFrameStyleOptions,
} from '../../../../../atoms/settings';
import { LanguageMenu } from '../../../language-menu';
import { IS_EXHIBITION } from '../../config';
import { DateFormatSetting } from './date-format-setting';
import { settingWrapper } from './style.css';
@@ -31,9 +30,15 @@ export const ThemeSettings = () => {
[setTheme]
)}
>
<RadioButton value="system">{t['system']()}</RadioButton>
<RadioButton value="light">{t['light']()}</RadioButton>
<RadioButton value="dark">{t['dark']()}</RadioButton>
<RadioButton value="system" data-testid="system-theme-trigger">
{t['system']()}
</RadioButton>
<RadioButton value="light" data-testid="light-theme-trigger">
{t['light']()}
</RadioButton>
<RadioButton value="dark" data-testid="dark-theme-trigger">
{t['dark']()}
</RadioButton>
</RadioButtonGroup>
);
};
@@ -70,7 +75,7 @@ export const AppearanceSettings = () => {
<LanguageMenu />
</div>
</SettingRow>
{IS_EXHIBITION && environment.isDesktop ? (
{runtimeConfig.enableNewSettingUnstableApi && environment.isDesktop ? (
<SettingRow
name={t['Client Border Style']()}
desc={t['Customize the appearance of the client.']()}
@@ -87,11 +92,12 @@ export const AppearanceSettings = () => {
desc={t['Maximum display of content within a page.']()}
>
<Switch
data-testid="full-width-layout-trigger"
checked={appSettings.fullWidthLayout}
onChange={checked => changeSwitch('fullWidthLayout', checked)}
/>
</SettingRow>
{IS_EXHIBITION && environment.isDesktop ? (
{runtimeConfig.enableNewSettingUnstableApi && environment.isDesktop ? (
<SettingRow
name={t['Window frame style']()}
desc={t['Customize appearance of Windows Client.']()}
@@ -114,7 +120,7 @@ export const AppearanceSettings = () => {
</SettingRow>
) : null}
</SettingWrapper>
{IS_EXHIBITION ? (
{runtimeConfig.enableNewSettingUnstableApi ? (
<SettingWrapper title={t['Date']()}>
<SettingRow
name={t['Date Format']()}
@@ -154,9 +160,9 @@ export const AppearanceSettings = () => {
desc={t['None yet']()}
>
<Switch
checked={appSettings.disableBlurBackground}
checked={!appSettings.disableBlurBackground}
onChange={checked =>
changeSwitch('disableBlurBackground', checked)
changeSwitch('disableBlurBackground', !checked)
}
/>
</SettingRow>

View File

@@ -1,3 +1,4 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import {
AppearanceIcon,
InformationIcon,
@@ -15,25 +16,32 @@ export type GeneralSettingList = {
key: GeneralSettingKeys;
title: string;
icon: FC<SVGProps<SVGSVGElement>>;
testId: string;
}[];
export const generalSettingList: GeneralSettingList = [
{
key: 'appearance',
title: 'Appearance',
icon: AppearanceIcon,
},
{
key: 'shortcuts',
title: 'Keyboard Shortcuts',
icon: KeyboardIcon,
},
{
key: 'about',
title: 'About AFFiNE',
icon: InformationIcon,
},
];
export const useGeneralSettingList = (): GeneralSettingList => {
const t = useAFFiNEI18N();
return [
{
key: 'appearance',
title: t['com.affine.settings.appearance'](),
icon: AppearanceIcon,
testId: 'appearance-panel-trigger',
},
{
key: 'shortcuts',
title: t['Keyboard Shortcuts'](),
icon: KeyboardIcon,
testId: 'shortcuts-panel-trigger',
},
{
key: 'about',
title: t['About AFFiNE'](),
icon: InformationIcon,
testId: 'about-panel-trigger',
},
];
};
export const GeneralSetting = ({
generalKey,

View File

@@ -23,6 +23,7 @@ export const Shortcuts = () => {
<SettingHeader
title={t['Keyboard Shortcuts']()}
subtitle={t['Check Keyboard Shortcuts quickly']()}
data-testid="keyboard-shortcuts-title"
/>
<SettingWrapper title={t['General']()}>
{Object.entries(generalShortcuts).map(([title, shortcuts]) => {

View File

@@ -18,7 +18,7 @@ import { AccountSetting } from './account-setting';
import {
GeneralSetting,
type GeneralSettingKeys,
generalSettingList,
useGeneralSettingList,
} from './general-setting';
import { SettingSidebar } from './setting-sidebar';
import { settingContent } from './style.css';
@@ -32,7 +32,7 @@ export const SettingModal: React.FC<SettingModalProps> = ({
const t = useAFFiNEI18N();
const workspaces = useWorkspaces();
const [currentWorkspace] = useCurrentWorkspace();
const generalSettingList = useGeneralSettingList();
const workspaceList = useMemo(() => {
return workspaces.filter(
({ flavour }) => flavour !== WorkspaceFlavour.PUBLIC

View File

@@ -4,9 +4,9 @@ import type {
AffineLegacyCloudWorkspace,
LocalWorkspace,
} from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
import clsx from 'clsx';
import React from 'react';
import type {
GeneralSettingKeys,
@@ -44,12 +44,13 @@ export const SettingSidebar = ({
selectedGeneralKey: string | null;
onAccountSettingClick: () => void;
}) => {
const t = useAFFiNEI18N();
return (
<div className={settingSlideBar}>
<div className={sidebarTitle}>Settings</div>
<div className={sidebarSubtitle}>General</div>
<div className={settingSlideBar} data-testid="settings-sidebar">
<div className={sidebarTitle}>{t['Settings']()}</div>
<div className={sidebarSubtitle}>{t['General']()}</div>
<div className={sidebarItemsWrapper}>
{generalSettingList.map(({ title, icon, key }) => {
{generalSettingList.map(({ title, icon, key, testId }) => {
return (
<div
className={clsx(sidebarSelectItem, {
@@ -60,6 +61,7 @@ export const SettingSidebar = ({
onClick={() => {
onGeneralSettingClick(key);
}}
data-testid={testId}
>
{icon({ className: 'icon' })}
<span className="setting-name">{title}</span>
@@ -68,7 +70,9 @@ export const SettingSidebar = ({
})}
</div>
<div className={sidebarSubtitle}>Workspace</div>
<div className={sidebarSubtitle}>
{t['com.affine.settings.workspace']()}
</div>
<div className={clsx(sidebarItemsWrapper, 'scroll')}>
{workspaceList.map(workspace => {
return (

View File

@@ -3,7 +3,8 @@ import { globalStyle, style } from '@vanilla-extract/css';
export const settingContent = style({
flexGrow: '1',
height: '100%',
padding: '40px 0',
padding: '40px 15px 20px',
overflowX: 'auto',
});
globalStyle(`${settingContent} .wrapper`, {

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