Compare commits

..

51 Commits

Author SHA1 Message Date
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
Alex Yang
d0d04ce376 v0.7.0-canary.22 2023-06-29 00:27:17 +08:00
Alex Yang
2250f42d2a ci: fix tag version 2023-06-29 00:26:48 +08:00
Alex Yang
887434fea4 v0.7.0-canary.21 2023-06-29 00:23:06 +08:00
Alex Yang
9b817c4b79 ci: automatically build canary release (#2911) 2023-06-28 15:53:32 +00:00
Alex Yang
ea03bbfb2d ci: add codeql check to merge group (#2909) 2023-06-28 15:07:27 +00:00
Qi
db40cd35c6 feat: migrate workspace setting with new design to setting modal (#2900)
Co-authored-by: Alex Yang <himself65@outlook.com>
2023-06-28 14:45:33 +00:00
Alex Yang
aabac9e921 chore: bump typescript version (#2906) 2023-06-28 12:57:33 +00:00
Alex Yang
0a91c41e0a chore: codesandbox setup (#2907) 2023-06-28 12:32:56 +00:00
DarkSky
d6addc0d0b docs: improve helm ci & document (#2902) 2023-06-28 12:30:02 +00:00
Alex Yang
91d3b76be5 refactor(storybook): move to apps folder (#2901) 2023-06-28 12:29:52 +00:00
Alex Yang
3eed009270 feat: add rule 'sonarjs/no-identical-functions' (#2905) 2023-06-28 12:29:12 +00:00
Alex Yang
bc14d54cfa chore: update pre-commit hook (#2904) 2023-06-28 11:24:37 +00:00
Alex Yang
5496969e58 refactor: environment setup (#2898)
Co-authored-by: Simon He <57086651+Simon-He95@users.noreply.github.com>
2023-06-28 11:19:19 +00:00
Alex Yang
80c2a78273 fix(web): bypass adapter list error (#2903) 2023-06-28 11:06:13 +00:00
Alex Yang
92f378aefc test(server): watch mode (#2893) 2023-06-28 10:00:06 +00:00
Alex Yang
877ceee698 ci: enable merge group (#2899) 2023-06-28 09:56:02 +00:00
Alex Yang
7960b6a22e feat: update migration test page (#2871) 2023-06-28 16:46:08 +08:00
Alex Yang
fa45d8a718 build: unify build flags (#2891) 2023-06-28 16:45:05 +08:00
Alex Yang
87574c9993 build: fix i18n output (#2896) 2023-06-28 16:40:41 +08:00
Alex Yang
2dd62f7603 v0.7.0-canary.20 2023-06-28 16:03:21 +08:00
Peng Xiao
79b3b1dabc fix: disable sqlite provider (#2888)
Co-authored-by: Alex Yang <himself65@outlook.com>
2023-06-28 15:01:15 +08:00
regischen
fd0aa4a2ee fix: migration props:elements (#2889) 2023-06-28 14:24:14 +08:00
3720
da57fbeadd fix: database migration (#2887) 2023-06-28 13:44:11 +08:00
JimmFly
3f12e4925f style: remove switch button shadow (#2890) 2023-06-28 05:07:33 +00:00
Alex Yang
21cb05a30c build(web): fix debug local (#2886) 2023-06-28 11:43:13 +08:00
Kushagra Singh
7a8ff2c489 docs: update CLA.md (#2884) 2023-06-28 03:12:15 +08:00
Alex Yang
d108434881 fix: preloading page (#2876)
Co-authored-by: Mirone <Saul-Mirone@outlook.com>
2023-06-28 03:11:14 +08:00
Alex Yang
20fd9b6574 feat: upload 0.7.0-canary.18 static output (#2883) 2023-06-28 03:10:08 +08:00
Alex Yang
26ac56e163 test: remove deprecated test (#2880) 2023-06-28 00:53:04 +08:00
Alex Yang
78b74d5b15 feat(docs): update document (#2877) 2023-06-28 00:52:42 +08:00
DarkSky
1556167262 feat: add helm releaser (#2875) 2023-06-28 00:16:40 +08:00
273 changed files with 16263 additions and 9116 deletions

20
.codesandbox/task.json Normal file
View File

@@ -0,0 +1,20 @@
{
"$schema": "https://codesandbox.io/schemas/tasks.json",
"setupTasks": [
{
"name": "Install Dependencies",
"command": "yarn install"
}
],
"tasks": {
"start-web": {
"name": "Start Web",
"command": "yarn nx dev @affine/web --port 8080",
"runAtStart": true,
"preview": {
"port": 8080
}
}
}
}

View File

@@ -9,6 +9,7 @@
"server",
"web",
"docs",
"storybook",
"component",
"workspace",
"env",
@@ -21,7 +22,7 @@
"templates",
"y-indexeddb",
"debug",
"theme"
"storage"
]
]
}

View File

@@ -41,6 +41,7 @@ const allPackages = [
'apps/web',
'apps/server',
'apps/electron',
'apps/storybook',
'plugins/copilot',
'plugins/bookmark-block',
];
@@ -64,6 +65,7 @@ const config = {
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'plugin:@typescript-eslint/recommended',
'prettier',
],
parser: '@typescript-eslint/parser',
parserOptions: {
@@ -157,6 +159,7 @@ const config = {
'sonarjs/no-duplicated-branches': 'error',
'sonarjs/no-collection-size-mischeck': 'error',
'sonarjs/no-useless-catch': 'error',
'sonarjs/no-identical-functions': 'error',
},
overrides: [
{

1
.github/CLA.md vendored
View File

@@ -59,3 +59,4 @@ Example:
- 三咲智子 Kevin Deng, @sxzz, 2023/04/21
- Moeyua, @moeyua, 2023/04/22
- Shishu, @shishudesu, 2023/05/19
- Kushagra Singh, @kush002, 2023/06/28

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

View File

@@ -82,7 +82,7 @@ runs:
id: playwright-version
if: ${{ inputs.playwright-install == 'true' }}
shell: bash
run: echo "version=$(yarn why --json @playwright/test | grep -h 'workspace:.' | jq --raw-output '.children[].locator' | sed -e 's/@playwright\/test@.*://')" >> $GITHUB_OUTPUT
run: echo "version=$(yarn why --json @playwright/test | grep -h 'workspace:.' | jq --raw-output '.children[].locator' | sed -e 's/@playwright\/test@.*://' | head -n 1)" >> $GITHUB_OUTPUT
# Attempt to restore the correct Playwright browser binaries based on the
# currently installed version of Playwright (The browser binary versions

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

1
.github/helm/affine-cloud/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
charts/

View File

@@ -20,4 +20,4 @@
.project
.idea/
*.tmproj
.vscode/
.vscode/

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

30
.github/helm/affine-cloud/readme.md vendored Normal file
View File

@@ -0,0 +1,30 @@
# Helm Chart Configuration
The following table lists the configurable parameters of this Helm chart and their default values.
## AFFiNE Cloud Server parameters
| Parameter | Description | Default |
| ------------------------------ | -------------------------------------------------- | ------------------ |
| `affineCloud.tag` | The Docker tag of the AffineCloud image to be used | `'nightly-latest'` |
| `affineCloud.resources.cpu` | The CPU resources allocated for AffineCloud | `'250m'` |
| `affineCloud.resources.memory` | The memory resources allocated for AffineCloud | `'0.5Gi'` |
| `affineCloud.signKey` | The key used to sign the JWT tokens | `'c2VjcmV0'` |
| `affineCloud.service.type` | The type of the Kubernetes service | `'ClusterIP'` |
| `affineCloud.service.port` | The port of the Kubernetes service | `'http'` |
| `affineCloud.mail.account` | The email account used to send emails | `''` |
| `affineCloud.mail.password` | The password of the email account | `''` |
## PostgreSQL parameters
| Parameter | Description | Default |
| -------------------------------------------- | ------------------------------------------------------------------------------------- | ------------ |
| `postgresql.auth.username` | Username for the PostgreSQL database | `'affine'` |
| `postgresql.auth.password` | Password for the PostgreSQL database. Please change this for production environments. | `'password'` |
| `postgresql.auth.database` | The name of the default database that will be created on image startup | `'affine'` |
| `postgresql.primary.resources.limits.cpu` | The CPU resources allocated for the PostgreSQL primary node | `'500m'` |
| `postgresql.primary.resources.limits.memory` | The memory resources allocated for the PostgreSQL primary node | `'0.5Gi'` |
For more postgres parameters, please refer to: https://artifacthub.io/packages/helm/bitnami/postgresql
Please note that for the `postgresql.auth.password`, you should provide your own password for production environments. The default value is provided only for demonstration purposes.

View File

@@ -0,0 +1,51 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "affine-cloud.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-cloud.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-cloud.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "affine-cloud.labels" -}}
helm.sh/chart: {{ include "affine-cloud.chart" . }}
{{ include "affine-cloud.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "affine-cloud.selectorLabels" -}}
app.kubernetes.io/name: {{ include "affine-cloud.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}

View File

@@ -1,12 +1,14 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: affine-cloud
name: "{{ include "affine-cloud.fullname" . }}"
labels:
{{- include "affine-cloud.labels" . | nindent 4 }}
spec:
replicas: 1
selector:
matchLabels:
app: affine-cloud
{{- include "affine-cloud.selectorLabels" . | nindent 6 }}
strategy:
type: RollingUpdate
rollingUpdate:
@@ -14,7 +16,7 @@ spec:
template:
metadata:
labels:
app: affine-cloud
{{- include "affine-cloud.selectorLabels" . | nindent 8 }}
spec:
restartPolicy: Always
containers:
@@ -28,9 +30,9 @@ 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: "{{ default "postgresql://$(PG_USER):$(PG_PASS)@$(PG_HOST)/$(PG_DATABASE)" .Values.affineCloud.databaseUrl }}"
value: "{{ .Values.affineCloud.databaseUrl | default "postgresql://$(PG_USER):$(PG_PASS)@$(PG_HOST)/$(PG_DATABASE)" }}"
envFrom:
- secretRef:
name: affine-cloud-secret

View File

@@ -4,7 +4,6 @@ metadata:
name: affine-cloud-secret
type: Opaque
data:
# only for demo, please modify it at prod env
SIGN_KEY: TUFtdFdzQTJhdGJuem01TA==
# MAIL_ACCOUNT: XXXX
# MAIL_PASSWORD: XXXX
SIGN_KEY: "{{ .Values.affineCloud.signKey }}"
MAIL_ACCOUNT: "{{ .Values.affineCloud.mail.account }}"
MAIL_PASSWORD: "{{ .Values.affineCloud.mail.password }}"

View File

@@ -1,13 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: affine-cloud
name: "{{ include "affine-cloud.fullname" . }}"
labels:
{{- include "affine-cloud.labels" . | nindent 4 }}
spec:
type: ClusterIP
selector:
app: affine-cloud
type: "{{ .Values.affineCloud.service.type }}"
ports:
- name: affine-cloud
- name: http
protocol: TCP
port: 3000
port: {{ .Values.affineCloud.service.port }}
targetPort: 3000
selector:
{{- include "affine-cloud.selectorLabels" . | nindent 4 }}

View File

@@ -1,13 +1,22 @@
affineCloud:
tag: 'nightly-latest'
tag: 'canary-5e0d5e0cc65ea46f326fdde12658bfac59b38c9f-0949'
# databaseUrl: 'postgresql://affine:password@affine-cloud-postgresql:5432/affine'
signKey: TUFtdFdzQTJhdGJuem01TA==
mail:
account: ''
password: ''
service:
type: ClusterIP
port: 80
resources:
cpu: '250m'
memory: 0.5Gi
postgresql:
fullnameOverride: tcp-postgresql
auth:
# only for demo, please modify it at prod env
username: affine
password: XJYMLnuBJS27a2du
password: password
database: affine
primary:
initdb:

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.

2
.github/helm/releaser.yaml vendored Normal file
View File

@@ -0,0 +1,2 @@
owner: toeverything
git-repo: helm-charts

View File

@@ -13,6 +13,7 @@ on:
- '!.github/actions/build-rust/action.yml'
- '!.github/actions/setup-node/action.yml'
pull_request:
merge_group:
branches:
- master
- v[0-9]+.[0-9]+.x-staging
@@ -26,6 +27,7 @@ on:
env:
DEBUG: napi:*
BUILD_TYPE: canary
APP_NAME: affine
COVERAGE: true
MACOSX_DEPLOYMENT_TARGET: '10.13'
@@ -41,12 +43,16 @@ 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
build-docs:
name: Build Docs
@@ -77,21 +83,13 @@ jobs:
uses: actions/upload-artifact@v3
with:
name: storybook
path: ./packages/storybook/storybook-static
path: ./apps/storybook/storybook-static
if-no-files-found: error
build-web:
name: Build @affine/web
runs-on: ubuntu-latest
environment: development
env:
API_SERVER_PROFILE: local
ENABLE_DEBUG_PAGE: 1
ENABLE_PLUGIN: true
ENABLE_ALL_PAGE_FILTER: true
ENABLE_LEGACY_PROVIDER: true
ENABLE_PRELOADING: false
ENABLE_NEW_SETTING_MODAL: false
steps:
- uses: actions/checkout@v3
@@ -110,14 +108,6 @@ jobs:
name: Build @affine/web (Desktop)
runs-on: ubuntu-latest
environment: development
env:
API_SERVER_PROFILE: affine
ENABLE_DEBUG_PAGE: 1
ENABLE_PLUGIN: true
ENABLE_ALL_PAGE_FILTER: true
ENABLE_LEGACY_PROVIDER: false
ENABLE_PRELOADING: false
ENABLE_NEW_SETTING_MODAL: false
steps:
- uses: actions/checkout@v3
@@ -173,9 +163,14 @@ 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 server test coverage results
uses: codecov/codecov-action@v3
@@ -184,7 +179,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
@@ -201,9 +196,9 @@ jobs:
uses: actions/download-artifact@v3
with:
name: storybook
path: ./packages/storybook/storybook-static
path: ./apps/storybook/storybook-static
- name: Run storybook tests
working-directory: ./packages/storybook
working-directory: ./apps/storybook
run: |
yarn exec concurrently -k -s first -n "SB,TEST" -c "magenta,blue" "yarn exec serve ./storybook-static -l 6006" "yarn exec wait-on tcp:6006 && yarn test"
@@ -242,7 +237,7 @@ jobs:
uses: actions/download-artifact@v3
with:
name: storybook
path: ./packages/storybook/storybook-static
path: ./apps/storybook/storybook-static
- name: Wait for Octobase Ready
run: |
@@ -263,7 +258,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() }}
@@ -273,6 +268,45 @@ jobs:
path: ./test-results
if-no-files-found: ignore
e2e-migration-test:
name: E2E Migration Test
runs-on: ubuntu-latest
environment: development
needs: [build-web]
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
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
- name: Run legacy playwright tests
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 }}
@@ -360,7 +394,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() }}
@@ -399,7 +433,7 @@ 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'

View File

@@ -15,6 +15,7 @@ on:
push:
branches: [master]
pull_request:
merge_group:
# The branches below must be a subset of the branches above
branches: [master]

65
.github/workflows/helm-releaser.yml vendored Normal file
View File

@@ -0,0 +1,65 @@
name: Release Charts
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Checkout Helm chart repo
uses: actions/checkout@v3
with:
repository: toeverything/helm-charts
path: .helm-chart-repo
ref: gh-pages
token: ${{ secrets.HELM_RELEASER_TOKEN }}
- name: Install Helm
uses: azure/setup-helm@v3
- name: Install chart releaser
run: |
set -e
arch="$(dpkg --print-architecture)"
curl -s https://api.github.com/repos/helm/chart-releaser/releases/latest \
| yq --indent 0 --no-colors --input-format json --unwrapScalar \
".assets[] | select(.name | test("\""^chart-releaser_.+_linux_${arch}\.tar\.gz$"\"")) | .browser_download_url" \
| xargs curl -SsL \
| tar zxf - -C /usr/local/bin
- name: Package charts
working-directory: .helm-chart-repo
run: |
mkdir -p .cr-index
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
helm dependencies build ../.github/helm/affine-cloud
cr package ../.github/helm/affine-cloud
- name: Package charts
if: github.ref == 'refs/heads/master'
working-directory: .helm-chart-repo
run: |
set -ex
git config --local user.name "$GITHUB_ACTOR"
git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com"
owner=$(cut -d '/' -f 1 <<< '${{ github.repository }}')
repo=helm-charts
git_hash=$(git rev-parse HEAD)
cr upload --commit "$git_hash" \
--git-repo "$repo" --owner "$owner" \
--token '${{ secrets.HELM_RELEASER_TOKEN }}' \
--skip-existing
cr index --git-repo "$repo" --owner "$owner" \
--token '${{ secrets.HELM_RELEASER_TOKEN }}' \
--index-path .cr-index --push

View File

@@ -61,6 +61,7 @@ jobs:
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)

View File

@@ -1,6 +1,9 @@
name: Release Desktop App
on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+-canary.[0-9]+'
workflow_dispatch:
inputs:
version:
@@ -28,14 +31,8 @@ permissions:
contents: write
security-events: write
concurrency:
# The concurrency group contains the workflow name and the branch name for
# pull requests or the commit hash for any other events.
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }}
cancel-in-progress: true
env:
BUILD_TYPE: ${{ github.event.inputs.build-type }}
BUILD_TYPE: ${{ github.event.inputs.build-type || (github.ref_type == 'tag' && contains(github.ref, 'canary') && 'canary') }}
DEBUG: napi:*
APP_NAME: affine
MACOSX_DEPLOYMENT_TARGET: '10.13'
@@ -43,21 +40,31 @@ 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:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: ./.github/actions/setup-node
- name: Get canary version
id: get-canary-version
if: ${{ github.ref_type == 'tag' }}
run: |
TAG_VERSION=${GITHUB_REF#refs/tags/v}
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 "RELEASE_VERSION=$(node -p "require('./apps/electron/package.json').version")" >> $GITHUB_OUTPUT
- name: generate-assets
run: yarn workspace @affine/electron generate-assets
env:
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
API_SERVER_PROFILE: prod
ENABLE_TEST_PROPERTIES: false
ENABLE_BOOKMARK_OPERATION: true
RELEASE_VERSION: ${{ github.event.inputs.version }}
RELEASE_VERSION: ${{ github.event.inputs.version || steps.get-canary-version.outputs.RELEASE_VERSION }}
- name: Upload Artifact (web-static)
uses: actions/upload-artifact@v3
@@ -66,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:
@@ -157,7 +164,7 @@ jobs:
path: builds
release:
needs: make-distribution
needs: [before-make, make-distribution]
runs-on: ubuntu-latest
steps:
@@ -190,16 +197,16 @@ jobs:
cp ./apps/electron/scripts/generate-yml.js .
node generate-yml.js
env:
RELEASE_VERSION: ${{ github.event.inputs.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: Desktop APP ${{ github.event.inputs.version }}
body: 'TODO: Add release notes here'
draft: ${{ github.event.inputs.is-draft }}
prerelease: ${{ github.event.inputs.is-pre-release }}
name: ${{ github.event.inputs.version || needs.before-make.outputs.RELEASE_VERSION }}
body: ''
draft: ${{ github.event.inputs.is-draft || true }}
prerelease: ${{ github.event.inputs.is-pre-release || needs.before-make.outputs.version }}
files: |
./VERSION
./*.zip

View File

@@ -2,7 +2,10 @@
. "$(dirname -- "$0")/_/husky.sh"
# check lockfile is up to date
yarn install
yarn install --mode=update-lockfile
# lint staged files
yarn exec lint-staged
# type check
yarn typecheck

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

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.19",
"version": "0.7.0-canary.24",
"type": "module",
"private": true,
"scripts": {
@@ -10,12 +10,12 @@
},
"dependencies": {
"@affine/component": "workspace:*",
"@blocksuite/block-std": "0.0.0-20230626181619-90507cfc-nightly",
"@blocksuite/blocks": "0.0.0-20230626181619-90507cfc-nightly",
"@blocksuite/editor": "0.0.0-20230626181619-90507cfc-nightly",
"@blocksuite/global": "0.0.0-20230626181619-90507cfc-nightly",
"@blocksuite/lit": "0.0.0-20230626181619-90507cfc-nightly",
"@blocksuite/store": "0.0.0-20230626181619-90507cfc-nightly",
"@blocksuite/block-std": "0.0.0-20230629084521-542de4e8-nightly",
"@blocksuite/blocks": "0.0.0-20230629084521-542de4e8-nightly",
"@blocksuite/editor": "0.0.0-20230629084521-542de4e8-nightly",
"@blocksuite/global": "0.0.0-20230629084521-542de4e8-nightly",
"@blocksuite/lit": "0.0.0-20230629084521-542de4e8-nightly",
"@blocksuite/store": "0.0.0-20230629084521-542de4e8-nightly",
"express": "^4.18.2",
"jotai": "^2.2.1",
"react": "18.3.0-canary-8ec962d82-20230623",
@@ -30,6 +30,6 @@
"@vanilla-extract/vite-plugin": "^3.8.2",
"autoprefixer": "^10.4.14",
"tailwindcss": "^3.3.2",
"typescript": "^5.1.3"
"typescript": "^5.1.5"
}
}

View File

@@ -25,11 +25,11 @@ const AppCreator = (pathname: string) =>
const buffer = [...readFileSync(path)];
return (
<div className="flex flex-col-reverse sm:flex-row">
<div className="flex flex-col-reverse sm:flex-row h-screen">
<nav className="w-full sm:w-64">
<Sidebar />
</nav>
<main className="flex-1 p-6 w-full sm:w-[calc(100%-16rem)]">
<main className="flex-1 p-6 w-full sm:w-[calc(100%-16rem)] overflow-scroll">
<Editor
workspaceId={pathname}
pageId="1"

Binary file not shown.

View File

@@ -1,7 +1,7 @@
{
"name": "@affine/electron",
"private": true,
"version": "0.7.0-canary.19",
"version": "0.7.0-canary.24",
"author": "affine",
"repository": {
"url": "https://github.com/toeverything/AFFiNE",
@@ -29,10 +29,10 @@
"devDependencies": {
"@affine-test/kit": "workspace:*",
"@affine/native": "workspace:*",
"@blocksuite/blocks": "0.0.0-20230626181619-90507cfc-nightly",
"@blocksuite/editor": "0.0.0-20230626181619-90507cfc-nightly",
"@blocksuite/lit": "0.0.0-20230626181619-90507cfc-nightly",
"@blocksuite/store": "0.0.0-20230626181619-90507cfc-nightly",
"@blocksuite/blocks": "0.0.0-20230629084521-542de4e8-nightly",
"@blocksuite/editor": "0.0.0-20230629084521-542de4e8-nightly",
"@blocksuite/lit": "0.0.0-20230629084521-542de4e8-nightly",
"@blocksuite/store": "0.0.0-20230629084521-542de4e8-nightly",
"@electron-forge/cli": "^6.2.1",
"@electron-forge/core": "^6.2.1",
"@electron-forge/core-utils": "^6.2.1",

View File

@@ -5,7 +5,7 @@ import fs from 'fs-extra';
import { test } from './fixture';
test('check workspace has a DB file', async ({ appInfo, workspace }) => {
test.skip('check workspace has a DB file', async ({ appInfo, workspace }) => {
const w = await workspace.current();
const dbPath = path.join(
appInfo.sessionData,
@@ -17,7 +17,7 @@ test('check workspace has a DB file', async ({ appInfo, workspace }) => {
expect(await fs.exists(dbPath)).toBe(true);
});
test('move workspace db file', async ({ page, appInfo, workspace }) => {
test.skip('move workspace db file', async ({ page, appInfo, workspace }) => {
const w = await workspace.current();
const settingButton = page.getByTestId('slider-bar-workspace-setting-button');
// goto settings
@@ -42,7 +42,7 @@ test('move workspace db file', async ({ page, appInfo, workspace }) => {
expect(files.some(f => f.endsWith('.affine'))).toBe(true);
});
test('export then add', async ({ page, appInfo, workspace }) => {
test.skip('export then add', async ({ page, appInfo, workspace }) => {
const w = await workspace.current();
const settingButton = page.getByTestId('slider-bar-workspace-setting-button');
// goto settings

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.19",
"version": "0.7.0-canary.24",
"description": "Affine Node.js server",
"type": "module",
"bin": {
@@ -10,10 +10,12 @@
"scripts": {
"dev": "nodemon ./src/index.ts",
"test": "yarn exec ts-node-esm ./scripts/run-test.ts all",
"test:watch": "yarn exec ts-node-esm ./scripts/run-test.ts all --watch",
"test:coverage": "c8 yarn exec ts-node-esm ./scripts/run-test.ts all",
"postinstall": "prisma generate"
},
"dependencies": {
"@affine/storage": "workspace:*",
"@apollo/server": "^4.7.4",
"@auth/prisma-adapter": "^1.0.0",
"@aws-sdk/client-s3": "^3.359.0",
@@ -49,7 +51,7 @@
"nodemon": "^2.0.22",
"supertest": "^6.3.3",
"ts-node": "^10.9.1",
"typescript": "^5.1.3"
"typescript": "^5.1.5"
},
"nodemonConfig": {
"exec": "node",

View File

@@ -8,10 +8,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 +89,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

@@ -12,7 +12,13 @@ const root = fileURLToPath(new URL('..', import.meta.url));
const testDir = resolve(root, 'src', 'tests');
const files = await readdir(testDir);
const sharedArgs = [...pkg.nodemonConfig.nodeArgs, '--test'];
const watchMode = process.argv.includes('--watch');
const sharedArgs = [
...pkg.nodemonConfig.nodeArgs,
'--test',
watchMode ? '--watch' : '',
];
const env = {
PATH: process.env.PATH,

View File

@@ -1,16 +1,17 @@
/// <reference types="./global.d.ts" />
import { Module } from '@nestjs/common';
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,
],
})

View File

@@ -132,6 +132,13 @@ export interface AFFiNEConfig {
*/
get origin(): string;
/**
* the database config
*/
db: {
url: string;
};
/**
* the apollo driver config
*/

View File

@@ -52,6 +52,9 @@ export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => ({
get baseUrl() {
return `${this.origin}${this.path}`;
},
db: {
url: '',
},
graphql: {
buildSchemaOptions: {
numberScalarMode: 'integer',
@@ -81,3 +84,5 @@ export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => ({
},
},
});
export { registerEnvs } from './env';

View File

@@ -2,14 +2,16 @@ import { set } from 'lodash-es';
import { 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 registerEnvs() {
for (const env in globalThis.AFFiNE.ENV_MAP) {
const config = globalThis.AFFiNE.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(globalThis.AFFiNE, path, process.env[env]);
}
}
}

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

View File

@@ -62,16 +62,4 @@ export class AuthResolver {
ctx.req.user = user;
return user;
}
@Mutation(() => UserType)
async signUp(
@Context() ctx: { req: Request },
@Args('email') email: string,
@Args('password') password: string,
@Args('name') name: string
) {
const user = await this.auth.register(name, email, password);
ctx.req.user = user;
return user;
}
}

View File

@@ -0,0 +1,27 @@
import { Storage } from '@affine/storage';
import { Controller, Get, NotFoundException, Param, Res } from '@nestjs/common';
import type { Response } from 'express';
@Controller('/api/workspaces')
export class WorkspacesController {
constructor(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,3 +1,4 @@
import { Storage } from '@affine/storage';
import { ForbiddenException, NotFoundException } from '@nestjs/common';
import {
Args,
@@ -16,8 +17,11 @@ 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 type { FileUpload } from '../../types';
import { Auth, CurrentUser } from '../auth';
import { UserType } from '../users/resolver';
import { PermissionService } from './permission';
@@ -55,7 +59,8 @@ export class UpdateWorkspaceInput extends PickType(
export class WorkspaceResolver {
constructor(
private readonly prisma: PrismaService,
private readonly permissionProvider: PermissionService
private readonly permissionProvider: PermissionService,
private readonly storage: Storage
) {}
@ResolveField(() => Permission, {
@@ -174,8 +179,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 +213,10 @@ export class WorkspaceResolver {
},
},
});
await this.storage.createWorkspace(workspace.id, buffer);
return workspace;
}
@Mutation(() => WorkspaceType, {
@@ -221,8 +247,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 +316,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

@@ -1,6 +1,12 @@
import 'reflect-metadata';
import 'dotenv/config';
import { getDefaultAFFiNEConfig } from './config/default';
import { getDefaultAFFiNEConfig, registerEnvs } from './config/default';
globalThis.AFFiNE = getDefaultAFFiNEConfig();
globalThis.AFFiNE.ENV_MAP = {
DATABASE_URL: 'db.url',
};
registerEnvs();

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,23 @@
import { Storage } from '@affine/storage';
import { type DynamicModule, type FactoryProvider } from '@nestjs/common';
import { Config } from '../config';
export class StorageModule {
static forRoot(): DynamicModule {
const storageProvider: FactoryProvider = {
provide: Storage,
useFactory: async (config: Config) => {
return Storage.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

@@ -2,9 +2,13 @@ import '@affine/component/theme/global.css';
import '@affine/component/theme/theme.css';
import { LOCALES, createI18n } from '@affine/i18n';
import { ThemeProvider, useTheme } from 'next-themes';
import { ComponentType, useEffect } from 'react';
import { setupGlobal } from '@affine/env/global';
import type { ComponentType } from 'react';
import { useEffect } from 'react';
import { useDarkMode } from 'storybook-dark-mode';
setupGlobal();
export const parameters = {
backgrounds: { disable: true },
actions: { argTypesRegex: '^on[A-Z].*' },

View File

@@ -30,13 +30,13 @@
"wait-on": "^7.0.1"
},
"devDependencies": {
"@blocksuite/block-std": "0.0.0-20230626181619-90507cfc-nightly",
"@blocksuite/blocks": "0.0.0-20230626181619-90507cfc-nightly",
"@blocksuite/editor": "0.0.0-20230626181619-90507cfc-nightly",
"@blocksuite/global": "0.0.0-20230626181619-90507cfc-nightly",
"@blocksuite/block-std": "0.0.0-20230629084521-542de4e8-nightly",
"@blocksuite/blocks": "0.0.0-20230629084521-542de4e8-nightly",
"@blocksuite/editor": "0.0.0-20230629084521-542de4e8-nightly",
"@blocksuite/global": "0.0.0-20230629084521-542de4e8-nightly",
"@blocksuite/icons": "^2.1.21",
"@blocksuite/lit": "0.0.0-20230626181619-90507cfc-nightly",
"@blocksuite/store": "0.0.0-20230626181619-90507cfc-nightly",
"@blocksuite/lit": "0.0.0-20230629084521-542de4e8-nightly",
"@blocksuite/store": "0.0.0-20230629084521-542de4e8-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.19"
"version": "0.7.0-canary.24"
}

View File

@@ -3,7 +3,7 @@ import {
PublicLinkDisableModal,
StyledDisableButton,
} from '@affine/component/share-menu';
import { ShareMenu } from '@affine/component/share-menu/share-menu';
import { ShareMenu } from '@affine/component/share-menu';
import type {
AffineLegacyCloudWorkspace,
LocalWorkspace,

View File

@@ -2,17 +2,22 @@
"extends": "../../tsconfig.json",
"include": ["./src"],
"compilerOptions": {
// Workaround for storybook build
"baseUrl": "../..",
"composite": true,
"noEmit": false,
"outDir": "lib",
"paths": {
"@affine/component": ["../component/src"],
"@affine/component/*": ["../component/src/components/*"]
}
"types": ["react/experimental"]
},
"references": [
{
"path": "../component"
"path": "../../packages/component"
},
{
"path": "../../packages/env"
},
{
"path": "../../packages/workspace"
},
{
"path": "./tsconfig.node.json"

View File

@@ -12,5 +12,10 @@
},
"include": [".storybook/**/*"],
"exclude": ["lib"],
"references": [{ "path": "../i18n" }]
"references": [
{ "path": "../../packages/i18n" },
{
"path": "../../packages/env"
}
]
}

View File

@@ -1,23 +1,5 @@
NEXT_PUBLIC_FIREBASE_API_KEY=
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=
NEXT_PUBLIC_FIREBASE_PROJECT_ID=
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=
NEXT_PUBLIC_FIREBASE_APP_ID=
NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID=
# absolute path to the block suite directory
LOCAL_BLOCK_SUITE=
# see next.config.js
API_SERVER_PROFILE=
# save workspace to idb
ENABLE_IDB_PROVIDER=1
PREFETCH_WORKSPACE=1
ENABLE_BC_PROVIDER=1
EXPOSE_INTERNAL=1
ENABLE_DEBUG_PAGE=
ENABLE_SUBPAGE=
ENABLE_CHANGELOG=1
ENABLE_LEGACY_PROVIDER=true
# Sentry
SENTRY_AUTH_TOKEN=

View File

@@ -180,6 +180,7 @@ const baseDir = process.env.LOCAL_BLOCK_SUITE ?? '/';
const withDebugLocal = debugLocal(
{
'@blocksuite/editor': path.resolve(baseDir, 'packages', 'editor'),
'@blocksuite/block-std': path.resolve(baseDir, 'packages', 'block-std'),
'@blocksuite/blocks/models': path.resolve(
baseDir,
'packages',
@@ -203,6 +204,14 @@ const withDebugLocal = debugLocal(
),
'@blocksuite/blocks': path.resolve(baseDir, 'packages', 'blocks'),
'@blocksuite/store': path.resolve(baseDir, 'packages', 'store'),
'@blocksuite/store/providers/broadcast-channel': path.resolve(
baseDir,
'packages',
'store',
'src',
'providers',
'broadcast-channel'
),
},
{
enable: enableDebugLocal,

View File

@@ -1,7 +1,7 @@
{
"name": "@affine/web",
"private": true,
"version": "0.7.0-canary.19",
"version": "0.7.0-canary.24",
"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-20230626181619-90507cfc-nightly",
"@blocksuite/blocks": "0.0.0-20230626181619-90507cfc-nightly",
"@blocksuite/editor": "0.0.0-20230626181619-90507cfc-nightly",
"@blocksuite/global": "0.0.0-20230626181619-90507cfc-nightly",
"@blocksuite/block-std": "0.0.0-20230629084521-542de4e8-nightly",
"@blocksuite/blocks": "0.0.0-20230629084521-542de4e8-nightly",
"@blocksuite/editor": "0.0.0-20230629084521-542de4e8-nightly",
"@blocksuite/global": "0.0.0-20230629084521-542de4e8-nightly",
"@blocksuite/icons": "^2.1.21",
"@blocksuite/lit": "0.0.0-20230626181619-90507cfc-nightly",
"@blocksuite/store": "0.0.0-20230626181619-90507cfc-nightly",
"@blocksuite/lit": "0.0.0-20230629084521-542de4e8-nightly",
"@blocksuite/store": "0.0.0-20230629084521-542de4e8-nightly",
"@dnd-kit/core": "^6.0.8",
"@dnd-kit/sortable": "^7.0.2",
"@emotion/cache": "^11.11.0",
@@ -79,7 +79,7 @@
"raw-loader": "^4.0.2",
"redux": "^4.2.1",
"swc-plugin-coverage-instrument": "^0.0.18",
"typescript": "^5.1.3",
"typescript": "^5.1.5",
"webpack": "^5.88.0"
},
"stableVersion": "0.0.0"

View File

@@ -11,40 +11,96 @@ export const blockSuiteFeatureFlags = {
enable_drag_handle: true,
enable_surface: true,
enable_linked_page: true,
enable_bookmark_operation: process.env.ENABLE_BOOKMARK_OPERATION === 'true',
enable_bookmark_operation: false,
};
/**
* @type {Record<string, import('@affine/env').BuildFlags>}
*/
const buildPreset = {
stable: {
enableAllPageSaving: false,
enablePlugin: false,
enableTestProperties: false,
enableBroadcastChannelProvider: true,
enableDebugPage: true,
enableLegacyCloud: false,
changelogUrl: 'https://affine.pro/blog/whats-new-affine-0630',
enablePreloading: true,
enableNewSettingModal: false,
enableNewSettingUnstableApi: false,
enableSQLiteProvider: false,
},
beta: {},
internal: {},
// canary will be aggressive and enable all features
canary: {
enableAllPageSaving: true,
enablePlugin: true,
enableTestProperties: true,
enableBroadcastChannelProvider: true,
enableDebugPage: true,
enableLegacyCloud: false,
changelogUrl: 'https://affine.pro/blog/whats-new-affine-0630',
enablePreloading: true,
enableNewSettingModal: true,
enableNewSettingUnstableApi: false,
enableSQLiteProvider: false,
},
};
// beta and internal versions are the same as stable
buildPreset.beta = buildPreset.stable;
buildPreset.internal = buildPreset.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'
: currentBuildPreset.enablePlugin,
enableAllPageSaving: process.env.ENABLE_ALL_PAGE_SAVING
? process.env.ENABLE_ALL_PAGE_FILTER === 'true'
: currentBuildPreset.enableAllPageSaving,
enableTestProperties: process.env.ENABLE_TEST_PROPERTIES
? process.env.ENABLE_TEST_PROPERTIES === 'true'
: currentBuildPreset.enableTestProperties,
enableLegacyCloud: process.env.ENABLE_LEGACY_PROVIDER
? process.env.ENABLE_LEGACY_PROVIDER === 'true'
: currentBuildPreset.enableLegacyCloud,
enableBroadcastChannelProvider: process.env.ENABLE_BC_PROVIDER
? process.env.ENABLE_BC_PROVIDER !== 'false'
: currentBuildPreset.enableBroadcastChannelProvider,
changelogUrl: process.env.CHANGELOG_URL ?? currentBuildPreset.changelogUrl,
enablePreloading: process.env.ENABLE_PRELOADING
? process.env.ENABLE_PRELOADING === 'true'
: currentBuildPreset.enablePreloading,
enableNewSettingModal: process.env.ENABLE_NEW_SETTING_MODAL
? process.env.ENABLE_NEW_SETTING_MODAL === 'true'
: currentBuildPreset.enableNewSettingModal,
enableSQLiteProvider: process.env.ENABLE_SQLITE_PROVIDER
? process.env.ENABLE_SQLITE_PROVIDER === 'true'
: currentBuildPreset.enableSQLiteProvider,
enableNewSettingUnstableApi: process.env.ENABLE_NEW_SETTING_UNSTABLE_API
? process.env.ENABLE_NEW_SETTING_UNSTABLE_API === 'true'
: currentBuildPreset.enableNewSettingUnstableApi,
};
/**
* @type {import('@affine/env').BuildFlags}
*/
export const buildFlags = {
enablePlugin: process.env.ENABLE_PLUGIN === 'true',
enableAllPageFilter:
!!process.env.VERCEL ||
(process.env.ENABLE_ALL_PAGE_FILTER
? process.env.ENABLE_ALL_PAGE_FILTER === 'true'
: false),
enableTestProperties: process.env.ENABLE_TEST_PROPERTIES
? process.env.ENABLE_TEST_PROPERTIES === 'true'
: true,
enableLegacyCloud: process.env.ENABLE_LEGACY_PROVIDER
? process.env.ENABLE_LEGACY_PROVIDER === 'true'
: true,
enableBroadcastChannelProvider: Boolean(
process.env.ENABLE_BC_PROVIDER ?? '1'
),
enableDebugPage: Boolean(
process.env.ENABLE_DEBUG_PAGE ?? process.env.NODE_ENV === 'development'
),
changelogUrl:
process.env.CHANGELOG_URL ??
'https://affine.pro/blog/what-is-new-affine-0620',
enablePreloading:
process.env.ENABLE_PRELOADING === undefined
? true
: process.env.ENABLE_PRELOADING === 'true',
enableNewSettingModal:
process.env.ENABLE_NEW_SETTING_MODAL === undefined
? true
: process.env.ENABLE_PRELOADING === 'true',
const buildFlags = {
...currentBuildPreset,
// environment preset will overwrite current build preset
// this environment variable is for debug proposes only
// do not put them into CI
...(process.env.CI ? {} : environmentPreset),
};
export { buildFlags };

View File

@@ -2,14 +2,14 @@
* This file has deprecated because we do not maintain legacy affine cloud,
* please use new affine cloud instead.
*/
import { AFFINE_STORAGE_KEY, config } from '@affine/env';
import { initEmptyPage } from '@affine/env/blocksuite';
import { PageNotFoundError } from '@affine/env/constant';
import { AFFINE_STORAGE_KEY, PageNotFoundError } from '@affine/env/constant';
import type {
AffineDownloadProvider,
AffineLegacyCloudWorkspace,
LocalIndexedDBDownloadProvider,
} from '@affine/env/workspace';
import type { WorkspaceAdapter } from '@affine/env/workspace';
import {
LoadPriority,
ReleaseType,
@@ -50,7 +50,6 @@ import {
WorkspaceHeader,
WorkspaceSettingDetail,
} from '../shared';
import type { WorkspaceAdapter } from '../type';
import { QueryKey } from './fetcher';
const storage = createJSONStorage(() => localStorage);
@@ -109,7 +108,7 @@ export const AffineAdapter: WorkspaceAdapter<WorkspaceFlavour.AFFINE> = {
loadPriority: LoadPriority.HIGH,
Events: {
'workspace:access': async () => {
if (!config.enableLegacyCloud) {
if (!runtimeConfig.enableLegacyCloud) {
console.warn('Legacy cloud is disabled');
return;
}
@@ -123,11 +122,11 @@ export const AffineAdapter: WorkspaceAdapter<WorkspaceFlavour.AFFINE> = {
}
},
'workspace:revoke': async () => {
if (!config.enableLegacyCloud) {
if (!runtimeConfig.enableLegacyCloud) {
console.warn('Legacy cloud is disabled');
return;
}
rootStore.set(rootWorkspacesMetadataAtom, workspaces =>
await rootStore.set(rootWorkspacesMetadataAtom, workspaces =>
workspaces.filter(
workspace => workspace.flavour !== WorkspaceFlavour.AFFINE
)

View File

@@ -1,12 +1,12 @@
import { DebugLogger } from '@affine/debug';
import { initEmptyPage, initPageWithPreloading } from '@affine/env/blocksuite';
import {
config,
DEFAULT_HELLO_WORLD_PAGE_ID,
DEFAULT_WORKSPACE_NAME,
} from '@affine/env';
import { initEmptyPage, initPageWithPreloading } from '@affine/env/blocksuite';
import { PageNotFoundError } from '@affine/env/constant';
PageNotFoundError,
} from '@affine/env/constant';
import type { LocalIndexedDBDownloadProvider } from '@affine/env/workspace';
import type { WorkspaceAdapter } from '@affine/env/workspace';
import {
LoadPriority,
ReleaseType,
@@ -27,7 +27,6 @@ import {
WorkspaceHeader,
WorkspaceSettingDetail,
} from '../shared';
import type { WorkspaceAdapter } from '../type';
const logger = new DebugLogger('use-create-first-workspace');
@@ -45,7 +44,7 @@ export const LocalAdapter: WorkspaceAdapter<WorkspaceFlavour.LOCAL> = {
const page = blockSuiteWorkspace.createPage({
id: DEFAULT_HELLO_WORLD_PAGE_ID,
});
if (config.enablePreloading) {
if (runtimeConfig.enablePreloading) {
initPageWithPreloading(page).catch(err => {
logger.error('init page with preloading failed', err);
});

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';
import type { AppEvents, WorkspaceUISchema } from '@affine/env/workspace';
import { Unreachable } from '@affine/env/constant';
import type {
AppEvents,
WorkspaceAdapter,
WorkspaceUISchema,
} from '@affine/env/workspace';
import {
LoadPriority,
ReleaseType,
@@ -8,12 +12,15 @@ import {
import { AffineAdapter } from './affine';
import { LocalAdapter } from './local';
import type { WorkspaceAdapter } from './type';
const unimplemented = () => {
throw new Error('Not implemented');
};
const bypassList = async () => {
return [];
};
export const WorkspaceAdapters = {
[WorkspaceFlavour.AFFINE]: AffineAdapter,
[WorkspaceFlavour.LOCAL]: LocalAdapter,
@@ -25,7 +32,7 @@ export const WorkspaceAdapters = {
// todo: implement this
CRUD: {
get: unimplemented,
list: unimplemented,
list: bypassList,
delete: unimplemented,
create: unimplemented,
},
@@ -47,7 +54,7 @@ export const WorkspaceAdapters = {
// todo: implement this
CRUD: {
get: unimplemented,
list: unimplemented,
list: bypassList,
delete: unimplemented,
create: unimplemented,
},

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) {
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,5 +1,4 @@
import type { BlockSuiteFeatureFlags } from '@affine/env';
import { config } from '@affine/env';
import type { BlockSuiteFeatureFlags } from '@affine/env/global';
import type { AffinePublicWorkspace } from '@affine/env/workspace';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { affineApis } from '@affine/workspace/affine/shared';
@@ -25,7 +24,7 @@ function createPublicWorkspace(
blockSuiteWorkspace.doc,
new Uint8Array(binary)
);
Object.entries(config.editorFlags).forEach(([key, value]) => {
Object.entries(runtimeConfig.editorFlags).forEach(([key, value]) => {
blockSuiteWorkspace.awarenessStore.setFlag(
key as keyof BlockSuiteFeatureFlags,
value

View File

@@ -1,7 +1,9 @@
//#region async atoms that to load the real workspace data
import { DebugLogger } from '@affine/debug';
import { config } from '@affine/env';
import type { WorkspaceRegistry } from '@affine/env/workspace';
import type {
WorkspaceAdapter,
WorkspaceRegistry,
} from '@affine/env/workspace';
import { WorkspaceFlavour } from '@affine/env/workspace';
import {
rootCurrentWorkspaceIdAtom,
@@ -24,17 +26,17 @@ 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
)
.filter(workspace =>
!config.enableLegacyCloud
!runtimeConfig.enableLegacyCloud
? 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);
@@ -45,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(
@@ -94,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(
@@ -106,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);
@@ -116,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,9 +1,12 @@
import { migrateToSubdoc } from '@affine/env/blocksuite';
import { config, setupGlobal } from '@affine/env/config';
import type { LocalIndexedDBDownloadProvider } from '@affine/env/workspace';
import { WorkspaceFlavour, WorkspaceVersion } from '@affine/env/workspace';
import { 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,
@@ -16,7 +19,19 @@ import { WorkspaceAdapters } from '../adapters/workspace';
setupGlobal();
if (config.enablePlugin && !environment.isServer) {
rootStore.set(
workspaceAdaptersAtom,
WorkspaceAdapters as Record<
WorkspaceFlavour,
WorkspaceAdapter<WorkspaceFlavour>
>
);
if (process.env.NODE_ENV === 'development') {
console.log('Runtime Preset', runtimeConfig);
}
if (runtimeConfig.enablePlugin && !environment.isServer) {
import('@affine/copilot');
}
@@ -47,65 +62,76 @@ if (!environment.isDesktop && !environment.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

@@ -8,7 +8,6 @@ import {
Tooltip,
} from '@affine/component';
import { DebugLogger } from '@affine/debug';
import { config } from '@affine/env';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { HelpIcon } from '@blocksuite/icons';
import { useSetAtom } from 'jotai';
@@ -290,7 +289,11 @@ export const CreateWorkspaceModal = ({
console.error(err);
});
} else if (mode === 'new') {
setStep(environment.isDesktop ? 'set-db-location' : 'name-workspace');
setStep(
environment.isDesktop && runtimeConfig.enableSQLiteProvider
? 'set-db-location'
: 'name-workspace'
);
} else {
setStep(undefined);
}
@@ -302,7 +305,7 @@ export const CreateWorkspaceModal = ({
const onConfirmEnableCloudSyncing = useCallback(
(enableCloudSyncing: boolean) => {
(async function () {
if (!config.enableLegacyCloud && enableCloudSyncing) {
if (!runtimeConfig.enableLegacyCloud && enableCloudSyncing) {
setOpenDisableCloudAlertModal(true);
} else {
let id = addedId;
@@ -342,7 +345,7 @@ export const CreateWorkspaceModal = ({
const onConfirmName = useCallback(
(name: string) => {
setWorkspaceName(name);
if (environment.isDesktop) {
if (environment.isDesktop && runtimeConfig.enableSQLiteProvider) {
setStep('set-syncing-mode');
} else {
// this will be the last step for web for now

View File

@@ -0,0 +1,112 @@
import { Button, Input, Modal, ModalCloseButton } from '@affine/component';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { Trans } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
import { useCallback, useState } from 'react';
import type { AffineOfficialWorkspace } from '../../../../../shared';
import { toast } from '../../../../../utils';
import {
StyledButtonContent,
StyledInputContent,
StyledModalHeader,
StyledModalWrapper,
StyledTextContent,
StyledWorkspaceName,
} from './style';
interface WorkspaceDeleteProps {
open: boolean;
onClose: () => void;
workspace: AffineOfficialWorkspace;
onDeleteWorkspace: () => Promise<void>;
}
export const WorkspaceDeleteModal = ({
open,
onClose,
workspace,
onDeleteWorkspace,
}: WorkspaceDeleteProps) => {
const [workspaceName] = useBlockSuiteWorkspaceName(
workspace.blockSuiteWorkspace ?? null
);
const [deleteStr, setDeleteStr] = useState<string>('');
const allowDelete = deleteStr === workspaceName;
const t = useAFFiNEI18N();
const handleDelete = useCallback(() => {
onDeleteWorkspace()
.then(() => {
toast(t['Successfully deleted'](), {
portal: document.body,
});
})
.catch(() => {
// ignore error
});
}, [onDeleteWorkspace, t]);
return (
<Modal open={open} onClose={onClose}>
<StyledModalWrapper>
<ModalCloseButton onClick={onClose} />
<StyledModalHeader>{t['Delete Workspace']()}?</StyledModalHeader>
{workspace.flavour === WorkspaceFlavour.LOCAL ? (
<StyledTextContent>
<Trans i18nKey="Delete Workspace Description">
Deleting (
<StyledWorkspaceName>
{{ workspace: workspaceName } as any}
</StyledWorkspaceName>
) cannot be undone, please proceed with caution. All contents will
be lost.
</Trans>
</StyledTextContent>
) : (
<StyledTextContent>
<Trans i18nKey="Delete Workspace Description2">
Deleting (
<StyledWorkspaceName>
{{ workspace: workspaceName } as any}
</StyledWorkspaceName>
) will delete both local and cloud data, this operation cannot be
undone, please proceed with caution.
</Trans>
</StyledTextContent>
)}
<StyledInputContent>
<Input
ref={ref => {
if (ref) {
setTimeout(() => ref.focus(), 0);
}
}}
onChange={setDeleteStr}
data-testid="delete-workspace-input"
placeholder={t['Placeholder of delete workspace']()}
value={deleteStr}
width={315}
height={42}
/>
</StyledInputContent>
<StyledButtonContent>
<Button shape="circle" onClick={onClose}>
{t['Cancel']()}
</Button>
<Button
data-testid="delete-workspace-confirm-button"
disabled={!allowDelete}
onClick={handleDelete}
type="danger"
shape="circle"
style={{ marginLeft: '24px' }}
>
{t['Delete']()}
</Button>
</StyledButtonContent>
</StyledModalWrapper>
</Modal>
);
};

View File

@@ -0,0 +1,76 @@
import { styled } from '@affine/component';
export const StyledModalWrapper = styled('div')(() => {
return {
position: 'relative',
padding: '0px',
width: '560px',
background: 'var(--affine-white)',
borderRadius: '12px',
// height: '312px',
};
});
export const StyledModalHeader = styled('div')(() => {
return {
margin: '44px 0px 12px 0px',
width: '560px',
fontWeight: '600',
fontSize: '20px;',
textAlign: 'center',
};
});
// export const StyledModalContent = styled('div')(({ theme }) => {});
export const StyledTextContent = styled('div')(() => {
return {
margin: 'auto',
width: '425px',
fontFamily: 'Avenir Next',
fontStyle: 'normal',
fontWeight: '400',
fontSize: '18px',
lineHeight: '26px',
textAlign: 'left',
};
});
export const StyledInputContent = styled('div')(() => {
return {
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
margin: '24px 0',
fontSize: 'var(--affine-font-base)',
};
});
export const StyledButtonContent = styled('div')(() => {
return {
marginBottom: '42px',
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
};
});
export const StyledWorkspaceName = styled('span')(() => {
return {
fontWeight: '600',
};
});
// export const StyledCancelButton = styled(Button)(({ theme }) => {
// return {
// width: '100px',
// justifyContent: 'center',
// };
// });
// export const StyledDeleteButton = styled(Button)(({ theme }) => {
// return {
// width: '100px',
// justifyContent: 'center',
// };
// });

View File

@@ -0,0 +1,56 @@
import { SettingRow } from '@affine/component/setting-components';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { ArrowRightSmallIcon } from '@blocksuite/icons';
import { type FC, useState } from 'react';
import { useIsWorkspaceOwner } from '../../../../hooks/affine/use-is-workspace-owner';
import type { AffineOfficialWorkspace } from '../../../../shared';
import type { WorkspaceSettingDetailProps } from '../index';
import { WorkspaceDeleteModal } from './delete';
import { WorkspaceLeave } from './leave';
export const DeleteLeaveWorkspace: FC<{
workspace: AffineOfficialWorkspace;
onDeleteWorkspace: WorkspaceSettingDetailProps['onDeleteWorkspace'];
}> = ({ workspace, onDeleteWorkspace }) => {
const t = useAFFiNEI18N();
const isOwner = useIsWorkspaceOwner(workspace);
const [showDelete, setShowDelete] = useState(false);
const [showLeave, setShowLeave] = useState(false);
return (
<>
<SettingRow
name={
<span style={{ color: 'var(--affine-error-color)' }}>
{isOwner ? t['Delete Workspace']() : t['Leave Workspace']()}
</span>
}
desc={t['None yet']()}
style={{ cursor: 'pointer' }}
onClick={() => {
setShowDelete(true);
}}
>
<ArrowRightSmallIcon />
</SettingRow>
{isOwner ? (
<WorkspaceDeleteModal
onDeleteWorkspace={onDeleteWorkspace}
open={showDelete}
onClose={() => {
setShowDelete(false);
}}
workspace={workspace}
/>
) : (
<WorkspaceLeave
open={showLeave}
onClose={() => {
setShowLeave(false);
}}
/>
)}
</>
);
};

View File

@@ -0,0 +1,50 @@
import { Modal } from '@affine/component';
import { ModalCloseButton } from '@affine/component';
import { Button } from '@affine/component';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import {
StyledButtonContent,
StyledModalHeader,
StyledModalWrapper,
StyledTextContent,
} from './style';
interface WorkspaceDeleteProps {
open: boolean;
onClose: () => void;
}
export const WorkspaceLeave = ({ open, onClose }: WorkspaceDeleteProps) => {
// const { leaveWorkSpace } = useWorkspaceHelper();
const t = useAFFiNEI18N();
const handleLeave = async () => {
// await leaveWorkSpace();
onClose();
};
return (
<Modal open={open} onClose={onClose}>
<StyledModalWrapper>
<ModalCloseButton onClick={onClose} />
<StyledModalHeader>{t['Leave Workspace']()}</StyledModalHeader>
<StyledTextContent>
{t['Leave Workspace Description']()}
</StyledTextContent>
<StyledButtonContent>
<Button shape="circle" onClick={onClose}>
{t['Cancel']()}
</Button>
<Button
onClick={handleLeave}
type="danger"
shape="circle"
style={{ marginLeft: '24px' }}
>
{t['Leave']()}
</Button>
</StyledButtonContent>
</StyledModalWrapper>
</Modal>
);
};

View File

@@ -0,0 +1,45 @@
import { styled } from '@affine/component';
export const StyledModalWrapper = styled('div')(() => {
return {
position: 'relative',
padding: '0px',
width: '460px',
background: 'var(--affine-white)',
borderRadius: '12px',
};
});
export const StyledModalHeader = styled('div')(() => {
return {
margin: '44px 0px 12px 0px',
width: '460px',
fontWeight: '600',
fontSize: '20px;',
textAlign: 'center',
};
});
// export const StyledModalContent = styled('div')(({ theme }) => {});
export const StyledTextContent = styled('div')(() => {
return {
margin: 'auto',
width: '425px',
fontFamily: 'Avenir Next',
fontStyle: 'normal',
fontWeight: '400',
fontSize: '18px',
lineHeight: '26px',
textAlign: 'center',
};
});
export const StyledButtonContent = styled('div')(() => {
return {
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
margin: '0px 0 32px 0',
};
});

View File

@@ -0,0 +1,34 @@
import { Button, toast } from '@affine/component';
import { SettingRow } from '@affine/component/setting-components';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { FC } from 'react';
import type { AffineOfficialWorkspace } from '../../../shared';
export const ExportPanel: FC<{
workspace: AffineOfficialWorkspace;
}> = ({ workspace }) => {
const workspaceId = workspace.id;
const t = useAFFiNEI18N();
return (
<>
<SettingRow name={t['Export']()} desc={t['Export Description']()}>
<Button
size="small"
data-testid="export-affine-backup"
onClick={async () => {
const result = await window.apis?.dialog.saveDBFileAs(workspaceId);
if (result?.error) {
// @ts-expect-error: result.error is dynamic
toast(t[result.error]());
} else if (!result?.canceled) {
toast(t['Export success']());
}
}}
>
{t['Export']()}
</Button>
</SettingRow>
</>
);
};

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