mirror of
https://github.com/toeverything/AFFiNE.git
synced 2024-12-22 14:41:37 +03:00
feat!: affine cloud support (#3813)
Co-authored-by: Hongtao Lye <codert.sn@gmail.com> Co-authored-by: liuyi <forehalo@gmail.com> Co-authored-by: LongYinan <lynweklm@gmail.com> Co-authored-by: X1a0t <405028157@qq.com> Co-authored-by: JimmFly <yangjinfei001@gmail.com> Co-authored-by: Peng Xiao <pengxiao@outlook.com> Co-authored-by: xiaodong zuo <53252747+zuoxiaodong0815@users.noreply.github.com> Co-authored-by: DarkSky <25152247+darkskygit@users.noreply.github.com> Co-authored-by: Qi <474021214@qq.com> Co-authored-by: danielchim <kahungchim@gmail.com>
This commit is contained in:
parent
d0145c6f38
commit
2f6c4e3696
@ -9,3 +9,5 @@ ENABLE_NEW_SETTING_UNSTABLE_API=
|
||||
ENABLE_NOTIFICATION_CENTER=
|
||||
ENABLE_CLOUD=
|
||||
ENABLE_MOVE_DATABASE=
|
||||
SHOULD_REPORT_TRACE=
|
||||
TRACE_REPORT_ENDPOINT=
|
||||
|
12
.eslintrc.js
12
.eslintrc.js
@ -31,6 +31,12 @@ const createPattern = packageName => [
|
||||
message: 'Use `useNavigateHelper` instead',
|
||||
importNames: ['useNavigate'],
|
||||
},
|
||||
{
|
||||
group: ['next-auth/react'],
|
||||
message: "Import hooks from 'use-current-user.tsx'",
|
||||
// useSession is type unsafe
|
||||
importNames: ['useSession'],
|
||||
},
|
||||
{
|
||||
group: ['yjs'],
|
||||
message: 'Do not use this API because it has a bug',
|
||||
@ -160,6 +166,12 @@ const config = {
|
||||
message: 'Use `useNavigateHelper` instead',
|
||||
importNames: ['useNavigate'],
|
||||
},
|
||||
{
|
||||
group: ['next-auth/react'],
|
||||
message: "Import hooks from 'use-current-user.tsx'",
|
||||
// useSession is type unsafe
|
||||
importNames: ['useSession'],
|
||||
},
|
||||
{
|
||||
group: ['yjs'],
|
||||
message: 'Do not use this API because it has a bug',
|
||||
|
11
.github/actions/build-rust/action.yml
vendored
11
.github/actions/build-rust/action.yml
vendored
@ -4,6 +4,9 @@ inputs:
|
||||
target:
|
||||
description: 'Cargo target'
|
||||
required: true
|
||||
package:
|
||||
description: 'Package to build'
|
||||
required: true
|
||||
nx_token:
|
||||
description: 'Nx Cloud access token'
|
||||
required: false
|
||||
@ -31,7 +34,7 @@ runs:
|
||||
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 }}
|
||||
yarn nx build ${{ inputs.package }} --target ${{ inputs.target }}
|
||||
env:
|
||||
NX_CLOUD_ACCESS_TOKEN: ${{ inputs.nx_token }}
|
||||
|
||||
@ -44,7 +47,8 @@ runs:
|
||||
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 }}
|
||||
export RUSTFLAGS="-C debuginfo=1"
|
||||
yarn nx build ${{ inputs.package }} --target ${{ inputs.target }}
|
||||
chmod -R 777 node_modules/.cache
|
||||
chmod -R 777 target
|
||||
|
||||
@ -55,6 +59,7 @@ runs:
|
||||
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: |
|
||||
yarn nx build @affine/native --target ${{ inputs.target }}
|
||||
export RUSTFLAGS="-C debuginfo=1"
|
||||
yarn nx build ${{ inputs.package }} --target ${{ inputs.target }}
|
||||
chmod -R 777 node_modules/.cache
|
||||
chmod -R 777 target
|
||||
|
50
.github/actions/deploy/action.yml
vendored
Normal file
50
.github/actions/deploy/action.yml
vendored
Normal file
@ -0,0 +1,50 @@
|
||||
name: 'Deploy to Cluster'
|
||||
description: 'Deploy AFFiNE Cloud to cluster'
|
||||
inputs:
|
||||
build-type:
|
||||
description: 'Align with App build type, canary|beta|stable|internal'
|
||||
default: 'canary'
|
||||
gcp-project-number:
|
||||
description: 'GCP project number'
|
||||
required: true
|
||||
gcp-project-id:
|
||||
description: 'GCP project id'
|
||||
required: true
|
||||
service-account:
|
||||
description: 'Service account'
|
||||
cluster-name:
|
||||
description: 'Cluster name'
|
||||
cluster-location:
|
||||
description: 'Cluster location'
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Setup Git short hash
|
||||
shell: bash
|
||||
run: |
|
||||
echo "GIT_SHORT_HASH=$(git rev-parse --short HEAD)" >> "$GITHUB_ENV"
|
||||
- uses: azure/setup-helm@v3
|
||||
- id: auth
|
||||
uses: google-github-actions/auth@v1
|
||||
with:
|
||||
workload_identity_provider: 'projects/${{ inputs.gcp-project-number }}/locations/global/workloadIdentityPools/github-actions/providers/github-actions-helm-deploy'
|
||||
service_account: '${{ inputs.service-account }}'
|
||||
token_format: 'access_token'
|
||||
project_id: '${{ inputs.gcp-project-id }}'
|
||||
|
||||
- name: 'Setup gcloud cli'
|
||||
uses: 'google-github-actions/setup-gcloud@v1'
|
||||
with:
|
||||
install_components: 'gke-gcloud-auth-plugin'
|
||||
|
||||
- id: get-gke-credentials
|
||||
shell: bash
|
||||
run: |
|
||||
gcloud container clusters get-credentials ${{ inputs.cluster-name }} --region ${{ inputs.cluster-location }} --project ${{ inputs.gcp-project-id }}
|
||||
|
||||
- name: Deploy
|
||||
shell: bash
|
||||
run: node ./.github/actions/deploy/deploy.mjs
|
||||
env:
|
||||
BUILD_TYPE: '${{ inputs.build-type }}'
|
116
.github/actions/deploy/deploy.mjs
vendored
Normal file
116
.github/actions/deploy/deploy.mjs
vendored
Normal file
@ -0,0 +1,116 @@
|
||||
import { execSync } from 'node:child_process';
|
||||
|
||||
const {
|
||||
BUILD_TYPE,
|
||||
DEPLOY_HOST,
|
||||
CANARY_DEPLOY_HOST,
|
||||
GIT_SHORT_HASH,
|
||||
DATABASE_URL,
|
||||
DATABASE_USERNAME,
|
||||
DATABASE_PASSWORD,
|
||||
DATABASE_NAME,
|
||||
R2_ACCOUNT_ID,
|
||||
R2_ACCESS_KEY_ID,
|
||||
R2_SECRET_ACCESS_KEY,
|
||||
R2_BUCKET,
|
||||
OAUTH_EMAIL_SENDER,
|
||||
OAUTH_EMAIL_LOGIN,
|
||||
OAUTH_EMAIL_PASSWORD,
|
||||
AFFINE_GOOGLE_CLIENT_ID,
|
||||
AFFINE_GOOGLE_CLIENT_SECRET,
|
||||
CLOUD_SQL_IAM_ACCOUNT,
|
||||
GCLOUD_CONNECTION_NAME,
|
||||
GCLOUD_CLOUD_SQL_INTERNAL_ENDPOINT,
|
||||
REDIS_HOST,
|
||||
REDIS_PASSWORD,
|
||||
} = process.env;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const buildType = BUILD_TYPE || 'canary';
|
||||
|
||||
const isProduction = buildType === 'stable';
|
||||
const isBeta = buildType === 'beta';
|
||||
|
||||
const createHelmCommand = ({ isDryRun }) => {
|
||||
const flag = isDryRun ? '--dry-run' : '--atomic';
|
||||
const imageTag = `${buildType}-${GIT_SHORT_HASH}`;
|
||||
const staticIpName = isProduction
|
||||
? 'affine-cluster-production'
|
||||
: isBeta
|
||||
? 'affine-cluster-beta'
|
||||
: 'affine-cluster-dev';
|
||||
const redisAndPostgres =
|
||||
isProduction || isBeta
|
||||
? [
|
||||
`--set-string global.database.url=${DATABASE_URL}`,
|
||||
`--set-string global.database.user=${DATABASE_USERNAME}`,
|
||||
`--set-string global.database.password=${DATABASE_PASSWORD}`,
|
||||
`--set-string global.database.name=${DATABASE_NAME}`,
|
||||
`--set global.database.gcloud.enabled=true`,
|
||||
`--set-string global.database.gcloud.connectionName="${GCLOUD_CONNECTION_NAME}"`,
|
||||
`--set-string global.database.gcloud.cloudSqlInternal="${GCLOUD_CLOUD_SQL_INTERNAL_ENDPOINT}"`,
|
||||
`--set-string global.redis.host="${REDIS_HOST}"`,
|
||||
`--set-string global.redis.password="${REDIS_PASSWORD}"`,
|
||||
]
|
||||
: [];
|
||||
const serviceAnnotations =
|
||||
isProduction || isBeta
|
||||
? [
|
||||
`--set-json web.service.annotations=\"{ \\"cloud.google.com/neg\\": \\"{\\\\\\"ingress\\\\\\": true}\\" }\"`,
|
||||
`--set-json graphql.serviceAccount.annotations=\"{ \\"iam.gke.io/gcp-service-account\\": \\"${CLOUD_SQL_IAM_ACCOUNT}\\" }\"`,
|
||||
`--set-json graphql.service.annotations=\"{ \\"cloud.google.com/neg\\": \\"{\\\\\\"ingress\\\\\\": true}\\" }\"`,
|
||||
`--set-json sync.serviceAccount.annotations=\"{ \\"iam.gke.io/gcp-service-account\\": \\"${CLOUD_SQL_IAM_ACCOUNT}\\" }\"`,
|
||||
`--set-json sync.service.annotations=\"{ \\"cloud.google.com/neg\\": \\"{\\\\\\"ingress\\\\\\": true}\\" }\"`,
|
||||
]
|
||||
: [];
|
||||
const webReplicaCount = isProduction ? 3 : isBeta ? 2 : 1;
|
||||
const graphqlReplicaCount = isProduction ? 3 : isBeta ? 2 : 1;
|
||||
const syncReplicaCount = isProduction ? 6 : isBeta ? 3 : 1;
|
||||
const namespace = isProduction ? 'production' : isBeta ? 'beta' : 'dev';
|
||||
const deployCommand = [
|
||||
`helm upgrade --install affine .github/helm/affine`,
|
||||
`--namespace ${namespace}`,
|
||||
`--set global.ingress.enabled=true`,
|
||||
`--set-json global.ingress.annotations=\"{ \\"kubernetes.io/ingress.class\\": \\"gce\\", \\"kubernetes.io/ingress.allow-http\\": \\"true\\", \\"kubernetes.io/ingress.global-static-ip-name\\": \\"${staticIpName}\\" }\"`,
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
`--set-string global.ingress.host="${DEPLOY_HOST || CANARY_DEPLOY_HOST}"`,
|
||||
...redisAndPostgres,
|
||||
`--set web.replicaCount=${webReplicaCount}`,
|
||||
`--set-string web.image.tag="${imageTag}"`,
|
||||
`--set graphql.replicaCount=${graphqlReplicaCount}`,
|
||||
`--set-string graphql.image.tag="${imageTag}"`,
|
||||
`--set graphql.app.objectStorage.r2.enabled=true`,
|
||||
`--set-string graphql.app.objectStorage.r2.accountId="${R2_ACCOUNT_ID}"`,
|
||||
`--set-string graphql.app.objectStorage.r2.accessKeyId="${R2_ACCESS_KEY_ID}"`,
|
||||
`--set-string graphql.app.objectStorage.r2.secretAccessKey="${R2_SECRET_ACCESS_KEY}"`,
|
||||
`--set-string graphql.app.objectStorage.r2.bucket="${R2_BUCKET}"`,
|
||||
`--set-string graphql.app.oauth.email.sender="${OAUTH_EMAIL_SENDER}"`,
|
||||
`--set-string graphql.app.oauth.email.login="${OAUTH_EMAIL_LOGIN}"`,
|
||||
`--set-string graphql.app.oauth.email.password="${OAUTH_EMAIL_PASSWORD}"`,
|
||||
`--set-string graphql.app.oauth.google.enabled=true`,
|
||||
`--set-string graphql.app.oauth.google.clientId="${AFFINE_GOOGLE_CLIENT_ID}"`,
|
||||
`--set-string graphql.app.oauth.google.clientSecret="${AFFINE_GOOGLE_CLIENT_SECRET}"`,
|
||||
`--set graphql.app.experimental.enableJwstCodec=true`,
|
||||
`--set sync.replicaCount=${syncReplicaCount}`,
|
||||
`--set-string sync.image.tag="${imageTag}"`,
|
||||
...serviceAnnotations,
|
||||
`--version "0.0.0-${buildType}.${GIT_SHORT_HASH}" --timeout 10m`,
|
||||
flag,
|
||||
].join(' ');
|
||||
return deployCommand;
|
||||
};
|
||||
|
||||
const output = execSync(createHelmCommand({ isDryRun: true }), {
|
||||
encoding: 'utf-8',
|
||||
stdio: ['inherit', 'pipe', 'inherit'],
|
||||
});
|
||||
const templates = output
|
||||
.split('---')
|
||||
.filter(yml => !yml.split('\n').some(line => line.trim() === 'kind: Secret'))
|
||||
.join('---');
|
||||
console.log(templates);
|
||||
|
||||
execSync(createHelmCommand({ isDryRun: false }), {
|
||||
encoding: 'utf-8',
|
||||
stdio: 'inherit',
|
||||
});
|
31
.github/actions/setup-rust/action.yml
vendored
31
.github/actions/setup-rust/action.yml
vendored
@ -1,31 +0,0 @@
|
||||
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 }}-
|
2
.github/deployment/front/affine.nginx.conf
vendored
2
.github/deployment/front/affine.nginx.conf
vendored
@ -3,7 +3,7 @@ server {
|
||||
root /app/dist;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/index.html $uri.html =404;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
error_page 404 /404.html;
|
||||
|
@ -40,6 +40,7 @@ helm.sh/chart: {{ include "graphql.chart" . }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
monitoring: enabled
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
@ -75,58 +76,3 @@ key: {{ $secret.data.private }}
|
||||
key: {{ genPrivateKey "ecdsa" | b64enc }}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "objectStorage.r2" -}}
|
||||
{{- $secret := lookup "v1" "Secret" .Release.Namespace .Values.app.objectStorage.r2.secretName -}}
|
||||
{{- if $secret -}}
|
||||
{{/*
|
||||
Reusing existing secret data
|
||||
*/}}
|
||||
accountId: {{ $secret.data.accountId }}
|
||||
accessKeyId: {{ $secret.data.accessKeyId }}
|
||||
secretAccessKey: {{ $secret.data.secretAccessKey }}
|
||||
bucket: {{ $secret.data.bucket }}
|
||||
{{- else -}}
|
||||
{{/*
|
||||
Generate new data
|
||||
*/}}
|
||||
accountId: {{ .Values.app.objectStorage.r2.accountId | b64enc }}
|
||||
accessKeyId: {{ .Values.app.objectStorage.r2.accessKeyId | b64enc }}
|
||||
secretAccessKey: {{ .Values.app.objectStorage.r2.secretAccessKey | b64enc }}
|
||||
bucket: {{ .Values.app.objectStorage.r2.bucket | b64enc }}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "objectStorage.oauth.google" -}}
|
||||
{{- $secret := lookup "v1" "Secret" .Release.Namespace .Values.app.oauth.google.secretName -}}
|
||||
{{- if $secret -}}
|
||||
{{/*
|
||||
Reusing existing secret data
|
||||
*/}}
|
||||
clientId: {{ $secret.data.clientId }}
|
||||
clientSecret: {{ $secret.data.clientSecret }}
|
||||
{{- else -}}
|
||||
{{/*
|
||||
Generate new data
|
||||
*/}}
|
||||
clientId: "{{ .Values.app.oauth.google.clientId | b64enc }}"
|
||||
clientSecret: "{{ .Values.app.oauth.google.clientSecret | b64enc }}"
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "objectStorage.oauth.github" -}}
|
||||
{{- $secret := lookup "v1" "Secret" .Release.Namespace .Values.app.oauth.github.secretName -}}
|
||||
{{- if $secret -}}
|
||||
{{/*
|
||||
Reusing existing secret data
|
||||
*/}}
|
||||
clientId: {{ $secret.data.clientId }}
|
||||
clientSecret: {{ $secret.data.clientSecret }}
|
||||
{{- else -}}
|
||||
{{/*
|
||||
Generate new data
|
||||
*/}}
|
||||
clientId: "{{ .Values.app.oauth.github.clientId | b64enc }}"
|
||||
clientSecret: "{{ .Values.app.oauth.github.clientSecret | b64enc }}"
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
@ -35,13 +35,36 @@ spec:
|
||||
key: key
|
||||
- name: NODE_ENV
|
||||
value: "{{ .Values.env }}"
|
||||
- name: DATABSE_PASSWORD
|
||||
- name: NO_COLOR
|
||||
value: "1"
|
||||
- name: SERVER_FLAVOR
|
||||
value: "graphql"
|
||||
- name: AFFINE_ENV
|
||||
value: "{{ .Release.Namespace }}"
|
||||
- name: NEXTAUTH_URL
|
||||
value: "{{ .Values.global.ingress.host }}"
|
||||
- name: DATABASE_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: pg-postgresql
|
||||
key: postgres-password
|
||||
- name: DATABASE_URL
|
||||
value: postgres://{{ .Values.database.user }}:$(DATABSE_PASSWORD)@{{ .Values.database.url }}:{{ .Values.database.port }}/{{ .Values.database.name }}
|
||||
value: postgres://{{ .Values.global.database.user }}:$(DATABASE_PASSWORD)@{{ .Values.global.database.url }}:{{ .Values.global.database.port }}/{{ .Values.global.database.name }}
|
||||
- name: REDIS_SERVER_ENABLED
|
||||
value: "true"
|
||||
- name: REDIS_SERVER_HOST
|
||||
value: "{{ .Values.global.redis.host }}"
|
||||
- name: REDIS_SERVER_PORT
|
||||
value: "{{ .Values.global.redis.port }}"
|
||||
- name: REDIS_SERVER_USER
|
||||
value: "{{ .Values.global.redis.username }}"
|
||||
- name: REDIS_SERVER_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: redis
|
||||
key: redis-password
|
||||
- name: REDIS_SERVER_DATABASE
|
||||
value: "{{ .Values.global.redis.database }}"
|
||||
- name: AFFINE_SERVER_PORT
|
||||
value: "{{ .Values.service.port }}"
|
||||
- name: AFFINE_SERVER_SUB_PATH
|
||||
@ -50,6 +73,37 @@ spec:
|
||||
value: "{{ .Values.app.host }}"
|
||||
- name: ENABLE_R2_OBJECT_STORAGE
|
||||
value: "{{ .Values.app.objectStorage.r2.enabled }}"
|
||||
- name: OAUTH_EMAIL_SENDER
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: "{{ .Values.app.oauth.email.secretName }}"
|
||||
key: sender
|
||||
- name: OAUTH_EMAIL_LOGIN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: "{{ .Values.app.oauth.email.secretName }}"
|
||||
key: login
|
||||
- name: OAUTH_EMAIL_SERVER
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: "{{ .Values.app.oauth.email.secretName }}"
|
||||
key: server
|
||||
- name: OAUTH_EMAIL_PORT
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: "{{ .Values.app.oauth.email.secretName }}"
|
||||
key: port
|
||||
- name: OAUTH_EMAIL_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: "{{ .Values.app.oauth.email.secretName }}"
|
||||
key: password
|
||||
- name: DOC_MERGE_INTERVAL
|
||||
value: "{{ .Values.app.doc.mergeInterval }}"
|
||||
{{ if .Values.app.experimental.enableJwstCodec }}
|
||||
- name: DOC_MERGE_USE_JWST_CODEC
|
||||
value: "true"
|
||||
{{ end }}
|
||||
{{ if .Values.app.objectStorage.r2.enabled }}
|
||||
- name: R2_OBJECT_STORAGE_ACCOUNT_ID
|
||||
valueFrom:
|
||||
@ -112,6 +166,20 @@ spec:
|
||||
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
{{ if .Values.global.database.gcloud.enabled }}
|
||||
- name: cloud-sql-proxy
|
||||
image: gcr.io/cloud-sql-connectors/cloud-sql-proxy:2.6.0
|
||||
args:
|
||||
- "--structured-logs"
|
||||
- "--auto-iam-authn"
|
||||
- "{{ .Values.global.database.gcloud.connectionName }}"
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
resources:
|
||||
requests:
|
||||
memory: "2Gi"
|
||||
cpu: "1"
|
||||
{{ end }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
|
@ -5,13 +5,14 @@ metadata:
|
||||
labels:
|
||||
{{- include "graphql.labels" . | nindent 4 }}
|
||||
annotations:
|
||||
"helm.sh/hook": pre-install,pre-upgrade
|
||||
"helm.sh/hook": post-install,pre-upgrade
|
||||
"helm.sh/hook-weight": "-1"
|
||||
"helm.sh/hook-delete-policy": before-hook-creation
|
||||
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
serviceAccountName: {{ include "graphql.serviceAccountName" . }}
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||
@ -19,13 +20,21 @@ spec:
|
||||
env:
|
||||
- name: NODE_ENV
|
||||
value: "{{ .Values.env }}"
|
||||
- name: DATABSE_PASSWORD
|
||||
- name: AFFINE_ENV
|
||||
value: "{{ .Release.Namespace }}"
|
||||
- name: DATABASE_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: pg-postgresql
|
||||
key: postgres-password
|
||||
{{ if not .Values.global.database.gcloud.enabled }}
|
||||
- name: DATABASE_URL
|
||||
value: postgres://{{ .Values.database.user }}:$(DATABSE_PASSWORD)@{{ .Values.database.url }}:{{ .Values.database.port }}/{{ .Values.database.name }}
|
||||
value: postgres://{{ .Values.global.database.user }}:$(DATABASE_PASSWORD)@{{ .Values.global.database.url }}:{{ .Values.global.database.port }}/{{ .Values.global.database.name }}
|
||||
{{ end }}
|
||||
{{ if .Values.global.database.gcloud.enabled }}
|
||||
- name: DATABASE_URL
|
||||
value: postgres://{{ .Values.global.database.user }}:$(DATABASE_PASSWORD)@{{ .Values.global.database.gcloud.cloudSqlInternal }}:{{ .Values.global.database.port }}/{{ .Values.global.database.name }}
|
||||
{{ end }}
|
||||
resources:
|
||||
requests:
|
||||
cpu: '100m'
|
||||
|
13
.github/helm/affine/charts/graphql/templates/monitoring.yaml
vendored
Normal file
13
.github/helm/affine/charts/graphql/templates/monitoring.yaml
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
{{- if .Values.global.gke.enabled -}}
|
||||
apiVersion: monitoring.googleapis.com/v1
|
||||
kind: PodMonitoring
|
||||
metadata:
|
||||
name: "{{ .Chart.Name }}-monitoring"
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: "{{ include "graphql.name" . }}"
|
||||
endpoints:
|
||||
- port: {{ .Values.service.port }}
|
||||
interval: 30s
|
||||
{{- end }}
|
@ -1,10 +0,0 @@
|
||||
{{- if .Values.app.oauth.github.enabled -}}
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: "{{ .Values.app.oauth.github.secretName }}"
|
||||
type: Opaque
|
||||
data:
|
||||
{{- ( include "objectStorage.oauth.github" . ) | indent 2 -}}
|
||||
|
||||
{{- end }}
|
@ -1,10 +0,0 @@
|
||||
{{- if .Values.app.oauth.google.enabled -}}
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: "{{ .Values.app.oauth.google.secretName }}"
|
||||
type: Opaque
|
||||
data:
|
||||
{{- ( include "objectStorage.oauth.google" . ) | indent 2 -}}
|
||||
|
||||
{{- end }}
|
33
.github/helm/affine/charts/graphql/templates/oauth.yaml
vendored
Normal file
33
.github/helm/affine/charts/graphql/templates/oauth.yaml
vendored
Normal file
@ -0,0 +1,33 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: "{{ .Values.app.oauth.email.secretName }}"
|
||||
type: Opaque
|
||||
data:
|
||||
sender: "{{ .Values.app.oauth.email.sender | b64enc }}"
|
||||
login: "{{ .Values.app.oauth.email.login | b64enc }}"
|
||||
password: "{{ .Values.app.oauth.email.password | b64enc }}"
|
||||
server: "{{ .Values.app.oauth.email.server | b64enc }}"
|
||||
port: "{{ .Values.app.oauth.email.port | b64enc }}"
|
||||
---
|
||||
{{- if .Values.app.oauth.google.enabled -}}
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: "{{ .Values.app.oauth.google.secretName }}"
|
||||
type: Opaque
|
||||
data:
|
||||
clientId: "{{ .Values.app.oauth.google.clientId | b64enc }}"
|
||||
clientSecret: "{{ .Values.app.oauth.google.clientSecret | b64enc }}"
|
||||
{{- end }}
|
||||
---
|
||||
{{- if .Values.app.oauth.github.enabled -}}
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: "{{ .Values.app.oauth.github.secretName }}"
|
||||
type: Opaque
|
||||
data:
|
||||
clientId: "{{ .Values.app.oauth.github.clientId | b64enc }}"
|
||||
clientSecret: "{{ .Values.app.oauth.github.clientSecret | b64enc }}"
|
||||
{{- end }}
|
9
.github/helm/affine/charts/graphql/templates/pg-secret.yaml
vendored
Normal file
9
.github/helm/affine/charts/graphql/templates/pg-secret.yaml
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
{{- if .Values.global.database.password -}}
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: pg-postgresql
|
||||
type: Opaque
|
||||
data:
|
||||
postgres-password: {{ .Values.global.database.password | b64enc }}
|
||||
{{- end }}
|
@ -5,5 +5,8 @@ metadata:
|
||||
name: "{{ .Values.app.objectStorage.r2.secretName }}"
|
||||
type: Opaque
|
||||
data:
|
||||
{{- ( include "objectStorage.r2" . ) | indent 2 -}}
|
||||
accountId: {{ .Values.app.objectStorage.r2.accountId | b64enc }}
|
||||
accessKeyId: {{ .Values.app.objectStorage.r2.accessKeyId | b64enc }}
|
||||
secretAccessKey: {{ .Values.app.objectStorage.r2.secretAccessKey | b64enc }}
|
||||
bucket: {{ .Values.app.objectStorage.r2.bucket | b64enc }}
|
||||
{{- end }}
|
||||
|
9
.github/helm/affine/charts/graphql/templates/redis-secret.yaml
vendored
Normal file
9
.github/helm/affine/charts/graphql/templates/redis-secret.yaml
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
{{- if .Values.global.redis.password -}}
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: redis
|
||||
type: Opaque
|
||||
data:
|
||||
redis-password: {{ .Values.global.redis.password | b64enc }}
|
||||
{{- end }}
|
24
.github/helm/affine/charts/graphql/values.yaml
vendored
24
.github/helm/affine/charts/graphql/values.yaml
vendored
@ -9,16 +9,15 @@ nameOverride: ''
|
||||
fullnameOverride: ''
|
||||
# map to NODE_ENV environment variable
|
||||
env: 'production'
|
||||
database:
|
||||
user: 'postgres'
|
||||
url: 'pg-postgresql'
|
||||
port: '5432'
|
||||
name: 'affine'
|
||||
app:
|
||||
experimental:
|
||||
enableJwstCodec: true
|
||||
# AFFINE_SERVER_SUB_PATH
|
||||
path: ''
|
||||
# AFFINE_SERVER_HOST
|
||||
host: '0.0.0.0'
|
||||
doc:
|
||||
mergeInterval: "3000"
|
||||
jwt:
|
||||
secretName: jwt-private-key
|
||||
# base64 encoded ecdsa private key
|
||||
@ -32,6 +31,13 @@ app:
|
||||
secretAccessKey: ''
|
||||
bucket: ''
|
||||
oauth:
|
||||
email:
|
||||
secretName: 'oauth-email'
|
||||
sender: 'noreply@toeverything.info'
|
||||
login: ''
|
||||
password: ''
|
||||
server: 'smtp.gmail.com'
|
||||
port: '465'
|
||||
google:
|
||||
enabled: false
|
||||
secretName: oauth-google
|
||||
@ -55,11 +61,11 @@ podSecurityContext:
|
||||
|
||||
resources:
|
||||
limits:
|
||||
cpu: '2000m'
|
||||
memory: 4Gi
|
||||
cpu: '4'
|
||||
memory: 8Gi
|
||||
requests:
|
||||
cpu: '1000m'
|
||||
memory: 2Gi
|
||||
cpu: '2'
|
||||
memory: 4Gi
|
||||
|
||||
probe:
|
||||
initialDelaySeconds: 20
|
||||
|
23
.github/helm/affine/charts/sync/.helmignore
vendored
Normal file
23
.github/helm/affine/charts/sync/.helmignore
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
# Patterns to ignore when building packages.
|
||||
# This supports shell glob matching, relative path matching, and
|
||||
# negation (prefixed with !). Only one pattern per line.
|
||||
.DS_Store
|
||||
# Common VCS dirs
|
||||
.git/
|
||||
.gitignore
|
||||
.bzr/
|
||||
.bzrignore
|
||||
.hg/
|
||||
.hgignore
|
||||
.svn/
|
||||
# Common backup files
|
||||
*.swp
|
||||
*.bak
|
||||
*.tmp
|
||||
*.orig
|
||||
*~
|
||||
# Various IDEs
|
||||
.project
|
||||
.idea/
|
||||
*.tmproj
|
||||
.vscode/
|
6
.github/helm/affine/charts/sync/Chart.yaml
vendored
Normal file
6
.github/helm/affine/charts/sync/Chart.yaml
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
apiVersion: v2
|
||||
name: sync
|
||||
description: A Helm chart for Kubernetes
|
||||
type: application
|
||||
version: 0.0.0
|
||||
appVersion: "0.7.0-canary.18"
|
16
.github/helm/affine/charts/sync/templates/NOTES.txt
vendored
Normal file
16
.github/helm/affine/charts/sync/templates/NOTES.txt
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
1. Get the application URL by running these commands:
|
||||
{{- if contains "NodePort" .Values.service.type }}
|
||||
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "sync.fullname" . }})
|
||||
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
|
||||
echo http://$NODE_IP:$NODE_PORT
|
||||
{{- else if contains "LoadBalancer" .Values.service.type }}
|
||||
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
|
||||
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "sync.fullname" . }}'
|
||||
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "sync.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
|
||||
echo http://$SERVICE_IP:{{ .Values.service.port }}
|
||||
{{- else if contains "ClusterIP" .Values.service.type }}
|
||||
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "sync.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
|
||||
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
|
||||
echo "Visit http://127.0.0.1:8080 to use your application"
|
||||
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
|
||||
{{- end }}
|
63
.github/helm/affine/charts/sync/templates/_helpers.tpl
vendored
Normal file
63
.github/helm/affine/charts/sync/templates/_helpers.tpl
vendored
Normal file
@ -0,0 +1,63 @@
|
||||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "sync.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||
If release name contains chart name it will be used as a full name.
|
||||
*/}}
|
||||
{{- define "sync.fullname" -}}
|
||||
{{- if .Values.fullnameOverride }}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride }}
|
||||
{{- if contains $name .Release.Name }}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
||||
{{- else }}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label.
|
||||
*/}}
|
||||
{{- define "sync.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Common labels
|
||||
*/}}
|
||||
{{- define "sync.labels" -}}
|
||||
helm.sh/chart: {{ include "sync.chart" . }}
|
||||
{{ include "sync.selectorLabels" . }}
|
||||
{{- if .Chart.AppVersion }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
monitoring: enabled
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Selector labels
|
||||
*/}}
|
||||
{{- define "sync.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "sync.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create the name of the service account to use
|
||||
*/}}
|
||||
{{- define "sync.serviceAccountName" -}}
|
||||
{{- if .Values.serviceAccount.create }}
|
||||
{{- default (include "sync.fullname" .) .Values.serviceAccount.name }}
|
||||
{{- else }}
|
||||
{{- default "default" .Values.serviceAccount.name }}
|
||||
{{- end }}
|
||||
{{- end }}
|
110
.github/helm/affine/charts/sync/templates/deployment.yaml
vendored
Normal file
110
.github/helm/affine/charts/sync/templates/deployment.yaml
vendored
Normal file
@ -0,0 +1,110 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "sync.fullname" . }}
|
||||
labels:
|
||||
{{- include "sync.labels" . | nindent 4 }}
|
||||
spec:
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "sync.selectorLabels" . | nindent 6 }}
|
||||
template:
|
||||
metadata:
|
||||
{{- with .Values.podAnnotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "sync.selectorLabels" . | nindent 8 }}
|
||||
spec:
|
||||
{{- with .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
serviceAccountName: {{ include "sync.serviceAccountName" . }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
env:
|
||||
- name: NODE_ENV
|
||||
value: "{{ .Values.env }}"
|
||||
- name: NO_COLOR
|
||||
value: "1"
|
||||
- name: SERVER_FLAVOR
|
||||
value: "sync"
|
||||
- name: NEXTAUTH_URL
|
||||
value: "{{ .Values.global.ingress.host }}"
|
||||
- name: AFFINE_ENV
|
||||
value: "{{ .Release.Namespace }}"
|
||||
- name: DATABASE_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: pg-postgresql
|
||||
key: postgres-password
|
||||
- name: DATABASE_URL
|
||||
value: postgres://{{ .Values.global.database.user }}:$(DATABASE_PASSWORD)@{{ .Values.global.database.url }}:{{ .Values.global.database.port }}/{{ .Values.global.database.name }}
|
||||
- name: REDIS_SERVER_ENABLED
|
||||
value: "true"
|
||||
- name: REDIS_SERVER_HOST
|
||||
value: "{{ .Values.global.redis.host }}"
|
||||
- name: REDIS_SERVER_PORT
|
||||
value: "{{ .Values.global.redis.port }}"
|
||||
- name: REDIS_SERVER_USER
|
||||
value: "{{ .Values.global.redis.username }}"
|
||||
- name: REDIS_SERVER_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: redis
|
||||
key: redis-password
|
||||
- name: REDIS_SERVER_DATABASE
|
||||
value: "{{ .Values.global.redis.database }}"
|
||||
- name: AFFINE_SERVER_PORT
|
||||
value: "{{ .Values.service.port }}"
|
||||
- name: AFFINE_SERVER_HOST
|
||||
value: "{{ .Values.app.host }}"
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: {{ .Values.service.port }}
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
port: http
|
||||
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
|
||||
readinessProbe:
|
||||
tcpSocket:
|
||||
port: http
|
||||
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
{{ if .Values.global.database.gcloud.enabled }}
|
||||
- name: cloud-sql-proxy
|
||||
image: gcr.io/cloud-sql-connectors/cloud-sql-proxy:2.6.0
|
||||
args:
|
||||
- "--structured-logs"
|
||||
- "--auto-iam-authn"
|
||||
- "{{ .Values.global.database.gcloud.connectionName }}"
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
resources:
|
||||
requests:
|
||||
memory: "2Gi"
|
||||
cpu: "1"
|
||||
{{ end }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
13
.github/helm/affine/charts/sync/templates/monitoring.yaml
vendored
Normal file
13
.github/helm/affine/charts/sync/templates/monitoring.yaml
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
{{- if .Values.global.gke.enabled -}}
|
||||
apiVersion: monitoring.googleapis.com/v1
|
||||
kind: PodMonitoring
|
||||
metadata:
|
||||
name: "{{ .Chart.Name }}-monitoring"
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: "{{ include "sync.name" . }}"
|
||||
endpoints:
|
||||
- port: {{ .Values.service.port }}
|
||||
interval: 30s
|
||||
{{- end }}
|
19
.github/helm/affine/charts/sync/templates/service.yaml
vendored
Normal file
19
.github/helm/affine/charts/sync/templates/service.yaml
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "sync.fullname" . }}
|
||||
labels:
|
||||
{{- include "sync.labels" . | nindent 4 }}
|
||||
{{- with .Values.service.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.service.port }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
{{- include "sync.selectorLabels" . | nindent 4 }}
|
12
.github/helm/affine/charts/sync/templates/serviceaccount.yaml
vendored
Normal file
12
.github/helm/affine/charts/sync/templates/serviceaccount.yaml
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{{- if .Values.serviceAccount.create -}}
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ include "sync.serviceAccountName" . }}
|
||||
labels:
|
||||
{{- include "sync.labels" . | nindent 4 }}
|
||||
{{- with .Values.serviceAccount.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
15
.github/helm/affine/charts/sync/templates/tests/test-connection.yaml
vendored
Normal file
15
.github/helm/affine/charts/sync/templates/tests/test-connection.yaml
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: "{{ include "sync.fullname" . }}-test-connection"
|
||||
labels:
|
||||
{{- include "sync.labels" . | nindent 4 }}
|
||||
annotations:
|
||||
"helm.sh/hook": test
|
||||
spec:
|
||||
containers:
|
||||
- name: wget
|
||||
image: busybox
|
||||
command: ['wget']
|
||||
args: ['{{ include "sync.fullname" . }}:{{ .Values.service.port }}']
|
||||
restartPolicy: Never
|
39
.github/helm/affine/charts/sync/values.yaml
vendored
Normal file
39
.github/helm/affine/charts/sync/values.yaml
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
replicaCount: 1
|
||||
image:
|
||||
repository: ghcr.io/toeverything/affine-graphql
|
||||
pullPolicy: IfNotPresent
|
||||
tag: ''
|
||||
|
||||
imagePullSecrets: []
|
||||
nameOverride: ''
|
||||
fullnameOverride: ''
|
||||
# map to NODE_ENV environment variable
|
||||
env: 'production'
|
||||
app:
|
||||
# AFFINE_SERVER_HOST
|
||||
host: '0.0.0.0'
|
||||
|
||||
serviceAccount:
|
||||
create: true
|
||||
annotations: {}
|
||||
name: 'affine-sync'
|
||||
|
||||
podAnnotations: {}
|
||||
|
||||
podSecurityContext:
|
||||
fsGroup: 2000
|
||||
|
||||
resources:
|
||||
limits:
|
||||
cpu: '4'
|
||||
memory: 8Gi
|
||||
requests:
|
||||
cpu: '2'
|
||||
memory: 4Gi
|
||||
|
||||
probe:
|
||||
initialDelaySeconds: 20
|
||||
|
||||
nodeSelector: {}
|
||||
tolerations: []
|
||||
affinity: {}
|
@ -40,6 +40,7 @@ helm.sh/chart: {{ include "web.chart" . }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
monitoring: enabled
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
|
28
.github/helm/affine/templates/ingress.yaml
vendored
28
.github/helm/affine/templates/ingress.yaml
vendored
@ -1,8 +1,8 @@
|
||||
{{- if .Values.ingress.enabled -}}
|
||||
{{- if .Values.global.ingress.enabled -}}
|
||||
{{- $fullName := include "affine.fullname" . -}}
|
||||
{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
|
||||
{{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }}
|
||||
{{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}}
|
||||
{{- if and .Values.global.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
|
||||
{{- if not (hasKey .Values.global.ingress.annotations "kubernetes.io/ingress.class") }}
|
||||
{{- $_ := set .Values.global.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||
@ -17,17 +17,17 @@ metadata:
|
||||
name: {{ $fullName }}
|
||||
labels:
|
||||
{{- include "affine.labels" . | nindent 4 }}
|
||||
{{- with .Values.ingress.annotations }}
|
||||
{{- with .Values.global.ingress.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
|
||||
ingressClassName: {{ .Values.ingress.className }}
|
||||
{{- if and .Values.global.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
|
||||
ingressClassName: {{ .Values.global.ingress.className }}
|
||||
{{- end }}
|
||||
{{- if .Values.ingress.tls }}
|
||||
{{- if .Values.global.ingress.tls }}
|
||||
tls:
|
||||
{{- range .Values.ingress.tls }}
|
||||
{{- range .Values.global.ingress.tls }}
|
||||
- hosts:
|
||||
{{- range .hosts }}
|
||||
- {{ . | quote }}
|
||||
@ -36,9 +36,16 @@ spec:
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
rules:
|
||||
- host: "{{ .Values.ingress.host }}"
|
||||
- host: "{{ .Values.global.ingress.host }}"
|
||||
http:
|
||||
paths:
|
||||
- path: /socket.io
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: affine-sync
|
||||
port:
|
||||
number: {{ .Values.sync.service.port }}
|
||||
- path: /graphql
|
||||
pathType: Prefix
|
||||
backend:
|
||||
@ -60,5 +67,4 @@ spec:
|
||||
name: affine-web
|
||||
port:
|
||||
number: {{ .Values.web.service.port }}
|
||||
|
||||
{{- end }}
|
||||
|
41
.github/helm/affine/values.yaml
vendored
41
.github/helm/affine/values.yaml
vendored
@ -1,16 +1,43 @@
|
||||
ingress:
|
||||
enabled: false
|
||||
className: ''
|
||||
annotations:
|
||||
kubernetes.io/ingress.class: nginx
|
||||
host: affine.pro
|
||||
tls: []
|
||||
global:
|
||||
ingress:
|
||||
enabled: false
|
||||
className: ''
|
||||
host: affine.pro
|
||||
tls: []
|
||||
database:
|
||||
user: 'postgres'
|
||||
url: 'pg-postgresql'
|
||||
port: '5432'
|
||||
name: 'affine'
|
||||
password: ''
|
||||
gcloud:
|
||||
enabled: false
|
||||
# use for migration
|
||||
cloudSqlInternal: ''
|
||||
connectionName: ''
|
||||
serviceAccount: ''
|
||||
redis:
|
||||
enabled: true
|
||||
host: 'redis-master'
|
||||
port: '6379'
|
||||
username: ''
|
||||
password: ''
|
||||
database: 0
|
||||
gke:
|
||||
enabled: true
|
||||
|
||||
graphql:
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 3000
|
||||
|
||||
sync:
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 3010
|
||||
annotations:
|
||||
cloud.google.com/backend-config: '{"default": "affine-backendconfig"}'
|
||||
|
||||
web:
|
||||
service:
|
||||
type: ClusterIP
|
||||
|
1
.github/workflows/build-desktop.yml
vendored
1
.github/workflows/build-desktop.yml
vendored
@ -105,6 +105,7 @@ jobs:
|
||||
uses: ./.github/actions/build-rust
|
||||
with:
|
||||
target: ${{ matrix.spec.target }}
|
||||
package: '@affine/native'
|
||||
nx_token: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
|
||||
- name: Run unit tests
|
||||
if: ${{ matrix.spec.test }}
|
||||
|
105
.github/workflows/build.yml
vendored
105
.github/workflows/build.yml
vendored
@ -29,6 +29,7 @@ env:
|
||||
DEBUG: napi:*
|
||||
BUILD_TYPE: canary
|
||||
APP_NAME: affine
|
||||
AFFINE_ENV: dev
|
||||
COVERAGE: true
|
||||
DISTRIBUTION: browser
|
||||
MACOSX_DEPLOYMENT_TARGET: '10.13'
|
||||
@ -113,30 +114,11 @@ jobs:
|
||||
env:
|
||||
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
|
||||
|
||||
build-storybook:
|
||||
name: Build Storybook
|
||||
runs-on: ubuntu-latest
|
||||
environment: development
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-node
|
||||
with:
|
||||
electron-install: false
|
||||
- run: yarn nx build @affine/storybook
|
||||
env:
|
||||
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
|
||||
- name: Upload storybook artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: storybook
|
||||
path: ./apps/storybook/storybook-static
|
||||
if-no-files-found: error
|
||||
|
||||
build-storage:
|
||||
name: Build Storage
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUSTFLAGS: '-C debuginfo=1'
|
||||
environment: development
|
||||
|
||||
steps:
|
||||
@ -144,11 +126,11 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-node
|
||||
- name: Setup Rust
|
||||
uses: ./.github/actions/setup-rust
|
||||
uses: ./.github/actions/build-rust
|
||||
with:
|
||||
target: 'x86_64-unknown-linux-gnu'
|
||||
- name: Build Storage
|
||||
run: yarn build:storage
|
||||
package: '@affine/storage'
|
||||
nx_token: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
|
||||
- name: Upload storage.node
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
@ -216,6 +198,81 @@ jobs:
|
||||
name: affine
|
||||
fail_ci_if_error: false
|
||||
|
||||
server-e2e-test:
|
||||
name: Server E2E Test
|
||||
runs-on: ubuntu-latest
|
||||
environment: development
|
||||
needs: build-storage
|
||||
services:
|
||||
postgres:
|
||||
image: postgres
|
||||
env:
|
||||
POSTGRES_PASSWORD: affine
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 5432:5432
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-node
|
||||
with:
|
||||
playwright-install: true
|
||||
- name: Initialize database
|
||||
run: |
|
||||
psql -h localhost -U postgres -c "CREATE DATABASE affine;"
|
||||
psql -h localhost -U postgres -c "CREATE USER affine WITH PASSWORD 'affine';"
|
||||
psql -h localhost -U postgres -c "ALTER USER affine WITH SUPERUSER;"
|
||||
env:
|
||||
PGPASSWORD: affine
|
||||
- name: Generate prisma client
|
||||
run: |
|
||||
yarn exec prisma generate
|
||||
yarn exec prisma db push
|
||||
working-directory: apps/server
|
||||
env:
|
||||
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
|
||||
- name: Run init-db script
|
||||
run: yarn exec ts-node-esm ./scripts/init-db.ts
|
||||
working-directory: apps/server
|
||||
env:
|
||||
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
|
||||
- name: Download storage.node
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: storage.node
|
||||
path: ./apps/server
|
||||
|
||||
- name: Run playwright tests
|
||||
run: yarn e2e --forbid-only
|
||||
working-directory: tests/affine-cloud
|
||||
env:
|
||||
COVERAGE: true
|
||||
DATABASE_URL: postgresql://affine:affine@localhost:5432/affine
|
||||
|
||||
- name: Collect code coverage report
|
||||
run: yarn exec nyc report -t .nyc_output --report-dir .coverage --reporter=lcov
|
||||
|
||||
- name: Upload e2e test coverage results
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./.coverage/lcov.info
|
||||
flags: server-e2etest
|
||||
name: affine
|
||||
fail_ci_if_error: false
|
||||
|
||||
- name: Upload test results
|
||||
if: ${{ failure() }}
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: test-results-e2e-server
|
||||
path: ./tests/affine-cloud/test-results
|
||||
if-no-files-found: ignore
|
||||
|
||||
e2e-plugin-test:
|
||||
name: E2E Plugin Test
|
||||
runs-on: ubuntu-latest
|
||||
|
211
.github/workflows/deploy.yml
vendored
Normal file
211
.github/workflows/deploy.yml
vendored
Normal file
@ -0,0 +1,211 @@
|
||||
name: Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
flavor:
|
||||
description: 'Build type (canary, beta, internal or stable)'
|
||||
type: string
|
||||
default: canary
|
||||
|
||||
env:
|
||||
BUILD_TYPE: canary
|
||||
APP_NAME: affine
|
||||
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
|
||||
|
||||
jobs:
|
||||
build-server:
|
||||
name: Build Server
|
||||
runs-on: ubuntu-latest
|
||||
environment: ${{ github.event.inputs.flavor }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-node
|
||||
with:
|
||||
electron-install: false
|
||||
- name: Build Server
|
||||
run: yarn workspace @affine/server build
|
||||
- name: Upload server dist
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: server-dist
|
||||
path: ./apps/server/dist
|
||||
if-no-files-found: error
|
||||
build-core:
|
||||
name: Build @affine/core
|
||||
runs-on: ubuntu-latest
|
||||
environment: production
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-node
|
||||
- name: Build Plugins
|
||||
run: yarn run build:plugins
|
||||
- name: Build Core
|
||||
run: yarn nx build @affine/core
|
||||
env:
|
||||
R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
|
||||
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||
BUILD_TYPE_OVERRIDE: ${{ github.event.inputs.flavor }}
|
||||
SHOULD_REPORT_TRACE: true
|
||||
TRACE_REPORT_ENDPOINT: ${{ secrets.TRACE_REPORT_ENDPOINT }}
|
||||
- name: Upload core artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: core
|
||||
path: ./apps/core/dist
|
||||
if-no-files-found: error
|
||||
|
||||
build-storage:
|
||||
name: Build Storage
|
||||
runs-on: ubuntu-latest
|
||||
environment: ${{ github.event.inputs.flavor }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-node
|
||||
- name: Setup Rust
|
||||
uses: ./.github/actions/build-rust
|
||||
with:
|
||||
target: 'x86_64-unknown-linux-gnu'
|
||||
package: '@affine/storage'
|
||||
nx_token: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
|
||||
- name: Upload storage.node
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: storage.node
|
||||
path: ./packages/storage/storage.node
|
||||
if-no-files-found: error
|
||||
|
||||
build-docker:
|
||||
name: Build Docker
|
||||
runs-on: ubuntu-latest
|
||||
environment: ${{ github.event.inputs.flavor }}
|
||||
needs:
|
||||
- build-server
|
||||
- build-core
|
||||
- build-storage
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Download core artifact
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: core
|
||||
path: ./apps/core/dist
|
||||
- name: Download server dist
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: server-dist
|
||||
path: ./apps/server/dist
|
||||
- name: Download storage.node
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: storage.node
|
||||
path: ./apps/server
|
||||
- name: Setup env
|
||||
run: |
|
||||
echo "GIT_SHORT_HASH=$(git rev-parse --short HEAD)" >> "$GITHUB_ENV"
|
||||
if [ -z "${{ inputs.flavor }}" ]
|
||||
then
|
||||
echo "RELEASE_FLAVOR=canary" >> "$GITHUB_ENV"
|
||||
else
|
||||
echo "RELEASE_FLAVOR=${{ inputs.flavor }}" >> "$GITHUB_ENV"
|
||||
fi
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
logout: false
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: Build front Dockerfile
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
pull: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
provenance: true
|
||||
file: .github/deployment/front/Dockerfile
|
||||
tags: ghcr.io/toeverything/affine-front:${{env.RELEASE_FLAVOR}}-${{ env.GIT_SHORT_HASH }},ghcr.io/toeverything/affine-front:${{env.RELEASE_FLAVOR}}
|
||||
|
||||
# setup node without cache configuration
|
||||
# Prisma cache is not compatible with docker build cache
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
registry-url: https://npm.pkg.github.com
|
||||
scope: '@toeverything'
|
||||
|
||||
- name: Install Node.js dependencies
|
||||
run: yarn workspaces focus @affine/server --production
|
||||
|
||||
- name: Generate Prisma client
|
||||
run: yarn workspace @affine/server prisma generate
|
||||
|
||||
- name: Build graphql Dockerfile
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
pull: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
provenance: true
|
||||
file: .github/deployment/node/Dockerfile
|
||||
tags: ghcr.io/toeverything/affine-graphql:${{env.RELEASE_FLAVOR}}-${{ env.GIT_SHORT_HASH }},ghcr.io/toeverything/affine-front:${{env.RELEASE_FLAVOR}}
|
||||
|
||||
deploy:
|
||||
name: Deploy to cluster
|
||||
environment: ${{ github.event.inputs.flavor }}
|
||||
permissions:
|
||||
contents: 'write'
|
||||
id-token: 'write'
|
||||
needs:
|
||||
- build-docker
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Deploy to dev
|
||||
uses: ./.github/actions/deploy
|
||||
with:
|
||||
build-type: ${{ github.event.inputs.flavor }}
|
||||
gcp-project-number: ${{ secrets.GCP_PROJECT_NUMBER }}
|
||||
gcp-project-id: ${{ secrets.GCP_PROJECT_ID }}
|
||||
service-account: ${{ secrets.GCP_HELM_DEPLOY_SERVICE_ACCOUNT }}
|
||||
cluster-name: ${{ secrets.GCP_CLUSTER_NAME }}
|
||||
cluster-location: ${{ secrets.GCP_CLUSTER_LOCATION }}
|
||||
env:
|
||||
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
|
||||
CANARY_DEPLOY_HOST: ${{ secrets.CANARY_DEPLOY_HOST }}
|
||||
R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
|
||||
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||
R2_BUCKET: ${{ secrets.R2_BUCKET }}
|
||||
OAUTH_EMAIL_SENDER: ${{ secrets.OAUTH_EMAIL_SENDER }}
|
||||
OAUTH_EMAIL_LOGIN: ${{ secrets.OAUTH_EMAIL_LOGIN }}
|
||||
OAUTH_EMAIL_PASSWORD: ${{ secrets.OAUTH_EMAIL_PASSWORD }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
AFFINE_GOOGLE_CLIENT_ID: ${{ secrets.AFFINE_GOOGLE_CLIENT_ID }}
|
||||
AFFINE_GOOGLE_CLIENT_SECRET: ${{ secrets.AFFINE_GOOGLE_CLIENT_SECRET }}
|
||||
DATABASE_URL: ${{ secrets.DATABASE_URL }}
|
||||
DATABASE_USERNAME: ${{ secrets.DATABASE_USERNAME }}
|
||||
DATABASE_PASSWORD: ${{ secrets.DATABASE_PASSWORD }}
|
||||
DATABASE_NAME: ${{ secrets.DATABASE_NAME }}
|
||||
GCLOUD_CONNECTION_NAME: ${{ secrets.GCLOUD_CONNECTION_NAME }}
|
||||
GCLOUD_CLOUD_SQL_INTERNAL_ENDPOINT: ${{ secrets.GCLOUD_CLOUD_SQL_INTERNAL_ENDPOINT }}
|
||||
REDIS_HOST: ${{ secrets.REDIS_HOST }}
|
||||
REDIS_PASSWORD: ${{ secrets.REDIS_PASSWORD }}
|
||||
CLOUD_SQL_IAM_ACCOUNT: ${{ secrets.CLOUD_SQL_IAM_ACCOUNT }}
|
2
.github/workflows/nightly-build.yml
vendored
2
.github/workflows/nightly-build.yml
vendored
@ -12,7 +12,6 @@ on:
|
||||
- .github/**
|
||||
- '!.github/workflows/nightly-build.yml'
|
||||
- '!.github/actions/build-rust/action.yml'
|
||||
- '!.github/actions/setup-rust/action.yml'
|
||||
- '!.github/actions/setup-node/action.yml'
|
||||
|
||||
permissions:
|
||||
@ -114,6 +113,7 @@ jobs:
|
||||
uses: ./.github/actions/build-rust
|
||||
with:
|
||||
target: ${{ matrix.spec.target }}
|
||||
package: '@affine/native'
|
||||
nx_token: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
|
||||
- name: Replace Version
|
||||
run: ./scripts/set-version.sh ${{ needs.set-build-version.outputs.version }}
|
||||
|
7
.github/workflows/publish-storybook.yml
vendored
7
.github/workflows/publish-storybook.yml
vendored
@ -1,5 +1,8 @@
|
||||
name: Publish Storybook
|
||||
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
@ -34,5 +37,7 @@ jobs:
|
||||
with:
|
||||
workingDir: apps/storybook
|
||||
buildScriptName: build
|
||||
onlyChanged: true
|
||||
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
||||
zip: true
|
||||
env:
|
||||
NODE_OPTIONS: ${{ env.NODE_OPTIONS }}
|
||||
|
2
.github/workflows/release-desktop-app.yml
vendored
2
.github/workflows/release-desktop-app.yml
vendored
@ -112,6 +112,7 @@ jobs:
|
||||
uses: ./.github/actions/build-rust
|
||||
with:
|
||||
target: ${{ matrix.spec.target }}
|
||||
package: '@affine/native'
|
||||
nx_token: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
@ -182,6 +183,7 @@ jobs:
|
||||
uses: ./.github/actions/build-rust
|
||||
with:
|
||||
target: ${{ matrix.spec.target }}
|
||||
package: '@affine/native'
|
||||
nx_token: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
|
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@ -76,11 +76,11 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: ./.github/actions/setup-node
|
||||
- name: Setup Rust
|
||||
uses: ./.github/actions/setup-rust
|
||||
uses: ./.github/actions/build-rust
|
||||
with:
|
||||
target: 'x86_64-unknown-linux-gnu'
|
||||
- name: Build Storage
|
||||
run: yarn build:storage
|
||||
package: '@affine/storage'
|
||||
nx_token: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
|
||||
- name: Upload storage.node
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -19,7 +19,7 @@
|
||||
node_modules
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
**/.idea
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
@ -77,3 +77,4 @@ target
|
||||
tsconfig.node.tsbuildinfo
|
||||
lib
|
||||
affine.db
|
||||
apps/web/next-routes.conf
|
||||
|
@ -14,3 +14,4 @@ _next
|
||||
storybook-static
|
||||
web-static
|
||||
public
|
||||
apps/server/src/schema.gql
|
||||
|
16
.vscode/launch.template.json
vendored
16
.vscode/launch.template.json
vendored
@ -2,16 +2,24 @@
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"command": "yarn run dev",
|
||||
"name": "Run Dev",
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
"command": "yarn run dev"
|
||||
},
|
||||
{
|
||||
"command": "yarn run dev:local",
|
||||
"name": "Run Dev Locally",
|
||||
"type": "node-terminal",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
"command": "yarn run dev:local"
|
||||
},
|
||||
{
|
||||
"name": "Launch AFFiNE Cloud",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"runtimeExecutable": "yarn",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"runtimeArgs": ["workspace", "@affine/server", "dev"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
3261
Cargo.lock
generated
3261
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
11
Cargo.toml
11
Cargo.toml
@ -1,5 +1,10 @@
|
||||
[workspace]
|
||||
members = ["./packages/native", "./packages/native/schema"]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"./packages/native",
|
||||
"./packages/native/schema",
|
||||
"./packages/storage",
|
||||
]
|
||||
|
||||
[profile.dev.package.sqlx-macros]
|
||||
opt-level = 3
|
||||
@ -9,3 +14,7 @@ lto = true
|
||||
codegen-units = 1
|
||||
opt-level = 3
|
||||
strip = "symbols"
|
||||
|
||||
[patch.crates-io]
|
||||
lib0 = { git = "https://github.com/toeverything/y-crdt", rev = "a700f09" }
|
||||
yrs = { git = "https://github.com/toeverything/y-crdt", rev = "a700f09" }
|
||||
|
409
LICENSE
409
LICENSE
@ -1,384 +1,25 @@
|
||||
# Mozilla Public License Version 2.0
|
||||
|
||||
Copyright (c) TOEVERYTHING PTE. LTD. and its affiliates.
|
||||
|
||||
1. Definitions
|
||||
|
||||
---
|
||||
|
||||
1.1. "Contributor"
|
||||
means each individual or legal entity that creates, contributes to
|
||||
the creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
means the combination of the Contributions of others (if any) used
|
||||
by a Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
means Source Code Form to which the initial Contributor has attached
|
||||
the notice in Exhibit A, the Executable Form of such Source Code
|
||||
Form, and Modifications of such Source Code Form, in each case
|
||||
including portions thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
(a) that the initial Contributor has attached the notice described
|
||||
in Exhibit B to the Covered Software; or
|
||||
|
||||
(b) that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the
|
||||
terms of a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
means a work that combines Covered Software with other material, in
|
||||
a separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
means having the right to grant, to the maximum extent possible,
|
||||
whether at the time of the initial grant or subsequently, any and
|
||||
all of the rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
means any of the following:
|
||||
|
||||
(a) any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered
|
||||
Software; or
|
||||
|
||||
(b) any new file in Source Code Form that contains any Covered
|
||||
Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the
|
||||
License, by the making, using, selling, offering for sale, having
|
||||
made, import, or transfer of either its Contributions or its
|
||||
Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
means either the GNU General Public License, Version 2.0, the GNU
|
||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||
Public License, Version 3.0, or any later versions of those
|
||||
licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that
|
||||
controls, is controlled by, or is under common control with You. For
|
||||
purposes of this definition, "control" means (a) the power, direct
|
||||
or indirect, to cause the direction or management of such entity,
|
||||
whether by contract or otherwise, or (b) ownership of more than
|
||||
fifty percent (50%) of the outstanding shares or beneficial
|
||||
ownership of such entity.
|
||||
|
||||
2. License Grants and Conditions
|
||||
|
||||
---
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
(a) under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||
for sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
(a) for any code that a Contributor has removed from Covered Software;
|
||||
or
|
||||
|
||||
(b) for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights
|
||||
to grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||
in Section 2.1.
|
||||
|
||||
3. Responsibilities
|
||||
|
||||
---
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
(a) such Covered Software must also be made available in Source Code
|
||||
Form, as described in Section 3.1, and You must inform recipients of
|
||||
the Executable Form how they can obtain a copy of such Source Code
|
||||
Form by reasonable means in a timely manner, at a charge no more
|
||||
than the cost of distribution to the recipient; and
|
||||
|
||||
(b) You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter
|
||||
the recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty,
|
||||
or limitations of liability) contained within the Source Code Form of
|
||||
the Covered Software, except that You may alter any license notices to
|
||||
the extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
|
||||
---
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this
|
||||
License with respect to some or all of the Covered Software due to
|
||||
statute, judicial order, or regulation then You must: (a) comply with
|
||||
the terms of this License to the maximum extent possible; and (b)
|
||||
describe the limitations and the code they affect. Such description must
|
||||
be placed in a text file included with all distributions of the Covered
|
||||
Software under this License. Except to the extent prohibited by statute
|
||||
or regulation, such description must be sufficiently detailed for a
|
||||
recipient of ordinary skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
|
||||
---
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically
|
||||
if You fail to comply with any of its terms. However, if You become
|
||||
compliant, then the rights granted under this License from a particular
|
||||
Contributor are reinstated (a) provisionally, unless and until such
|
||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||
ongoing basis, if such Contributor fails to notify You of the
|
||||
non-compliance by some reasonable means prior to 60 days after You have
|
||||
come back into compliance. Moreover, Your grants from a particular
|
||||
Contributor are reinstated on an ongoing basis if such Contributor
|
||||
notifies You of the non-compliance by some reasonable means, this is the
|
||||
first time You have received notice of non-compliance with this License
|
||||
from such Contributor, and You become compliant prior to 30 days after
|
||||
Your receipt of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||
end user license agreements (excluding distributors and resellers) which
|
||||
have been validly granted by You or Your distributors under this License
|
||||
prior to termination shall survive termination.
|
||||
|
||||
---
|
||||
|
||||
- *
|
||||
- 6. Disclaimer of Warranty \*
|
||||
- ------------------------- \*
|
||||
- *
|
||||
- Covered Software is provided under this License on an "as is" \*
|
||||
- basis, without warranty of any kind, either expressed, implied, or \*
|
||||
- statutory, including, without limitation, warranties that the \*
|
||||
- Covered Software is free of defects, merchantable, fit for a \*
|
||||
- particular purpose or non-infringing. The entire risk as to the \*
|
||||
- quality and performance of the Covered Software is with You. \*
|
||||
- Should any Covered Software prove defective in any respect, You \*
|
||||
- (not any Contributor) assume the cost of any necessary servicing, \*
|
||||
- repair, or correction. This disclaimer of warranty constitutes an \*
|
||||
- essential part of this License. No use of any Covered Software is \*
|
||||
- authorized under this License except under this disclaimer. \*
|
||||
- *
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
- *
|
||||
- 7. Limitation of Liability \*
|
||||
- -------------------------- \*
|
||||
- *
|
||||
- Under no circumstances and under no legal theory, whether tort \*
|
||||
- (including negligence), contract, or otherwise, shall any \*
|
||||
- Contributor, or anyone who distributes Covered Software as \*
|
||||
- permitted above, be liable to You for any direct, indirect, \*
|
||||
- special, incidental, or consequential damages of any character \*
|
||||
- including, without limitation, damages for lost profits, loss of \*
|
||||
- goodwill, work stoppage, computer failure or malfunction, or any \*
|
||||
- and all other commercial damages or losses, even if such party \*
|
||||
- shall have been informed of the possibility of such damages. This \*
|
||||
- limitation of liability shall not apply to liability for death or \*
|
||||
- personal injury resulting from such party's negligence to the \*
|
||||
- extent applicable law prohibits such limitation. Some \*
|
||||
- jurisdictions do not allow the exclusion or limitation of \*
|
||||
- incidental or consequential damages, so this exclusion and \*
|
||||
- limitation may not apply to You. \*
|
||||
- *
|
||||
|
||||
---
|
||||
|
||||
8. Litigation
|
||||
|
||||
---
|
||||
|
||||
Any litigation relating to this License may be brought only in the
|
||||
courts of a jurisdiction where the defendant maintains its principal
|
||||
place of business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions.
|
||||
Nothing in this Section shall prevent a party's ability to bring
|
||||
cross-claims or counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
|
||||
---
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides
|
||||
that the language of a contract shall be construed against the drafter
|
||||
shall not be used to construe this License against a Contributor.
|
||||
|
||||
10. Versions of the License
|
||||
|
||||
---
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses
|
||||
|
||||
If You choose to distribute Source Code Form that is Incompatible With
|
||||
Secondary Licenses under the terms of this version of the License, the
|
||||
notice described in Exhibit B of this License must be attached.
|
||||
|
||||
## Exhibit A - Source Code Form License Notice
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
file in a relevant directory) where a recipient would be likely to look
|
||||
for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
## Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
|
||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
defined by the Mozilla Public License, v. 2.0.
|
||||
Copyright (c) 2022-present TOEVERYTHING PTE. LTD. and its affiliates.
|
||||
|
||||
Portions of this software are licensed as follows:
|
||||
|
||||
* All content that resides under the "apps/server" directory of this repository, if that directory exists, is licensed under the license defined in "apps/server/LICENSE".
|
||||
* All third party components incorporated into the AFFiNE Software are licensed under the original license provided by the owner of the applicable component.
|
||||
* Content outside of the above mentioned directories or restrictions above is available under the "MPL2.0" license as defined in "LICENSE-MPL2.0".
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
384
LICENSE-MPL2.0
Normal file
384
LICENSE-MPL2.0
Normal file
@ -0,0 +1,384 @@
|
||||
# Mozilla Public License Version 2.0
|
||||
|
||||
Copyright (c) TOEVERYTHING PTE. LTD. and its affiliates.
|
||||
|
||||
1. Definitions
|
||||
|
||||
---
|
||||
|
||||
1.1. "Contributor"
|
||||
means each individual or legal entity that creates, contributes to
|
||||
the creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
means the combination of the Contributions of others (if any) used
|
||||
by a Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
means Source Code Form to which the initial Contributor has attached
|
||||
the notice in Exhibit A, the Executable Form of such Source Code
|
||||
Form, and Modifications of such Source Code Form, in each case
|
||||
including portions thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
(a) that the initial Contributor has attached the notice described
|
||||
in Exhibit B to the Covered Software; or
|
||||
|
||||
(b) that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the
|
||||
terms of a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
means a work that combines Covered Software with other material, in
|
||||
a separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
means having the right to grant, to the maximum extent possible,
|
||||
whether at the time of the initial grant or subsequently, any and
|
||||
all of the rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
means any of the following:
|
||||
|
||||
(a) any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered
|
||||
Software; or
|
||||
|
||||
(b) any new file in Source Code Form that contains any Covered
|
||||
Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the
|
||||
License, by the making, using, selling, offering for sale, having
|
||||
made, import, or transfer of either its Contributions or its
|
||||
Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
means either the GNU General Public License, Version 2.0, the GNU
|
||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||
Public License, Version 3.0, or any later versions of those
|
||||
licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that
|
||||
controls, is controlled by, or is under common control with You. For
|
||||
purposes of this definition, "control" means (a) the power, direct
|
||||
or indirect, to cause the direction or management of such entity,
|
||||
whether by contract or otherwise, or (b) ownership of more than
|
||||
fifty percent (50%) of the outstanding shares or beneficial
|
||||
ownership of such entity.
|
||||
|
||||
2. License Grants and Conditions
|
||||
|
||||
---
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
(a) under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||
for sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
(a) for any code that a Contributor has removed from Covered Software;
|
||||
or
|
||||
|
||||
(b) for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights
|
||||
to grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||
in Section 2.1.
|
||||
|
||||
3. Responsibilities
|
||||
|
||||
---
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
(a) such Covered Software must also be made available in Source Code
|
||||
Form, as described in Section 3.1, and You must inform recipients of
|
||||
the Executable Form how they can obtain a copy of such Source Code
|
||||
Form by reasonable means in a timely manner, at a charge no more
|
||||
than the cost of distribution to the recipient; and
|
||||
|
||||
(b) You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter
|
||||
the recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty,
|
||||
or limitations of liability) contained within the Source Code Form of
|
||||
the Covered Software, except that You may alter any license notices to
|
||||
the extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
|
||||
---
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this
|
||||
License with respect to some or all of the Covered Software due to
|
||||
statute, judicial order, or regulation then You must: (a) comply with
|
||||
the terms of this License to the maximum extent possible; and (b)
|
||||
describe the limitations and the code they affect. Such description must
|
||||
be placed in a text file included with all distributions of the Covered
|
||||
Software under this License. Except to the extent prohibited by statute
|
||||
or regulation, such description must be sufficiently detailed for a
|
||||
recipient of ordinary skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
|
||||
---
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically
|
||||
if You fail to comply with any of its terms. However, if You become
|
||||
compliant, then the rights granted under this License from a particular
|
||||
Contributor are reinstated (a) provisionally, unless and until such
|
||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||
ongoing basis, if such Contributor fails to notify You of the
|
||||
non-compliance by some reasonable means prior to 60 days after You have
|
||||
come back into compliance. Moreover, Your grants from a particular
|
||||
Contributor are reinstated on an ongoing basis if such Contributor
|
||||
notifies You of the non-compliance by some reasonable means, this is the
|
||||
first time You have received notice of non-compliance with this License
|
||||
from such Contributor, and You become compliant prior to 30 days after
|
||||
Your receipt of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||
end user license agreements (excluding distributors and resellers) which
|
||||
have been validly granted by You or Your distributors under this License
|
||||
prior to termination shall survive termination.
|
||||
|
||||
---
|
||||
|
||||
- *
|
||||
- 6. Disclaimer of Warranty \*
|
||||
- ------------------------- \*
|
||||
- *
|
||||
- Covered Software is provided under this License on an "as is" \*
|
||||
- basis, without warranty of any kind, either expressed, implied, or \*
|
||||
- statutory, including, without limitation, warranties that the \*
|
||||
- Covered Software is free of defects, merchantable, fit for a \*
|
||||
- particular purpose or non-infringing. The entire risk as to the \*
|
||||
- quality and performance of the Covered Software is with You. \*
|
||||
- Should any Covered Software prove defective in any respect, You \*
|
||||
- (not any Contributor) assume the cost of any necessary servicing, \*
|
||||
- repair, or correction. This disclaimer of warranty constitutes an \*
|
||||
- essential part of this License. No use of any Covered Software is \*
|
||||
- authorized under this License except under this disclaimer. \*
|
||||
- *
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
- *
|
||||
- 7. Limitation of Liability \*
|
||||
- -------------------------- \*
|
||||
- *
|
||||
- Under no circumstances and under no legal theory, whether tort \*
|
||||
- (including negligence), contract, or otherwise, shall any \*
|
||||
- Contributor, or anyone who distributes Covered Software as \*
|
||||
- permitted above, be liable to You for any direct, indirect, \*
|
||||
- special, incidental, or consequential damages of any character \*
|
||||
- including, without limitation, damages for lost profits, loss of \*
|
||||
- goodwill, work stoppage, computer failure or malfunction, or any \*
|
||||
- and all other commercial damages or losses, even if such party \*
|
||||
- shall have been informed of the possibility of such damages. This \*
|
||||
- limitation of liability shall not apply to liability for death or \*
|
||||
- personal injury resulting from such party's negligence to the \*
|
||||
- extent applicable law prohibits such limitation. Some \*
|
||||
- jurisdictions do not allow the exclusion or limitation of \*
|
||||
- incidental or consequential damages, so this exclusion and \*
|
||||
- limitation may not apply to You. \*
|
||||
- *
|
||||
|
||||
---
|
||||
|
||||
8. Litigation
|
||||
|
||||
---
|
||||
|
||||
Any litigation relating to this License may be brought only in the
|
||||
courts of a jurisdiction where the defendant maintains its principal
|
||||
place of business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions.
|
||||
Nothing in this Section shall prevent a party's ability to bring
|
||||
cross-claims or counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
|
||||
---
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides
|
||||
that the language of a contract shall be construed against the drafter
|
||||
shall not be used to construe this License against a Contributor.
|
||||
|
||||
10. Versions of the License
|
||||
|
||||
---
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses
|
||||
|
||||
If You choose to distribute Source Code Form that is Incompatible With
|
||||
Secondary Licenses under the terms of this version of the License, the
|
||||
notice described in Exhibit B of this License must be attached.
|
||||
|
||||
## Exhibit A - Source Code Form License Notice
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
file in a relevant directory) where a recipient would be likely to look
|
||||
for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
## Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
|
||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
defined by the Mozilla Public License, v. 2.0.
|
@ -3,11 +3,15 @@ function testPackageName(regexp: RegExp): (module: any) => boolean {
|
||||
module.nameForCondition && regexp.test(module.nameForCondition());
|
||||
}
|
||||
|
||||
// https://hackernoon.com/the-100-correct-way-to-split-your-chunks-with-webpack-f8a9df5b7758
|
||||
export const productionCacheGroups = {
|
||||
asyncVendor: {
|
||||
test: /[\\/]node_modules[\\/]/,
|
||||
name(module: any) {
|
||||
// https://hackernoon.com/the-100-correct-way-to-split-your-chunks-with-webpack-f8a9df5b7758
|
||||
// monorepo linked in node_modules, so it's not a npm package
|
||||
if (!module.context.includes('node_modules')) {
|
||||
return `app-async`;
|
||||
}
|
||||
const name = module.context.match(
|
||||
/[\\/]node_modules[\\/](.*?)([\\/]|$)/
|
||||
)?.[1];
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { join, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { createRequire } from 'node:module';
|
||||
|
||||
import type { Configuration as DevServerConfiguration } from 'webpack-dev-server';
|
||||
import { PerfseePlugin } from '@perfsee/webpack';
|
||||
import { sentryWebpackPlugin } from '@sentry/webpack-plugin';
|
||||
@ -10,13 +11,14 @@ import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin';
|
||||
import TerserPlugin from 'terser-webpack-plugin';
|
||||
import webpack from 'webpack';
|
||||
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
|
||||
import { compact } from 'lodash-es';
|
||||
|
||||
import { productionCacheGroups } from './cache-group.js';
|
||||
import type { BuildFlags } from '@affine/cli/config';
|
||||
import { projectRoot } from '@affine/cli/config';
|
||||
import { VanillaExtractPlugin } from '@vanilla-extract/webpack-plugin';
|
||||
import { computeCacheKey } from './utils.js';
|
||||
import type { RuntimeConfig } from '@affine/env/global';
|
||||
import { WebpackS3Plugin, gitShortHash } from './s3-plugin.js';
|
||||
|
||||
const IN_CI = !!process.env.CI;
|
||||
|
||||
@ -67,16 +69,26 @@ const OptimizeOptionOptions: (
|
||||
},
|
||||
});
|
||||
|
||||
export const publicPath = (function () {
|
||||
const { BUILD_TYPE } = process.env;
|
||||
const publicPath = process.env.PUBLIC_PATH ?? '/';
|
||||
if (process.env.COVERAGE) {
|
||||
return publicPath;
|
||||
}
|
||||
if (BUILD_TYPE === 'canary') {
|
||||
return `https://dev.affineassets.com/${gitShortHash()}/`;
|
||||
} else if (BUILD_TYPE === 'beta' || BUILD_TYPE === 'stable') {
|
||||
return `https://prod.affineassets.com/${gitShortHash()}/`;
|
||||
}
|
||||
return publicPath;
|
||||
})();
|
||||
|
||||
export const createConfiguration: (
|
||||
buildFlags: BuildFlags,
|
||||
runtimeConfig: RuntimeConfig
|
||||
) => webpack.Configuration = (buildFlags, runtimeConfig) => {
|
||||
let publicPath = process.env.PUBLIC_PATH ?? '/';
|
||||
|
||||
const blocksuiteBaseDir = buildFlags.localBlockSuite;
|
||||
|
||||
const cacheKey = computeCacheKey(buildFlags);
|
||||
|
||||
const config = {
|
||||
name: 'affine',
|
||||
// to set a correct base path for the source map
|
||||
@ -96,8 +108,11 @@ export const createConfiguration: (
|
||||
? 'js/[name]-[contenthash:8].js'
|
||||
: 'js/[name].js',
|
||||
// In some cases webpack will emit files starts with "_" which is reserved in web extension.
|
||||
chunkFilename: 'js/chunk.[name].js',
|
||||
assetModuleFilename: 'assets/[contenthash:8][ext][query]',
|
||||
chunkFilename:
|
||||
buildFlags.mode === 'production'
|
||||
? 'js/chunk.[name]-[contenthash:8].js'
|
||||
: 'js/chunk.[name].js',
|
||||
assetModuleFilename: 'assets/[name]-[contenthash:8][ext][query]',
|
||||
devtoolModuleFilenameTemplate: 'webpack://[namespace]/[resource-path]',
|
||||
hotUpdateChunkFilename: 'hot/[id].[fullhash].js',
|
||||
hotUpdateMainFilename: 'hot/[runtime].[fullhash].json',
|
||||
@ -192,14 +207,6 @@ export const createConfiguration: (
|
||||
},
|
||||
},
|
||||
|
||||
cache: {
|
||||
type: 'filesystem',
|
||||
buildDependencies: {
|
||||
config: [fileURLToPath(import.meta.url)],
|
||||
},
|
||||
version: cacheKey,
|
||||
},
|
||||
|
||||
module: {
|
||||
parser: {
|
||||
javascript: {
|
||||
@ -308,7 +315,7 @@ export const createConfiguration: (
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
url: false,
|
||||
url: true,
|
||||
sourceMap: false,
|
||||
modules: false,
|
||||
import: true,
|
||||
@ -333,22 +340,23 @@ export const createConfiguration: (
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
plugins: [
|
||||
...(IN_CI ? [] : [new webpack.ProgressPlugin({ percentBy: 'entries' })]),
|
||||
...(buildFlags.mode === 'development'
|
||||
? [new ReactRefreshWebpackPlugin({ overlay: false, esModule: true })]
|
||||
: [
|
||||
new MiniCssExtractPlugin({
|
||||
filename: `[name].[contenthash:8].css`,
|
||||
ignoreOrder: true,
|
||||
}),
|
||||
]),
|
||||
plugins: compact([
|
||||
IN_CI ? null : new webpack.ProgressPlugin({ percentBy: 'entries' }),
|
||||
buildFlags.mode === 'development'
|
||||
? new ReactRefreshWebpackPlugin({ overlay: false, esModule: true })
|
||||
: new MiniCssExtractPlugin({
|
||||
filename: `[name].[contenthash:8].css`,
|
||||
ignoreOrder: true,
|
||||
}),
|
||||
new VanillaExtractPlugin(),
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': JSON.stringify({}),
|
||||
'process.env.COVERAGE': JSON.stringify(!!buildFlags.coverage),
|
||||
'process.env.NODE_ENV': JSON.stringify(buildFlags.mode),
|
||||
'process.env.SHOULD_REPORT_TRACE': `${Boolean(
|
||||
process.env.SHOULD_REPORT_TRACE
|
||||
)}`,
|
||||
'process.env.TRACE_REPORT_ENDPOINT': `"${process.env.TRACE_REPORT_ENDPOINT}"`,
|
||||
runtimeConfig: JSON.stringify(runtimeConfig),
|
||||
}),
|
||||
new CopyPlugin({
|
||||
@ -359,7 +367,10 @@ export const createConfiguration: (
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
buildFlags.mode === 'production' && process.env.R2_SECRET_ACCESS_KEY
|
||||
? new WebpackS3Plugin()
|
||||
: null,
|
||||
]),
|
||||
|
||||
optimization: OptimizeOptionOptions(buildFlags),
|
||||
|
||||
@ -373,6 +384,14 @@ export const createConfiguration: (
|
||||
publicPath: '/',
|
||||
watch: true,
|
||||
},
|
||||
proxy: {
|
||||
'/api': 'http://localhost:3010',
|
||||
'/socket.io': {
|
||||
target: 'http://localhost:3010',
|
||||
ws: true,
|
||||
},
|
||||
'/graphql': 'http://localhost:3010',
|
||||
},
|
||||
} as DevServerConfiguration,
|
||||
} satisfies webpack.Configuration;
|
||||
|
||||
|
@ -24,9 +24,9 @@ const editorFlags: BlockSuiteFeatureFlags = {
|
||||
};
|
||||
|
||||
export function getRuntimeConfig(buildFlags: BuildFlags): RuntimeConfig {
|
||||
const buildPreset: Record<string, RuntimeConfig> = {
|
||||
const buildPreset: Record<BuildFlags['channel'], RuntimeConfig> = {
|
||||
stable: {
|
||||
enablePlugin: false,
|
||||
enablePlugin: true,
|
||||
enableTestProperties: false,
|
||||
enableBroadcastChannelProvider: true,
|
||||
enableDebugPage: true,
|
||||
@ -37,13 +37,26 @@ export function getRuntimeConfig(buildFlags: BuildFlags): RuntimeConfig {
|
||||
enableNewSettingUnstableApi: false,
|
||||
enableSQLiteProvider: true,
|
||||
enableMoveDatabase: false,
|
||||
enableNotificationCenter: false,
|
||||
enableCloud: false,
|
||||
serverAPI: 'https://localhost:3010',
|
||||
enableNotificationCenter: true,
|
||||
enableCloud: true,
|
||||
enableEnhanceShareMode: false,
|
||||
serverUrlPrefix: 'https://app.affine.pro',
|
||||
editorFlags,
|
||||
appVersion: packageJson.version,
|
||||
editorVersion: packageJson.dependencies['@blocksuite/editor'],
|
||||
},
|
||||
get beta() {
|
||||
return {
|
||||
...this.stable,
|
||||
serverUrlPrefix: 'https://ambassador.affine.pro',
|
||||
};
|
||||
},
|
||||
get internal() {
|
||||
return {
|
||||
...this.stable,
|
||||
serverUrlPrefix: 'https://affine.fail',
|
||||
};
|
||||
},
|
||||
// canary will be aggressive and enable all features
|
||||
canary: {
|
||||
enablePlugin: true,
|
||||
@ -58,18 +71,15 @@ export function getRuntimeConfig(buildFlags: BuildFlags): RuntimeConfig {
|
||||
enableSQLiteProvider: true,
|
||||
enableMoveDatabase: false,
|
||||
enableNotificationCenter: true,
|
||||
enableCloud: false,
|
||||
serverAPI: 'https://localhost:3010',
|
||||
enableCloud: true,
|
||||
enableEnhanceShareMode: false,
|
||||
serverUrlPrefix: 'https://affine.fail',
|
||||
editorFlags,
|
||||
appVersion: packageJson.version,
|
||||
editorVersion: packageJson.dependencies['@blocksuite/editor'],
|
||||
},
|
||||
};
|
||||
|
||||
// beta and internal versions are the same as stable
|
||||
buildPreset.beta = buildPreset.stable;
|
||||
buildPreset.internal = buildPreset.stable;
|
||||
|
||||
const currentBuild = buildFlags.channel;
|
||||
|
||||
if (!(currentBuild in buildPreset)) {
|
||||
@ -107,11 +117,18 @@ export function getRuntimeConfig(buildFlags: BuildFlags): RuntimeConfig {
|
||||
enableCloud: process.env.ENABLE_CLOUD
|
||||
? process.env.ENABLE_CLOUD === 'true'
|
||||
: currentBuildPreset.enableCloud,
|
||||
enableEnhanceShareMode: process.env.ENABLE_ENHANCE_SHARE_MODE
|
||||
? process.env.ENABLE_ENHANCE_SHARE_MODE === 'true'
|
||||
: currentBuildPreset.enableEnhanceShareMode,
|
||||
enableMoveDatabase: process.env.ENABLE_MOVE_DATABASE
|
||||
? process.env.ENABLE_MOVE_DATABASE === 'true'
|
||||
: currentBuildPreset.enableMoveDatabase,
|
||||
};
|
||||
|
||||
if (buildFlags.mode === 'development') {
|
||||
currentBuildPreset.serverUrlPrefix = 'http://localhost:8080';
|
||||
}
|
||||
|
||||
return {
|
||||
...currentBuildPreset,
|
||||
// environment preset will overwrite current build preset
|
||||
|
58
apps/core/.webpack/s3-plugin.ts
Normal file
58
apps/core/.webpack/s3-plugin.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import { join } from 'node:path';
|
||||
import { execSync } from 'node:child_process';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
|
||||
import type { PutObjectCommandInput } from '@aws-sdk/client-s3';
|
||||
import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
|
||||
import { once } from 'lodash-es';
|
||||
import { lookup } from 'mime-types';
|
||||
import type { Compiler, WebpackPluginInstance } from 'webpack';
|
||||
|
||||
export const gitShortHash = once(() => {
|
||||
const { GITHUB_SHA } = process.env;
|
||||
if (GITHUB_SHA) {
|
||||
return GITHUB_SHA.substring(0, 9);
|
||||
}
|
||||
const sha = execSync(`git rev-parse --short HEAD`, {
|
||||
encoding: 'utf-8',
|
||||
}).trim();
|
||||
return sha;
|
||||
});
|
||||
|
||||
export const R2_BUCKET =
|
||||
process.env.R2_BUCKET! ??
|
||||
(process.env.BUILD_TYPE === 'canary' ? 'assets-dev' : 'assets-prod');
|
||||
|
||||
export class WebpackS3Plugin implements WebpackPluginInstance {
|
||||
private readonly s3 = new S3Client({
|
||||
region: 'auto',
|
||||
endpoint: `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
|
||||
credentials: {
|
||||
accessKeyId: process.env.R2_ACCESS_KEY_ID!,
|
||||
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
|
||||
},
|
||||
});
|
||||
|
||||
apply(compiler: Compiler) {
|
||||
compiler.hooks.assetEmitted.tapPromise(
|
||||
'WebpackS3Plugin',
|
||||
async (asset, { outputPath }) => {
|
||||
if (asset === 'index.html') {
|
||||
return;
|
||||
}
|
||||
const assetPath = join(outputPath, asset);
|
||||
const assetSource = await readFile(assetPath);
|
||||
const putObjectCommandOptions: PutObjectCommandInput = {
|
||||
Body: assetSource,
|
||||
Bucket: R2_BUCKET,
|
||||
Key: join(gitShortHash(), asset),
|
||||
};
|
||||
const contentType = lookup(asset);
|
||||
if (contentType) {
|
||||
putObjectCommandOptions.ContentType = contentType;
|
||||
}
|
||||
await this.s3.send(new PutObjectCommand(putObjectCommandOptions));
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
@ -17,14 +17,14 @@
|
||||
<meta name="twitter:url" content="https://app.affine.pro/" />
|
||||
<meta
|
||||
name="twitter:title"
|
||||
content="AFFiNE:There can be more than Notion and Miro."
|
||||
content="AFFiNE: There can be more than Notion and Miro."
|
||||
/>
|
||||
<meta name="twitter:description" content="{description}" />
|
||||
<meta name="twitter:site" content="@AffineOfficial" />
|
||||
<meta name="twitter:image" content="https://affine.pro/og.jpeg" />
|
||||
<meta
|
||||
property="og:title"
|
||||
content="AFFiNE:There can be more than Notion and Miro."
|
||||
content="AFFiNE: There can be more than Notion and Miro."
|
||||
/>
|
||||
<meta property="og:type" content="website" />
|
||||
<meta
|
||||
@ -39,7 +39,8 @@
|
||||
href="https://affine.pro/favicon.ico"
|
||||
/>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<div id="app" data-version="<%= GIT_SHORT_SHA %>"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { createConfiguration, rootPath } from './config.js';
|
||||
import { createConfiguration, rootPath, publicPath } from './config.js';
|
||||
import { merge } from 'webpack-merge';
|
||||
import { join, resolve } from 'node:path';
|
||||
import type { BuildFlags } from '@affine/cli/config';
|
||||
import { getRuntimeConfig } from './runtime-config.js';
|
||||
import HTMLPlugin from 'html-webpack-plugin';
|
||||
|
||||
import { gitShortHash } from './s3-plugin.js';
|
||||
|
||||
export default async function (cli_env: any, _: any) {
|
||||
const flags: BuildFlags = JSON.parse(
|
||||
Buffer.from(cli_env.flags, 'hex').toString('utf-8')
|
||||
@ -44,12 +46,16 @@ export default async function (cli_env: any, _: any) {
|
||||
minify: false,
|
||||
chunks: ['app', 'plugin', 'polyfill/intl-segmenter', 'polyfill/ses'],
|
||||
filename: 'index.html',
|
||||
templateParameters: {
|
||||
GIT_SHORT_SHA: gitShortHash(),
|
||||
},
|
||||
}),
|
||||
new HTMLPlugin({
|
||||
template: join(rootPath, '.webpack', 'template.html'),
|
||||
inject: 'body',
|
||||
scriptLoading: 'module',
|
||||
minify: false,
|
||||
publicPath,
|
||||
chunks: [
|
||||
'_plugin/index.test',
|
||||
'plugin',
|
||||
@ -57,6 +63,9 @@ export default async function (cli_env: any, _: any) {
|
||||
'polyfill/ses',
|
||||
],
|
||||
filename: '_plugin/index.html',
|
||||
templateParameters: {
|
||||
GIT_SHORT_SHA: gitShortHash(),
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
@ -38,6 +38,7 @@
|
||||
"@emotion/server": "^11.11.0",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@mui/material": "^5.14.6",
|
||||
"@radix-ui/react-select": "^1.2.2",
|
||||
"@react-hookz/web": "^23.1.0",
|
||||
"@toeverything/components": "^0.0.19",
|
||||
"async-call-rpc": "^6.3.1",
|
||||
@ -52,6 +53,7 @@
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lottie-web": "^5.12.2",
|
||||
"mini-css-extract-plugin": "^2.7.6",
|
||||
"next-auth": "^4.22.1",
|
||||
"next-themes": "^0.2.1",
|
||||
"postcss-loader": "^7.3.3",
|
||||
"react": "18.2.0",
|
||||
@ -62,22 +64,27 @@
|
||||
"rxjs": "^7.8.1",
|
||||
"ses": "^0.18.7",
|
||||
"swr": "2.2.1",
|
||||
"valtio": "^1.10.6",
|
||||
"y-protocols": "^1.0.5",
|
||||
"yjs": "^13.6.7",
|
||||
"zod": "^3.22.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aws-sdk/client-s3": "3.400.0",
|
||||
"@perfsee/webpack": "^1.8.4",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.11",
|
||||
"@sentry/webpack-plugin": "^2.7.0",
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@swc/core": "^1.3.80",
|
||||
"@types/lodash-es": "^4.17.8",
|
||||
"@types/lodash.debounce": "^4.0.7",
|
||||
"@types/webpack-env": "^1.18.1",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"css-loader": "^6.8.1",
|
||||
"express": "^4.18.2",
|
||||
"html-webpack-plugin": "^5.5.3",
|
||||
"lodash-es": "^4.17.21",
|
||||
"mime-types": "^2.1.35",
|
||||
"raw-loader": "^4.0.2",
|
||||
"source-map-loader": "^4.0.1",
|
||||
"style-loader": "^3.3.3",
|
||||
|
@ -10,20 +10,18 @@
|
||||
"target": "build",
|
||||
"params": "ignore"
|
||||
},
|
||||
{
|
||||
"projects": ["tag:infra"],
|
||||
"target": "build",
|
||||
"params": "ignore"
|
||||
},
|
||||
"^build"
|
||||
],
|
||||
"inputs": [
|
||||
"{projectRoot}/.webpack/**/*",
|
||||
"{projectRoot}/**/*",
|
||||
"{projectRoot}/public/**/*",
|
||||
"{workspaceRoot}/packages/env/src/**/*",
|
||||
"{workspaceRoot}/packages/component/src/**/*",
|
||||
"{workspaceRoot}/packages/debug/src/**/*",
|
||||
"{workspaceRoot}/packages/graphql/src/**/*",
|
||||
"{workspaceRoot}/packages/hooks/src/**/*",
|
||||
"{workspaceRoot}/packages/jotai/src/**/*",
|
||||
"{workspaceRoot}/packages/templates/src/**/*",
|
||||
"{workspaceRoot}/packages/workspace/src/**/*",
|
||||
"{workspaceRoot}/packages/**/*",
|
||||
{
|
||||
"env": "BUILD_TYPE"
|
||||
},
|
||||
|
164
apps/core/src/adapters/cloud/crud.ts
Normal file
164
apps/core/src/adapters/cloud/crud.ts
Normal file
@ -0,0 +1,164 @@
|
||||
import type {
|
||||
AffineCloudWorkspace,
|
||||
WorkspaceCRUD,
|
||||
} from '@affine/env/workspace';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import {
|
||||
createWorkspaceMutation,
|
||||
deleteWorkspaceMutation,
|
||||
getWorkspaceQuery,
|
||||
getWorkspacesQuery,
|
||||
} from '@affine/graphql';
|
||||
import { fetcher } from '@affine/workspace/affine/gql';
|
||||
import { getOrCreateWorkspace } from '@affine/workspace/manager';
|
||||
import { createIndexeddbStorage, Workspace } from '@blocksuite/store';
|
||||
import { migrateLocalBlobStorage } from '@toeverything/infra/blocksuite';
|
||||
import {
|
||||
createIndexedDBProvider,
|
||||
DEFAULT_DB_NAME,
|
||||
} from '@toeverything/y-indexeddb';
|
||||
import { getSession } from 'next-auth/react';
|
||||
import { proxy } from 'valtio/vanilla';
|
||||
|
||||
const Y = Workspace.Y;
|
||||
|
||||
async function deleteLocalBlobStorage(id: string) {
|
||||
const storage = createIndexeddbStorage(id);
|
||||
const keys = await storage.crud.list();
|
||||
for (const key of keys) {
|
||||
await storage.crud.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
// we don't need to persistence the state into local storage
|
||||
// because if a user clicks create multiple time and nothing happened
|
||||
// because of the server delay or something, he/she will wait.
|
||||
// and also the user journey of creating workspace is long.
|
||||
const createdWorkspaces = proxy<string[]>([]);
|
||||
|
||||
export const CRUD: WorkspaceCRUD<WorkspaceFlavour.AFFINE_CLOUD> = {
|
||||
create: async blockSuiteWorkspace => {
|
||||
if (createdWorkspaces.some(id => id === blockSuiteWorkspace.id)) {
|
||||
throw new Error('workspace already created');
|
||||
}
|
||||
const { createWorkspace } = await fetcher({
|
||||
query: createWorkspaceMutation,
|
||||
variables: {
|
||||
init: new File(
|
||||
[Y.encodeStateAsUpdate(blockSuiteWorkspace.doc)],
|
||||
'initBinary.yDoc'
|
||||
),
|
||||
},
|
||||
});
|
||||
createdWorkspaces.push(blockSuiteWorkspace.id);
|
||||
const newBLockSuiteWorkspace = getOrCreateWorkspace(
|
||||
createWorkspace.id,
|
||||
WorkspaceFlavour.AFFINE_CLOUD
|
||||
);
|
||||
|
||||
Y.applyUpdate(
|
||||
newBLockSuiteWorkspace.doc,
|
||||
Y.encodeStateAsUpdate(blockSuiteWorkspace.doc)
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
[...blockSuiteWorkspace.doc.subdocs].map(async subdoc => {
|
||||
subdoc.load();
|
||||
return subdoc.whenLoaded.then(() => {
|
||||
newBLockSuiteWorkspace.doc.subdocs.forEach(newSubdoc => {
|
||||
if (newSubdoc.guid === subdoc.guid) {
|
||||
Y.applyUpdate(newSubdoc, Y.encodeStateAsUpdate(subdoc));
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
const provider = createIndexedDBProvider(
|
||||
newBLockSuiteWorkspace.doc,
|
||||
DEFAULT_DB_NAME
|
||||
);
|
||||
provider.connect();
|
||||
migrateLocalBlobStorage(blockSuiteWorkspace.id, createWorkspace.id)
|
||||
.then(() => deleteLocalBlobStorage(blockSuiteWorkspace.id))
|
||||
.catch(e => {
|
||||
console.error('error when moving blob storage:', e);
|
||||
});
|
||||
// todo(himself65): delete old workspace in the future
|
||||
return createWorkspace.id;
|
||||
},
|
||||
delete: async workspace => {
|
||||
await fetcher({
|
||||
query: deleteWorkspaceMutation,
|
||||
variables: {
|
||||
id: workspace.id,
|
||||
},
|
||||
});
|
||||
},
|
||||
get: async id => {
|
||||
if (!environment.isServer && !navigator.onLine) {
|
||||
// no network
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
!(await getSession()
|
||||
.then(() => true)
|
||||
.catch(() => false))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
await fetcher({
|
||||
query: getWorkspaceQuery,
|
||||
variables: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
return {
|
||||
id,
|
||||
flavour: WorkspaceFlavour.AFFINE_CLOUD,
|
||||
blockSuiteWorkspace: getOrCreateWorkspace(
|
||||
id,
|
||||
WorkspaceFlavour.AFFINE_CLOUD
|
||||
),
|
||||
} satisfies AffineCloudWorkspace;
|
||||
} catch (e) {
|
||||
console.error('error when fetching cloud workspace:', e);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
list: async () => {
|
||||
if (!environment.isServer && !navigator.onLine) {
|
||||
// no network
|
||||
return [];
|
||||
}
|
||||
if (
|
||||
!(await getSession()
|
||||
.then(() => true)
|
||||
.catch(() => false))
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const { workspaces } = await fetcher({
|
||||
query: getWorkspacesQuery,
|
||||
});
|
||||
const ids = workspaces.map(({ id }) => id);
|
||||
|
||||
return ids.map(
|
||||
id =>
|
||||
({
|
||||
id,
|
||||
flavour: WorkspaceFlavour.AFFINE_CLOUD,
|
||||
blockSuiteWorkspace: getOrCreateWorkspace(
|
||||
id,
|
||||
WorkspaceFlavour.AFFINE_CLOUD
|
||||
),
|
||||
}) satisfies AffineCloudWorkspace
|
||||
);
|
||||
} catch (e) {
|
||||
console.error('error when fetching cloud workspaces:', e);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
};
|
75
apps/core/src/adapters/cloud/ui.tsx
Normal file
75
apps/core/src/adapters/cloud/ui.tsx
Normal file
@ -0,0 +1,75 @@
|
||||
import { initEmptyPage } from '@affine/env/blocksuite';
|
||||
import { PageNotFoundError } from '@affine/env/constant';
|
||||
import type {
|
||||
WorkspaceFlavour,
|
||||
WorkspaceUISchema,
|
||||
} from '@affine/env/workspace';
|
||||
import { lazy, useCallback } from 'react';
|
||||
|
||||
import { useIsWorkspaceOwner } from '../../hooks/affine/use-is-workspace-owner';
|
||||
import { useWorkspace } from '../../hooks/use-workspace';
|
||||
import {
|
||||
BlockSuitePageList,
|
||||
NewWorkspaceSettingDetail,
|
||||
PageDetailEditor,
|
||||
Provider,
|
||||
WorkspaceHeader,
|
||||
} from '../shared';
|
||||
|
||||
const LoginCard = lazy(() =>
|
||||
import('../../components/cloud/login-card').then(({ LoginCard }) => ({
|
||||
default: LoginCard,
|
||||
}))
|
||||
);
|
||||
|
||||
export const UI = {
|
||||
Provider,
|
||||
LoginCard,
|
||||
Header: WorkspaceHeader,
|
||||
PageDetail: ({ currentWorkspaceId, currentPageId, onLoadEditor }) => {
|
||||
const workspace = useWorkspace(currentWorkspaceId);
|
||||
const page = workspace.blockSuiteWorkspace.getPage(currentPageId);
|
||||
if (!page) {
|
||||
throw new PageNotFoundError(workspace.blockSuiteWorkspace, currentPageId);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<PageDetailEditor
|
||||
pageId={currentPageId}
|
||||
onInit={useCallback(async page => initEmptyPage(page), [])}
|
||||
onLoad={onLoadEditor}
|
||||
workspace={workspace.blockSuiteWorkspace}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
},
|
||||
PageList: ({ blockSuiteWorkspace, onOpenPage, collection }) => {
|
||||
return (
|
||||
<BlockSuitePageList
|
||||
listType="all"
|
||||
collection={collection}
|
||||
onOpenPage={onOpenPage}
|
||||
blockSuiteWorkspace={blockSuiteWorkspace}
|
||||
/>
|
||||
);
|
||||
},
|
||||
NewSettingsDetail: ({
|
||||
currentWorkspaceId,
|
||||
onTransformWorkspace,
|
||||
onDeleteLocalWorkspace,
|
||||
onDeleteCloudWorkspace,
|
||||
onLeaveWorkspace,
|
||||
}) => {
|
||||
const isOwner = useIsWorkspaceOwner(currentWorkspaceId);
|
||||
return (
|
||||
<NewWorkspaceSettingDetail
|
||||
onDeleteLocalWorkspace={onDeleteLocalWorkspace}
|
||||
onDeleteCloudWorkspace={onDeleteCloudWorkspace}
|
||||
onLeaveWorkspace={onLeaveWorkspace}
|
||||
workspaceId={currentWorkspaceId}
|
||||
onTransferWorkspace={onTransformWorkspace}
|
||||
isOwner={isOwner}
|
||||
/>
|
||||
);
|
||||
},
|
||||
} satisfies WorkspaceUISchema<WorkspaceFlavour.AFFINE_CLOUD>;
|
@ -29,6 +29,7 @@ import {
|
||||
BlockSuitePageList,
|
||||
NewWorkspaceSettingDetail,
|
||||
PageDetailEditor,
|
||||
Provider,
|
||||
WorkspaceHeader,
|
||||
} from '../shared';
|
||||
|
||||
@ -39,6 +40,7 @@ export const LocalAdapter: WorkspaceAdapter<WorkspaceFlavour.LOCAL> = {
|
||||
flavour: WorkspaceFlavour.LOCAL,
|
||||
loadPriority: LoadPriority.LOW,
|
||||
Events: {
|
||||
'app:access': async () => true,
|
||||
'app:init': () => {
|
||||
const blockSuiteWorkspace = getOrCreateWorkspace(
|
||||
nanoid(),
|
||||
@ -79,9 +81,7 @@ export const LocalAdapter: WorkspaceAdapter<WorkspaceFlavour.LOCAL> = {
|
||||
CRUD,
|
||||
UI: {
|
||||
Header: WorkspaceHeader,
|
||||
Provider: ({ children }) => {
|
||||
return <>{children}</>;
|
||||
},
|
||||
Provider,
|
||||
PageDetail: ({ currentWorkspaceId, currentPageId, onLoadEditor }) => {
|
||||
const workspace = useStaticBlockSuiteWorkspace(currentWorkspaceId);
|
||||
const page = workspace.getPage(currentPageId);
|
||||
@ -111,14 +111,19 @@ export const LocalAdapter: WorkspaceAdapter<WorkspaceFlavour.LOCAL> = {
|
||||
},
|
||||
NewSettingsDetail: ({
|
||||
currentWorkspaceId,
|
||||
onDeleteWorkspace,
|
||||
onTransformWorkspace,
|
||||
onDeleteLocalWorkspace,
|
||||
onDeleteCloudWorkspace,
|
||||
onLeaveWorkspace,
|
||||
}) => {
|
||||
return (
|
||||
<NewWorkspaceSettingDetail
|
||||
onDeleteWorkspace={onDeleteWorkspace}
|
||||
onDeleteLocalWorkspace={onDeleteLocalWorkspace}
|
||||
onDeleteCloudWorkspace={onDeleteCloudWorkspace}
|
||||
onLeaveWorkspace={onLeaveWorkspace}
|
||||
workspaceId={currentWorkspaceId}
|
||||
onTransferWorkspace={onTransformWorkspace}
|
||||
isOwner={true}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
45
apps/core/src/adapters/public-cloud/ui.tsx
Normal file
45
apps/core/src/adapters/public-cloud/ui.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
import { initEmptyPage } from '@affine/env/blocksuite';
|
||||
import { PageNotFoundError } from '@affine/env/constant';
|
||||
import type { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { type WorkspaceUISchema } from '@affine/env/workspace';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useWorkspace } from '../../hooks/use-workspace';
|
||||
import { BlockSuitePageList, PageDetailEditor, Provider } from '../shared';
|
||||
|
||||
export const UI = {
|
||||
Provider,
|
||||
Header: () => {
|
||||
return null;
|
||||
},
|
||||
PageDetail: ({ currentWorkspaceId, currentPageId, onLoadEditor }) => {
|
||||
const workspace = useWorkspace(currentWorkspaceId);
|
||||
const page = workspace.blockSuiteWorkspace.getPage(currentPageId);
|
||||
if (!page) {
|
||||
throw new PageNotFoundError(workspace.blockSuiteWorkspace, currentPageId);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<PageDetailEditor
|
||||
pageId={currentPageId}
|
||||
onInit={useCallback(async page => initEmptyPage(page), [])}
|
||||
onLoad={onLoadEditor}
|
||||
workspace={workspace.blockSuiteWorkspace}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
},
|
||||
PageList: ({ blockSuiteWorkspace, onOpenPage, collection }) => {
|
||||
return (
|
||||
<BlockSuitePageList
|
||||
listType="all"
|
||||
collection={collection}
|
||||
onOpenPage={onOpenPage}
|
||||
blockSuiteWorkspace={blockSuiteWorkspace}
|
||||
/>
|
||||
);
|
||||
},
|
||||
NewSettingsDetail: () => {
|
||||
throw new Error('Not implemented');
|
||||
},
|
||||
} satisfies WorkspaceUISchema<WorkspaceFlavour.AFFINE_PUBLIC>;
|
@ -1,5 +1,11 @@
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const Provider = lazy(() =>
|
||||
import('../components/cloud/provider').then(({ Provider }) => ({
|
||||
default: Provider,
|
||||
}))
|
||||
);
|
||||
|
||||
export const NewWorkspaceSettingDetail = lazy(() =>
|
||||
import('../components/affine/new-workspace-setting-detail').then(
|
||||
({ WorkspaceSettingDetail }) => ({
|
||||
|
@ -10,7 +10,10 @@ import {
|
||||
WorkspaceFlavour,
|
||||
} from '@affine/env/workspace';
|
||||
|
||||
import { CRUD as CloudCRUD } from './cloud/crud';
|
||||
import { UI as CloudUI } from './cloud/ui';
|
||||
import { LocalAdapter } from './local';
|
||||
import { UI as PublicCloudUI } from './public-cloud/ui';
|
||||
|
||||
const unimplemented = () => {
|
||||
throw new Error('Not implemented');
|
||||
@ -26,26 +29,24 @@ export const WorkspaceAdapters = {
|
||||
releaseType: ReleaseType.UNRELEASED,
|
||||
flavour: WorkspaceFlavour.AFFINE_CLOUD,
|
||||
loadPriority: LoadPriority.HIGH,
|
||||
Events: {} as Partial<AppEvents>,
|
||||
// todo: implement this
|
||||
CRUD: {
|
||||
get: unimplemented,
|
||||
list: bypassList,
|
||||
delete: unimplemented,
|
||||
create: unimplemented,
|
||||
},
|
||||
// todo: implement this
|
||||
UI: {
|
||||
Provider: unimplemented,
|
||||
Header: unimplemented,
|
||||
PageDetail: unimplemented,
|
||||
PageList: unimplemented,
|
||||
NewSettingsDetail: unimplemented,
|
||||
},
|
||||
Events: {
|
||||
'app:access': async () => {
|
||||
try {
|
||||
const { getSession } = await import('next-auth/react');
|
||||
const session = await getSession();
|
||||
return !!session;
|
||||
} catch (e) {
|
||||
console.error('failed to get session', e);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
} as Partial<AppEvents>,
|
||||
CRUD: CloudCRUD,
|
||||
UI: CloudUI,
|
||||
},
|
||||
[WorkspaceFlavour.PUBLIC]: {
|
||||
[WorkspaceFlavour.AFFINE_PUBLIC]: {
|
||||
releaseType: ReleaseType.UNRELEASED,
|
||||
flavour: WorkspaceFlavour.PUBLIC,
|
||||
flavour: WorkspaceFlavour.AFFINE_PUBLIC,
|
||||
loadPriority: LoadPriority.LOW,
|
||||
Events: {} as Partial<AppEvents>,
|
||||
// todo: implement this
|
||||
@ -55,14 +56,7 @@ export const WorkspaceAdapters = {
|
||||
delete: unimplemented,
|
||||
create: unimplemented,
|
||||
},
|
||||
// todo: implement this
|
||||
UI: {
|
||||
Provider: unimplemented,
|
||||
Header: unimplemented,
|
||||
PageDetail: unimplemented,
|
||||
PageList: unimplemented,
|
||||
NewSettingsDetail: unimplemented,
|
||||
},
|
||||
UI: PublicCloudUI,
|
||||
},
|
||||
} satisfies {
|
||||
[Key in WorkspaceFlavour]: WorkspaceAdapter<Key>;
|
||||
|
@ -7,6 +7,7 @@ import { WorkspaceFallback } from '@affine/component/workspace';
|
||||
import { CacheProvider } from '@emotion/react';
|
||||
import { getCurrentStore } from '@toeverything/infra/atom';
|
||||
import { use } from 'foxact/use';
|
||||
import { SessionProvider } from 'next-auth/react';
|
||||
import type { PropsWithChildren, ReactElement } from 'react';
|
||||
import { lazy, memo, Suspense } from 'react';
|
||||
import { RouterProvider } from 'react-router-dom';
|
||||
@ -47,16 +48,18 @@ const languageLoadingPromise = loadLanguage().catch(console.error);
|
||||
export const App = memo(function App() {
|
||||
use(languageLoadingPromise);
|
||||
return (
|
||||
<CacheProvider value={cache}>
|
||||
<AffineContext store={getCurrentStore()}>
|
||||
<DebugProvider>
|
||||
<RouterProvider
|
||||
fallbackElement={<WorkspaceFallback key="RouterFallback" />}
|
||||
router={router}
|
||||
future={future}
|
||||
/>
|
||||
</DebugProvider>
|
||||
</AffineContext>
|
||||
</CacheProvider>
|
||||
<SessionProvider refetchOnWindowFocus>
|
||||
<CacheProvider value={cache}>
|
||||
<AffineContext store={getCurrentStore()}>
|
||||
<DebugProvider>
|
||||
<RouterProvider
|
||||
fallbackElement={<WorkspaceFallback key="RouterFallback" />}
|
||||
router={router}
|
||||
future={future}
|
||||
/>
|
||||
</DebugProvider>
|
||||
</AffineContext>
|
||||
</CacheProvider>
|
||||
</SessionProvider>
|
||||
);
|
||||
});
|
||||
|
@ -3,9 +3,9 @@ import { atom } from 'jotai';
|
||||
import { atomFamily, atomWithStorage } from 'jotai/utils';
|
||||
import type { AtomFamily } from 'jotai/vanilla/utils/atomFamily';
|
||||
|
||||
import type { AuthProps } from '../components/affine/auth';
|
||||
import type { CreateWorkspaceMode } from '../components/affine/create-workspace-modal';
|
||||
import type { SettingProps } from '../components/affine/setting-modal';
|
||||
|
||||
// modal atoms
|
||||
export const openWorkspacesModalAtom = atom(false);
|
||||
export const openCreateWorkspaceModalAtom = atom<CreateWorkspaceMode>(false);
|
||||
@ -22,6 +22,22 @@ export const openSettingModalAtom = atom<SettingAtom>({
|
||||
open: false,
|
||||
});
|
||||
|
||||
export type AuthAtom = {
|
||||
openModal: boolean;
|
||||
state: AuthProps['state'];
|
||||
email?: string;
|
||||
emailType?: AuthProps['emailType'];
|
||||
// Only used for sign in page callback, after called, it will be set to undefined
|
||||
onceSignedIn?: () => void;
|
||||
};
|
||||
|
||||
export const authAtom = atom<AuthAtom>({
|
||||
openModal: false,
|
||||
state: 'signIn',
|
||||
email: '',
|
||||
emailType: 'changeEmail',
|
||||
});
|
||||
|
||||
export const openDisableCloudAlertModalAtom = atom(false);
|
||||
|
||||
type PageMode = 'page' | 'edgeless';
|
||||
|
13
apps/core/src/components/adapter-worksapce-wrapper.tsx
Normal file
13
apps/core/src/components/adapter-worksapce-wrapper.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import type { FC, PropsWithChildren } from 'react';
|
||||
|
||||
import { WorkspaceAdapters } from '../adapters/workspace';
|
||||
import { useCurrentWorkspace } from '../hooks/current/use-current-workspace';
|
||||
|
||||
export const AdapterProviderWrapper: FC<PropsWithChildren> = ({ children }) => {
|
||||
const [currentWorkspace] = useCurrentWorkspace();
|
||||
|
||||
const Provider = WorkspaceAdapters[currentWorkspace.flavour].UI.Provider;
|
||||
assertExists(Provider);
|
||||
return <Provider>{children}</Provider>;
|
||||
};
|
11
apps/core/src/components/affine/any-error-boundary/index.tsx
Normal file
11
apps/core/src/components/affine/any-error-boundary/index.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import type { ReactElement } from 'react';
|
||||
import type { FallbackProps } from 'react-error-boundary';
|
||||
|
||||
export const AnyErrorBoundary = (props: FallbackProps): ReactElement => {
|
||||
return (
|
||||
<div>
|
||||
<p>Something went wrong:</p>
|
||||
<p>{props.error.toString()}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,67 @@
|
||||
import {
|
||||
AuthContent,
|
||||
BackButton,
|
||||
ModalHeader,
|
||||
ResendButton,
|
||||
} from '@affine/component/auth-components';
|
||||
import { Trans } from '@affine/i18n';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { signIn } from 'next-auth/react';
|
||||
import { type FC, useCallback } from 'react';
|
||||
|
||||
import { buildCallbackUrl } from './callback-url';
|
||||
import type { AuthPanelProps } from './index';
|
||||
import * as style from './style.css';
|
||||
|
||||
export const AfterSignInSendEmail: FC<AuthPanelProps> = ({
|
||||
setAuthState,
|
||||
email,
|
||||
}) => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
return (
|
||||
<>
|
||||
<ModalHeader
|
||||
title={t['com.affine.auth.sign.in']()}
|
||||
subTitle={t['com.affine.auth.sign.in.sent.email.subtitle']()}
|
||||
/>
|
||||
<AuthContent style={{ height: 162 }}>
|
||||
{t['com.affine.auth.sign.sent.email.message.start']()}
|
||||
<a href={`mailto:${email}`}>{email}</a>
|
||||
{t['com.affine.auth.sign.sent.email.message.end']()}
|
||||
</AuthContent>
|
||||
|
||||
<ResendButton
|
||||
onClick={useCallback(() => {
|
||||
signIn('email', {
|
||||
email,
|
||||
callbackUrl: buildCallbackUrl('signIn'),
|
||||
redirect: true,
|
||||
}).catch(console.error);
|
||||
}, [email])}
|
||||
/>
|
||||
|
||||
<div className={style.authMessage} style={{ marginTop: 20 }}>
|
||||
{/*prettier-ignore*/}
|
||||
<Trans i18nKey="com.affine.auth.sign.auth.code.message.password">
|
||||
If you haven't received the email, please check your spam folder.
|
||||
Or <span
|
||||
className="link"
|
||||
data-testid='sign-in-with-password'
|
||||
onClick={useCallback(() => {
|
||||
setAuthState('signInWithPassword');
|
||||
}, [setAuthState])}
|
||||
>
|
||||
sign in with password
|
||||
</span> instead.
|
||||
</Trans>
|
||||
</div>
|
||||
|
||||
<BackButton
|
||||
onClick={useCallback(() => {
|
||||
setAuthState('signIn');
|
||||
}, [setAuthState])}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
@ -0,0 +1,54 @@
|
||||
import {
|
||||
AuthContent,
|
||||
BackButton,
|
||||
ModalHeader,
|
||||
ResendButton,
|
||||
} from '@affine/component/auth-components';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { signIn } from 'next-auth/react';
|
||||
import { type FC, useCallback } from 'react';
|
||||
|
||||
import { buildCallbackUrl } from './callback-url';
|
||||
import type { AuthPanelProps } from './index';
|
||||
import * as style from './style.css';
|
||||
|
||||
export const AfterSignUpSendEmail: FC<AuthPanelProps> = ({
|
||||
setAuthState,
|
||||
email,
|
||||
}) => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
return (
|
||||
<>
|
||||
<ModalHeader
|
||||
title={t['com.affine.auth.sign.up']()}
|
||||
subTitle={t['com.affine.auth.sign.up.sent.email.subtitle']()}
|
||||
/>
|
||||
<AuthContent style={{ height: 162 }}>
|
||||
{t['com.affine.auth.sign.sent.email.message.start']()}
|
||||
<a href={`mailto:${email}`}>{email}</a>
|
||||
{t['com.affine.auth.sign.sent.email.message.end']()}
|
||||
</AuthContent>
|
||||
|
||||
<ResendButton
|
||||
onClick={useCallback(() => {
|
||||
signIn('email', {
|
||||
email: email,
|
||||
callbackUrl: buildCallbackUrl('signUp'),
|
||||
redirect: true,
|
||||
}).catch(console.error);
|
||||
}, [email])}
|
||||
/>
|
||||
|
||||
<div className={style.authMessage} style={{ marginTop: 20 }}>
|
||||
{t['com.affine.auth.sign.auth.code.message']()}
|
||||
</div>
|
||||
|
||||
<BackButton
|
||||
onClick={useCallback(() => {
|
||||
setAuthState('signIn');
|
||||
}, [setAuthState])}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
16
apps/core/src/components/affine/auth/callback-url.ts
Normal file
16
apps/core/src/components/affine/auth/callback-url.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { isDesktop } from '@affine/env/constant';
|
||||
|
||||
type Action = 'signUp' | 'changePassword' | 'signIn' | 'signUp';
|
||||
|
||||
export function buildCallbackUrl(action: Action) {
|
||||
const callbackUrl = `/auth/${action}`;
|
||||
const params: string[][] = [];
|
||||
if (isDesktop && window.appInfo.schema) {
|
||||
params.push(['schema', window.appInfo.schema]);
|
||||
}
|
||||
const query =
|
||||
params.length > 0
|
||||
? '?' + params.map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join('&')
|
||||
: '';
|
||||
return callbackUrl + query;
|
||||
}
|
155
apps/core/src/components/affine/auth/index.tsx
Normal file
155
apps/core/src/components/affine/auth/index.tsx
Normal file
@ -0,0 +1,155 @@
|
||||
import {
|
||||
AuthModal as AuthModalBase,
|
||||
type AuthModalProps as AuthModalBaseProps,
|
||||
} from '@affine/component/auth-components';
|
||||
import { isDesktop } from '@affine/env/constant';
|
||||
import { atom, useAtom } from 'jotai';
|
||||
import { type FC, useCallback, useEffect, useMemo } from 'react';
|
||||
|
||||
import { AfterSignInSendEmail } from './after-sign-in-send-email';
|
||||
import { AfterSignUpSendEmail } from './after-sign-up-send-email';
|
||||
import { SendEmail } from './send-email';
|
||||
import { SignIn } from './sign-in';
|
||||
import { SignInWithPassword } from './sign-in-with-password';
|
||||
|
||||
export type AuthProps = {
|
||||
state:
|
||||
| 'signIn'
|
||||
| 'afterSignUpSendEmail'
|
||||
| 'afterSignInSendEmail'
|
||||
// throw away
|
||||
| 'signInWithPassword'
|
||||
| 'sendEmail';
|
||||
setAuthState: (state: AuthProps['state']) => void;
|
||||
setAuthEmail: (state: AuthProps['email']) => void;
|
||||
setEmailType: (state: AuthProps['emailType']) => void;
|
||||
email: string;
|
||||
emailType: 'setPassword' | 'changePassword' | 'changeEmail';
|
||||
onSignedIn?: () => void;
|
||||
};
|
||||
|
||||
export type AuthPanelProps = {
|
||||
email: string;
|
||||
setAuthState: AuthProps['setAuthState'];
|
||||
setAuthEmail: AuthProps['setAuthEmail'];
|
||||
setEmailType: AuthProps['setEmailType'];
|
||||
emailType: AuthProps['emailType'];
|
||||
onSignedIn?: () => void;
|
||||
authStore: AuthStoreAtom;
|
||||
setAuthStore: (data: Partial<AuthStoreAtom>) => void;
|
||||
};
|
||||
|
||||
const config: {
|
||||
[k in AuthProps['state']]: FC<AuthPanelProps>;
|
||||
} = {
|
||||
signIn: SignIn,
|
||||
afterSignUpSendEmail: AfterSignUpSendEmail,
|
||||
afterSignInSendEmail: AfterSignInSendEmail,
|
||||
signInWithPassword: SignInWithPassword,
|
||||
sendEmail: SendEmail,
|
||||
};
|
||||
|
||||
type AuthStoreAtom = {
|
||||
hasSentEmail: boolean;
|
||||
resendCountDown: number;
|
||||
};
|
||||
export const authStoreAtom = atom<AuthStoreAtom>({
|
||||
hasSentEmail: false,
|
||||
resendCountDown: 60,
|
||||
});
|
||||
|
||||
export const AuthModal: FC<AuthModalBaseProps & AuthProps> = ({
|
||||
open,
|
||||
state,
|
||||
setOpen,
|
||||
email,
|
||||
setAuthEmail,
|
||||
setAuthState,
|
||||
setEmailType,
|
||||
emailType,
|
||||
}) => {
|
||||
const [, setAuthStore] = useAtom(authStoreAtom);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setAuthStore({
|
||||
hasSentEmail: false,
|
||||
resendCountDown: 60,
|
||||
});
|
||||
setAuthEmail('');
|
||||
}
|
||||
}, [open, setAuthEmail, setAuthStore]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isDesktop) {
|
||||
return window.events?.ui.onFinishLogin(() => {
|
||||
setOpen(false);
|
||||
});
|
||||
}
|
||||
return;
|
||||
}, [setOpen]);
|
||||
|
||||
const onSignedIn = useCallback(() => {
|
||||
setOpen(false);
|
||||
}, [setOpen]);
|
||||
|
||||
return (
|
||||
<AuthModalBase open={open} setOpen={setOpen}>
|
||||
<AuthPanel
|
||||
state={state}
|
||||
email={email}
|
||||
setAuthEmail={setAuthEmail}
|
||||
setAuthState={setAuthState}
|
||||
setEmailType={setEmailType}
|
||||
emailType={emailType}
|
||||
onSignedIn={onSignedIn}
|
||||
/>
|
||||
</AuthModalBase>
|
||||
);
|
||||
};
|
||||
|
||||
export const AuthPanel: FC<AuthProps> = ({
|
||||
state,
|
||||
email,
|
||||
setAuthEmail,
|
||||
setAuthState,
|
||||
setEmailType,
|
||||
emailType,
|
||||
onSignedIn,
|
||||
}) => {
|
||||
const [authStore, setAuthStore] = useAtom(authStoreAtom);
|
||||
|
||||
const CurrentPanel = useMemo(() => {
|
||||
return config[state];
|
||||
}, [state]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
setAuthStore({
|
||||
hasSentEmail: false,
|
||||
resendCountDown: 60,
|
||||
});
|
||||
};
|
||||
}, [setAuthEmail, setAuthStore]);
|
||||
|
||||
return (
|
||||
<CurrentPanel
|
||||
email={email}
|
||||
setAuthState={setAuthState}
|
||||
setAuthEmail={setAuthEmail}
|
||||
setEmailType={setEmailType}
|
||||
authStore={authStore}
|
||||
emailType={emailType}
|
||||
onSignedIn={onSignedIn}
|
||||
setAuthStore={useCallback(
|
||||
(data: Partial<AuthStoreAtom>) => {
|
||||
setAuthStore(prev => ({
|
||||
...prev,
|
||||
...data,
|
||||
}));
|
||||
},
|
||||
[setAuthStore]
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
185
apps/core/src/components/affine/auth/send-email.tsx
Normal file
185
apps/core/src/components/affine/auth/send-email.tsx
Normal file
@ -0,0 +1,185 @@
|
||||
import { Wrapper } from '@affine/component';
|
||||
import {
|
||||
AuthContent,
|
||||
AuthInput,
|
||||
BackButton,
|
||||
ModalHeader,
|
||||
} from '@affine/component/auth-components';
|
||||
import { pushNotificationAtom } from '@affine/component/notification-center';
|
||||
import { isDesktop } from '@affine/env/constant';
|
||||
import {
|
||||
sendChangeEmailMutation,
|
||||
sendChangePasswordEmailMutation,
|
||||
sendSetPasswordEmailMutation,
|
||||
} from '@affine/graphql';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useMutation } from '@affine/workspace/affine/gql';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import { useSetAtom } from 'jotai/react';
|
||||
import { type FC, useCallback } from 'react';
|
||||
|
||||
import type { AuthPanelProps } from './index';
|
||||
|
||||
const useEmailTitle = (emailType: AuthPanelProps['emailType']) => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
switch (emailType) {
|
||||
case 'setPassword':
|
||||
return t['com.affine.auth.set.password']();
|
||||
case 'changePassword':
|
||||
return t['com.affine.auth.reset.password']();
|
||||
case 'changeEmail':
|
||||
return t['com.affine.settings.email.action']();
|
||||
}
|
||||
};
|
||||
|
||||
const useNotificationHint = (emailType: AuthPanelProps['emailType']) => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
switch (emailType) {
|
||||
case 'setPassword':
|
||||
return t['com.affine.auth.sent.set.password.hint']();
|
||||
case 'changePassword':
|
||||
return t['com.affine.auth.sent.change.password.hint']();
|
||||
case 'changeEmail':
|
||||
return t['com.affine.auth.sent.change.email.hint']();
|
||||
}
|
||||
};
|
||||
const useButtonContent = (emailType: AuthPanelProps['emailType']) => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
switch (emailType) {
|
||||
case 'setPassword':
|
||||
return t['com.affine.auth.send.set.password.link']();
|
||||
case 'changePassword':
|
||||
return t['com.affine.auth.send.reset.password.link']();
|
||||
case 'changeEmail':
|
||||
return t['com.affine.auth.send.change.email.link']();
|
||||
}
|
||||
};
|
||||
|
||||
const useSendEmail = (emailType: AuthPanelProps['emailType']) => {
|
||||
const {
|
||||
trigger: sendChangePasswordEmail,
|
||||
isMutating: isChangePasswordMutating,
|
||||
} = useMutation({
|
||||
mutation: sendChangePasswordEmailMutation,
|
||||
});
|
||||
const { trigger: sendSetPasswordEmail, isMutating: isSetPasswordMutating } =
|
||||
useMutation({
|
||||
mutation: sendSetPasswordEmailMutation,
|
||||
});
|
||||
const { trigger: sendChangeEmail, isMutating: isChangeEmailMutating } =
|
||||
useMutation({
|
||||
mutation: sendChangeEmailMutation,
|
||||
});
|
||||
|
||||
return {
|
||||
loading:
|
||||
isChangePasswordMutating ||
|
||||
isSetPasswordMutating ||
|
||||
isChangeEmailMutating,
|
||||
sendEmail: useCallback(
|
||||
(email: string) => {
|
||||
let trigger: (args: {
|
||||
email: string;
|
||||
callbackUrl: string;
|
||||
}) => Promise<unknown>;
|
||||
let callbackUrl;
|
||||
switch (emailType) {
|
||||
case 'setPassword':
|
||||
trigger = sendSetPasswordEmail;
|
||||
callbackUrl = 'setPassword';
|
||||
break;
|
||||
case 'changePassword':
|
||||
trigger = sendChangePasswordEmail;
|
||||
callbackUrl = 'changePassword';
|
||||
break;
|
||||
case 'changeEmail':
|
||||
trigger = sendChangeEmail;
|
||||
callbackUrl = 'changeEmail';
|
||||
break;
|
||||
}
|
||||
// TODO: add error handler
|
||||
return trigger({
|
||||
email,
|
||||
callbackUrl: `/auth/${callbackUrl}?isClient=${
|
||||
isDesktop ? 'true' : 'false'
|
||||
}`,
|
||||
});
|
||||
},
|
||||
[
|
||||
emailType,
|
||||
sendChangeEmail,
|
||||
sendChangePasswordEmail,
|
||||
sendSetPasswordEmail,
|
||||
]
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
export const SendEmail: FC<AuthPanelProps> = ({
|
||||
setAuthState,
|
||||
setAuthStore,
|
||||
email,
|
||||
authStore: { hasSentEmail },
|
||||
emailType,
|
||||
}) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const pushNotification = useSetAtom(pushNotificationAtom);
|
||||
|
||||
const title = useEmailTitle(emailType);
|
||||
const hint = useNotificationHint(emailType);
|
||||
const buttonContent = useButtonContent(emailType);
|
||||
const { loading, sendEmail } = useSendEmail(emailType);
|
||||
|
||||
const onSendEmail = useCallback(async () => {
|
||||
// TODO: add error handler
|
||||
await sendEmail(email);
|
||||
|
||||
pushNotification({
|
||||
title: hint,
|
||||
message: '',
|
||||
key: Date.now().toString(),
|
||||
type: 'success',
|
||||
});
|
||||
setAuthStore({ hasSentEmail: true });
|
||||
}, [email, hint, pushNotification, sendEmail, setAuthStore]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ModalHeader title={t['AFFiNE Cloud']()} subTitle={title} />
|
||||
<AuthContent>{t['com.affine.auth.reset.password.message']()}</AuthContent>
|
||||
|
||||
<Wrapper
|
||||
marginTop={30}
|
||||
marginBottom={50}
|
||||
style={{
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<AuthInput
|
||||
label={t['com.affine.settings.email']()}
|
||||
disabled={true}
|
||||
value={email}
|
||||
/>
|
||||
</Wrapper>
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
size="extraLarge"
|
||||
style={{ width: '100%' }}
|
||||
disabled={hasSentEmail}
|
||||
loading={loading}
|
||||
onClick={onSendEmail}
|
||||
>
|
||||
{hasSentEmail ? t['com.affine.auth.sent']() : buttonContent}
|
||||
</Button>
|
||||
<BackButton
|
||||
onClick={useCallback(() => {
|
||||
setAuthState('signIn');
|
||||
}, [setAuthState])}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
111
apps/core/src/components/affine/auth/sign-in-with-password.tsx
Normal file
111
apps/core/src/components/affine/auth/sign-in-with-password.tsx
Normal file
@ -0,0 +1,111 @@
|
||||
import { Wrapper } from '@affine/component';
|
||||
import {
|
||||
AuthInput,
|
||||
BackButton,
|
||||
ModalHeader,
|
||||
} from '@affine/component/auth-components';
|
||||
import { pushNotificationAtom } from '@affine/component/notification-center';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import { useSetAtom } from 'jotai';
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
import { signIn, useSession } from 'next-auth/react';
|
||||
import type { FC } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import type { AuthPanelProps } from './index';
|
||||
import { forgetPasswordButton } from './style.css';
|
||||
|
||||
export const SignInWithPassword: FC<AuthPanelProps> = ({
|
||||
setAuthState,
|
||||
email,
|
||||
onSignedIn,
|
||||
}) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const { update } = useSession();
|
||||
|
||||
const pushNotification = useSetAtom(pushNotificationAtom);
|
||||
|
||||
const [password, setPassword] = useState('');
|
||||
const [passwordError, setPasswordError] = useState(false);
|
||||
|
||||
const onSignIn = useCallback(async () => {
|
||||
const res = await signIn('credentials', {
|
||||
redirect: false,
|
||||
email,
|
||||
password,
|
||||
}).catch(console.error);
|
||||
|
||||
if (!res?.ok) {
|
||||
return setPasswordError(true);
|
||||
}
|
||||
|
||||
await update();
|
||||
onSignedIn?.();
|
||||
pushNotification({
|
||||
title: `${email}${t['com.affine.auth.has.signed']()}`,
|
||||
message: '',
|
||||
key: Date.now().toString(),
|
||||
type: 'success',
|
||||
});
|
||||
}, [email, password, pushNotification, onSignedIn, t, update]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ModalHeader
|
||||
title={t['com.affine.auth.sign.in']()}
|
||||
subTitle={t['AFFiNE Cloud']()}
|
||||
/>
|
||||
|
||||
<Wrapper
|
||||
marginTop={30}
|
||||
marginBottom={50}
|
||||
style={{
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<AuthInput
|
||||
label={t['com.affine.settings.email']()}
|
||||
disabled={true}
|
||||
value={email}
|
||||
/>
|
||||
<AuthInput
|
||||
data-testid="password-input"
|
||||
label={t['com.affine.auth.password']()}
|
||||
value={password}
|
||||
type="password"
|
||||
onChange={useCallback((value: string) => {
|
||||
setPassword(value);
|
||||
}, [])}
|
||||
error={passwordError}
|
||||
errorHint={t['com.affine.auth.password.error']()}
|
||||
onEnter={onSignIn}
|
||||
/>
|
||||
<span></span>
|
||||
<button
|
||||
className={forgetPasswordButton}
|
||||
// onClick={useCallback(() => {
|
||||
// setAuthState('sendPasswordEmail');
|
||||
// }, [setAuthState])}
|
||||
>
|
||||
{t['com.affine.auth.forget']()}
|
||||
</button>
|
||||
</Wrapper>
|
||||
<Button
|
||||
data-testid="sign-in-button"
|
||||
type="primary"
|
||||
size="extraLarge"
|
||||
style={{ width: '100%' }}
|
||||
onClick={onSignIn}
|
||||
>
|
||||
{t['com.affine.auth.sign.in']()}
|
||||
</Button>
|
||||
|
||||
<BackButton
|
||||
onClick={useCallback(() => {
|
||||
setAuthState('afterSignInSendEmail');
|
||||
}, [setAuthState])}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
151
apps/core/src/components/affine/auth/sign-in.tsx
Normal file
151
apps/core/src/components/affine/auth/sign-in.tsx
Normal file
@ -0,0 +1,151 @@
|
||||
import { AuthInput, ModalHeader } from '@affine/component/auth-components';
|
||||
import { pushNotificationAtom } from '@affine/component/notification-center';
|
||||
import type { Notification } from '@affine/component/notification-center/index.jotai';
|
||||
import { getUserQuery } from '@affine/graphql';
|
||||
import { Trans } from '@affine/i18n';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useMutation } from '@affine/workspace/affine/gql';
|
||||
import { ArrowDownBigIcon, GoogleDuotoneIcon } from '@blocksuite/icons';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { signIn, type SignInResponse } from 'next-auth/react';
|
||||
import { type FC, useState } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { emailRegex } from '../../../utils/email-regex';
|
||||
import { buildCallbackUrl } from './callback-url';
|
||||
import type { AuthPanelProps } from './index';
|
||||
import * as style from './style.css';
|
||||
|
||||
function validateEmail(email: string) {
|
||||
return emailRegex.test(email);
|
||||
}
|
||||
|
||||
function handleSendEmailError(
|
||||
res: SignInResponse | undefined,
|
||||
pushNotification: (notification: Notification) => void
|
||||
) {
|
||||
if (res?.error) {
|
||||
pushNotification({
|
||||
title: 'Send email error',
|
||||
message: 'Please back to home and try again',
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const SignIn: FC<AuthPanelProps> = ({
|
||||
setAuthState,
|
||||
setAuthEmail,
|
||||
email,
|
||||
}) => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const { trigger: verifyUser, isMutating } = useMutation({
|
||||
mutation: getUserQuery,
|
||||
});
|
||||
const [isValidEmail, setIsValidEmail] = useState(true);
|
||||
const pushNotification = useSetAtom(pushNotificationAtom);
|
||||
const onContinue = useCallback(async () => {
|
||||
if (!validateEmail(email)) {
|
||||
setIsValidEmail(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsValidEmail(true);
|
||||
const { user } = await verifyUser({ email });
|
||||
|
||||
setAuthEmail(email);
|
||||
if (user) {
|
||||
signIn('email', {
|
||||
email: email,
|
||||
callbackUrl: buildCallbackUrl('signIn'),
|
||||
redirect: false,
|
||||
})
|
||||
.then(res => handleSendEmailError(res, pushNotification))
|
||||
.catch(console.error);
|
||||
setAuthState('afterSignInSendEmail');
|
||||
} else {
|
||||
signIn('email', {
|
||||
email: email,
|
||||
callbackUrl: buildCallbackUrl('signUp'),
|
||||
redirect: false,
|
||||
})
|
||||
.then(res => handleSendEmailError(res, pushNotification))
|
||||
.catch(console.error);
|
||||
|
||||
setAuthState('afterSignUpSendEmail');
|
||||
}
|
||||
}, [email, setAuthEmail, setAuthState, verifyUser, pushNotification]);
|
||||
return (
|
||||
<>
|
||||
<ModalHeader
|
||||
title={t['com.affine.auth.sign.in']()}
|
||||
subTitle={t['AFFiNE Cloud']()}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
block
|
||||
size="extraLarge"
|
||||
style={{
|
||||
marginTop: 30,
|
||||
}}
|
||||
icon={<GoogleDuotoneIcon />}
|
||||
onClick={useCallback(() => {
|
||||
signIn('google').catch(console.error);
|
||||
}, [])}
|
||||
>
|
||||
{t['Continue with Google']()}
|
||||
</Button>
|
||||
|
||||
<div className={style.authModalContent}>
|
||||
<AuthInput
|
||||
label={t['com.affine.settings.email']()}
|
||||
placeholder={t['com.affine.auth.sign.email.placeholder']()}
|
||||
value={email}
|
||||
onChange={useCallback(
|
||||
(value: string) => {
|
||||
setAuthEmail(value);
|
||||
},
|
||||
[setAuthEmail]
|
||||
)}
|
||||
error={!isValidEmail}
|
||||
errorHint={
|
||||
isValidEmail ? '' : t['com.affine.auth.sign.email.error']()
|
||||
}
|
||||
onEnter={onContinue}
|
||||
/>
|
||||
|
||||
<Button
|
||||
size="extraLarge"
|
||||
data-testid="continue-login-button"
|
||||
block
|
||||
loading={isMutating}
|
||||
icon={
|
||||
<ArrowDownBigIcon
|
||||
width={20}
|
||||
height={20}
|
||||
style={{
|
||||
transform: 'rotate(-90deg)',
|
||||
color: 'var(--affine-blue)',
|
||||
}}
|
||||
/>
|
||||
}
|
||||
iconPosition="end"
|
||||
onClick={onContinue}
|
||||
>
|
||||
{t['com.affine.auth.sign.email.continue']()}
|
||||
</Button>
|
||||
|
||||
<div className={style.authMessage}>
|
||||
{/*prettier-ignore*/}
|
||||
<Trans i18nKey="com.affine.auth.sign.message">
|
||||
By clicking "Continue with Google/Email" above, you acknowledge that
|
||||
you agree to AFFiNE's <a href="https://affine.pro/terms" target="_blank" rel="noreferrer">Terms of Conditions</a> and <a href="https://affine.pro/privacy" target="_blank" rel="noreferrer">Privacy Policy</a>.
|
||||
</Trans>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
28
apps/core/src/components/affine/auth/style.css.ts
Normal file
28
apps/core/src/components/affine/auth/style.css.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { globalStyle, style } from '@vanilla-extract/css';
|
||||
|
||||
export const authModalContent = style({
|
||||
marginTop: '30px',
|
||||
});
|
||||
|
||||
export const authMessage = style({
|
||||
marginTop: '30px',
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
fontSize: 'var(--affine-font-xs)',
|
||||
lineHeight: 1.5,
|
||||
});
|
||||
globalStyle(`${authMessage} a`, {
|
||||
color: 'var(--affine-link-color)',
|
||||
});
|
||||
globalStyle(`${authMessage} .link`, {
|
||||
cursor: 'pointer',
|
||||
color: 'var(--affine-link-color)',
|
||||
});
|
||||
|
||||
export const forgetPasswordButton = style({
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
display: 'none',
|
||||
});
|
@ -1,9 +1,9 @@
|
||||
import { Modal, ModalWrapper, Wrapper } from '@affine/component';
|
||||
import { Modal, ModalWrapper } from '@affine/component';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { CloseIcon } from '@blocksuite/icons';
|
||||
import { Button, IconButton } from '@toeverything/components/button';
|
||||
|
||||
import { Content, ContentTitle, Header, StyleTips } from './style';
|
||||
import { ButtonContainer, Content, Header, StyleTips, Title } from './style';
|
||||
|
||||
interface EnableAffineCloudModalProps {
|
||||
open: boolean;
|
||||
@ -20,32 +20,32 @@ export const EnableAffineCloudModal = ({
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose} data-testid="logout-modal">
|
||||
<ModalWrapper width={560} height={292}>
|
||||
<ModalWrapper width={480}>
|
||||
<Header>
|
||||
<Title>{t['Enable AFFiNE Cloud']()}</Title>
|
||||
<IconButton onClick={onClose}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</Header>
|
||||
<Content>
|
||||
<ContentTitle>{t['Enable AFFiNE Cloud']()}?</ContentTitle>
|
||||
<StyleTips>{t['Enable AFFiNE Cloud Description']()}</StyleTips>
|
||||
{/* <StyleTips>{t('Retain cached cloud data')}</StyleTips> */}
|
||||
<Wrapper width={284} margin="auto">
|
||||
<Button
|
||||
data-testid="confirm-enable-affine-cloud-button"
|
||||
type="primary"
|
||||
block
|
||||
onClick={onConfirm}
|
||||
style={{
|
||||
marginBottom: '16px',
|
||||
}}
|
||||
>
|
||||
{t['Sign in and Enable']()}
|
||||
</Button>
|
||||
<Button onClick={onClose} block>
|
||||
{t['Not now']()}
|
||||
</Button>
|
||||
</Wrapper>
|
||||
<ButtonContainer>
|
||||
<div>
|
||||
<Button onClick={onClose} block>
|
||||
{t['Cancel']()}
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
data-testid="confirm-enable-affine-cloud-button"
|
||||
type="primary"
|
||||
block
|
||||
onClick={onConfirm}
|
||||
>
|
||||
{t['Sign in and Enable']()}
|
||||
</Button>
|
||||
</div>
|
||||
</ButtonContainer>
|
||||
</Content>
|
||||
</ModalWrapper>
|
||||
</Modal>
|
||||
|
@ -1,31 +1,35 @@
|
||||
import { styled } from '@affine/component';
|
||||
|
||||
export const Header = styled('div')({
|
||||
height: '44px',
|
||||
display: 'flex',
|
||||
flexDirection: 'row-reverse',
|
||||
paddingRight: '10px',
|
||||
paddingTop: '10px',
|
||||
flexShrink: 0,
|
||||
justifyContent: 'space-between',
|
||||
paddingRight: '20px',
|
||||
paddingTop: '20px',
|
||||
paddingLeft: '24px',
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
export const Content = styled('div')({
|
||||
textAlign: 'center',
|
||||
padding: '12px 24px 20px 24px',
|
||||
});
|
||||
|
||||
export const ContentTitle = styled('h1')({
|
||||
fontSize: '20px',
|
||||
lineHeight: '28px',
|
||||
export const Title = styled('div')({
|
||||
fontSize: 'var(--affine-font-h6)',
|
||||
lineHeight: '26px',
|
||||
fontWeight: 600,
|
||||
textAlign: 'center',
|
||||
});
|
||||
|
||||
export const StyleTips = styled('div')(() => {
|
||||
return {
|
||||
userSelect: 'none',
|
||||
width: '400px',
|
||||
margin: 'auto',
|
||||
marginBottom: '32px',
|
||||
marginTop: '12px',
|
||||
marginBottom: '20px',
|
||||
};
|
||||
});
|
||||
export const ButtonContainer = styled('div')(() => {
|
||||
return {
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
gap: '20px',
|
||||
paddingTop: '20px',
|
||||
};
|
||||
});
|
||||
|
@ -1,13 +1,12 @@
|
||||
import { Input, Modal, ModalCloseButton } from '@affine/component';
|
||||
import type { AffineOfficialWorkspace } from '@affine/env/workspace';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { Trans } from '@affine/i18n';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import type { AffineOfficialWorkspace } from '../../../../../shared';
|
||||
import { toast } from '../../../../../utils';
|
||||
import {
|
||||
StyledButtonContent,
|
||||
StyledInputContent,
|
||||
@ -21,14 +20,14 @@ interface WorkspaceDeleteProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
workspace: AffineOfficialWorkspace;
|
||||
onDeleteWorkspace: (id: string) => Promise<void>;
|
||||
onConfirm: () => void;
|
||||
}
|
||||
|
||||
export const WorkspaceDeleteModal = ({
|
||||
open,
|
||||
onClose,
|
||||
onConfirm,
|
||||
workspace,
|
||||
onDeleteWorkspace,
|
||||
}: WorkspaceDeleteProps) => {
|
||||
const [workspaceName] = useBlockSuiteWorkspaceName(
|
||||
workspace.blockSuiteWorkspace
|
||||
@ -37,19 +36,6 @@ export const WorkspaceDeleteModal = ({
|
||||
const allowDelete = deleteStr === workspaceName;
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const handleDelete = useCallback(() => {
|
||||
onDeleteWorkspace(workspace.id)
|
||||
.then(() => {
|
||||
toast(t['Successfully deleted'](), {
|
||||
portal: document.body,
|
||||
});
|
||||
onClose();
|
||||
})
|
||||
.catch(() => {
|
||||
// ignore error
|
||||
});
|
||||
}, [onClose, onDeleteWorkspace, t, workspace.id]);
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<StyledModalWrapper>
|
||||
@ -99,7 +85,7 @@ export const WorkspaceDeleteModal = ({
|
||||
<Button
|
||||
data-testid="delete-workspace-confirm-button"
|
||||
disabled={!allowDelete}
|
||||
onClick={handleDelete}
|
||||
onClick={onConfirm}
|
||||
size="large"
|
||||
type="error"
|
||||
style={{ marginLeft: '24px' }}
|
||||
|
@ -1,29 +1,55 @@
|
||||
import { ConfirmModal } from '@affine/component';
|
||||
import { SettingRow } from '@affine/component/setting-components';
|
||||
import type { AffineOfficialWorkspace } from '@affine/env/workspace';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { ArrowRightSmallIcon } from '@blocksuite/icons';
|
||||
import { useState } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import type { AffineOfficialWorkspace } from '../../../../shared';
|
||||
import type { WorkspaceSettingDetailProps } from '../index';
|
||||
import { WorkspaceDeleteModal } from './delete';
|
||||
import { WorkspaceLeave } from './leave';
|
||||
|
||||
interface DeleteLeaveWorkspaceProps {
|
||||
export interface DeleteLeaveWorkspaceProps extends WorkspaceSettingDetailProps {
|
||||
workspace: AffineOfficialWorkspace;
|
||||
onDeleteWorkspace: WorkspaceSettingDetailProps['onDeleteWorkspace'];
|
||||
}
|
||||
|
||||
export const DeleteLeaveWorkspace = ({
|
||||
workspace,
|
||||
onDeleteWorkspace,
|
||||
onDeleteCloudWorkspace,
|
||||
onDeleteLocalWorkspace,
|
||||
onLeaveWorkspace,
|
||||
isOwner,
|
||||
}: DeleteLeaveWorkspaceProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
// fixme: cloud regression
|
||||
const isOwner = true;
|
||||
|
||||
const [showDelete, setShowDelete] = useState(false);
|
||||
const [showLeave, setShowLeave] = useState(false);
|
||||
|
||||
const onLeaveOrDelete = useCallback(() => {
|
||||
if (isOwner) {
|
||||
setShowDelete(true);
|
||||
} else {
|
||||
setShowLeave(true);
|
||||
}
|
||||
}, [isOwner]);
|
||||
|
||||
const onCloseLeaveModal = useCallback(() => {
|
||||
setShowLeave(false);
|
||||
}, []);
|
||||
|
||||
const onLeaveConfirm = useCallback(() => {
|
||||
return onLeaveWorkspace();
|
||||
}, [onLeaveWorkspace]);
|
||||
|
||||
const onDeleteConfirm = useCallback(() => {
|
||||
if (workspace.flavour === WorkspaceFlavour.LOCAL) {
|
||||
return onDeleteLocalWorkspace();
|
||||
}
|
||||
if (workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD) {
|
||||
return onDeleteCloudWorkspace();
|
||||
}
|
||||
}, [onDeleteCloudWorkspace, onDeleteLocalWorkspace, workspace.flavour]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingRow
|
||||
@ -36,16 +62,14 @@ export const DeleteLeaveWorkspace = ({
|
||||
}
|
||||
desc={t['com.affine.settings.remove-workspace-description']()}
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
setShowDelete(true);
|
||||
}}
|
||||
onClick={onLeaveOrDelete}
|
||||
data-testid="delete-workspace-button"
|
||||
>
|
||||
<ArrowRightSmallIcon />
|
||||
</SettingRow>
|
||||
{isOwner ? (
|
||||
<WorkspaceDeleteModal
|
||||
onDeleteWorkspace={onDeleteWorkspace}
|
||||
onConfirm={onDeleteConfirm}
|
||||
open={showDelete}
|
||||
onClose={() => {
|
||||
setShowDelete(false);
|
||||
@ -53,11 +77,15 @@ export const DeleteLeaveWorkspace = ({
|
||||
workspace={workspace}
|
||||
/>
|
||||
) : (
|
||||
<WorkspaceLeave
|
||||
<ConfirmModal
|
||||
open={showLeave}
|
||||
onClose={() => {
|
||||
setShowLeave(false);
|
||||
}}
|
||||
onConfirm={onLeaveConfirm}
|
||||
onCancel={onCloseLeaveModal}
|
||||
onClose={onCloseLeaveModal}
|
||||
title={`${t['Leave Workspace']()}?`}
|
||||
content={t['Leave Workspace hint']()}
|
||||
confirmType="warning"
|
||||
confirmText={t['Leave']()}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
@ -1,49 +0,0 @@
|
||||
import { Modal } from '@affine/component';
|
||||
import { ModalCloseButton } from '@affine/component';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
|
||||
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="error"
|
||||
style={{ marginLeft: '24px' }}
|
||||
>
|
||||
{t['Leave']()}
|
||||
</Button>
|
||||
</StyledButtonContent>
|
||||
</StyledModalWrapper>
|
||||
</Modal>
|
||||
);
|
||||
};
|
@ -1,44 +0,0 @@
|
||||
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',
|
||||
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',
|
||||
};
|
||||
});
|
@ -1,13 +1,12 @@
|
||||
import { toast } from '@affine/component';
|
||||
import { SettingRow } from '@affine/component/setting-components';
|
||||
import { isDesktop } from '@affine/env/constant';
|
||||
import type { AffineOfficialWorkspace } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import type { SaveDBFileResult } from '@toeverything/infra/type';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import type { AffineOfficialWorkspace } from '../../../shared';
|
||||
|
||||
async function syncBlobsToSqliteDb(workspace: AffineOfficialWorkspace) {
|
||||
if (window.apis && isDesktop) {
|
||||
const bs = workspace.blockSuiteWorkspace.blobs;
|
||||
@ -41,7 +40,7 @@ export const ExportPanel = ({ workspace }: ExportPanelProps) => {
|
||||
const result: SaveDBFileResult =
|
||||
await window.apis?.dialog.saveDBFileAs(workspaceId);
|
||||
if (result?.error) {
|
||||
toast(t[result.error]());
|
||||
toast(result.error);
|
||||
} else if (!result?.canceled) {
|
||||
toast(t['Export success']());
|
||||
}
|
||||
|
@ -14,13 +14,17 @@ import { useMemo } from 'react';
|
||||
import { useWorkspace } from '../../../hooks/use-workspace';
|
||||
import { DeleteLeaveWorkspace } from './delete-leave-workspace';
|
||||
import { ExportPanel } from './export';
|
||||
import { MembersPanel } from './members';
|
||||
import { ProfilePanel } from './profile';
|
||||
import { PublishPanel } from './publish';
|
||||
import { StoragePanel } from './storage';
|
||||
|
||||
export interface WorkspaceSettingDetailProps {
|
||||
workspaceId: string;
|
||||
onDeleteWorkspace: (id: string) => Promise<void>;
|
||||
isOwner: boolean;
|
||||
onDeleteLocalWorkspace: () => void;
|
||||
onDeleteCloudWorkspace: () => void;
|
||||
onLeaveWorkspace: () => void;
|
||||
onTransferWorkspace: <
|
||||
From extends WorkspaceFlavour,
|
||||
To extends WorkspaceFlavour,
|
||||
@ -31,11 +35,8 @@ export interface WorkspaceSettingDetailProps {
|
||||
) => void;
|
||||
}
|
||||
|
||||
export const WorkspaceSettingDetail = ({
|
||||
workspaceId,
|
||||
onDeleteWorkspace,
|
||||
...props
|
||||
}: WorkspaceSettingDetailProps) => {
|
||||
export const WorkspaceSettingDetail = (props: WorkspaceSettingDetailProps) => {
|
||||
const { workspaceId } = props;
|
||||
const t = useAFFiNEI18N();
|
||||
const workspace = useWorkspace(workspaceId);
|
||||
const [name] = useBlockSuiteWorkspaceName(workspace.blockSuiteWorkspace);
|
||||
@ -67,22 +68,16 @@ export const WorkspaceSettingDetail = ({
|
||||
desc={t['com.affine.settings.workspace.not-owner']()}
|
||||
spreadCol={false}
|
||||
>
|
||||
<ProfilePanel workspace={workspace} />
|
||||
<ProfilePanel workspace={workspace} {...props} />
|
||||
</SettingRow>
|
||||
</SettingWrapper>
|
||||
<SettingWrapper title={t['AFFiNE Cloud']()}>
|
||||
<PublishPanel
|
||||
workspace={workspace}
|
||||
onDeleteWorkspace={onDeleteWorkspace}
|
||||
{...props}
|
||||
/>
|
||||
<PublishPanel workspace={workspace} {...props} />
|
||||
<MembersPanel workspace={workspace} {...props} />
|
||||
</SettingWrapper>
|
||||
{storageAndExportSetting}
|
||||
<SettingWrapper>
|
||||
<DeleteLeaveWorkspace
|
||||
workspace={workspace}
|
||||
onDeleteWorkspace={onDeleteWorkspace}
|
||||
/>
|
||||
<DeleteLeaveWorkspace workspace={workspace} {...props} />
|
||||
</SettingWrapper>
|
||||
</>
|
||||
);
|
||||
|
@ -0,0 +1,228 @@
|
||||
import { Menu, MenuItem } from '@affine/component';
|
||||
import {
|
||||
InviteModal,
|
||||
type InviteModalProps,
|
||||
} from '@affine/component/member-components';
|
||||
import { pushNotificationAtom } from '@affine/component/notification-center';
|
||||
import { SettingRow } from '@affine/component/setting-components';
|
||||
import type { AffineOfficialWorkspace } from '@affine/env/workspace';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { Permission } from '@affine/graphql';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { MoreVerticalIcon } from '@blocksuite/icons';
|
||||
import { Avatar } from '@toeverything/components/avatar';
|
||||
import { Button, IconButton } from '@toeverything/components/button';
|
||||
import { Tooltip } from '@toeverything/components/tooltip';
|
||||
import clsx from 'clsx';
|
||||
import { useSetAtom } from 'jotai/react';
|
||||
import type { ReactElement } from 'react';
|
||||
import { Suspense, useCallback, useMemo, useState } from 'react';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
|
||||
import type { CheckedUser } from '../../../hooks/affine/use-current-user';
|
||||
import { useCurrentUser } from '../../../hooks/affine/use-current-user';
|
||||
import { useInviteMember } from '../../../hooks/affine/use-invite-member';
|
||||
import { type Member, useMembers } from '../../../hooks/affine/use-members';
|
||||
import { useRevokeMemberPermission } from '../../../hooks/affine/use-revoke-member-permission';
|
||||
import { AnyErrorBoundary } from '../any-error-boundary';
|
||||
import { type WorkspaceSettingDetailProps } from './index';
|
||||
import * as style from './style.css';
|
||||
|
||||
export interface MembersPanelProps extends WorkspaceSettingDetailProps {
|
||||
workspace: AffineOfficialWorkspace;
|
||||
}
|
||||
|
||||
const MembersPanelLocal = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
return (
|
||||
<Tooltip content={t['com.affine.settings.member-tooltip']()}>
|
||||
<div className={style.fakeWrapper}>
|
||||
<SettingRow name={`${t['Members']()} (0)`} desc={t['Members hint']()}>
|
||||
<Button size="large">{t['Invite Members']()}</Button>
|
||||
</SettingRow>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export const CloudWorkspaceMembersPanel = ({
|
||||
workspace,
|
||||
isOwner,
|
||||
}: MembersPanelProps): ReactElement => {
|
||||
const workspaceId = workspace.id;
|
||||
const members = useMembers(workspaceId);
|
||||
const t = useAFFiNEI18N();
|
||||
const currentUser = useCurrentUser();
|
||||
const { invite, isMutating } = useInviteMember(workspaceId);
|
||||
const [open, setOpen] = useState(false);
|
||||
const pushNotification = useSetAtom(pushNotificationAtom);
|
||||
const revokeMemberPermission = useRevokeMemberPermission(workspaceId);
|
||||
|
||||
const memberCount = members.length;
|
||||
const memberList = useMemo(
|
||||
() =>
|
||||
members.sort((a, b) => {
|
||||
if (
|
||||
a.permission === Permission.Owner &&
|
||||
b.permission !== Permission.Owner
|
||||
) {
|
||||
return -1;
|
||||
}
|
||||
if (
|
||||
a.permission !== Permission.Owner &&
|
||||
b.permission === Permission.Owner
|
||||
) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}),
|
||||
[members]
|
||||
);
|
||||
|
||||
const openModal = useCallback(() => {
|
||||
setOpen(true);
|
||||
}, []);
|
||||
|
||||
const onInviteConfirm = useCallback<InviteModalProps['onConfirm']>(
|
||||
async ({ email, permission }) => {
|
||||
const success = await invite(
|
||||
email,
|
||||
permission,
|
||||
// send invite email
|
||||
true
|
||||
);
|
||||
if (success) {
|
||||
pushNotification({
|
||||
title: t['Invitation sent'](),
|
||||
message: t['Invitation sent hint'](),
|
||||
type: 'success',
|
||||
});
|
||||
setOpen(false);
|
||||
}
|
||||
},
|
||||
[invite, pushNotification, t]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingRow
|
||||
name={`${t['Members']()} (${memberCount})`}
|
||||
desc={t['Members hint']()}
|
||||
>
|
||||
{isOwner ? (
|
||||
<>
|
||||
<Button onClick={openModal}>{t['Invite Members']()}</Button>
|
||||
<InviteModal
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
onConfirm={onInviteConfirm}
|
||||
isMutating={isMutating}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</SettingRow>
|
||||
<div className={style.membersList}>
|
||||
{memberList.map(member => (
|
||||
<MemberItem
|
||||
key={member.id}
|
||||
member={member}
|
||||
isOwner={isOwner}
|
||||
currentUser={currentUser}
|
||||
onRevoke={revokeMemberPermission}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const MemberItem = ({
|
||||
member,
|
||||
isOwner,
|
||||
currentUser,
|
||||
onRevoke,
|
||||
}: {
|
||||
member: Member;
|
||||
isOwner: boolean;
|
||||
currentUser: CheckedUser;
|
||||
onRevoke: (memberId: string) => void;
|
||||
}) => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const handleRevoke = useCallback(() => {
|
||||
onRevoke(member.id);
|
||||
}, [onRevoke, member.id]);
|
||||
|
||||
const operationButtonInfo = useMemo(() => {
|
||||
return {
|
||||
show: isOwner && currentUser.id !== member.id,
|
||||
leaveOrRevokeText: t['Remove from workspace'](),
|
||||
};
|
||||
}, [currentUser.id, isOwner, member.id, t]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div key={member.id} className={style.listItem}>
|
||||
<Avatar
|
||||
size={36}
|
||||
url={member.avatarUrl}
|
||||
name={(member.emailVerified ? member.name : member.email) as string}
|
||||
/>
|
||||
<div className={style.memberContainer}>
|
||||
{member.emailVerified ? (
|
||||
<>
|
||||
<div className={style.memberName}>{member.name}</div>
|
||||
<div className={style.memberEmail}>{member.email}</div>
|
||||
</>
|
||||
) : (
|
||||
<div className={style.memberName}>{member.email}</div>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={clsx(style.roleOrStatus, {
|
||||
pending: !member.accepted,
|
||||
})}
|
||||
>
|
||||
{member.accepted
|
||||
? member.permission === Permission.Owner
|
||||
? 'Workspace Owner'
|
||||
: 'Member'
|
||||
: 'Pending'}
|
||||
</div>
|
||||
<Menu
|
||||
content={
|
||||
<MenuItem data-member-id={member.id} onClick={handleRevoke}>
|
||||
{operationButtonInfo.leaveOrRevokeText}
|
||||
</MenuItem>
|
||||
}
|
||||
placement="bottom"
|
||||
disablePortal={true}
|
||||
trigger="click"
|
||||
>
|
||||
<IconButton
|
||||
disabled={!operationButtonInfo.show}
|
||||
style={{
|
||||
visibility: operationButtonInfo.show ? 'visible' : 'hidden',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<MoreVerticalIcon />
|
||||
</IconButton>
|
||||
</Menu>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const MembersPanel = (props: MembersPanelProps): ReactElement | null => {
|
||||
if (props.workspace.flavour === WorkspaceFlavour.LOCAL) {
|
||||
return <MembersPanelLocal />;
|
||||
}
|
||||
return (
|
||||
<ErrorBoundary FallbackComponent={AnyErrorBoundary}>
|
||||
<Suspense>
|
||||
<CloudWorkspaceMembersPanel {...props} />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
@ -1,21 +1,23 @@
|
||||
import { FlexWrapper, Input, toast, Wrapper } from '@affine/component';
|
||||
import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
|
||||
import type { AffineOfficialWorkspace } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { CameraIcon, DoneIcon } from '@blocksuite/icons';
|
||||
import { IconButton } from '@toeverything/components/button';
|
||||
import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-block-suite-workspace-avatar-url';
|
||||
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
|
||||
import clsx from 'clsx';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import type { AffineOfficialWorkspace } from '../../../shared';
|
||||
import { Upload } from '../../pure/file-upload';
|
||||
import { type WorkspaceSettingDetailProps } from './index';
|
||||
import * as style from './style.css';
|
||||
|
||||
interface ProfilePanelProps {
|
||||
export interface ProfilePanelProps extends WorkspaceSettingDetailProps {
|
||||
workspace: AffineOfficialWorkspace;
|
||||
}
|
||||
|
||||
export const ProfilePanel = ({ workspace }: ProfilePanelProps) => {
|
||||
export const ProfilePanel = ({ workspace, isOwner }: ProfilePanelProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const [, update] = useBlockSuiteWorkspaceAvatarUrl(
|
||||
@ -38,7 +40,7 @@ export const ProfilePanel = ({ workspace }: ProfilePanelProps) => {
|
||||
|
||||
return (
|
||||
<div className={style.profileWrapper}>
|
||||
<div className={style.avatarWrapper}>
|
||||
<div className={clsx(style.avatarWrapper, { disable: !isOwner })}>
|
||||
<Upload
|
||||
accept="image/gif,image/jpeg,image/jpg,image/png,image/svg"
|
||||
fileChange={update}
|
||||
@ -59,6 +61,7 @@ export const ProfilePanel = ({ workspace }: ProfilePanelProps) => {
|
||||
<div className={style.label}>{t['Workspace Name']()}</div>
|
||||
<FlexWrapper alignItems="center" flexGrow="1">
|
||||
<Input
|
||||
disabled={!isOwner}
|
||||
width={280}
|
||||
height={32}
|
||||
defaultValue={input}
|
||||
|
@ -1,18 +1,19 @@
|
||||
import { FlexWrapper, Switch } from '@affine/component';
|
||||
import { FlexWrapper, Input, Switch } from '@affine/component';
|
||||
import { SettingRow } from '@affine/component/setting-components';
|
||||
import { Unreachable } from '@affine/env/constant';
|
||||
import type {
|
||||
AffineCloudWorkspace,
|
||||
AffinePublicWorkspace,
|
||||
LocalWorkspace,
|
||||
} from '@affine/env/workspace';
|
||||
import type { AffineOfficialWorkspace } from '@affine/env/workspace';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import { Tooltip } from '@toeverything/components/tooltip';
|
||||
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import type { AffineOfficialWorkspace } from '../../../shared';
|
||||
import { toast } from '../../../utils';
|
||||
import { EnableAffineCloudModal } from '../enable-affine-cloud-modal';
|
||||
import { TmpDisableAffineCloudModal } from '../tmp-disable-affine-cloud-modal';
|
||||
@ -29,14 +30,16 @@ export interface PublishPanelLocalProps
|
||||
}
|
||||
export interface PublishPanelAffineProps
|
||||
extends Omit<WorkspaceSettingDetailProps, 'workspaceId'> {
|
||||
workspace: AffineCloudWorkspace;
|
||||
workspace: AffineCloudWorkspace | AffinePublicWorkspace;
|
||||
}
|
||||
|
||||
const PublishPanelAffine = (props: PublishPanelAffineProps) => {
|
||||
const { workspace } = props;
|
||||
const t = useAFFiNEI18N();
|
||||
// const toggleWorkspacePublish = useToggleWorkspacePublish(workspace);
|
||||
|
||||
const isPublic = useMemo(() => {
|
||||
return workspace.flavour === WorkspaceFlavour.AFFINE_PUBLIC;
|
||||
}, [workspace]);
|
||||
const [origin, setOrigin] = useState('');
|
||||
const shareUrl = origin + '/public-workspace/' + workspace.id;
|
||||
|
||||
@ -54,35 +57,35 @@ const PublishPanelAffine = (props: PublishPanelAffineProps) => {
|
||||
}, [shareUrl, t]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ display: 'none' }}>
|
||||
<SettingRow
|
||||
name={t['Publish']()}
|
||||
desc={
|
||||
// workspace.public ? t['Unpublished hint']() : t['Published hint']()
|
||||
'UNFINISHED'
|
||||
}
|
||||
desc={isPublic ? t['Unpublished hint']() : t['Published hint']()}
|
||||
style={{
|
||||
marginBottom: isPublic ? '12px' : '25px',
|
||||
}}
|
||||
>
|
||||
{/* <Switch
|
||||
checked={workspace.public}
|
||||
onChange={checked => toggleWorkspacePublish(checked)}
|
||||
/> */}
|
||||
<Switch
|
||||
checked={isPublic}
|
||||
// onChange={useCallback(value => {
|
||||
// console.log('onChange', value);
|
||||
// }, [])}
|
||||
/>
|
||||
</SettingRow>
|
||||
<FlexWrapper justifyContent="space-between">
|
||||
<Button
|
||||
className={style.urlButton}
|
||||
size="large"
|
||||
onClick={useCallback(() => {
|
||||
window.open(shareUrl, '_blank');
|
||||
}, [shareUrl])}
|
||||
title={shareUrl}
|
||||
>
|
||||
{shareUrl}
|
||||
</Button>
|
||||
<Button size="large" onClick={copyUrl}>
|
||||
{t['Copy']()}
|
||||
</Button>
|
||||
</FlexWrapper>
|
||||
</>
|
||||
{isPublic ? (
|
||||
<FlexWrapper justifyContent="space-between" marginBottom={25}>
|
||||
<Input value={shareUrl} disabled />
|
||||
<Button
|
||||
onClick={copyUrl}
|
||||
style={{
|
||||
marginLeft: '20px',
|
||||
}}
|
||||
>
|
||||
{t['Copy']()}
|
||||
</Button>
|
||||
</FlexWrapper>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -164,7 +167,10 @@ const PublishPanelLocal = ({
|
||||
};
|
||||
|
||||
export const PublishPanel = (props: PublishPanelProps) => {
|
||||
if (props.workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD) {
|
||||
if (
|
||||
props.workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD ||
|
||||
props.workspace.flavour === WorkspaceFlavour.AFFINE_PUBLIC
|
||||
) {
|
||||
return <PublishPanelAffine {...props} workspace={props.workspace} />;
|
||||
} else if (props.workspace.flavour === WorkspaceFlavour.LOCAL) {
|
||||
return <PublishPanelLocal {...props} workspace={props.workspace} />;
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { FlexWrapper, toast } from '@affine/component';
|
||||
import { SettingRow } from '@affine/component/setting-components';
|
||||
import type { AffineOfficialWorkspace } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import { Tooltip } from '@toeverything/components/tooltip';
|
||||
@ -7,9 +8,6 @@ import type { MoveDBFileResult } from '@toeverything/infra/type';
|
||||
import { useMemo } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import type { AffineOfficialWorkspace } from '../../../shared';
|
||||
import * as style from './style.css';
|
||||
|
||||
const useDBFileSecondaryPath = (workspaceId: string) => {
|
||||
const [path, setPath] = useState<string | undefined>(undefined);
|
||||
useEffect(() => {
|
||||
@ -83,7 +81,7 @@ export const StoragePanel = ({ workspace }: StoragePanelProps) => {
|
||||
>
|
||||
<Button
|
||||
data-testid="move-folder"
|
||||
className={style.urlButton}
|
||||
// className={style.urlButton}
|
||||
size="large"
|
||||
onClick={handleMoveTo}
|
||||
>
|
||||
|
@ -5,6 +5,12 @@ export const profileWrapper = style({
|
||||
alignItems: 'flex-end',
|
||||
marginTop: '12px',
|
||||
});
|
||||
export const profileHandlerWrapper = style({
|
||||
flexGrow: '1',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
marginLeft: '20px',
|
||||
});
|
||||
|
||||
export const avatarWrapper = style({
|
||||
width: '56px',
|
||||
@ -24,6 +30,9 @@ export const avatarWrapper = style({
|
||||
globalStyle(`${avatarWrapper}:hover .camera-icon-wrapper`, {
|
||||
display: 'flex',
|
||||
});
|
||||
globalStyle(`${avatarWrapper}:hover .camera-icon-wrapper`, {
|
||||
display: 'flex',
|
||||
});
|
||||
globalStyle(`${avatarWrapper} .camera-icon-wrapper`, {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
@ -68,6 +77,70 @@ export const fakeWrapper = style({
|
||||
},
|
||||
});
|
||||
|
||||
export const membersList = style({
|
||||
marginTop: '24px',
|
||||
padding: '4px',
|
||||
borderRadius: '12px',
|
||||
background: 'var(--affine-background-primary-color)',
|
||||
maxHeight: '464px',
|
||||
overflow: 'hidden',
|
||||
});
|
||||
|
||||
export const listItem = style({
|
||||
padding: '0 4px 0 16px',
|
||||
height: '58px',
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
':hover': {
|
||||
background: 'var(--affine-hover-color)',
|
||||
borderRadius: '8px',
|
||||
},
|
||||
});
|
||||
export const memberContainer = style({
|
||||
width: '250px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flexShrink: 0,
|
||||
marginLeft: '12px',
|
||||
marginRight: '20px',
|
||||
});
|
||||
export const roleOrStatus = style({
|
||||
// width: '20%',
|
||||
flexGrow: 1,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
selectors: {
|
||||
'&.pending': {
|
||||
color: 'var(--affine-primary-color)',
|
||||
},
|
||||
},
|
||||
});
|
||||
export const memberName = style({
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
lineHeight: '22px',
|
||||
});
|
||||
export const memberEmail = style({
|
||||
fontSize: 'var(--affine-font-xs)',
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
lineHeight: '20px',
|
||||
});
|
||||
export const iconButton = style({});
|
||||
|
||||
globalStyle(`${listItem}:hover ${iconButton}`, {
|
||||
opacity: 1,
|
||||
pointerEvents: 'all',
|
||||
});
|
||||
|
||||
export const label = style({
|
||||
fontSize: 'var(--affine-font-xs)',
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
|
@ -1,3 +1,185 @@
|
||||
export const AccountSetting = () => {
|
||||
return <div>AccountSetting</div>;
|
||||
import { FlexWrapper, Input } from '@affine/component';
|
||||
import {
|
||||
SettingHeader,
|
||||
SettingRow,
|
||||
} from '@affine/component/setting-components';
|
||||
import { UserAvatar } from '@affine/component/user-avatar';
|
||||
import { uploadAvatarMutation } from '@affine/graphql';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useMutation } from '@affine/workspace/affine/gql';
|
||||
import { ArrowRightSmallIcon, CameraIcon, DoneIcon } from '@blocksuite/icons';
|
||||
import { Button, IconButton } from '@toeverything/components/button';
|
||||
import { useAtom } from 'jotai/index';
|
||||
import { signOut } from 'next-auth/react';
|
||||
import { type FC, useCallback, useState } from 'react';
|
||||
|
||||
import { authAtom } from '../../../../atoms';
|
||||
import { useCurrentUser } from '../../../../hooks/affine/use-current-user';
|
||||
import { Upload } from '../../../pure/file-upload';
|
||||
import * as style from './style.css';
|
||||
|
||||
export const AvatarAndName = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const user = useCurrentUser();
|
||||
|
||||
const [input, setInput] = useState<string>(user.name);
|
||||
|
||||
const { trigger: avatarTrigger } = useMutation({
|
||||
mutation: uploadAvatarMutation,
|
||||
});
|
||||
|
||||
const handleUpdateUserName = useCallback(
|
||||
(newName: string) => {
|
||||
user.update({ name: newName }).catch(console.error);
|
||||
},
|
||||
[user]
|
||||
);
|
||||
|
||||
const handleUpdateUserAvatar = useCallback(
|
||||
async (file: File) => {
|
||||
await avatarTrigger({
|
||||
id: user.id,
|
||||
avatar: file,
|
||||
});
|
||||
// XXX: This is a hack to force the user to update, since next-auth can not only use update function without params
|
||||
user.update({ name: user.name }).catch(console.error);
|
||||
},
|
||||
[avatarTrigger, user]
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<SettingRow
|
||||
name={t['com.affine.settings.profile']()}
|
||||
desc={t['com.affine.settings.profile.message']()}
|
||||
spreadCol={false}
|
||||
>
|
||||
<FlexWrapper style={{ margin: '12px 0 24px 0' }} alignItems="center">
|
||||
<div className={style.avatarWrapper}>
|
||||
<Upload
|
||||
accept="image/gif,image/jpeg,image/jpg,image/png,image/svg"
|
||||
fileChange={handleUpdateUserAvatar}
|
||||
data-testid="upload-user-avatar"
|
||||
>
|
||||
<>
|
||||
<div className="camera-icon-wrapper">
|
||||
<CameraIcon />
|
||||
</div>
|
||||
<UserAvatar
|
||||
size={56}
|
||||
name={user.name}
|
||||
url={user.image}
|
||||
className="avatar"
|
||||
/>
|
||||
</>
|
||||
</Upload>
|
||||
</div>
|
||||
|
||||
<div className={style.profileInputWrapper}>
|
||||
<label>{t['com.affine.settings.profile.name']()}</label>
|
||||
<FlexWrapper alignItems="center">
|
||||
<Input
|
||||
defaultValue={input}
|
||||
data-testid="user-name-input"
|
||||
placeholder={t['com.affine.settings.profile.placeholder']()}
|
||||
maxLength={64}
|
||||
minLength={0}
|
||||
width={280}
|
||||
height={28}
|
||||
onChange={setInput}
|
||||
/>
|
||||
{input && input === user.name ? null : (
|
||||
<IconButton
|
||||
data-testid="save-user-name"
|
||||
onClick={() => {
|
||||
handleUpdateUserName(input);
|
||||
}}
|
||||
style={{
|
||||
color: 'var(--affine-primary-color)',
|
||||
marginLeft: '12px',
|
||||
}}
|
||||
>
|
||||
<DoneIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
</FlexWrapper>
|
||||
</div>
|
||||
</FlexWrapper>
|
||||
</SettingRow>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const AccountSetting: FC = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const user = useCurrentUser();
|
||||
const [, setAuthModal] = useAtom(authAtom);
|
||||
|
||||
const onChangeEmail = useCallback(() => {
|
||||
setAuthModal({
|
||||
openModal: true,
|
||||
state: 'sendEmail',
|
||||
email: user.email,
|
||||
emailType: 'changeEmail',
|
||||
});
|
||||
}, [setAuthModal, user.email]);
|
||||
const onChangePassword = useCallback(() => {
|
||||
setAuthModal({
|
||||
openModal: true,
|
||||
state: 'sendEmail',
|
||||
email: user.email,
|
||||
emailType: 'changePassword',
|
||||
});
|
||||
}, [setAuthModal, user.email]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingHeader
|
||||
title={t['com.affine.setting.account']()}
|
||||
subtitle={t['com.affine.setting.account.message']()}
|
||||
data-testid="account-title"
|
||||
/>
|
||||
<AvatarAndName />
|
||||
<SettingRow name={t['com.affine.settings.email']()} desc={user.email}>
|
||||
<Button onClick={onChangeEmail}>
|
||||
{t['com.affine.settings.email.action']()}
|
||||
</Button>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name={t['com.affine.settings.password']()}
|
||||
desc={t['com.affine.settings.password.message']()}
|
||||
>
|
||||
<Button onClick={onChangePassword}>
|
||||
{user.hasPassword
|
||||
? t['com.affine.settings.password.action.change']()
|
||||
: t['com.affine.settings.password.action.set']()}
|
||||
</Button>
|
||||
</SettingRow>
|
||||
|
||||
<SettingRow
|
||||
name={t[`Sign out`]()}
|
||||
desc={t['com.affine.setting.sign.out.message']()}
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={useCallback(() => {
|
||||
signOut().catch(console.error);
|
||||
}, [])}
|
||||
>
|
||||
<ArrowRightSmallIcon />
|
||||
</SettingRow>
|
||||
{/*<SettingRow*/}
|
||||
{/* name={*/}
|
||||
{/* <span style={{ color: 'var(--affine-warning-color)' }}>*/}
|
||||
{/* {t['com.affine.setting.account.delete']()}*/}
|
||||
{/* </span>*/}
|
||||
{/* }*/}
|
||||
{/* desc={t['com.affine.setting.account.delete.message']()}*/}
|
||||
{/* style={{ cursor: 'pointer' }}*/}
|
||||
{/* onClick={useCallback(() => {*/}
|
||||
{/* toast('Function coming soon');*/}
|
||||
{/* }, [])}*/}
|
||||
{/* testId="delete-account-button"*/}
|
||||
{/*>*/}
|
||||
{/* <ArrowRightSmallIcon />*/}
|
||||
{/*</SettingRow>*/}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -0,0 +1,41 @@
|
||||
import { globalStyle, style } from '@vanilla-extract/css';
|
||||
export const profileInputWrapper = style({
|
||||
marginLeft: '20px',
|
||||
});
|
||||
globalStyle(`${profileInputWrapper} label`, {
|
||||
display: 'block',
|
||||
fontSize: 'var(--affine-font-xs)',
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
marginBottom: '4px',
|
||||
});
|
||||
|
||||
export const avatarWrapper = style({
|
||||
width: '56px',
|
||||
height: '56px',
|
||||
borderRadius: '50%',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
cursor: 'pointer',
|
||||
flexShrink: '0',
|
||||
selectors: {
|
||||
'&.disable': {
|
||||
cursor: 'default',
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
},
|
||||
});
|
||||
globalStyle(`${avatarWrapper}:hover .camera-icon-wrapper`, {
|
||||
display: 'flex',
|
||||
});
|
||||
globalStyle(`${avatarWrapper} .camera-icon-wrapper`, {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
display: 'none',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'rgba(60, 61, 63, 0.5)',
|
||||
zIndex: '1',
|
||||
color: 'var(--affine-white)',
|
||||
fontSize: 'var(--affine-font-h-4)',
|
||||
});
|
@ -7,6 +7,7 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { ContactWithUsIcon } from '@blocksuite/icons';
|
||||
import { Suspense, useCallback } from 'react';
|
||||
|
||||
import { useCurrenLoginStatus } from '../../../hooks/affine/use-curren-login-status';
|
||||
import { AccountSetting } from './account-setting';
|
||||
import {
|
||||
GeneralSetting,
|
||||
@ -38,6 +39,7 @@ export const SettingModal = ({
|
||||
onSettingClick,
|
||||
}: SettingModalProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const loginStatus = useCurrenLoginStatus();
|
||||
|
||||
const generalSettingList = useGeneralSettingList();
|
||||
|
||||
@ -85,7 +87,9 @@ export const SettingModal = ({
|
||||
{generalSettingList.find(v => v.key === activeTab) ? (
|
||||
<GeneralSetting generalKey={activeTab as GeneralSettingKeys} />
|
||||
) : null}
|
||||
{activeTab === 'account' ? <AccountSetting /> : null}
|
||||
{activeTab === 'account' && loginStatus === 'authenticated' ? (
|
||||
<AccountSetting />
|
||||
) : null}
|
||||
</div>
|
||||
<div className="footer">
|
||||
<div className={footerIconWrapper}>
|
||||
|
@ -1,41 +1,101 @@
|
||||
import { ScrollableContainer } from '@affine/component';
|
||||
import {
|
||||
WorkspaceListItemSkeleton,
|
||||
WorkspaceListSkeleton,
|
||||
} from '@affine/component/setting-components';
|
||||
import { UserAvatar } from '@affine/component/user-avatar';
|
||||
import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
|
||||
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
||||
import { Logo1Icon } from '@blocksuite/icons';
|
||||
import { Tooltip } from '@toeverything/components/tooltip';
|
||||
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
|
||||
import { useStaticBlockSuiteWorkspace } from '@toeverything/infra/__internal__/react';
|
||||
import clsx from 'clsx';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { Suspense, useRef } from 'react';
|
||||
import { useAtom, useAtomValue } from 'jotai/react';
|
||||
import {
|
||||
type ReactElement,
|
||||
Suspense,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react';
|
||||
|
||||
import { authAtom } from '../../../../atoms';
|
||||
import { useCurrenLoginStatus } from '../../../../hooks/affine/use-curren-login-status';
|
||||
import { useCurrentUser } from '../../../../hooks/affine/use-current-user';
|
||||
import { useCurrentWorkspace } from '../../../../hooks/current/use-current-workspace';
|
||||
import type {
|
||||
GeneralSettingKeys,
|
||||
GeneralSettingList,
|
||||
} from '../general-setting';
|
||||
import {
|
||||
accountButton,
|
||||
currentWorkspaceLabel,
|
||||
settingSlideBar,
|
||||
sidebarFooter,
|
||||
sidebarItemsWrapper,
|
||||
sidebarSelectItem,
|
||||
sidebarSubtitle,
|
||||
sidebarTitle,
|
||||
} from './style.css';
|
||||
|
||||
interface SettingSidebarProps {
|
||||
generalSettingList: GeneralSettingList;
|
||||
onGeneralSettingClick: (key: GeneralSettingKeys) => void;
|
||||
onWorkspaceSettingClick: (workspaceId: string) => void;
|
||||
selectedWorkspaceId: string | null;
|
||||
selectedGeneralKey: string | null;
|
||||
export type UserInfoProps = {
|
||||
onAccountSettingClick: () => void;
|
||||
}
|
||||
};
|
||||
|
||||
export const UserInfo = ({
|
||||
onAccountSettingClick,
|
||||
}: UserInfoProps): ReactElement => {
|
||||
const user = useCurrentUser();
|
||||
return (
|
||||
<div className={accountButton} onClick={onAccountSettingClick}>
|
||||
<UserAvatar
|
||||
size={28}
|
||||
name={user.name}
|
||||
url={user.image}
|
||||
className="avatar"
|
||||
/>
|
||||
|
||||
<div className="content">
|
||||
<div className="name" title="xxx">
|
||||
{user.name}
|
||||
</div>
|
||||
<div className="email" title="xxx">
|
||||
{user.email}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const SignInButton = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const [, setAuthModal] = useAtom(authAtom);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={accountButton}
|
||||
onClick={useCallback(() => {
|
||||
setAuthModal({ openModal: true, state: 'signIn' });
|
||||
}, [setAuthModal])}
|
||||
>
|
||||
<div className="avatar not-sign">
|
||||
<Logo1Icon />
|
||||
</div>
|
||||
|
||||
<div className="content">
|
||||
<div className="name" title={t['com.affine.settings.sign']()}>
|
||||
{t['com.affine.settings.sign']()}
|
||||
</div>
|
||||
<div className="email" title={t['com.affine.setting.sign.message']()}>
|
||||
{t['com.affine.setting.sign.message']()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const SettingSidebar = ({
|
||||
generalSettingList,
|
||||
@ -43,9 +103,17 @@ export const SettingSidebar = ({
|
||||
onWorkspaceSettingClick,
|
||||
selectedWorkspaceId,
|
||||
selectedGeneralKey,
|
||||
}: SettingSidebarProps) => {
|
||||
onAccountSettingClick,
|
||||
}: {
|
||||
generalSettingList: GeneralSettingList;
|
||||
onGeneralSettingClick: (key: GeneralSettingKeys) => void;
|
||||
onWorkspaceSettingClick: (workspaceId: string) => void;
|
||||
selectedWorkspaceId: string | null;
|
||||
selectedGeneralKey: string | null;
|
||||
onAccountSettingClick: () => void;
|
||||
}) => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const loginStatus = useCurrenLoginStatus();
|
||||
return (
|
||||
<div className={settingSlideBar} data-testid="settings-sidebar">
|
||||
<div className={sidebarTitle}>{t['Settings']()}</div>
|
||||
@ -79,32 +147,43 @@ export const SettingSidebar = ({
|
||||
</div>
|
||||
<div className={clsx(sidebarItemsWrapper, 'scroll')}>
|
||||
<Suspense fallback={<WorkspaceListSkeleton />}>
|
||||
<ScrollableContainer>
|
||||
<WorkspaceList
|
||||
onWorkspaceSettingClick={onWorkspaceSettingClick}
|
||||
selectedWorkspaceId={selectedWorkspaceId}
|
||||
/>
|
||||
</ScrollableContainer>
|
||||
<WorkspaceList
|
||||
onWorkspaceSettingClick={onWorkspaceSettingClick}
|
||||
selectedWorkspaceId={selectedWorkspaceId}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
|
||||
<div className={sidebarFooter}>
|
||||
{runtimeConfig.enableCloud && loginStatus === 'unauthenticated' ? (
|
||||
<SignInButton />
|
||||
) : null}
|
||||
|
||||
{runtimeConfig.enableCloud && loginStatus === 'authenticated' ? (
|
||||
<UserInfo onAccountSettingClick={onAccountSettingClick} />
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface WorkspaceListProps {
|
||||
onWorkspaceSettingClick: (workspaceId: string) => void;
|
||||
selectedWorkspaceId: string | null;
|
||||
}
|
||||
|
||||
export const WorkspaceList = ({
|
||||
onWorkspaceSettingClick,
|
||||
selectedWorkspaceId,
|
||||
}: WorkspaceListProps) => {
|
||||
}: {
|
||||
onWorkspaceSettingClick: (workspaceId: string) => void;
|
||||
selectedWorkspaceId: string | null;
|
||||
}) => {
|
||||
const workspaces = useAtomValue(rootWorkspacesMetadataAtom);
|
||||
const [currentWorkspace] = useCurrentWorkspace();
|
||||
const workspaceList = useMemo(() => {
|
||||
return workspaces.filter(
|
||||
({ flavour }) => flavour !== WorkspaceFlavour.AFFINE_PUBLIC
|
||||
);
|
||||
}, [workspaces]);
|
||||
return (
|
||||
<>
|
||||
{workspaces.map(workspace => {
|
||||
{workspaceList.map(workspace => {
|
||||
return (
|
||||
<Suspense key={workspace.id} fallback={<WorkspaceListItemSkeleton />}>
|
||||
<WorkspaceListItem
|
||||
@ -122,19 +201,17 @@ export const WorkspaceList = ({
|
||||
);
|
||||
};
|
||||
|
||||
interface WorkspaceListItemProps {
|
||||
meta: RootWorkspaceMetadata;
|
||||
onClick: () => void;
|
||||
isCurrent: boolean;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
const WorkspaceListItem = ({
|
||||
meta,
|
||||
onClick,
|
||||
isCurrent,
|
||||
isActive,
|
||||
}: WorkspaceListItemProps) => {
|
||||
}: {
|
||||
meta: RootWorkspaceMetadata;
|
||||
onClick: () => void;
|
||||
isCurrent: boolean;
|
||||
isActive: boolean;
|
||||
}) => {
|
||||
const workspace = useStaticBlockSuiteWorkspace(meta.id);
|
||||
const [workspaceName] = useBlockSuiteWorkspaceName(workspace);
|
||||
const ref = useRef(null);
|
||||
|
@ -34,7 +34,7 @@ export const sidebarItemsWrapper = style({
|
||||
selectors: {
|
||||
'&.scroll': {
|
||||
flexGrow: 1,
|
||||
overflowY: 'hidden',
|
||||
overflowY: 'auto',
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -92,6 +92,8 @@ export const currentWorkspaceLabel = style({
|
||||
},
|
||||
});
|
||||
|
||||
export const sidebarFooter = style({ padding: '0 16px' });
|
||||
|
||||
export const accountButton = style({
|
||||
height: '42px',
|
||||
padding: '4px 8px',
|
||||
@ -110,6 +112,20 @@ globalStyle(`${accountButton} .avatar`, {
|
||||
border: '1px solid',
|
||||
borderColor: 'var(--affine-white)',
|
||||
marginRight: '10px',
|
||||
flexShrink: 0,
|
||||
});
|
||||
|
||||
globalStyle(`${accountButton} .avatar.not-sign`, {
|
||||
width: '28px',
|
||||
height: '28px',
|
||||
borderRadius: '50%',
|
||||
fontSize: '22px',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderColor: 'var(--affine-border-color)',
|
||||
color: 'var(--affine-border-color)',
|
||||
background: 'var(--affine-white)',
|
||||
});
|
||||
globalStyle(`${accountButton} .content`, {
|
||||
flexGrow: '1',
|
||||
|
@ -3,16 +3,17 @@ import { globalStyle, style } from '@vanilla-extract/css';
|
||||
export const settingContent = style({
|
||||
flexGrow: '1',
|
||||
height: '100%',
|
||||
padding: '40px 15px 20px',
|
||||
overflow: 'auto',
|
||||
padding: '40px 15px',
|
||||
overflow: 'hidden',
|
||||
});
|
||||
|
||||
globalStyle(`${settingContent} .wrapper`, {
|
||||
width: '66%',
|
||||
minWidth: '450px',
|
||||
width: '60%',
|
||||
padding: '0 15px',
|
||||
height: '100%',
|
||||
maxWidth: '560px',
|
||||
minWidth: '560px',
|
||||
margin: '0 auto',
|
||||
overflowY: 'auto',
|
||||
});
|
||||
|
||||
globalStyle(`${settingContent} .wrapper::-webkit-scrollbar`, {
|
||||
|
@ -1,9 +1,16 @@
|
||||
import { pushNotificationAtom } from '@affine/component/notification-center';
|
||||
import { WorkspaceSubPath } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
||||
import { usePassiveWorkspaceEffect } from '@toeverything/infra/__internal__/react';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { getUIAdapter } from '../../../../adapters/workspace';
|
||||
import { openSettingModalAtom } from '../../../../atoms';
|
||||
import { useLeaveWorkspace } from '../../../../hooks/affine/use-leave-workspace';
|
||||
import { useCurrentWorkspace } from '../../../../hooks/current/use-current-workspace';
|
||||
import { useOnTransformWorkspace } from '../../../../hooks/root/use-on-transform-workspace';
|
||||
import {
|
||||
RouteLogic,
|
||||
@ -13,28 +20,83 @@ import { useWorkspace } from '../../../../hooks/use-workspace';
|
||||
import { useAppHelper } from '../../../../hooks/use-workspaces';
|
||||
|
||||
export const WorkspaceSetting = ({ workspaceId }: { workspaceId: string }) => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const { jumpToSubPath, jumpToIndex } = useNavigateHelper();
|
||||
const [currentWorkspace] = useCurrentWorkspace();
|
||||
const workspace = useWorkspace(workspaceId);
|
||||
const workspaces = useAtomValue(rootWorkspacesMetadataAtom);
|
||||
const pushNotification = useSetAtom(pushNotificationAtom);
|
||||
|
||||
const leaveWorkspace = useLeaveWorkspace();
|
||||
usePassiveWorkspaceEffect(workspace.blockSuiteWorkspace);
|
||||
const setSettingModal = useSetAtom(openSettingModalAtom);
|
||||
const helper = useAppHelper();
|
||||
const { jumpToIndex } = useNavigateHelper();
|
||||
const { deleteWorkspace } = useAppHelper();
|
||||
|
||||
const { NewSettingsDetail } = getUIAdapter(workspace.flavour);
|
||||
|
||||
const onDeleteWorkspace = useCallback(
|
||||
async (id: string) => {
|
||||
await helper.deleteWorkspace(id);
|
||||
setSettingModal(prev => ({ ...prev, open: false, workspaceId: null }));
|
||||
jumpToIndex(RouteLogic.REPLACE);
|
||||
},
|
||||
[helper, jumpToIndex, setSettingModal]
|
||||
);
|
||||
const closeAndJumpOut = useCallback(() => {
|
||||
setSettingModal(prev => ({ ...prev, open: false, workspaceId: null }));
|
||||
|
||||
if (currentWorkspace.id === workspaceId) {
|
||||
const backWorkspace = workspaces.find(ws => ws.id !== workspaceId);
|
||||
// TODO: if there is no workspace, jump to a new page(wait for design)
|
||||
if (backWorkspace) {
|
||||
jumpToSubPath(
|
||||
backWorkspace?.id || '',
|
||||
WorkspaceSubPath.ALL,
|
||||
RouteLogic.REPLACE
|
||||
);
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
jumpToIndex(RouteLogic.REPLACE);
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
currentWorkspace.id,
|
||||
jumpToIndex,
|
||||
jumpToSubPath,
|
||||
setSettingModal,
|
||||
workspaceId,
|
||||
workspaces,
|
||||
]);
|
||||
|
||||
const handleDeleteWorkspace = useCallback(async () => {
|
||||
closeAndJumpOut();
|
||||
await deleteWorkspace(workspaceId);
|
||||
|
||||
pushNotification({
|
||||
title: t['Successfully deleted'](),
|
||||
type: 'success',
|
||||
});
|
||||
}, [closeAndJumpOut, deleteWorkspace, pushNotification, t, workspaceId]);
|
||||
|
||||
const handleLeaveWorkspace = useCallback(async () => {
|
||||
closeAndJumpOut();
|
||||
await leaveWorkspace(workspaceId);
|
||||
|
||||
pushNotification({
|
||||
title: 'Successfully leave',
|
||||
type: 'success',
|
||||
});
|
||||
}, [closeAndJumpOut, leaveWorkspace, pushNotification, workspaceId]);
|
||||
|
||||
const onTransformWorkspace = useOnTransformWorkspace();
|
||||
// const handleDelete = useCallback(async () => {
|
||||
// await onDeleteWorkspace();
|
||||
// toast(t['Successfully deleted'](), {
|
||||
// portal: document.body,
|
||||
// });
|
||||
// onClose();
|
||||
// }, [onClose, onDeleteWorkspace, t, workspace.id]);
|
||||
|
||||
return (
|
||||
<NewSettingsDetail
|
||||
onDeleteCloudWorkspace={handleDeleteWorkspace}
|
||||
onDeleteLocalWorkspace={handleDeleteWorkspace}
|
||||
onLeaveWorkspace={handleLeaveWorkspace}
|
||||
onTransformWorkspace={onTransformWorkspace}
|
||||
onDeleteWorkspace={onDeleteWorkspace}
|
||||
currentWorkspaceId={workspaceId}
|
||||
/>
|
||||
);
|
||||
|
48
apps/core/src/components/affine/share-page-modal/index.tsx
Normal file
48
apps/core/src/components/affine/share-page-modal/index.tsx
Normal file
@ -0,0 +1,48 @@
|
||||
import { ShareMenu } from '@affine/component/share-menu';
|
||||
import {
|
||||
type AffineOfficialWorkspace,
|
||||
WorkspaceFlavour,
|
||||
} from '@affine/env/workspace';
|
||||
import type { Page } from '@blocksuite/store';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { useIsSharedPage } from '../../../hooks/affine/use-is-shared-page';
|
||||
import { useOnTransformWorkspace } from '../../../hooks/root/use-on-transform-workspace';
|
||||
import { EnableAffineCloudModal } from '../enable-affine-cloud-modal';
|
||||
|
||||
type SharePageModalProps = {
|
||||
workspace: AffineOfficialWorkspace;
|
||||
page: Page;
|
||||
};
|
||||
|
||||
export const SharePageModal = ({ workspace, page }: SharePageModalProps) => {
|
||||
const onTransformWorkspace = useOnTransformWorkspace();
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<ShareMenu
|
||||
workspace={workspace}
|
||||
currentPage={page}
|
||||
useIsSharedPage={useIsSharedPage}
|
||||
onEnableAffineCloud={() => setOpen(true)}
|
||||
togglePagePublic={async () => {}}
|
||||
/>
|
||||
{workspace.flavour === WorkspaceFlavour.LOCAL ? (
|
||||
<EnableAffineCloudModal
|
||||
open={open}
|
||||
onClose={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
onConfirm={() => {
|
||||
onTransformWorkspace(
|
||||
WorkspaceFlavour.LOCAL,
|
||||
WorkspaceFlavour.AFFINE_CLOUD,
|
||||
workspace
|
||||
);
|
||||
setOpen(false);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,3 +1,4 @@
|
||||
import type { AffineOfficialWorkspace } from '@affine/env/workspace';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import {
|
||||
useBlockSuitePageMeta,
|
||||
@ -12,7 +13,6 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import type { AffineOfficialWorkspace } from '../../../shared';
|
||||
import { EditorModeSwitch } from '../block-suite-mode-switch';
|
||||
import { PageMenu } from './operation-menu';
|
||||
import * as styles from './styles.css';
|
||||
@ -139,7 +139,7 @@ const BlockSuiteTitleWithRename = (props: BlockSuiteHeaderTitleProps) => {
|
||||
};
|
||||
|
||||
export const BlockSuiteHeaderTitle = (props: BlockSuiteHeaderTitleProps) => {
|
||||
if (props.workspace.flavour === WorkspaceFlavour.PUBLIC) {
|
||||
if (props.workspace.flavour === WorkspaceFlavour.AFFINE_PUBLIC) {
|
||||
return <StableTitle {...props} />;
|
||||
}
|
||||
return <BlockSuiteTitleWithRename {...props} />;
|
||||
|
53
apps/core/src/components/cloud/login-card.tsx
Normal file
53
apps/core/src/components/cloud/login-card.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
import { UserAvatar } from '@affine/component/user-avatar';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { CloudWorkspaceIcon } from '@blocksuite/icons';
|
||||
import { signIn } from 'next-auth/react';
|
||||
|
||||
import { useCurrenLoginStatus } from '../../hooks/affine/use-curren-login-status';
|
||||
import { useCurrentUser } from '../../hooks/affine/use-current-user';
|
||||
import { StyledSignInButton } from '../pure/footer/styles';
|
||||
|
||||
export const LoginCard = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const loginStatus = useCurrenLoginStatus();
|
||||
if (loginStatus === 'authenticated') {
|
||||
return <UserCard />;
|
||||
}
|
||||
return (
|
||||
<StyledSignInButton
|
||||
data-testid="sign-in-button"
|
||||
onClick={async () => {
|
||||
// jump to login page
|
||||
signIn().catch(console.error);
|
||||
}}
|
||||
>
|
||||
<div className="circle">
|
||||
<CloudWorkspaceIcon />
|
||||
</div>{' '}
|
||||
{t['Sign in']()}
|
||||
</StyledSignInButton>
|
||||
);
|
||||
};
|
||||
|
||||
const UserCard = () => {
|
||||
const user = useCurrentUser();
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<UserAvatar
|
||||
size={28}
|
||||
name={user.name}
|
||||
url={user.image}
|
||||
className="avatar"
|
||||
/>
|
||||
<div style={{ marginLeft: '15px' }}>
|
||||
<div>{user.name}</div>
|
||||
<div>{user.email}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
58
apps/core/src/components/cloud/provider.tsx
Normal file
58
apps/core/src/components/cloud/provider.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
import { pushNotificationAtom } from '@affine/component/notification-center';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import { GraphQLError } from 'graphql';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import type { PropsWithChildren, ReactElement } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import type { SWRConfiguration } from 'swr';
|
||||
import { SWRConfig } from 'swr';
|
||||
|
||||
const cloudConfig: SWRConfiguration = {
|
||||
suspense: true,
|
||||
use: [
|
||||
useSWRNext => (key, fetcher, config) => {
|
||||
const pushNotification = useSetAtom(pushNotificationAtom);
|
||||
const fetcherWrapper = useCallback(
|
||||
async (...args: any[]) => {
|
||||
assertExists(fetcher);
|
||||
const d = fetcher(...args);
|
||||
if (d instanceof Promise) {
|
||||
return d.catch(e => {
|
||||
if (
|
||||
e instanceof GraphQLError ||
|
||||
(Array.isArray(e) && e[0] instanceof GraphQLError)
|
||||
) {
|
||||
const graphQLError = e instanceof GraphQLError ? e : e[0];
|
||||
pushNotification({
|
||||
title: 'GraphQL Error',
|
||||
message: graphQLError.toString(),
|
||||
key: Date.now().toString(),
|
||||
type: 'error',
|
||||
});
|
||||
} else {
|
||||
pushNotification({
|
||||
title: 'Error',
|
||||
message: e.toString(),
|
||||
key: Date.now().toString(),
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
return d;
|
||||
},
|
||||
[fetcher, pushNotification]
|
||||
);
|
||||
return useSWRNext(key, fetcher ? fetcherWrapper : fetcher, config);
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const Provider = (props: PropsWithChildren): ReactElement => {
|
||||
if (!runtimeConfig.enableCloud) {
|
||||
return <>{props.children}</>;
|
||||
}
|
||||
|
||||
return <SWRConfig value={cloudConfig}>{props.children}</SWRConfig>;
|
||||
};
|
@ -1,39 +1,42 @@
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { CloudWorkspaceIcon } from '@blocksuite/icons';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { type CSSProperties, forwardRef } from 'react';
|
||||
import { signIn } from 'next-auth/react';
|
||||
import { type CSSProperties, type FC, forwardRef, useCallback } from 'react';
|
||||
|
||||
import { openDisableCloudAlertModalAtom } from '../../../atoms';
|
||||
import { useCurrenLoginStatus } from '../../../hooks/affine/use-curren-login-status';
|
||||
// import { openDisableCloudAlertModalAtom } from '../../../atoms';
|
||||
import { stringToColour } from '../../../utils';
|
||||
import { StyledFooter } from './styles';
|
||||
|
||||
export const Footer = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const setOpen = useSetAtom(openDisableCloudAlertModalAtom);
|
||||
import { StyledFooter, StyledSignInButton } from './styles';
|
||||
export const Footer: FC = () => {
|
||||
const loginStatus = useCurrenLoginStatus();
|
||||
|
||||
// const setOpen = useSetAtom(openDisableCloudAlertModalAtom);
|
||||
return (
|
||||
<StyledFooter data-testid="workspace-list-modal-footer">
|
||||
<Button
|
||||
data-testid="sign-in-button"
|
||||
type="plain"
|
||||
icon={
|
||||
<CloudWorkspaceIcon
|
||||
style={{ color: 'var(--affine-primary-color)' }}
|
||||
/>
|
||||
}
|
||||
onClick={async () => {
|
||||
if (!runtimeConfig.enableCloud) {
|
||||
setOpen(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t['Sign in']()}
|
||||
</Button>
|
||||
{loginStatus === 'authenticated' ? null : <SignInButton />}
|
||||
</StyledFooter>
|
||||
);
|
||||
};
|
||||
|
||||
const SignInButton = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
return (
|
||||
<StyledSignInButton
|
||||
data-testid="sign-in-button"
|
||||
onClick={useCallback(() => {
|
||||
signIn().catch(console.error);
|
||||
}, [])}
|
||||
>
|
||||
<div className="circle">
|
||||
<CloudWorkspaceIcon />
|
||||
</div>
|
||||
|
||||
{t['Sign in']()}
|
||||
</StyledSignInButton>
|
||||
);
|
||||
};
|
||||
|
||||
interface WorkspaceAvatarProps {
|
||||
size: number;
|
||||
name: string | undefined;
|
||||
|
@ -1,4 +1,19 @@
|
||||
import { displayFlex, styled, textEllipsis } from '@affine/component';
|
||||
import {
|
||||
displayFlex,
|
||||
displayInlineFlex,
|
||||
styled,
|
||||
textEllipsis,
|
||||
} from '@affine/component';
|
||||
|
||||
export const StyledSplitLine = styled('div')(() => {
|
||||
return {
|
||||
width: '1px',
|
||||
height: '20px',
|
||||
background: 'var(--affine-border-color)',
|
||||
marginRight: '24px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyleWorkspaceInfo = styled('div')(() => {
|
||||
return {
|
||||
marginLeft: '15px',
|
||||
@ -110,3 +125,28 @@ export const StyledModalHeader = styled('div')(() => {
|
||||
...displayFlex('space-between', 'center'),
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledSignInButton = styled('button')(() => {
|
||||
return {
|
||||
fontWeight: 600,
|
||||
paddingLeft: 0,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingRight: '15px',
|
||||
borderRadius: '8px',
|
||||
'&:hover': {
|
||||
backgroundColor: 'var(--affine-hover-color)',
|
||||
},
|
||||
'.circle': {
|
||||
width: '40px',
|
||||
height: '40px',
|
||||
borderRadius: '20px',
|
||||
color: 'var(--affine-primary-color)',
|
||||
fontSize: '24px',
|
||||
flexShrink: 0,
|
||||
marginRight: '16px',
|
||||
...displayInlineFlex('center', 'center'),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
@ -8,19 +8,27 @@ import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
|
||||
import {
|
||||
AccountIcon,
|
||||
CloudWorkspaceIcon,
|
||||
ImportIcon,
|
||||
MoreHorizontalIcon,
|
||||
PlusIcon,
|
||||
SignOutIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import type { DragEndEvent } from '@dnd-kit/core';
|
||||
import { Popover } from '@mui/material';
|
||||
import { IconButton } from '@toeverything/components/button';
|
||||
import { Divider } from '@toeverything/components/divider';
|
||||
import { useSetAtom } from 'jotai';
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
import { signOut, useSession } from 'next-auth/react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { openDisableCloudAlertModalAtom } from '../../../atoms';
|
||||
import {
|
||||
authAtom,
|
||||
openDisableCloudAlertModalAtom,
|
||||
openSettingModalAtom,
|
||||
} from '../../../atoms';
|
||||
import type { AllWorkspace } from '../../../shared';
|
||||
import {
|
||||
StyledCreateWorkspaceCardPill,
|
||||
@ -39,6 +47,7 @@ import {
|
||||
StyledSignInCardPillTextCotainer,
|
||||
StyledSignInCardPillTextPrimary,
|
||||
StyledSignInCardPillTextSecondary,
|
||||
StyledWorkspaceFlavourTitle,
|
||||
} from './styles';
|
||||
|
||||
interface WorkspaceModalProps {
|
||||
@ -56,18 +65,31 @@ interface WorkspaceModalProps {
|
||||
|
||||
const AccountMenu = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const setOpen = useSetAtom(openSettingModalAtom);
|
||||
return (
|
||||
<div>
|
||||
<div>Unlimted</div>
|
||||
{/* <div>Unlimted</div>
|
||||
<Divider size="thinner" dividerColor="var(--affine-border-color)" />
|
||||
<MenuItem icon={<ImportIcon />} data-testid="editor-option-menu-import">
|
||||
{t['com.affine.workspace.cloud.join']()}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<ImportIcon />} data-testid="editor-option-menu-import">
|
||||
</MenuItem> */}
|
||||
<MenuItem
|
||||
icon={<AccountIcon />}
|
||||
data-testid="editor-option-menu-import"
|
||||
onClick={useCallback(() => {
|
||||
setOpen(prev => ({ ...prev, open: true, activeTab: 'account' }));
|
||||
}, [setOpen])}
|
||||
>
|
||||
{t['com.affine.workspace.cloud.account.settings']()}
|
||||
</MenuItem>
|
||||
<Divider size="thinner" dividerColor="var(--affine-border-color)" />
|
||||
<MenuItem icon={<ImportIcon />} data-testid="editor-option-menu-import">
|
||||
<Divider />
|
||||
<MenuItem
|
||||
icon={<SignOutIcon />}
|
||||
data-testid="editor-option-menu-import"
|
||||
onClick={useCallback(() => {
|
||||
signOut().catch(console.error);
|
||||
}, [])}
|
||||
>
|
||||
{t['com.affine.workspace.cloud.account.logout']()}
|
||||
</MenuItem>
|
||||
</div>
|
||||
@ -89,31 +111,16 @@ const CloudWorkSpaceList = ({
|
||||
<StyledModalHeader>
|
||||
<StyledModalHeaderLeft>
|
||||
<StyledModalTitle>
|
||||
{t['com.affine.workspace.cloud.sync']()}
|
||||
{t['com.affine.workspace.cloud']()}
|
||||
</StyledModalTitle>
|
||||
</StyledModalHeaderLeft>
|
||||
|
||||
<StyledOperationWrapper>
|
||||
<Menu
|
||||
placement="bottom-end"
|
||||
trigger={['click']}
|
||||
content={<AccountMenu />}
|
||||
zIndex={1000}
|
||||
>
|
||||
<IconButton
|
||||
data-testid="previous-image-button"
|
||||
icon={<MoreHorizontalIcon />}
|
||||
type="plain"
|
||||
/>
|
||||
</Menu>
|
||||
</StyledOperationWrapper>
|
||||
</StyledModalHeader>
|
||||
<StyledModalContent>
|
||||
<WorkspaceList
|
||||
disabled={disabled}
|
||||
items={
|
||||
workspaces.filter(
|
||||
({ flavour }) => flavour !== WorkspaceFlavour.PUBLIC
|
||||
({ flavour }) => flavour === WorkspaceFlavour.AFFINE_CLOUD
|
||||
) as (AffineCloudWorkspace | LocalWorkspace)[]
|
||||
}
|
||||
currentWorkspaceId={currentWorkspaceId}
|
||||
@ -129,7 +136,6 @@ const CloudWorkSpaceList = ({
|
||||
[onMoveWorkspace]
|
||||
)}
|
||||
/>
|
||||
<Divider size="thinner" dividerColor="var(--affine-border-color)" />
|
||||
</StyledModalContent>
|
||||
</>
|
||||
);
|
||||
@ -148,11 +154,18 @@ export const WorkspaceListModal = ({
|
||||
onMoveWorkspace,
|
||||
}: WorkspaceModalProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const setOpen = useSetAtom(openDisableCloudAlertModalAtom);
|
||||
const setOpen = useSetAtom(authAtom);
|
||||
const setDisableCloudOpen = useSetAtom(openDisableCloudAlertModalAtom);
|
||||
// TODO: AFFiNE Cloud support
|
||||
const isLoggedIn = false;
|
||||
const { data: session, status } = useSession();
|
||||
const isLoggedIn = status === 'authenticated' ? true : false;
|
||||
const anchorEl = document.getElementById('current-workspace');
|
||||
|
||||
const cloudWorkspaces = workspaces.filter(
|
||||
({ flavour }) => flavour === WorkspaceFlavour.AFFINE_CLOUD
|
||||
) as (AffineCloudWorkspace | LocalWorkspace)[];
|
||||
const localWorkspaces = workspaces.filter(
|
||||
({ flavour }) => flavour === WorkspaceFlavour.LOCAL
|
||||
) as (AffineCloudWorkspace | LocalWorkspace)[];
|
||||
return (
|
||||
<Popover
|
||||
sx={{
|
||||
@ -164,6 +177,7 @@ export const WorkspaceListModal = ({
|
||||
flexDirection: 'column',
|
||||
boxShadow: 'var(--affine-shadow-2)',
|
||||
backgroundColor: 'var(--affine-background-overlay-panel-color)',
|
||||
padding: '16px 12px',
|
||||
},
|
||||
maxHeight: '90vh',
|
||||
}}
|
||||
@ -171,63 +185,92 @@ export const WorkspaceListModal = ({
|
||||
anchorEl={anchorEl}
|
||||
onClose={onClose}
|
||||
>
|
||||
<StyledModalHeaderContent>
|
||||
<StyledSignInCardPill>
|
||||
<MenuItem
|
||||
style={{
|
||||
height: 'auto',
|
||||
padding: '8px 12px',
|
||||
}}
|
||||
onClick={async () => {
|
||||
if (!runtimeConfig.enableCloud) {
|
||||
setOpen(true);
|
||||
}
|
||||
}}
|
||||
data-testid="cloud-signin-button"
|
||||
>
|
||||
<StyledCreateWorkspaceCardPillContent>
|
||||
<StyledCreateWorkspaceCardPillIcon>
|
||||
<CloudWorkspaceIcon />
|
||||
</StyledCreateWorkspaceCardPillIcon>
|
||||
<StyledSignInCardPillTextCotainer>
|
||||
<StyledSignInCardPillTextPrimary>
|
||||
{t['com.affine.workspace.cloud.auth']()}
|
||||
</StyledSignInCardPillTextPrimary>
|
||||
<StyledSignInCardPillTextSecondary>
|
||||
Sync with AFFiNE Cloud
|
||||
</StyledSignInCardPillTextSecondary>
|
||||
</StyledSignInCardPillTextCotainer>
|
||||
</StyledCreateWorkspaceCardPillContent>
|
||||
</MenuItem>
|
||||
</StyledSignInCardPill>
|
||||
<Divider size="thinner" dividerColor="var(--affine-border-color)" />
|
||||
</StyledModalHeaderContent>
|
||||
{!isLoggedIn ? (
|
||||
<StyledModalHeaderContent>
|
||||
<StyledSignInCardPill>
|
||||
<MenuItem
|
||||
style={{
|
||||
height: 'auto',
|
||||
padding: '0px 12px',
|
||||
}}
|
||||
onClick={async () => {
|
||||
if (!runtimeConfig.enableCloud) {
|
||||
setDisableCloudOpen(true);
|
||||
} else {
|
||||
setOpen(state => ({
|
||||
...state,
|
||||
openModal: true,
|
||||
}));
|
||||
}
|
||||
}}
|
||||
data-testid="cloud-signin-button"
|
||||
>
|
||||
<StyledCreateWorkspaceCardPillContent>
|
||||
<StyledCreateWorkspaceCardPillIcon>
|
||||
<CloudWorkspaceIcon />
|
||||
</StyledCreateWorkspaceCardPillIcon>
|
||||
<StyledSignInCardPillTextCotainer>
|
||||
<StyledSignInCardPillTextPrimary>
|
||||
{t['com.affine.workspace.cloud.auth']()}
|
||||
</StyledSignInCardPillTextPrimary>
|
||||
<StyledSignInCardPillTextSecondary>
|
||||
{t['com.affine.workspace.cloud.description']()}
|
||||
</StyledSignInCardPillTextSecondary>
|
||||
</StyledSignInCardPillTextCotainer>
|
||||
</StyledCreateWorkspaceCardPillContent>
|
||||
</MenuItem>
|
||||
</StyledSignInCardPill>
|
||||
<Divider style={{ margin: '12px 0px' }} />
|
||||
</StyledModalHeaderContent>
|
||||
) : (
|
||||
<StyledModalHeaderContent>
|
||||
<StyledModalHeader>
|
||||
<StyledModalTitle>{session?.user.email}</StyledModalTitle>
|
||||
<StyledOperationWrapper>
|
||||
<Menu
|
||||
placement="bottom-end"
|
||||
trigger={['click']}
|
||||
content={<AccountMenu />}
|
||||
zIndex={1000}
|
||||
>
|
||||
<IconButton
|
||||
data-testid="more-button"
|
||||
icon={<MoreHorizontalIcon />}
|
||||
type="plain"
|
||||
/>
|
||||
</Menu>
|
||||
</StyledOperationWrapper>
|
||||
</StyledModalHeader>
|
||||
<Divider style={{ margin: '12px 0px' }} />
|
||||
</StyledModalHeaderContent>
|
||||
)}
|
||||
<StyledModalBody>
|
||||
{isLoggedIn ? (
|
||||
<CloudWorkSpaceList
|
||||
disabled={disabled}
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
workspaces={workspaces}
|
||||
onClickWorkspace={onClickWorkspace}
|
||||
onClickWorkspaceSetting={onClickWorkspaceSetting}
|
||||
onNewWorkspace={onNewWorkspace}
|
||||
onAddWorkspace={onAddWorkspace}
|
||||
currentWorkspaceId={currentWorkspaceId}
|
||||
onMoveWorkspace={onMoveWorkspace}
|
||||
/>
|
||||
{isLoggedIn && cloudWorkspaces.length !== 0 ? (
|
||||
<>
|
||||
<CloudWorkSpaceList
|
||||
disabled={disabled}
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
workspaces={workspaces}
|
||||
onClickWorkspace={onClickWorkspace}
|
||||
onClickWorkspaceSetting={onClickWorkspaceSetting}
|
||||
onNewWorkspace={onNewWorkspace}
|
||||
onAddWorkspace={onAddWorkspace}
|
||||
currentWorkspaceId={currentWorkspaceId}
|
||||
onMoveWorkspace={onMoveWorkspace}
|
||||
/>
|
||||
<Divider style={{ margin: '12px 0px', minHeight: '1px' }} />
|
||||
</>
|
||||
) : null}
|
||||
<StyledModalHeader>
|
||||
<StyledModalTitle>{t['Local Workspace']()}</StyledModalTitle>
|
||||
<StyledWorkspaceFlavourTitle>
|
||||
{t['com.affine.workspace.local']()}
|
||||
</StyledWorkspaceFlavourTitle>
|
||||
</StyledModalHeader>
|
||||
<StyledModalContent>
|
||||
<WorkspaceList
|
||||
disabled={disabled}
|
||||
items={
|
||||
workspaces.filter(
|
||||
({ flavour }) => flavour !== WorkspaceFlavour.PUBLIC
|
||||
) as (AffineCloudWorkspace | LocalWorkspace)[]
|
||||
}
|
||||
items={localWorkspaces}
|
||||
currentWorkspaceId={currentWorkspaceId}
|
||||
onClick={onClickWorkspace}
|
||||
onSettingClick={onClickWorkspaceSetting}
|
||||
|
@ -70,7 +70,6 @@ export const StyledCreateWorkspaceCard = styled('div')(() => {
|
||||
});
|
||||
export const StyledCreateWorkspaceCardPillContainer = styled('div')(() => {
|
||||
return {
|
||||
padding: '12px',
|
||||
borderRadius: '10px',
|
||||
display: 'flex',
|
||||
margin: '-8px -4px',
|
||||
@ -173,6 +172,7 @@ export const StyledModalContent = styled('div')({
|
||||
flexWrap: 'wrap',
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
gap: '4px',
|
||||
});
|
||||
|
||||
export const StyledModalFooterContent = styled('div')({
|
||||
@ -180,7 +180,7 @@ export const StyledModalFooterContent = styled('div')({
|
||||
flexWrap: 'wrap',
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
padding: '12px',
|
||||
marginTop: '12px',
|
||||
backgroundColor: 'var(--affine-background-overlay-panel-color)',
|
||||
});
|
||||
|
||||
@ -189,7 +189,6 @@ export const StyledModalHeaderContent = styled('div')({
|
||||
flexWrap: 'wrap',
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
padding: '12px 12px 0px 12px',
|
||||
backgroundColor: 'var(--affine-background-overlay-panel-color)',
|
||||
});
|
||||
|
||||
@ -219,19 +218,27 @@ export const StyledModalHeader = styled('div')(() => {
|
||||
left: 0,
|
||||
top: 0,
|
||||
borderRadius: '24px 24px 0 0',
|
||||
padding: '12px 14px',
|
||||
padding: '0px 14px',
|
||||
...displayFlex('space-between', 'center'),
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledModalBody = styled('div')(() => {
|
||||
return {
|
||||
padding: '0px 12px',
|
||||
display: 'inline-flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start',
|
||||
gap: '12px',
|
||||
gap: '4px',
|
||||
flex: 1,
|
||||
overflowY: 'auto',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledWorkspaceFlavourTitle = styled('div')(() => {
|
||||
return {
|
||||
fontSize: '12px',
|
||||
fontWeight: 600,
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
lineHeight: '20px',
|
||||
};
|
||||
});
|
||||
|
@ -6,15 +6,16 @@ import {
|
||||
} from '@affine/component/page-list';
|
||||
import type { Collection } from '@affine/env/filter';
|
||||
import type { PropertiesMeta } from '@affine/env/filter';
|
||||
import type {
|
||||
import {
|
||||
WorkspaceFlavour,
|
||||
WorkspaceHeaderProps,
|
||||
type WorkspaceHeaderProps,
|
||||
} from '@affine/env/workspace';
|
||||
import { WorkspaceSubPath } from '@affine/env/workspace';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useGetPageInfoById } from '../hooks/use-get-page-info';
|
||||
import { useWorkspace } from '../hooks/use-workspace';
|
||||
import { SharePageModal } from './affine/share-page-modal';
|
||||
import { BlockSuiteHeaderTitle } from './blocksuite/block-suite-header-title';
|
||||
import { filterContainerStyle } from './filter-container.css';
|
||||
import { Header } from './pure/header';
|
||||
@ -77,7 +78,6 @@ export function WorkspaceHeader({
|
||||
const setting = useCollectionManager(currentWorkspaceId);
|
||||
|
||||
const currentWorkspace = useWorkspace(currentWorkspaceId);
|
||||
|
||||
const getPageInfoById = useGetPageInfoById(
|
||||
currentWorkspace.blockSuiteWorkspace
|
||||
);
|
||||
@ -117,6 +117,15 @@ export function WorkspaceHeader({
|
||||
|
||||
// route in edit page
|
||||
if ('pageId' in currentEntry) {
|
||||
const isCloudWorkspace =
|
||||
currentWorkspace.flavour === WorkspaceFlavour.AFFINE_CLOUD;
|
||||
const currentPage = currentWorkspace.blockSuiteWorkspace.getPage(
|
||||
currentEntry.pageId
|
||||
);
|
||||
const sharePageModal =
|
||||
isCloudWorkspace && currentPage ? (
|
||||
<SharePageModal workspace={currentWorkspace} page={currentPage} />
|
||||
) : null;
|
||||
return (
|
||||
<Header
|
||||
center={
|
||||
@ -125,7 +134,12 @@ export function WorkspaceHeader({
|
||||
pageId={currentEntry.pageId}
|
||||
/>
|
||||
}
|
||||
right={<PluginHeader />}
|
||||
right={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
{sharePageModal}
|
||||
<PluginHeader />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user