mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-10 19:38:39 +00:00
Compare commits
51 Commits
v0.7.0-can
...
v0.7.0-can
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c772bd81b | ||
|
|
7f00011542 | ||
|
|
f76d8b8818 | ||
|
|
1d6b39dec9 | ||
|
|
5cfdf6c7e2 | ||
|
|
8410d83744 | ||
|
|
8a2dac9718 | ||
|
|
5ad2908760 | ||
|
|
5b8771485e | ||
|
|
ed8480caf0 | ||
|
|
42ef3c0fc2 | ||
|
|
e08ee9b7ff | ||
|
|
2c95bfcc3d | ||
|
|
86616e152d | ||
|
|
b1f478ee5e | ||
|
|
6b0f9fbdad | ||
|
|
da3f2b784a | ||
|
|
acb140ab78 | ||
|
|
0b74bd9bfe | ||
|
|
acfc030d16 | ||
|
|
d0d04ce376 | ||
|
|
2250f42d2a | ||
|
|
887434fea4 | ||
|
|
9b817c4b79 | ||
|
|
ea03bbfb2d | ||
|
|
db40cd35c6 | ||
|
|
aabac9e921 | ||
|
|
0a91c41e0a | ||
|
|
d6addc0d0b | ||
|
|
91d3b76be5 | ||
|
|
3eed009270 | ||
|
|
bc14d54cfa | ||
|
|
5496969e58 | ||
|
|
80c2a78273 | ||
|
|
92f378aefc | ||
|
|
877ceee698 | ||
|
|
7960b6a22e | ||
|
|
fa45d8a718 | ||
|
|
87574c9993 | ||
|
|
2dd62f7603 | ||
|
|
79b3b1dabc | ||
|
|
fd0aa4a2ee | ||
|
|
da57fbeadd | ||
|
|
3f12e4925f | ||
|
|
21cb05a30c | ||
|
|
7a8ff2c489 | ||
|
|
d108434881 | ||
|
|
20fd9b6574 | ||
|
|
26ac56e163 | ||
|
|
78b74d5b15 | ||
|
|
1556167262 |
20
.codesandbox/task.json
Normal file
20
.codesandbox/task.json
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
"server",
|
||||
"web",
|
||||
"docs",
|
||||
"storybook",
|
||||
"component",
|
||||
"workspace",
|
||||
"env",
|
||||
@@ -21,7 +22,7 @@
|
||||
"templates",
|
||||
"y-indexeddb",
|
||||
"debug",
|
||||
"theme"
|
||||
"storage"
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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
1
.github/CLA.md
vendored
@@ -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
|
||||
|
||||
14
.github/actions/build-rust/action.yml
vendored
14
.github/actions/build-rust/action.yml
vendored
@@ -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
|
||||
|
||||
2
.github/actions/setup-node/action.yml
vendored
2
.github/actions/setup-node/action.yml
vendored
@@ -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
31
.github/actions/setup-rust/action.yml
vendored
Normal 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
1
.github/helm/affine-cloud/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
charts/
|
||||
2
.github/helm/affine-cloud/.helmignore
vendored
2
.github/helm/affine-cloud/.helmignore
vendored
@@ -20,4 +20,4 @@
|
||||
.project
|
||||
.idea/
|
||||
*.tmproj
|
||||
.vscode/
|
||||
.vscode/
|
||||
4
.github/helm/affine-cloud/Chart.yaml
vendored
4
.github/helm/affine-cloud/Chart.yaml
vendored
@@ -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
|
||||
|
||||
Binary file not shown.
30
.github/helm/affine-cloud/readme.md
vendored
Normal file
30
.github/helm/affine-cloud/readme.md
vendored
Normal 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.
|
||||
51
.github/helm/affine-cloud/templates/_helper.tpl
vendored
Normal file
51
.github/helm/affine-cloud/templates/_helper.tpl
vendored
Normal 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 }}
|
||||
@@ -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
|
||||
|
||||
@@ -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 }}"
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
13
.github/helm/affine-cloud/values.yaml
vendored
13
.github/helm/affine-cloud/values.yaml
vendored
@@ -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
60
.github/helm/deployment_guide.md
vendored
Normal 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
2
.github/helm/releaser.yaml
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
owner: toeverything
|
||||
git-repo: helm-charts
|
||||
94
.github/workflows/build.yml
vendored
94
.github/workflows/build.yml
vendored
@@ -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'
|
||||
|
||||
1
.github/workflows/codeql.yml
vendored
1
.github/workflows/codeql.yml
vendored
@@ -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
65
.github/workflows/helm-releaser.yml
vendored
Normal 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
|
||||
1
.github/workflows/nightly-build.yml
vendored
1
.github/workflows/nightly-build.yml
vendored
@@ -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)
|
||||
|
||||
45
.github/workflows/release-desktop-app.yml
vendored
45
.github/workflows/release-desktop-app.yml
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
25
apps/README.md
Normal 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/).
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -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",
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
})
|
||||
|
||||
@@ -132,6 +132,13 @@ export interface AFFiNEConfig {
|
||||
*/
|
||||
get origin(): string;
|
||||
|
||||
/**
|
||||
* the database config
|
||||
*/
|
||||
db: {
|
||||
url: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* the apollo driver config
|
||||
*/
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
27
apps/server/src/modules/workspaces/controller.ts
Normal file
27
apps/server/src/modules/workspaces/controller.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
23
apps/server/src/storage/index.ts
Normal file
23
apps/server/src/storage/index.ts
Normal 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],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -20,6 +20,9 @@
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/storage/tsconfig.json"
|
||||
}
|
||||
],
|
||||
"ts-node": {
|
||||
|
||||
@@ -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].*' },
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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,
|
||||
@@ -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"
|
||||
@@ -12,5 +12,10 @@
|
||||
},
|
||||
"include": [".storybook/**/*"],
|
||||
"exclude": ["lib"],
|
||||
"references": [{ "path": "../i18n" }]
|
||||
"references": [
|
||||
{ "path": "../../packages/i18n" },
|
||||
{
|
||||
"path": "../../packages/env"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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=
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}.`
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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',
|
||||
// };
|
||||
// });
|
||||
@@ -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);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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',
|
||||
};
|
||||
});
|
||||
@@ -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
Reference in New Issue
Block a user