mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-01 01:29:31 +08: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:
@@ -9,3 +9,5 @@ ENABLE_NEW_SETTING_UNSTABLE_API=
|
|||||||
ENABLE_NOTIFICATION_CENTER=
|
ENABLE_NOTIFICATION_CENTER=
|
||||||
ENABLE_CLOUD=
|
ENABLE_CLOUD=
|
||||||
ENABLE_MOVE_DATABASE=
|
ENABLE_MOVE_DATABASE=
|
||||||
|
SHOULD_REPORT_TRACE=
|
||||||
|
TRACE_REPORT_ENDPOINT=
|
||||||
|
|||||||
@@ -31,6 +31,12 @@ const createPattern = packageName => [
|
|||||||
message: 'Use `useNavigateHelper` instead',
|
message: 'Use `useNavigateHelper` instead',
|
||||||
importNames: ['useNavigate'],
|
importNames: ['useNavigate'],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
group: ['next-auth/react'],
|
||||||
|
message: "Import hooks from 'use-current-user.tsx'",
|
||||||
|
// useSession is type unsafe
|
||||||
|
importNames: ['useSession'],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
group: ['yjs'],
|
group: ['yjs'],
|
||||||
message: 'Do not use this API because it has a bug',
|
message: 'Do not use this API because it has a bug',
|
||||||
@@ -160,6 +166,12 @@ const config = {
|
|||||||
message: 'Use `useNavigateHelper` instead',
|
message: 'Use `useNavigateHelper` instead',
|
||||||
importNames: ['useNavigate'],
|
importNames: ['useNavigate'],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
group: ['next-auth/react'],
|
||||||
|
message: "Import hooks from 'use-current-user.tsx'",
|
||||||
|
// useSession is type unsafe
|
||||||
|
importNames: ['useSession'],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
group: ['yjs'],
|
group: ['yjs'],
|
||||||
message: 'Do not use this API because it has a bug',
|
message: 'Do not use this API because it has a bug',
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ inputs:
|
|||||||
target:
|
target:
|
||||||
description: 'Cargo target'
|
description: 'Cargo target'
|
||||||
required: true
|
required: true
|
||||||
|
package:
|
||||||
|
description: 'Package to build'
|
||||||
|
required: true
|
||||||
nx_token:
|
nx_token:
|
||||||
description: 'Nx Cloud access token'
|
description: 'Nx Cloud access token'
|
||||||
required: false
|
required: false
|
||||||
@@ -31,7 +34,7 @@ runs:
|
|||||||
if: ${{ inputs.target != 'x86_64-unknown-linux-gnu' && inputs.target != 'aarch64-unknown-linux-gnu' }}
|
if: ${{ inputs.target != 'x86_64-unknown-linux-gnu' && inputs.target != 'aarch64-unknown-linux-gnu' }}
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
yarn nx build @affine/native --target ${{ inputs.target }}
|
yarn nx build ${{ inputs.package }} --target ${{ inputs.target }}
|
||||||
env:
|
env:
|
||||||
NX_CLOUD_ACCESS_TOKEN: ${{ inputs.nx_token }}
|
NX_CLOUD_ACCESS_TOKEN: ${{ inputs.nx_token }}
|
||||||
|
|
||||||
@@ -44,7 +47,8 @@ runs:
|
|||||||
run: |
|
run: |
|
||||||
export CC=x86_64-unknown-linux-gnu-gcc
|
export CC=x86_64-unknown-linux-gnu-gcc
|
||||||
export CC_x86_64_unknown_linux_gnu=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 node_modules/.cache
|
||||||
chmod -R 777 target
|
chmod -R 777 target
|
||||||
|
|
||||||
@@ -55,6 +59,7 @@ runs:
|
|||||||
image: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64
|
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 }}
|
options: --user 0:0 -v ${{ github.workspace }}/.cargo-cache/git/db:/usr/local/cargo/git/db -v ${{ github.workspace }}/.cargo/registry/cache:/usr/local/cargo/registry/cache -v ${{ github.workspace }}/.cargo/registry/index:/usr/local/cargo/registry/index -v ${{ github.workspace }}:/build -w /build -e NX_CLOUD_ACCESS_TOKEN=${{ inputs.nx_token }}
|
||||||
run: |
|
run: |
|
||||||
yarn nx build @affine/native --target ${{ inputs.target }}
|
export RUSTFLAGS="-C debuginfo=1"
|
||||||
|
yarn nx build ${{ inputs.package }} --target ${{ inputs.target }}
|
||||||
chmod -R 777 node_modules/.cache
|
chmod -R 777 node_modules/.cache
|
||||||
chmod -R 777 target
|
chmod -R 777 target
|
||||||
|
|||||||
@@ -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 }}'
|
||||||
@@ -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',
|
||||||
|
});
|
||||||
@@ -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 }}-
|
|
||||||
@@ -3,7 +3,7 @@ server {
|
|||||||
root /app/dist;
|
root /app/dist;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
try_files $uri $uri/index.html $uri.html =404;
|
try_files $uri $uri/ /index.html;
|
||||||
}
|
}
|
||||||
|
|
||||||
error_page 404 /404.html;
|
error_page 404 /404.html;
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ helm.sh/chart: {{ include "graphql.chart" . }}
|
|||||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||||
|
monitoring: enabled
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
||||||
{{/*
|
{{/*
|
||||||
@@ -75,58 +76,3 @@ key: {{ $secret.data.private }}
|
|||||||
key: {{ genPrivateKey "ecdsa" | b64enc }}
|
key: {{ genPrivateKey "ecdsa" | b64enc }}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
{{- 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
|
key: key
|
||||||
- name: NODE_ENV
|
- name: NODE_ENV
|
||||||
value: "{{ .Values.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:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: pg-postgresql
|
name: pg-postgresql
|
||||||
key: postgres-password
|
key: postgres-password
|
||||||
- name: DATABASE_URL
|
- 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
|
- name: AFFINE_SERVER_PORT
|
||||||
value: "{{ .Values.service.port }}"
|
value: "{{ .Values.service.port }}"
|
||||||
- name: AFFINE_SERVER_SUB_PATH
|
- name: AFFINE_SERVER_SUB_PATH
|
||||||
@@ -50,6 +73,37 @@ spec:
|
|||||||
value: "{{ .Values.app.host }}"
|
value: "{{ .Values.app.host }}"
|
||||||
- name: ENABLE_R2_OBJECT_STORAGE
|
- name: ENABLE_R2_OBJECT_STORAGE
|
||||||
value: "{{ .Values.app.objectStorage.r2.enabled }}"
|
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 }}
|
{{ if .Values.app.objectStorage.r2.enabled }}
|
||||||
- name: R2_OBJECT_STORAGE_ACCOUNT_ID
|
- name: R2_OBJECT_STORAGE_ACCOUNT_ID
|
||||||
valueFrom:
|
valueFrom:
|
||||||
@@ -112,6 +166,20 @@ spec:
|
|||||||
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
|
initialDelaySeconds: {{ .Values.probe.initialDelaySeconds }}
|
||||||
resources:
|
resources:
|
||||||
{{- toYaml .Values.resources | nindent 12 }}
|
{{- 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 }}
|
{{- with .Values.nodeSelector }}
|
||||||
nodeSelector:
|
nodeSelector:
|
||||||
{{- toYaml . | nindent 8 }}
|
{{- toYaml . | nindent 8 }}
|
||||||
|
|||||||
@@ -5,13 +5,14 @@ metadata:
|
|||||||
labels:
|
labels:
|
||||||
{{- include "graphql.labels" . | nindent 4 }}
|
{{- include "graphql.labels" . | nindent 4 }}
|
||||||
annotations:
|
annotations:
|
||||||
"helm.sh/hook": pre-install,pre-upgrade
|
"helm.sh/hook": post-install,pre-upgrade
|
||||||
"helm.sh/hook-weight": "-1"
|
"helm.sh/hook-weight": "-1"
|
||||||
"helm.sh/hook-delete-policy": before-hook-creation
|
"helm.sh/hook-delete-policy": before-hook-creation
|
||||||
|
|
||||||
spec:
|
spec:
|
||||||
template:
|
template:
|
||||||
spec:
|
spec:
|
||||||
|
serviceAccountName: {{ include "graphql.serviceAccountName" . }}
|
||||||
containers:
|
containers:
|
||||||
- name: {{ .Chart.Name }}
|
- name: {{ .Chart.Name }}
|
||||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||||
@@ -19,13 +20,21 @@ spec:
|
|||||||
env:
|
env:
|
||||||
- name: NODE_ENV
|
- name: NODE_ENV
|
||||||
value: "{{ .Values.env }}"
|
value: "{{ .Values.env }}"
|
||||||
- name: DATABSE_PASSWORD
|
- name: AFFINE_ENV
|
||||||
|
value: "{{ .Release.Namespace }}"
|
||||||
|
- name: DATABASE_PASSWORD
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: pg-postgresql
|
name: pg-postgresql
|
||||||
key: postgres-password
|
key: postgres-password
|
||||||
|
{{ if not .Values.global.database.gcloud.enabled }}
|
||||||
- name: DATABASE_URL
|
- 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:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
cpu: '100m'
|
cpu: '100m'
|
||||||
|
|||||||
@@ -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 }}
|
|
||||||
@@ -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 }}
|
||||||
@@ -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 }}"
|
name: "{{ .Values.app.objectStorage.r2.secretName }}"
|
||||||
type: Opaque
|
type: Opaque
|
||||||
data:
|
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 }}
|
{{- end }}
|
||||||
|
|||||||
@@ -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 }}
|
||||||
@@ -9,16 +9,15 @@ nameOverride: ''
|
|||||||
fullnameOverride: ''
|
fullnameOverride: ''
|
||||||
# map to NODE_ENV environment variable
|
# map to NODE_ENV environment variable
|
||||||
env: 'production'
|
env: 'production'
|
||||||
database:
|
|
||||||
user: 'postgres'
|
|
||||||
url: 'pg-postgresql'
|
|
||||||
port: '5432'
|
|
||||||
name: 'affine'
|
|
||||||
app:
|
app:
|
||||||
|
experimental:
|
||||||
|
enableJwstCodec: true
|
||||||
# AFFINE_SERVER_SUB_PATH
|
# AFFINE_SERVER_SUB_PATH
|
||||||
path: ''
|
path: ''
|
||||||
# AFFINE_SERVER_HOST
|
# AFFINE_SERVER_HOST
|
||||||
host: '0.0.0.0'
|
host: '0.0.0.0'
|
||||||
|
doc:
|
||||||
|
mergeInterval: "3000"
|
||||||
jwt:
|
jwt:
|
||||||
secretName: jwt-private-key
|
secretName: jwt-private-key
|
||||||
# base64 encoded ecdsa private key
|
# base64 encoded ecdsa private key
|
||||||
@@ -32,6 +31,13 @@ app:
|
|||||||
secretAccessKey: ''
|
secretAccessKey: ''
|
||||||
bucket: ''
|
bucket: ''
|
||||||
oauth:
|
oauth:
|
||||||
|
email:
|
||||||
|
secretName: 'oauth-email'
|
||||||
|
sender: 'noreply@toeverything.info'
|
||||||
|
login: ''
|
||||||
|
password: ''
|
||||||
|
server: 'smtp.gmail.com'
|
||||||
|
port: '465'
|
||||||
google:
|
google:
|
||||||
enabled: false
|
enabled: false
|
||||||
secretName: oauth-google
|
secretName: oauth-google
|
||||||
@@ -55,11 +61,11 @@ podSecurityContext:
|
|||||||
|
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
cpu: '2000m'
|
cpu: '4'
|
||||||
memory: 4Gi
|
memory: 8Gi
|
||||||
requests:
|
requests:
|
||||||
cpu: '1000m'
|
cpu: '2'
|
||||||
memory: 2Gi
|
memory: 4Gi
|
||||||
|
|
||||||
probe:
|
probe:
|
||||||
initialDelaySeconds: 20
|
initialDelaySeconds: 20
|
||||||
|
|||||||
@@ -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/
|
||||||
@@ -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"
|
||||||
@@ -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 }}
|
||||||
@@ -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 }}
|
||||||
@@ -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 }}
|
||||||
@@ -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 }}
|
||||||
@@ -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 }}
|
||||||
@@ -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 }}
|
||||||
@@ -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
|
||||||
@@ -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 }}
|
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||||
|
monitoring: enabled
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
||||||
{{/*
|
{{/*
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{{- if .Values.ingress.enabled -}}
|
{{- if .Values.global.ingress.enabled -}}
|
||||||
{{- $fullName := include "affine.fullname" . -}}
|
{{- $fullName := include "affine.fullname" . -}}
|
||||||
{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
|
{{- if and .Values.global.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
|
||||||
{{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }}
|
{{- if not (hasKey .Values.global.ingress.annotations "kubernetes.io/ingress.class") }}
|
||||||
{{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}}
|
{{- $_ := set .Values.global.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
|
{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||||
@@ -17,17 +17,17 @@ metadata:
|
|||||||
name: {{ $fullName }}
|
name: {{ $fullName }}
|
||||||
labels:
|
labels:
|
||||||
{{- include "affine.labels" . | nindent 4 }}
|
{{- include "affine.labels" . | nindent 4 }}
|
||||||
{{- with .Values.ingress.annotations }}
|
{{- with .Values.global.ingress.annotations }}
|
||||||
annotations:
|
annotations:
|
||||||
{{- toYaml . | nindent 4 }}
|
{{- toYaml . | nindent 4 }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
spec:
|
spec:
|
||||||
{{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
|
{{- if and .Values.global.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
|
||||||
ingressClassName: {{ .Values.ingress.className }}
|
ingressClassName: {{ .Values.global.ingress.className }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- if .Values.ingress.tls }}
|
{{- if .Values.global.ingress.tls }}
|
||||||
tls:
|
tls:
|
||||||
{{- range .Values.ingress.tls }}
|
{{- range .Values.global.ingress.tls }}
|
||||||
- hosts:
|
- hosts:
|
||||||
{{- range .hosts }}
|
{{- range .hosts }}
|
||||||
- {{ . | quote }}
|
- {{ . | quote }}
|
||||||
@@ -36,9 +36,16 @@ spec:
|
|||||||
{{- end }}
|
{{- end }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
rules:
|
rules:
|
||||||
- host: "{{ .Values.ingress.host }}"
|
- host: "{{ .Values.global.ingress.host }}"
|
||||||
http:
|
http:
|
||||||
paths:
|
paths:
|
||||||
|
- path: /socket.io
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: affine-sync
|
||||||
|
port:
|
||||||
|
number: {{ .Values.sync.service.port }}
|
||||||
- path: /graphql
|
- path: /graphql
|
||||||
pathType: Prefix
|
pathType: Prefix
|
||||||
backend:
|
backend:
|
||||||
@@ -60,5 +67,4 @@ spec:
|
|||||||
name: affine-web
|
name: affine-web
|
||||||
port:
|
port:
|
||||||
number: {{ .Values.web.service.port }}
|
number: {{ .Values.web.service.port }}
|
||||||
|
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|||||||
@@ -1,16 +1,43 @@
|
|||||||
ingress:
|
global:
|
||||||
enabled: false
|
ingress:
|
||||||
className: ''
|
enabled: false
|
||||||
annotations:
|
className: ''
|
||||||
kubernetes.io/ingress.class: nginx
|
host: affine.pro
|
||||||
host: affine.pro
|
tls: []
|
||||||
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:
|
graphql:
|
||||||
service:
|
service:
|
||||||
type: ClusterIP
|
type: ClusterIP
|
||||||
port: 3000
|
port: 3000
|
||||||
|
|
||||||
|
sync:
|
||||||
|
service:
|
||||||
|
type: ClusterIP
|
||||||
|
port: 3010
|
||||||
|
annotations:
|
||||||
|
cloud.google.com/backend-config: '{"default": "affine-backendconfig"}'
|
||||||
|
|
||||||
web:
|
web:
|
||||||
service:
|
service:
|
||||||
type: ClusterIP
|
type: ClusterIP
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ jobs:
|
|||||||
uses: ./.github/actions/build-rust
|
uses: ./.github/actions/build-rust
|
||||||
with:
|
with:
|
||||||
target: ${{ matrix.spec.target }}
|
target: ${{ matrix.spec.target }}
|
||||||
|
package: '@affine/native'
|
||||||
nx_token: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
|
nx_token: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
|
||||||
- name: Run unit tests
|
- name: Run unit tests
|
||||||
if: ${{ matrix.spec.test }}
|
if: ${{ matrix.spec.test }}
|
||||||
|
|||||||
+81
-24
@@ -29,6 +29,7 @@ env:
|
|||||||
DEBUG: napi:*
|
DEBUG: napi:*
|
||||||
BUILD_TYPE: canary
|
BUILD_TYPE: canary
|
||||||
APP_NAME: affine
|
APP_NAME: affine
|
||||||
|
AFFINE_ENV: dev
|
||||||
COVERAGE: true
|
COVERAGE: true
|
||||||
DISTRIBUTION: browser
|
DISTRIBUTION: browser
|
||||||
MACOSX_DEPLOYMENT_TARGET: '10.13'
|
MACOSX_DEPLOYMENT_TARGET: '10.13'
|
||||||
@@ -113,30 +114,11 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
|
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:
|
build-storage:
|
||||||
name: Build Storage
|
name: Build Storage
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
RUSTFLAGS: '-C debuginfo=1'
|
||||||
environment: development
|
environment: development
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -144,11 +126,11 @@ jobs:
|
|||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: ./.github/actions/setup-node
|
uses: ./.github/actions/setup-node
|
||||||
- name: Setup Rust
|
- name: Setup Rust
|
||||||
uses: ./.github/actions/setup-rust
|
uses: ./.github/actions/build-rust
|
||||||
with:
|
with:
|
||||||
target: 'x86_64-unknown-linux-gnu'
|
target: 'x86_64-unknown-linux-gnu'
|
||||||
- name: Build Storage
|
package: '@affine/storage'
|
||||||
run: yarn build:storage
|
nx_token: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
|
||||||
- name: Upload storage.node
|
- name: Upload storage.node
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
@@ -216,6 +198,81 @@ jobs:
|
|||||||
name: affine
|
name: affine
|
||||||
fail_ci_if_error: false
|
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:
|
e2e-plugin-test:
|
||||||
name: E2E Plugin Test
|
name: E2E Plugin Test
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
@@ -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 }}
|
||||||
@@ -12,7 +12,6 @@ on:
|
|||||||
- .github/**
|
- .github/**
|
||||||
- '!.github/workflows/nightly-build.yml'
|
- '!.github/workflows/nightly-build.yml'
|
||||||
- '!.github/actions/build-rust/action.yml'
|
- '!.github/actions/build-rust/action.yml'
|
||||||
- '!.github/actions/setup-rust/action.yml'
|
|
||||||
- '!.github/actions/setup-node/action.yml'
|
- '!.github/actions/setup-node/action.yml'
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
@@ -114,6 +113,7 @@ jobs:
|
|||||||
uses: ./.github/actions/build-rust
|
uses: ./.github/actions/build-rust
|
||||||
with:
|
with:
|
||||||
target: ${{ matrix.spec.target }}
|
target: ${{ matrix.spec.target }}
|
||||||
|
package: '@affine/native'
|
||||||
nx_token: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
|
nx_token: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
|
||||||
- name: Replace Version
|
- name: Replace Version
|
||||||
run: ./scripts/set-version.sh ${{ needs.set-build-version.outputs.version }}
|
run: ./scripts/set-version.sh ${{ needs.set-build-version.outputs.version }}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
name: Publish Storybook
|
name: Publish Storybook
|
||||||
|
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: --max-old-space-size=4096
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
@@ -34,5 +37,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
workingDir: apps/storybook
|
workingDir: apps/storybook
|
||||||
buildScriptName: build
|
buildScriptName: build
|
||||||
onlyChanged: true
|
|
||||||
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
||||||
|
zip: true
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: ${{ env.NODE_OPTIONS }}
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ jobs:
|
|||||||
uses: ./.github/actions/build-rust
|
uses: ./.github/actions/build-rust
|
||||||
with:
|
with:
|
||||||
target: ${{ matrix.spec.target }}
|
target: ${{ matrix.spec.target }}
|
||||||
|
package: '@affine/native'
|
||||||
nx_token: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
|
nx_token: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
|
||||||
- uses: actions/download-artifact@v3
|
- uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
@@ -182,6 +183,7 @@ jobs:
|
|||||||
uses: ./.github/actions/build-rust
|
uses: ./.github/actions/build-rust
|
||||||
with:
|
with:
|
||||||
target: ${{ matrix.spec.target }}
|
target: ${{ matrix.spec.target }}
|
||||||
|
package: '@affine/native'
|
||||||
nx_token: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
|
nx_token: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
|
||||||
- uses: actions/download-artifact@v3
|
- uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -76,11 +76,11 @@ jobs:
|
|||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: ./.github/actions/setup-node
|
uses: ./.github/actions/setup-node
|
||||||
- name: Setup Rust
|
- name: Setup Rust
|
||||||
uses: ./.github/actions/setup-rust
|
uses: ./.github/actions/build-rust
|
||||||
with:
|
with:
|
||||||
target: 'x86_64-unknown-linux-gnu'
|
target: 'x86_64-unknown-linux-gnu'
|
||||||
- name: Build Storage
|
package: '@affine/storage'
|
||||||
run: yarn build:storage
|
nx_token: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
|
||||||
- name: Upload storage.node
|
- name: Upload storage.node
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
|
|||||||
+2
-1
@@ -19,7 +19,7 @@
|
|||||||
node_modules
|
node_modules
|
||||||
|
|
||||||
# IDEs and editors
|
# IDEs and editors
|
||||||
/.idea
|
**/.idea
|
||||||
.project
|
.project
|
||||||
.classpath
|
.classpath
|
||||||
.c9/
|
.c9/
|
||||||
@@ -77,3 +77,4 @@ target
|
|||||||
tsconfig.node.tsbuildinfo
|
tsconfig.node.tsbuildinfo
|
||||||
lib
|
lib
|
||||||
affine.db
|
affine.db
|
||||||
|
apps/web/next-routes.conf
|
||||||
|
|||||||
@@ -14,3 +14,4 @@ _next
|
|||||||
storybook-static
|
storybook-static
|
||||||
web-static
|
web-static
|
||||||
public
|
public
|
||||||
|
apps/server/src/schema.gql
|
||||||
|
|||||||
Vendored
+12
-4
@@ -2,16 +2,24 @@
|
|||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
"command": "yarn run dev",
|
|
||||||
"name": "Run Dev",
|
"name": "Run Dev",
|
||||||
|
"type": "node-terminal",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"type": "node-terminal"
|
"command": "yarn run dev"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "yarn run dev:local",
|
|
||||||
"name": "Run Dev Locally",
|
"name": "Run Dev Locally",
|
||||||
|
"type": "node-terminal",
|
||||||
"request": "launch",
|
"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"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+2968
-293
File diff suppressed because it is too large
Load Diff
+10
-1
@@ -1,5 +1,10 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = ["./packages/native", "./packages/native/schema"]
|
resolver = "2"
|
||||||
|
members = [
|
||||||
|
"./packages/native",
|
||||||
|
"./packages/native/schema",
|
||||||
|
"./packages/storage",
|
||||||
|
]
|
||||||
|
|
||||||
[profile.dev.package.sqlx-macros]
|
[profile.dev.package.sqlx-macros]
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
@@ -9,3 +14,7 @@ lto = true
|
|||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
strip = "symbols"
|
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" }
|
||||||
|
|||||||
@@ -1,384 +1,25 @@
|
|||||||
# Mozilla Public License Version 2.0
|
Copyright (c) 2022-present TOEVERYTHING PTE. LTD. and its affiliates.
|
||||||
|
|
||||||
Copyright (c) TOEVERYTHING PTE. LTD. and its affiliates.
|
Portions of this software are licensed as follows:
|
||||||
|
|
||||||
1. Definitions
|
* 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".
|
||||||
|
|
||||||
1.1. "Contributor"
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
means each individual or legal entity that creates, contributes to
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
the creation of, or owns Covered Software.
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
1.2. "Contributor Version"
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
means the combination of the Contributions of others (if any) used
|
furnished to do so, subject to the following conditions:
|
||||||
by a Contributor and that particular Contributor's Contribution.
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
1.3. "Contribution"
|
copies or substantial portions of the Software.
|
||||||
means Covered Software of a particular Contributor.
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
1.4. "Covered Software"
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
means Source Code Form to which the initial Contributor has attached
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
the notice in Exhibit A, the Executable Form of such Source Code
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
Form, and Modifications of such Source Code Form, in each case
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
including portions thereof.
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
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.
|
|
||||||
|
|||||||
+384
@@ -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());
|
module.nameForCondition && regexp.test(module.nameForCondition());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://hackernoon.com/the-100-correct-way-to-split-your-chunks-with-webpack-f8a9df5b7758
|
||||||
export const productionCacheGroups = {
|
export const productionCacheGroups = {
|
||||||
asyncVendor: {
|
asyncVendor: {
|
||||||
test: /[\\/]node_modules[\\/]/,
|
test: /[\\/]node_modules[\\/]/,
|
||||||
name(module: any) {
|
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(
|
const name = module.context.match(
|
||||||
/[\\/]node_modules[\\/](.*?)([\\/]|$)/
|
/[\\/]node_modules[\\/](.*?)([\\/]|$)/
|
||||||
)?.[1];
|
)?.[1];
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { join, resolve } from 'node:path';
|
import { join, resolve } from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import { createRequire } from 'node:module';
|
import { createRequire } from 'node:module';
|
||||||
|
|
||||||
import type { Configuration as DevServerConfiguration } from 'webpack-dev-server';
|
import type { Configuration as DevServerConfiguration } from 'webpack-dev-server';
|
||||||
import { PerfseePlugin } from '@perfsee/webpack';
|
import { PerfseePlugin } from '@perfsee/webpack';
|
||||||
import { sentryWebpackPlugin } from '@sentry/webpack-plugin';
|
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 TerserPlugin from 'terser-webpack-plugin';
|
||||||
import webpack from 'webpack';
|
import webpack from 'webpack';
|
||||||
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
|
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
|
||||||
|
import { compact } from 'lodash-es';
|
||||||
|
|
||||||
import { productionCacheGroups } from './cache-group.js';
|
import { productionCacheGroups } from './cache-group.js';
|
||||||
import type { BuildFlags } from '@affine/cli/config';
|
import type { BuildFlags } from '@affine/cli/config';
|
||||||
import { projectRoot } from '@affine/cli/config';
|
import { projectRoot } from '@affine/cli/config';
|
||||||
import { VanillaExtractPlugin } from '@vanilla-extract/webpack-plugin';
|
import { VanillaExtractPlugin } from '@vanilla-extract/webpack-plugin';
|
||||||
import { computeCacheKey } from './utils.js';
|
|
||||||
import type { RuntimeConfig } from '@affine/env/global';
|
import type { RuntimeConfig } from '@affine/env/global';
|
||||||
|
import { WebpackS3Plugin, gitShortHash } from './s3-plugin.js';
|
||||||
|
|
||||||
const IN_CI = !!process.env.CI;
|
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: (
|
export const createConfiguration: (
|
||||||
buildFlags: BuildFlags,
|
buildFlags: BuildFlags,
|
||||||
runtimeConfig: RuntimeConfig
|
runtimeConfig: RuntimeConfig
|
||||||
) => webpack.Configuration = (buildFlags, runtimeConfig) => {
|
) => webpack.Configuration = (buildFlags, runtimeConfig) => {
|
||||||
let publicPath = process.env.PUBLIC_PATH ?? '/';
|
|
||||||
|
|
||||||
const blocksuiteBaseDir = buildFlags.localBlockSuite;
|
const blocksuiteBaseDir = buildFlags.localBlockSuite;
|
||||||
|
|
||||||
const cacheKey = computeCacheKey(buildFlags);
|
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
name: 'affine',
|
name: 'affine',
|
||||||
// to set a correct base path for the source map
|
// to set a correct base path for the source map
|
||||||
@@ -96,8 +108,11 @@ export const createConfiguration: (
|
|||||||
? 'js/[name]-[contenthash:8].js'
|
? 'js/[name]-[contenthash:8].js'
|
||||||
: 'js/[name].js',
|
: 'js/[name].js',
|
||||||
// In some cases webpack will emit files starts with "_" which is reserved in web extension.
|
// In some cases webpack will emit files starts with "_" which is reserved in web extension.
|
||||||
chunkFilename: 'js/chunk.[name].js',
|
chunkFilename:
|
||||||
assetModuleFilename: 'assets/[contenthash:8][ext][query]',
|
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]',
|
devtoolModuleFilenameTemplate: 'webpack://[namespace]/[resource-path]',
|
||||||
hotUpdateChunkFilename: 'hot/[id].[fullhash].js',
|
hotUpdateChunkFilename: 'hot/[id].[fullhash].js',
|
||||||
hotUpdateMainFilename: 'hot/[runtime].[fullhash].json',
|
hotUpdateMainFilename: 'hot/[runtime].[fullhash].json',
|
||||||
@@ -192,14 +207,6 @@ export const createConfiguration: (
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
cache: {
|
|
||||||
type: 'filesystem',
|
|
||||||
buildDependencies: {
|
|
||||||
config: [fileURLToPath(import.meta.url)],
|
|
||||||
},
|
|
||||||
version: cacheKey,
|
|
||||||
},
|
|
||||||
|
|
||||||
module: {
|
module: {
|
||||||
parser: {
|
parser: {
|
||||||
javascript: {
|
javascript: {
|
||||||
@@ -308,7 +315,7 @@ export const createConfiguration: (
|
|||||||
{
|
{
|
||||||
loader: 'css-loader',
|
loader: 'css-loader',
|
||||||
options: {
|
options: {
|
||||||
url: false,
|
url: true,
|
||||||
sourceMap: false,
|
sourceMap: false,
|
||||||
modules: false,
|
modules: false,
|
||||||
import: true,
|
import: true,
|
||||||
@@ -333,22 +340,23 @@ export const createConfiguration: (
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
plugins: compact([
|
||||||
plugins: [
|
IN_CI ? null : new webpack.ProgressPlugin({ percentBy: 'entries' }),
|
||||||
...(IN_CI ? [] : [new webpack.ProgressPlugin({ percentBy: 'entries' })]),
|
buildFlags.mode === 'development'
|
||||||
...(buildFlags.mode === 'development'
|
? new ReactRefreshWebpackPlugin({ overlay: false, esModule: true })
|
||||||
? [new ReactRefreshWebpackPlugin({ overlay: false, esModule: true })]
|
: new MiniCssExtractPlugin({
|
||||||
: [
|
filename: `[name].[contenthash:8].css`,
|
||||||
new MiniCssExtractPlugin({
|
ignoreOrder: true,
|
||||||
filename: `[name].[contenthash:8].css`,
|
}),
|
||||||
ignoreOrder: true,
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
new VanillaExtractPlugin(),
|
new VanillaExtractPlugin(),
|
||||||
new webpack.DefinePlugin({
|
new webpack.DefinePlugin({
|
||||||
'process.env': JSON.stringify({}),
|
'process.env': JSON.stringify({}),
|
||||||
'process.env.COVERAGE': JSON.stringify(!!buildFlags.coverage),
|
'process.env.COVERAGE': JSON.stringify(!!buildFlags.coverage),
|
||||||
'process.env.NODE_ENV': JSON.stringify(buildFlags.mode),
|
'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),
|
runtimeConfig: JSON.stringify(runtimeConfig),
|
||||||
}),
|
}),
|
||||||
new CopyPlugin({
|
new CopyPlugin({
|
||||||
@@ -359,7 +367,10 @@ export const createConfiguration: (
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
],
|
buildFlags.mode === 'production' && process.env.R2_SECRET_ACCESS_KEY
|
||||||
|
? new WebpackS3Plugin()
|
||||||
|
: null,
|
||||||
|
]),
|
||||||
|
|
||||||
optimization: OptimizeOptionOptions(buildFlags),
|
optimization: OptimizeOptionOptions(buildFlags),
|
||||||
|
|
||||||
@@ -373,6 +384,14 @@ export const createConfiguration: (
|
|||||||
publicPath: '/',
|
publicPath: '/',
|
||||||
watch: true,
|
watch: true,
|
||||||
},
|
},
|
||||||
|
proxy: {
|
||||||
|
'/api': 'http://localhost:3010',
|
||||||
|
'/socket.io': {
|
||||||
|
target: 'http://localhost:3010',
|
||||||
|
ws: true,
|
||||||
|
},
|
||||||
|
'/graphql': 'http://localhost:3010',
|
||||||
|
},
|
||||||
} as DevServerConfiguration,
|
} as DevServerConfiguration,
|
||||||
} satisfies webpack.Configuration;
|
} satisfies webpack.Configuration;
|
||||||
|
|
||||||
|
|||||||
@@ -24,9 +24,9 @@ const editorFlags: BlockSuiteFeatureFlags = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function getRuntimeConfig(buildFlags: BuildFlags): RuntimeConfig {
|
export function getRuntimeConfig(buildFlags: BuildFlags): RuntimeConfig {
|
||||||
const buildPreset: Record<string, RuntimeConfig> = {
|
const buildPreset: Record<BuildFlags['channel'], RuntimeConfig> = {
|
||||||
stable: {
|
stable: {
|
||||||
enablePlugin: false,
|
enablePlugin: true,
|
||||||
enableTestProperties: false,
|
enableTestProperties: false,
|
||||||
enableBroadcastChannelProvider: true,
|
enableBroadcastChannelProvider: true,
|
||||||
enableDebugPage: true,
|
enableDebugPage: true,
|
||||||
@@ -37,13 +37,26 @@ export function getRuntimeConfig(buildFlags: BuildFlags): RuntimeConfig {
|
|||||||
enableNewSettingUnstableApi: false,
|
enableNewSettingUnstableApi: false,
|
||||||
enableSQLiteProvider: true,
|
enableSQLiteProvider: true,
|
||||||
enableMoveDatabase: false,
|
enableMoveDatabase: false,
|
||||||
enableNotificationCenter: false,
|
enableNotificationCenter: true,
|
||||||
enableCloud: false,
|
enableCloud: true,
|
||||||
serverAPI: 'https://localhost:3010',
|
enableEnhanceShareMode: false,
|
||||||
|
serverUrlPrefix: 'https://app.affine.pro',
|
||||||
editorFlags,
|
editorFlags,
|
||||||
appVersion: packageJson.version,
|
appVersion: packageJson.version,
|
||||||
editorVersion: packageJson.dependencies['@blocksuite/editor'],
|
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 will be aggressive and enable all features
|
||||||
canary: {
|
canary: {
|
||||||
enablePlugin: true,
|
enablePlugin: true,
|
||||||
@@ -58,18 +71,15 @@ export function getRuntimeConfig(buildFlags: BuildFlags): RuntimeConfig {
|
|||||||
enableSQLiteProvider: true,
|
enableSQLiteProvider: true,
|
||||||
enableMoveDatabase: false,
|
enableMoveDatabase: false,
|
||||||
enableNotificationCenter: true,
|
enableNotificationCenter: true,
|
||||||
enableCloud: false,
|
enableCloud: true,
|
||||||
serverAPI: 'https://localhost:3010',
|
enableEnhanceShareMode: false,
|
||||||
|
serverUrlPrefix: 'https://affine.fail',
|
||||||
editorFlags,
|
editorFlags,
|
||||||
appVersion: packageJson.version,
|
appVersion: packageJson.version,
|
||||||
editorVersion: packageJson.dependencies['@blocksuite/editor'],
|
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;
|
const currentBuild = buildFlags.channel;
|
||||||
|
|
||||||
if (!(currentBuild in buildPreset)) {
|
if (!(currentBuild in buildPreset)) {
|
||||||
@@ -107,11 +117,18 @@ export function getRuntimeConfig(buildFlags: BuildFlags): RuntimeConfig {
|
|||||||
enableCloud: process.env.ENABLE_CLOUD
|
enableCloud: process.env.ENABLE_CLOUD
|
||||||
? process.env.ENABLE_CLOUD === 'true'
|
? process.env.ENABLE_CLOUD === 'true'
|
||||||
: currentBuildPreset.enableCloud,
|
: currentBuildPreset.enableCloud,
|
||||||
|
enableEnhanceShareMode: process.env.ENABLE_ENHANCE_SHARE_MODE
|
||||||
|
? process.env.ENABLE_ENHANCE_SHARE_MODE === 'true'
|
||||||
|
: currentBuildPreset.enableEnhanceShareMode,
|
||||||
enableMoveDatabase: process.env.ENABLE_MOVE_DATABASE
|
enableMoveDatabase: process.env.ENABLE_MOVE_DATABASE
|
||||||
? process.env.ENABLE_MOVE_DATABASE === 'true'
|
? process.env.ENABLE_MOVE_DATABASE === 'true'
|
||||||
: currentBuildPreset.enableMoveDatabase,
|
: currentBuildPreset.enableMoveDatabase,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (buildFlags.mode === 'development') {
|
||||||
|
currentBuildPreset.serverUrlPrefix = 'http://localhost:8080';
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...currentBuildPreset,
|
...currentBuildPreset,
|
||||||
// environment preset will overwrite current build preset
|
// environment preset will overwrite current build preset
|
||||||
|
|||||||
@@ -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:url" content="https://app.affine.pro/" />
|
||||||
<meta
|
<meta
|
||||||
name="twitter:title"
|
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:description" content="{description}" />
|
||||||
<meta name="twitter:site" content="@AffineOfficial" />
|
<meta name="twitter:site" content="@AffineOfficial" />
|
||||||
<meta name="twitter:image" content="https://affine.pro/og.jpeg" />
|
<meta name="twitter:image" content="https://affine.pro/og.jpeg" />
|
||||||
<meta
|
<meta
|
||||||
property="og:title"
|
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 property="og:type" content="website" />
|
||||||
<meta
|
<meta
|
||||||
@@ -39,7 +39,8 @@
|
|||||||
href="https://affine.pro/favicon.ico"
|
href="https://affine.pro/favicon.ico"
|
||||||
/>
|
/>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app" data-version="<%= GIT_SHORT_SHA %>"></div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { createConfiguration, rootPath } from './config.js';
|
import { createConfiguration, rootPath, publicPath } from './config.js';
|
||||||
import { merge } from 'webpack-merge';
|
import { merge } from 'webpack-merge';
|
||||||
import { join, resolve } from 'node:path';
|
import { join, resolve } from 'node:path';
|
||||||
import type { BuildFlags } from '@affine/cli/config';
|
import type { BuildFlags } from '@affine/cli/config';
|
||||||
import { getRuntimeConfig } from './runtime-config.js';
|
import { getRuntimeConfig } from './runtime-config.js';
|
||||||
import HTMLPlugin from 'html-webpack-plugin';
|
import HTMLPlugin from 'html-webpack-plugin';
|
||||||
|
|
||||||
|
import { gitShortHash } from './s3-plugin.js';
|
||||||
|
|
||||||
export default async function (cli_env: any, _: any) {
|
export default async function (cli_env: any, _: any) {
|
||||||
const flags: BuildFlags = JSON.parse(
|
const flags: BuildFlags = JSON.parse(
|
||||||
Buffer.from(cli_env.flags, 'hex').toString('utf-8')
|
Buffer.from(cli_env.flags, 'hex').toString('utf-8')
|
||||||
@@ -44,12 +46,16 @@ export default async function (cli_env: any, _: any) {
|
|||||||
minify: false,
|
minify: false,
|
||||||
chunks: ['app', 'plugin', 'polyfill/intl-segmenter', 'polyfill/ses'],
|
chunks: ['app', 'plugin', 'polyfill/intl-segmenter', 'polyfill/ses'],
|
||||||
filename: 'index.html',
|
filename: 'index.html',
|
||||||
|
templateParameters: {
|
||||||
|
GIT_SHORT_SHA: gitShortHash(),
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
new HTMLPlugin({
|
new HTMLPlugin({
|
||||||
template: join(rootPath, '.webpack', 'template.html'),
|
template: join(rootPath, '.webpack', 'template.html'),
|
||||||
inject: 'body',
|
inject: 'body',
|
||||||
scriptLoading: 'module',
|
scriptLoading: 'module',
|
||||||
minify: false,
|
minify: false,
|
||||||
|
publicPath,
|
||||||
chunks: [
|
chunks: [
|
||||||
'_plugin/index.test',
|
'_plugin/index.test',
|
||||||
'plugin',
|
'plugin',
|
||||||
@@ -57,6 +63,9 @@ export default async function (cli_env: any, _: any) {
|
|||||||
'polyfill/ses',
|
'polyfill/ses',
|
||||||
],
|
],
|
||||||
filename: '_plugin/index.html',
|
filename: '_plugin/index.html',
|
||||||
|
templateParameters: {
|
||||||
|
GIT_SHORT_SHA: gitShortHash(),
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -38,6 +38,7 @@
|
|||||||
"@emotion/server": "^11.11.0",
|
"@emotion/server": "^11.11.0",
|
||||||
"@emotion/styled": "^11.11.0",
|
"@emotion/styled": "^11.11.0",
|
||||||
"@mui/material": "^5.14.6",
|
"@mui/material": "^5.14.6",
|
||||||
|
"@radix-ui/react-select": "^1.2.2",
|
||||||
"@react-hookz/web": "^23.1.0",
|
"@react-hookz/web": "^23.1.0",
|
||||||
"@toeverything/components": "^0.0.19",
|
"@toeverything/components": "^0.0.19",
|
||||||
"async-call-rpc": "^6.3.1",
|
"async-call-rpc": "^6.3.1",
|
||||||
@@ -52,6 +53,7 @@
|
|||||||
"lodash.debounce": "^4.0.8",
|
"lodash.debounce": "^4.0.8",
|
||||||
"lottie-web": "^5.12.2",
|
"lottie-web": "^5.12.2",
|
||||||
"mini-css-extract-plugin": "^2.7.6",
|
"mini-css-extract-plugin": "^2.7.6",
|
||||||
|
"next-auth": "^4.22.1",
|
||||||
"next-themes": "^0.2.1",
|
"next-themes": "^0.2.1",
|
||||||
"postcss-loader": "^7.3.3",
|
"postcss-loader": "^7.3.3",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
@@ -62,22 +64,27 @@
|
|||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"ses": "^0.18.7",
|
"ses": "^0.18.7",
|
||||||
"swr": "2.2.1",
|
"swr": "2.2.1",
|
||||||
|
"valtio": "^1.10.6",
|
||||||
"y-protocols": "^1.0.5",
|
"y-protocols": "^1.0.5",
|
||||||
"yjs": "^13.6.7",
|
"yjs": "^13.6.7",
|
||||||
"zod": "^3.22.2"
|
"zod": "^3.22.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@aws-sdk/client-s3": "3.400.0",
|
||||||
"@perfsee/webpack": "^1.8.4",
|
"@perfsee/webpack": "^1.8.4",
|
||||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.11",
|
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.11",
|
||||||
"@sentry/webpack-plugin": "^2.7.0",
|
"@sentry/webpack-plugin": "^2.7.0",
|
||||||
"@svgr/webpack": "^8.1.0",
|
"@svgr/webpack": "^8.1.0",
|
||||||
"@swc/core": "^1.3.80",
|
"@swc/core": "^1.3.80",
|
||||||
|
"@types/lodash-es": "^4.17.8",
|
||||||
"@types/lodash.debounce": "^4.0.7",
|
"@types/lodash.debounce": "^4.0.7",
|
||||||
"@types/webpack-env": "^1.18.1",
|
"@types/webpack-env": "^1.18.1",
|
||||||
"copy-webpack-plugin": "^11.0.0",
|
"copy-webpack-plugin": "^11.0.0",
|
||||||
"css-loader": "^6.8.1",
|
"css-loader": "^6.8.1",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"html-webpack-plugin": "^5.5.3",
|
"html-webpack-plugin": "^5.5.3",
|
||||||
|
"lodash-es": "^4.17.21",
|
||||||
|
"mime-types": "^2.1.35",
|
||||||
"raw-loader": "^4.0.2",
|
"raw-loader": "^4.0.2",
|
||||||
"source-map-loader": "^4.0.1",
|
"source-map-loader": "^4.0.1",
|
||||||
"style-loader": "^3.3.3",
|
"style-loader": "^3.3.3",
|
||||||
|
|||||||
@@ -10,20 +10,18 @@
|
|||||||
"target": "build",
|
"target": "build",
|
||||||
"params": "ignore"
|
"params": "ignore"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"projects": ["tag:infra"],
|
||||||
|
"target": "build",
|
||||||
|
"params": "ignore"
|
||||||
|
},
|
||||||
"^build"
|
"^build"
|
||||||
],
|
],
|
||||||
"inputs": [
|
"inputs": [
|
||||||
"{projectRoot}/.webpack/**/*",
|
"{projectRoot}/.webpack/**/*",
|
||||||
"{projectRoot}/**/*",
|
"{projectRoot}/**/*",
|
||||||
"{projectRoot}/public/**/*",
|
"{projectRoot}/public/**/*",
|
||||||
"{workspaceRoot}/packages/env/src/**/*",
|
"{workspaceRoot}/packages/**/*",
|
||||||
"{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/**/*",
|
|
||||||
{
|
{
|
||||||
"env": "BUILD_TYPE"
|
"env": "BUILD_TYPE"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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 [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -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,
|
BlockSuitePageList,
|
||||||
NewWorkspaceSettingDetail,
|
NewWorkspaceSettingDetail,
|
||||||
PageDetailEditor,
|
PageDetailEditor,
|
||||||
|
Provider,
|
||||||
WorkspaceHeader,
|
WorkspaceHeader,
|
||||||
} from '../shared';
|
} from '../shared';
|
||||||
|
|
||||||
@@ -39,6 +40,7 @@ export const LocalAdapter: WorkspaceAdapter<WorkspaceFlavour.LOCAL> = {
|
|||||||
flavour: WorkspaceFlavour.LOCAL,
|
flavour: WorkspaceFlavour.LOCAL,
|
||||||
loadPriority: LoadPriority.LOW,
|
loadPriority: LoadPriority.LOW,
|
||||||
Events: {
|
Events: {
|
||||||
|
'app:access': async () => true,
|
||||||
'app:init': () => {
|
'app:init': () => {
|
||||||
const blockSuiteWorkspace = getOrCreateWorkspace(
|
const blockSuiteWorkspace = getOrCreateWorkspace(
|
||||||
nanoid(),
|
nanoid(),
|
||||||
@@ -79,9 +81,7 @@ export const LocalAdapter: WorkspaceAdapter<WorkspaceFlavour.LOCAL> = {
|
|||||||
CRUD,
|
CRUD,
|
||||||
UI: {
|
UI: {
|
||||||
Header: WorkspaceHeader,
|
Header: WorkspaceHeader,
|
||||||
Provider: ({ children }) => {
|
Provider,
|
||||||
return <>{children}</>;
|
|
||||||
},
|
|
||||||
PageDetail: ({ currentWorkspaceId, currentPageId, onLoadEditor }) => {
|
PageDetail: ({ currentWorkspaceId, currentPageId, onLoadEditor }) => {
|
||||||
const workspace = useStaticBlockSuiteWorkspace(currentWorkspaceId);
|
const workspace = useStaticBlockSuiteWorkspace(currentWorkspaceId);
|
||||||
const page = workspace.getPage(currentPageId);
|
const page = workspace.getPage(currentPageId);
|
||||||
@@ -111,14 +111,19 @@ export const LocalAdapter: WorkspaceAdapter<WorkspaceFlavour.LOCAL> = {
|
|||||||
},
|
},
|
||||||
NewSettingsDetail: ({
|
NewSettingsDetail: ({
|
||||||
currentWorkspaceId,
|
currentWorkspaceId,
|
||||||
onDeleteWorkspace,
|
|
||||||
onTransformWorkspace,
|
onTransformWorkspace,
|
||||||
|
onDeleteLocalWorkspace,
|
||||||
|
onDeleteCloudWorkspace,
|
||||||
|
onLeaveWorkspace,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<NewWorkspaceSettingDetail
|
<NewWorkspaceSettingDetail
|
||||||
onDeleteWorkspace={onDeleteWorkspace}
|
onDeleteLocalWorkspace={onDeleteLocalWorkspace}
|
||||||
|
onDeleteCloudWorkspace={onDeleteCloudWorkspace}
|
||||||
|
onLeaveWorkspace={onLeaveWorkspace}
|
||||||
workspaceId={currentWorkspaceId}
|
workspaceId={currentWorkspaceId}
|
||||||
onTransferWorkspace={onTransformWorkspace}
|
onTransferWorkspace={onTransformWorkspace}
|
||||||
|
isOwner={true}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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';
|
import { lazy } from 'react';
|
||||||
|
|
||||||
|
export const Provider = lazy(() =>
|
||||||
|
import('../components/cloud/provider').then(({ Provider }) => ({
|
||||||
|
default: Provider,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
export const NewWorkspaceSettingDetail = lazy(() =>
|
export const NewWorkspaceSettingDetail = lazy(() =>
|
||||||
import('../components/affine/new-workspace-setting-detail').then(
|
import('../components/affine/new-workspace-setting-detail').then(
|
||||||
({ WorkspaceSettingDetail }) => ({
|
({ WorkspaceSettingDetail }) => ({
|
||||||
|
|||||||
@@ -10,7 +10,10 @@ import {
|
|||||||
WorkspaceFlavour,
|
WorkspaceFlavour,
|
||||||
} from '@affine/env/workspace';
|
} from '@affine/env/workspace';
|
||||||
|
|
||||||
|
import { CRUD as CloudCRUD } from './cloud/crud';
|
||||||
|
import { UI as CloudUI } from './cloud/ui';
|
||||||
import { LocalAdapter } from './local';
|
import { LocalAdapter } from './local';
|
||||||
|
import { UI as PublicCloudUI } from './public-cloud/ui';
|
||||||
|
|
||||||
const unimplemented = () => {
|
const unimplemented = () => {
|
||||||
throw new Error('Not implemented');
|
throw new Error('Not implemented');
|
||||||
@@ -26,26 +29,24 @@ export const WorkspaceAdapters = {
|
|||||||
releaseType: ReleaseType.UNRELEASED,
|
releaseType: ReleaseType.UNRELEASED,
|
||||||
flavour: WorkspaceFlavour.AFFINE_CLOUD,
|
flavour: WorkspaceFlavour.AFFINE_CLOUD,
|
||||||
loadPriority: LoadPriority.HIGH,
|
loadPriority: LoadPriority.HIGH,
|
||||||
Events: {} as Partial<AppEvents>,
|
Events: {
|
||||||
// todo: implement this
|
'app:access': async () => {
|
||||||
CRUD: {
|
try {
|
||||||
get: unimplemented,
|
const { getSession } = await import('next-auth/react');
|
||||||
list: bypassList,
|
const session = await getSession();
|
||||||
delete: unimplemented,
|
return !!session;
|
||||||
create: unimplemented,
|
} catch (e) {
|
||||||
},
|
console.error('failed to get session', e);
|
||||||
// todo: implement this
|
return false;
|
||||||
UI: {
|
}
|
||||||
Provider: unimplemented,
|
},
|
||||||
Header: unimplemented,
|
} as Partial<AppEvents>,
|
||||||
PageDetail: unimplemented,
|
CRUD: CloudCRUD,
|
||||||
PageList: unimplemented,
|
UI: CloudUI,
|
||||||
NewSettingsDetail: unimplemented,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
[WorkspaceFlavour.PUBLIC]: {
|
[WorkspaceFlavour.AFFINE_PUBLIC]: {
|
||||||
releaseType: ReleaseType.UNRELEASED,
|
releaseType: ReleaseType.UNRELEASED,
|
||||||
flavour: WorkspaceFlavour.PUBLIC,
|
flavour: WorkspaceFlavour.AFFINE_PUBLIC,
|
||||||
loadPriority: LoadPriority.LOW,
|
loadPriority: LoadPriority.LOW,
|
||||||
Events: {} as Partial<AppEvents>,
|
Events: {} as Partial<AppEvents>,
|
||||||
// todo: implement this
|
// todo: implement this
|
||||||
@@ -55,14 +56,7 @@ export const WorkspaceAdapters = {
|
|||||||
delete: unimplemented,
|
delete: unimplemented,
|
||||||
create: unimplemented,
|
create: unimplemented,
|
||||||
},
|
},
|
||||||
// todo: implement this
|
UI: PublicCloudUI,
|
||||||
UI: {
|
|
||||||
Provider: unimplemented,
|
|
||||||
Header: unimplemented,
|
|
||||||
PageDetail: unimplemented,
|
|
||||||
PageList: unimplemented,
|
|
||||||
NewSettingsDetail: unimplemented,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
} satisfies {
|
} satisfies {
|
||||||
[Key in WorkspaceFlavour]: WorkspaceAdapter<Key>;
|
[Key in WorkspaceFlavour]: WorkspaceAdapter<Key>;
|
||||||
|
|||||||
+14
-11
@@ -7,6 +7,7 @@ import { WorkspaceFallback } from '@affine/component/workspace';
|
|||||||
import { CacheProvider } from '@emotion/react';
|
import { CacheProvider } from '@emotion/react';
|
||||||
import { getCurrentStore } from '@toeverything/infra/atom';
|
import { getCurrentStore } from '@toeverything/infra/atom';
|
||||||
import { use } from 'foxact/use';
|
import { use } from 'foxact/use';
|
||||||
|
import { SessionProvider } from 'next-auth/react';
|
||||||
import type { PropsWithChildren, ReactElement } from 'react';
|
import type { PropsWithChildren, ReactElement } from 'react';
|
||||||
import { lazy, memo, Suspense } from 'react';
|
import { lazy, memo, Suspense } from 'react';
|
||||||
import { RouterProvider } from 'react-router-dom';
|
import { RouterProvider } from 'react-router-dom';
|
||||||
@@ -47,16 +48,18 @@ const languageLoadingPromise = loadLanguage().catch(console.error);
|
|||||||
export const App = memo(function App() {
|
export const App = memo(function App() {
|
||||||
use(languageLoadingPromise);
|
use(languageLoadingPromise);
|
||||||
return (
|
return (
|
||||||
<CacheProvider value={cache}>
|
<SessionProvider refetchOnWindowFocus>
|
||||||
<AffineContext store={getCurrentStore()}>
|
<CacheProvider value={cache}>
|
||||||
<DebugProvider>
|
<AffineContext store={getCurrentStore()}>
|
||||||
<RouterProvider
|
<DebugProvider>
|
||||||
fallbackElement={<WorkspaceFallback key="RouterFallback" />}
|
<RouterProvider
|
||||||
router={router}
|
fallbackElement={<WorkspaceFallback key="RouterFallback" />}
|
||||||
future={future}
|
router={router}
|
||||||
/>
|
future={future}
|
||||||
</DebugProvider>
|
/>
|
||||||
</AffineContext>
|
</DebugProvider>
|
||||||
</CacheProvider>
|
</AffineContext>
|
||||||
|
</CacheProvider>
|
||||||
|
</SessionProvider>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import { atom } from 'jotai';
|
|||||||
import { atomFamily, atomWithStorage } from 'jotai/utils';
|
import { atomFamily, atomWithStorage } from 'jotai/utils';
|
||||||
import type { AtomFamily } from 'jotai/vanilla/utils/atomFamily';
|
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 { CreateWorkspaceMode } from '../components/affine/create-workspace-modal';
|
||||||
import type { SettingProps } from '../components/affine/setting-modal';
|
import type { SettingProps } from '../components/affine/setting-modal';
|
||||||
|
|
||||||
// modal atoms
|
// modal atoms
|
||||||
export const openWorkspacesModalAtom = atom(false);
|
export const openWorkspacesModalAtom = atom(false);
|
||||||
export const openCreateWorkspaceModalAtom = atom<CreateWorkspaceMode>(false);
|
export const openCreateWorkspaceModalAtom = atom<CreateWorkspaceMode>(false);
|
||||||
@@ -22,6 +22,22 @@ export const openSettingModalAtom = atom<SettingAtom>({
|
|||||||
open: false,
|
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);
|
export const openDisableCloudAlertModalAtom = atom(false);
|
||||||
|
|
||||||
type PageMode = 'page' | 'edgeless';
|
type PageMode = 'page' | 'edgeless';
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
};
|
||||||
@@ -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])}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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]
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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])}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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])}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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 { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { CloseIcon } from '@blocksuite/icons';
|
import { CloseIcon } from '@blocksuite/icons';
|
||||||
import { Button, IconButton } from '@toeverything/components/button';
|
import { Button, IconButton } from '@toeverything/components/button';
|
||||||
|
|
||||||
import { Content, ContentTitle, Header, StyleTips } from './style';
|
import { ButtonContainer, Content, Header, StyleTips, Title } from './style';
|
||||||
|
|
||||||
interface EnableAffineCloudModalProps {
|
interface EnableAffineCloudModalProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -20,32 +20,32 @@ export const EnableAffineCloudModal = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal open={open} onClose={onClose} data-testid="logout-modal">
|
<Modal open={open} onClose={onClose} data-testid="logout-modal">
|
||||||
<ModalWrapper width={560} height={292}>
|
<ModalWrapper width={480}>
|
||||||
<Header>
|
<Header>
|
||||||
|
<Title>{t['Enable AFFiNE Cloud']()}</Title>
|
||||||
<IconButton onClick={onClose}>
|
<IconButton onClick={onClose}>
|
||||||
<CloseIcon />
|
<CloseIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Header>
|
</Header>
|
||||||
<Content>
|
<Content>
|
||||||
<ContentTitle>{t['Enable AFFiNE Cloud']()}?</ContentTitle>
|
|
||||||
<StyleTips>{t['Enable AFFiNE Cloud Description']()}</StyleTips>
|
<StyleTips>{t['Enable AFFiNE Cloud Description']()}</StyleTips>
|
||||||
{/* <StyleTips>{t('Retain cached cloud data')}</StyleTips> */}
|
<ButtonContainer>
|
||||||
<Wrapper width={284} margin="auto">
|
<div>
|
||||||
<Button
|
<Button onClick={onClose} block>
|
||||||
data-testid="confirm-enable-affine-cloud-button"
|
{t['Cancel']()}
|
||||||
type="primary"
|
</Button>
|
||||||
block
|
</div>
|
||||||
onClick={onConfirm}
|
<div>
|
||||||
style={{
|
<Button
|
||||||
marginBottom: '16px',
|
data-testid="confirm-enable-affine-cloud-button"
|
||||||
}}
|
type="primary"
|
||||||
>
|
block
|
||||||
{t['Sign in and Enable']()}
|
onClick={onConfirm}
|
||||||
</Button>
|
>
|
||||||
<Button onClick={onClose} block>
|
{t['Sign in and Enable']()}
|
||||||
{t['Not now']()}
|
</Button>
|
||||||
</Button>
|
</div>
|
||||||
</Wrapper>
|
</ButtonContainer>
|
||||||
</Content>
|
</Content>
|
||||||
</ModalWrapper>
|
</ModalWrapper>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -1,31 +1,35 @@
|
|||||||
import { styled } from '@affine/component';
|
import { styled } from '@affine/component';
|
||||||
|
|
||||||
export const Header = styled('div')({
|
export const Header = styled('div')({
|
||||||
height: '44px',
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'row-reverse',
|
justifyContent: 'space-between',
|
||||||
paddingRight: '10px',
|
paddingRight: '20px',
|
||||||
paddingTop: '10px',
|
paddingTop: '20px',
|
||||||
flexShrink: 0,
|
paddingLeft: '24px',
|
||||||
|
alignItems: 'center',
|
||||||
});
|
});
|
||||||
|
|
||||||
export const Content = styled('div')({
|
export const Content = styled('div')({
|
||||||
textAlign: 'center',
|
padding: '12px 24px 20px 24px',
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ContentTitle = styled('h1')({
|
export const Title = styled('div')({
|
||||||
fontSize: '20px',
|
fontSize: 'var(--affine-font-h6)',
|
||||||
lineHeight: '28px',
|
lineHeight: '26px',
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
textAlign: 'center',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const StyleTips = styled('div')(() => {
|
export const StyleTips = styled('div')(() => {
|
||||||
return {
|
return {
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
width: '400px',
|
marginBottom: '20px',
|
||||||
margin: 'auto',
|
};
|
||||||
marginBottom: '32px',
|
});
|
||||||
marginTop: '12px',
|
export const ButtonContainer = styled('div')(() => {
|
||||||
|
return {
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
gap: '20px',
|
||||||
|
paddingTop: '20px',
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
+5
-19
@@ -1,13 +1,12 @@
|
|||||||
import { Input, Modal, ModalCloseButton } from '@affine/component';
|
import { Input, Modal, ModalCloseButton } from '@affine/component';
|
||||||
|
import type { AffineOfficialWorkspace } from '@affine/env/workspace';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
import { Trans } from '@affine/i18n';
|
import { Trans } from '@affine/i18n';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { Button } from '@toeverything/components/button';
|
import { Button } from '@toeverything/components/button';
|
||||||
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
|
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 {
|
import {
|
||||||
StyledButtonContent,
|
StyledButtonContent,
|
||||||
StyledInputContent,
|
StyledInputContent,
|
||||||
@@ -21,14 +20,14 @@ interface WorkspaceDeleteProps {
|
|||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
workspace: AffineOfficialWorkspace;
|
workspace: AffineOfficialWorkspace;
|
||||||
onDeleteWorkspace: (id: string) => Promise<void>;
|
onConfirm: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WorkspaceDeleteModal = ({
|
export const WorkspaceDeleteModal = ({
|
||||||
open,
|
open,
|
||||||
onClose,
|
onClose,
|
||||||
|
onConfirm,
|
||||||
workspace,
|
workspace,
|
||||||
onDeleteWorkspace,
|
|
||||||
}: WorkspaceDeleteProps) => {
|
}: WorkspaceDeleteProps) => {
|
||||||
const [workspaceName] = useBlockSuiteWorkspaceName(
|
const [workspaceName] = useBlockSuiteWorkspaceName(
|
||||||
workspace.blockSuiteWorkspace
|
workspace.blockSuiteWorkspace
|
||||||
@@ -37,19 +36,6 @@ export const WorkspaceDeleteModal = ({
|
|||||||
const allowDelete = deleteStr === workspaceName;
|
const allowDelete = deleteStr === workspaceName;
|
||||||
const t = useAFFiNEI18N();
|
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 (
|
return (
|
||||||
<Modal open={open} onClose={onClose}>
|
<Modal open={open} onClose={onClose}>
|
||||||
<StyledModalWrapper>
|
<StyledModalWrapper>
|
||||||
@@ -99,7 +85,7 @@ export const WorkspaceDeleteModal = ({
|
|||||||
<Button
|
<Button
|
||||||
data-testid="delete-workspace-confirm-button"
|
data-testid="delete-workspace-confirm-button"
|
||||||
disabled={!allowDelete}
|
disabled={!allowDelete}
|
||||||
onClick={handleDelete}
|
onClick={onConfirm}
|
||||||
size="large"
|
size="large"
|
||||||
type="error"
|
type="error"
|
||||||
style={{ marginLeft: '24px' }}
|
style={{ marginLeft: '24px' }}
|
||||||
|
|||||||
+44
-16
@@ -1,29 +1,55 @@
|
|||||||
|
import { ConfirmModal } from '@affine/component';
|
||||||
import { SettingRow } from '@affine/component/setting-components';
|
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 { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { ArrowRightSmallIcon } from '@blocksuite/icons';
|
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 type { WorkspaceSettingDetailProps } from '../index';
|
||||||
import { WorkspaceDeleteModal } from './delete';
|
import { WorkspaceDeleteModal } from './delete';
|
||||||
import { WorkspaceLeave } from './leave';
|
|
||||||
|
|
||||||
interface DeleteLeaveWorkspaceProps {
|
export interface DeleteLeaveWorkspaceProps extends WorkspaceSettingDetailProps {
|
||||||
workspace: AffineOfficialWorkspace;
|
workspace: AffineOfficialWorkspace;
|
||||||
onDeleteWorkspace: WorkspaceSettingDetailProps['onDeleteWorkspace'];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DeleteLeaveWorkspace = ({
|
export const DeleteLeaveWorkspace = ({
|
||||||
workspace,
|
workspace,
|
||||||
onDeleteWorkspace,
|
onDeleteCloudWorkspace,
|
||||||
|
onDeleteLocalWorkspace,
|
||||||
|
onLeaveWorkspace,
|
||||||
|
isOwner,
|
||||||
}: DeleteLeaveWorkspaceProps) => {
|
}: DeleteLeaveWorkspaceProps) => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
// fixme: cloud regression
|
// fixme: cloud regression
|
||||||
const isOwner = true;
|
|
||||||
|
|
||||||
const [showDelete, setShowDelete] = useState(false);
|
const [showDelete, setShowDelete] = useState(false);
|
||||||
const [showLeave, setShowLeave] = 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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<SettingRow
|
<SettingRow
|
||||||
@@ -36,16 +62,14 @@ export const DeleteLeaveWorkspace = ({
|
|||||||
}
|
}
|
||||||
desc={t['com.affine.settings.remove-workspace-description']()}
|
desc={t['com.affine.settings.remove-workspace-description']()}
|
||||||
style={{ cursor: 'pointer' }}
|
style={{ cursor: 'pointer' }}
|
||||||
onClick={() => {
|
onClick={onLeaveOrDelete}
|
||||||
setShowDelete(true);
|
|
||||||
}}
|
|
||||||
data-testid="delete-workspace-button"
|
data-testid="delete-workspace-button"
|
||||||
>
|
>
|
||||||
<ArrowRightSmallIcon />
|
<ArrowRightSmallIcon />
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
{isOwner ? (
|
{isOwner ? (
|
||||||
<WorkspaceDeleteModal
|
<WorkspaceDeleteModal
|
||||||
onDeleteWorkspace={onDeleteWorkspace}
|
onConfirm={onDeleteConfirm}
|
||||||
open={showDelete}
|
open={showDelete}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setShowDelete(false);
|
setShowDelete(false);
|
||||||
@@ -53,11 +77,15 @@ export const DeleteLeaveWorkspace = ({
|
|||||||
workspace={workspace}
|
workspace={workspace}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<WorkspaceLeave
|
<ConfirmModal
|
||||||
open={showLeave}
|
open={showLeave}
|
||||||
onClose={() => {
|
onConfirm={onLeaveConfirm}
|
||||||
setShowLeave(false);
|
onCancel={onCloseLeaveModal}
|
||||||
}}
|
onClose={onCloseLeaveModal}
|
||||||
|
title={`${t['Leave Workspace']()}?`}
|
||||||
|
content={t['Leave Workspace hint']()}
|
||||||
|
confirmType="warning"
|
||||||
|
confirmText={t['Leave']()}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
-49
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
-44
@@ -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 { toast } from '@affine/component';
|
||||||
import { SettingRow } from '@affine/component/setting-components';
|
import { SettingRow } from '@affine/component/setting-components';
|
||||||
import { isDesktop } from '@affine/env/constant';
|
import { isDesktop } from '@affine/env/constant';
|
||||||
|
import type { AffineOfficialWorkspace } from '@affine/env/workspace';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { Button } from '@toeverything/components/button';
|
import { Button } from '@toeverything/components/button';
|
||||||
import type { SaveDBFileResult } from '@toeverything/infra/type';
|
import type { SaveDBFileResult } from '@toeverything/infra/type';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import type { AffineOfficialWorkspace } from '../../../shared';
|
|
||||||
|
|
||||||
async function syncBlobsToSqliteDb(workspace: AffineOfficialWorkspace) {
|
async function syncBlobsToSqliteDb(workspace: AffineOfficialWorkspace) {
|
||||||
if (window.apis && isDesktop) {
|
if (window.apis && isDesktop) {
|
||||||
const bs = workspace.blockSuiteWorkspace.blobs;
|
const bs = workspace.blockSuiteWorkspace.blobs;
|
||||||
@@ -41,7 +40,7 @@ export const ExportPanel = ({ workspace }: ExportPanelProps) => {
|
|||||||
const result: SaveDBFileResult =
|
const result: SaveDBFileResult =
|
||||||
await window.apis?.dialog.saveDBFileAs(workspaceId);
|
await window.apis?.dialog.saveDBFileAs(workspaceId);
|
||||||
if (result?.error) {
|
if (result?.error) {
|
||||||
toast(t[result.error]());
|
toast(result.error);
|
||||||
} else if (!result?.canceled) {
|
} else if (!result?.canceled) {
|
||||||
toast(t['Export success']());
|
toast(t['Export success']());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,13 +14,17 @@ import { useMemo } from 'react';
|
|||||||
import { useWorkspace } from '../../../hooks/use-workspace';
|
import { useWorkspace } from '../../../hooks/use-workspace';
|
||||||
import { DeleteLeaveWorkspace } from './delete-leave-workspace';
|
import { DeleteLeaveWorkspace } from './delete-leave-workspace';
|
||||||
import { ExportPanel } from './export';
|
import { ExportPanel } from './export';
|
||||||
|
import { MembersPanel } from './members';
|
||||||
import { ProfilePanel } from './profile';
|
import { ProfilePanel } from './profile';
|
||||||
import { PublishPanel } from './publish';
|
import { PublishPanel } from './publish';
|
||||||
import { StoragePanel } from './storage';
|
import { StoragePanel } from './storage';
|
||||||
|
|
||||||
export interface WorkspaceSettingDetailProps {
|
export interface WorkspaceSettingDetailProps {
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
onDeleteWorkspace: (id: string) => Promise<void>;
|
isOwner: boolean;
|
||||||
|
onDeleteLocalWorkspace: () => void;
|
||||||
|
onDeleteCloudWorkspace: () => void;
|
||||||
|
onLeaveWorkspace: () => void;
|
||||||
onTransferWorkspace: <
|
onTransferWorkspace: <
|
||||||
From extends WorkspaceFlavour,
|
From extends WorkspaceFlavour,
|
||||||
To extends WorkspaceFlavour,
|
To extends WorkspaceFlavour,
|
||||||
@@ -31,11 +35,8 @@ export interface WorkspaceSettingDetailProps {
|
|||||||
) => void;
|
) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WorkspaceSettingDetail = ({
|
export const WorkspaceSettingDetail = (props: WorkspaceSettingDetailProps) => {
|
||||||
workspaceId,
|
const { workspaceId } = props;
|
||||||
onDeleteWorkspace,
|
|
||||||
...props
|
|
||||||
}: WorkspaceSettingDetailProps) => {
|
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const workspace = useWorkspace(workspaceId);
|
const workspace = useWorkspace(workspaceId);
|
||||||
const [name] = useBlockSuiteWorkspaceName(workspace.blockSuiteWorkspace);
|
const [name] = useBlockSuiteWorkspaceName(workspace.blockSuiteWorkspace);
|
||||||
@@ -67,22 +68,16 @@ export const WorkspaceSettingDetail = ({
|
|||||||
desc={t['com.affine.settings.workspace.not-owner']()}
|
desc={t['com.affine.settings.workspace.not-owner']()}
|
||||||
spreadCol={false}
|
spreadCol={false}
|
||||||
>
|
>
|
||||||
<ProfilePanel workspace={workspace} />
|
<ProfilePanel workspace={workspace} {...props} />
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
</SettingWrapper>
|
</SettingWrapper>
|
||||||
<SettingWrapper title={t['AFFiNE Cloud']()}>
|
<SettingWrapper title={t['AFFiNE Cloud']()}>
|
||||||
<PublishPanel
|
<PublishPanel workspace={workspace} {...props} />
|
||||||
workspace={workspace}
|
<MembersPanel workspace={workspace} {...props} />
|
||||||
onDeleteWorkspace={onDeleteWorkspace}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</SettingWrapper>
|
</SettingWrapper>
|
||||||
{storageAndExportSetting}
|
{storageAndExportSetting}
|
||||||
<SettingWrapper>
|
<SettingWrapper>
|
||||||
<DeleteLeaveWorkspace
|
<DeleteLeaveWorkspace workspace={workspace} {...props} />
|
||||||
workspace={workspace}
|
|
||||||
onDeleteWorkspace={onDeleteWorkspace}
|
|
||||||
/>
|
|
||||||
</SettingWrapper>
|
</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 { FlexWrapper, Input, toast, Wrapper } from '@affine/component';
|
||||||
import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
|
import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
|
||||||
|
import type { AffineOfficialWorkspace } from '@affine/env/workspace';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { CameraIcon, DoneIcon } from '@blocksuite/icons';
|
import { CameraIcon, DoneIcon } from '@blocksuite/icons';
|
||||||
import { IconButton } from '@toeverything/components/button';
|
import { IconButton } from '@toeverything/components/button';
|
||||||
import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-block-suite-workspace-avatar-url';
|
import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-block-suite-workspace-avatar-url';
|
||||||
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
|
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
|
||||||
|
import clsx from 'clsx';
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
import type { AffineOfficialWorkspace } from '../../../shared';
|
|
||||||
import { Upload } from '../../pure/file-upload';
|
import { Upload } from '../../pure/file-upload';
|
||||||
|
import { type WorkspaceSettingDetailProps } from './index';
|
||||||
import * as style from './style.css';
|
import * as style from './style.css';
|
||||||
|
|
||||||
interface ProfilePanelProps {
|
export interface ProfilePanelProps extends WorkspaceSettingDetailProps {
|
||||||
workspace: AffineOfficialWorkspace;
|
workspace: AffineOfficialWorkspace;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProfilePanel = ({ workspace }: ProfilePanelProps) => {
|
export const ProfilePanel = ({ workspace, isOwner }: ProfilePanelProps) => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
|
|
||||||
const [, update] = useBlockSuiteWorkspaceAvatarUrl(
|
const [, update] = useBlockSuiteWorkspaceAvatarUrl(
|
||||||
@@ -38,7 +40,7 @@ export const ProfilePanel = ({ workspace }: ProfilePanelProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.profileWrapper}>
|
<div className={style.profileWrapper}>
|
||||||
<div className={style.avatarWrapper}>
|
<div className={clsx(style.avatarWrapper, { disable: !isOwner })}>
|
||||||
<Upload
|
<Upload
|
||||||
accept="image/gif,image/jpeg,image/jpg,image/png,image/svg"
|
accept="image/gif,image/jpeg,image/jpg,image/png,image/svg"
|
||||||
fileChange={update}
|
fileChange={update}
|
||||||
@@ -59,6 +61,7 @@ export const ProfilePanel = ({ workspace }: ProfilePanelProps) => {
|
|||||||
<div className={style.label}>{t['Workspace Name']()}</div>
|
<div className={style.label}>{t['Workspace Name']()}</div>
|
||||||
<FlexWrapper alignItems="center" flexGrow="1">
|
<FlexWrapper alignItems="center" flexGrow="1">
|
||||||
<Input
|
<Input
|
||||||
|
disabled={!isOwner}
|
||||||
width={280}
|
width={280}
|
||||||
height={32}
|
height={32}
|
||||||
defaultValue={input}
|
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 { SettingRow } from '@affine/component/setting-components';
|
||||||
import { Unreachable } from '@affine/env/constant';
|
import { Unreachable } from '@affine/env/constant';
|
||||||
import type {
|
import type {
|
||||||
AffineCloudWorkspace,
|
AffineCloudWorkspace,
|
||||||
|
AffinePublicWorkspace,
|
||||||
LocalWorkspace,
|
LocalWorkspace,
|
||||||
} from '@affine/env/workspace';
|
} from '@affine/env/workspace';
|
||||||
|
import type { AffineOfficialWorkspace } from '@affine/env/workspace';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { Button } from '@toeverything/components/button';
|
import { Button } from '@toeverything/components/button';
|
||||||
import { Tooltip } from '@toeverything/components/tooltip';
|
import { Tooltip } from '@toeverything/components/tooltip';
|
||||||
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
|
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 { toast } from '../../../utils';
|
||||||
import { EnableAffineCloudModal } from '../enable-affine-cloud-modal';
|
import { EnableAffineCloudModal } from '../enable-affine-cloud-modal';
|
||||||
import { TmpDisableAffineCloudModal } from '../tmp-disable-affine-cloud-modal';
|
import { TmpDisableAffineCloudModal } from '../tmp-disable-affine-cloud-modal';
|
||||||
@@ -29,14 +30,16 @@ export interface PublishPanelLocalProps
|
|||||||
}
|
}
|
||||||
export interface PublishPanelAffineProps
|
export interface PublishPanelAffineProps
|
||||||
extends Omit<WorkspaceSettingDetailProps, 'workspaceId'> {
|
extends Omit<WorkspaceSettingDetailProps, 'workspaceId'> {
|
||||||
workspace: AffineCloudWorkspace;
|
workspace: AffineCloudWorkspace | AffinePublicWorkspace;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PublishPanelAffine = (props: PublishPanelAffineProps) => {
|
const PublishPanelAffine = (props: PublishPanelAffineProps) => {
|
||||||
const { workspace } = props;
|
const { workspace } = props;
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
// const toggleWorkspacePublish = useToggleWorkspacePublish(workspace);
|
// const toggleWorkspacePublish = useToggleWorkspacePublish(workspace);
|
||||||
|
const isPublic = useMemo(() => {
|
||||||
|
return workspace.flavour === WorkspaceFlavour.AFFINE_PUBLIC;
|
||||||
|
}, [workspace]);
|
||||||
const [origin, setOrigin] = useState('');
|
const [origin, setOrigin] = useState('');
|
||||||
const shareUrl = origin + '/public-workspace/' + workspace.id;
|
const shareUrl = origin + '/public-workspace/' + workspace.id;
|
||||||
|
|
||||||
@@ -54,35 +57,35 @@ const PublishPanelAffine = (props: PublishPanelAffineProps) => {
|
|||||||
}, [shareUrl, t]);
|
}, [shareUrl, t]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div style={{ display: 'none' }}>
|
||||||
<SettingRow
|
<SettingRow
|
||||||
name={t['Publish']()}
|
name={t['Publish']()}
|
||||||
desc={
|
desc={isPublic ? t['Unpublished hint']() : t['Published hint']()}
|
||||||
// workspace.public ? t['Unpublished hint']() : t['Published hint']()
|
style={{
|
||||||
'UNFINISHED'
|
marginBottom: isPublic ? '12px' : '25px',
|
||||||
}
|
}}
|
||||||
>
|
>
|
||||||
{/* <Switch
|
<Switch
|
||||||
checked={workspace.public}
|
checked={isPublic}
|
||||||
onChange={checked => toggleWorkspacePublish(checked)}
|
// onChange={useCallback(value => {
|
||||||
/> */}
|
// console.log('onChange', value);
|
||||||
|
// }, [])}
|
||||||
|
/>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
<FlexWrapper justifyContent="space-between">
|
{isPublic ? (
|
||||||
<Button
|
<FlexWrapper justifyContent="space-between" marginBottom={25}>
|
||||||
className={style.urlButton}
|
<Input value={shareUrl} disabled />
|
||||||
size="large"
|
<Button
|
||||||
onClick={useCallback(() => {
|
onClick={copyUrl}
|
||||||
window.open(shareUrl, '_blank');
|
style={{
|
||||||
}, [shareUrl])}
|
marginLeft: '20px',
|
||||||
title={shareUrl}
|
}}
|
||||||
>
|
>
|
||||||
{shareUrl}
|
{t['Copy']()}
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="large" onClick={copyUrl}>
|
</FlexWrapper>
|
||||||
{t['Copy']()}
|
) : null}
|
||||||
</Button>
|
</div>
|
||||||
</FlexWrapper>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -164,7 +167,10 @@ const PublishPanelLocal = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const PublishPanel = (props: PublishPanelProps) => {
|
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} />;
|
return <PublishPanelAffine {...props} workspace={props.workspace} />;
|
||||||
} else if (props.workspace.flavour === WorkspaceFlavour.LOCAL) {
|
} else if (props.workspace.flavour === WorkspaceFlavour.LOCAL) {
|
||||||
return <PublishPanelLocal {...props} workspace={props.workspace} />;
|
return <PublishPanelLocal {...props} workspace={props.workspace} />;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { FlexWrapper, toast } from '@affine/component';
|
import { FlexWrapper, toast } from '@affine/component';
|
||||||
import { SettingRow } from '@affine/component/setting-components';
|
import { SettingRow } from '@affine/component/setting-components';
|
||||||
|
import type { AffineOfficialWorkspace } from '@affine/env/workspace';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { Button } from '@toeverything/components/button';
|
import { Button } from '@toeverything/components/button';
|
||||||
import { Tooltip } from '@toeverything/components/tooltip';
|
import { Tooltip } from '@toeverything/components/tooltip';
|
||||||
@@ -7,9 +8,6 @@ import type { MoveDBFileResult } from '@toeverything/infra/type';
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import type { AffineOfficialWorkspace } from '../../../shared';
|
|
||||||
import * as style from './style.css';
|
|
||||||
|
|
||||||
const useDBFileSecondaryPath = (workspaceId: string) => {
|
const useDBFileSecondaryPath = (workspaceId: string) => {
|
||||||
const [path, setPath] = useState<string | undefined>(undefined);
|
const [path, setPath] = useState<string | undefined>(undefined);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -83,7 +81,7 @@ export const StoragePanel = ({ workspace }: StoragePanelProps) => {
|
|||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
data-testid="move-folder"
|
data-testid="move-folder"
|
||||||
className={style.urlButton}
|
// className={style.urlButton}
|
||||||
size="large"
|
size="large"
|
||||||
onClick={handleMoveTo}
|
onClick={handleMoveTo}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -5,6 +5,12 @@ export const profileWrapper = style({
|
|||||||
alignItems: 'flex-end',
|
alignItems: 'flex-end',
|
||||||
marginTop: '12px',
|
marginTop: '12px',
|
||||||
});
|
});
|
||||||
|
export const profileHandlerWrapper = style({
|
||||||
|
flexGrow: '1',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginLeft: '20px',
|
||||||
|
});
|
||||||
|
|
||||||
export const avatarWrapper = style({
|
export const avatarWrapper = style({
|
||||||
width: '56px',
|
width: '56px',
|
||||||
@@ -24,6 +30,9 @@ export const avatarWrapper = style({
|
|||||||
globalStyle(`${avatarWrapper}:hover .camera-icon-wrapper`, {
|
globalStyle(`${avatarWrapper}:hover .camera-icon-wrapper`, {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
});
|
});
|
||||||
|
globalStyle(`${avatarWrapper}:hover .camera-icon-wrapper`, {
|
||||||
|
display: 'flex',
|
||||||
|
});
|
||||||
globalStyle(`${avatarWrapper} .camera-icon-wrapper`, {
|
globalStyle(`${avatarWrapper} .camera-icon-wrapper`, {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '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({
|
export const label = style({
|
||||||
fontSize: 'var(--affine-font-xs)',
|
fontSize: 'var(--affine-font-xs)',
|
||||||
color: 'var(--affine-text-secondary-color)',
|
color: 'var(--affine-text-secondary-color)',
|
||||||
|
|||||||
@@ -1,3 +1,185 @@
|
|||||||
export const AccountSetting = () => {
|
import { FlexWrapper, Input } from '@affine/component';
|
||||||
return <div>AccountSetting</div>;
|
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 { ContactWithUsIcon } from '@blocksuite/icons';
|
||||||
import { Suspense, useCallback } from 'react';
|
import { Suspense, useCallback } from 'react';
|
||||||
|
|
||||||
|
import { useCurrenLoginStatus } from '../../../hooks/affine/use-curren-login-status';
|
||||||
import { AccountSetting } from './account-setting';
|
import { AccountSetting } from './account-setting';
|
||||||
import {
|
import {
|
||||||
GeneralSetting,
|
GeneralSetting,
|
||||||
@@ -38,6 +39,7 @@ export const SettingModal = ({
|
|||||||
onSettingClick,
|
onSettingClick,
|
||||||
}: SettingModalProps) => {
|
}: SettingModalProps) => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
|
const loginStatus = useCurrenLoginStatus();
|
||||||
|
|
||||||
const generalSettingList = useGeneralSettingList();
|
const generalSettingList = useGeneralSettingList();
|
||||||
|
|
||||||
@@ -85,7 +87,9 @@ export const SettingModal = ({
|
|||||||
{generalSettingList.find(v => v.key === activeTab) ? (
|
{generalSettingList.find(v => v.key === activeTab) ? (
|
||||||
<GeneralSetting generalKey={activeTab as GeneralSettingKeys} />
|
<GeneralSetting generalKey={activeTab as GeneralSettingKeys} />
|
||||||
) : null}
|
) : null}
|
||||||
{activeTab === 'account' ? <AccountSetting /> : null}
|
{activeTab === 'account' && loginStatus === 'authenticated' ? (
|
||||||
|
<AccountSetting />
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="footer">
|
<div className="footer">
|
||||||
<div className={footerIconWrapper}>
|
<div className={footerIconWrapper}>
|
||||||
|
|||||||
@@ -1,41 +1,101 @@
|
|||||||
import { ScrollableContainer } from '@affine/component';
|
|
||||||
import {
|
import {
|
||||||
WorkspaceListItemSkeleton,
|
WorkspaceListItemSkeleton,
|
||||||
WorkspaceListSkeleton,
|
WorkspaceListSkeleton,
|
||||||
} from '@affine/component/setting-components';
|
} from '@affine/component/setting-components';
|
||||||
|
import { UserAvatar } from '@affine/component/user-avatar';
|
||||||
import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
|
import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
|
||||||
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
|
import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
|
||||||
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
||||||
|
import { Logo1Icon } from '@blocksuite/icons';
|
||||||
import { Tooltip } from '@toeverything/components/tooltip';
|
import { Tooltip } from '@toeverything/components/tooltip';
|
||||||
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
|
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
|
||||||
import { useStaticBlockSuiteWorkspace } from '@toeverything/infra/__internal__/react';
|
import { useStaticBlockSuiteWorkspace } from '@toeverything/infra/__internal__/react';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtom, useAtomValue } from 'jotai/react';
|
||||||
import { Suspense, useRef } from '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 { useCurrentWorkspace } from '../../../../hooks/current/use-current-workspace';
|
||||||
import type {
|
import type {
|
||||||
GeneralSettingKeys,
|
GeneralSettingKeys,
|
||||||
GeneralSettingList,
|
GeneralSettingList,
|
||||||
} from '../general-setting';
|
} from '../general-setting';
|
||||||
import {
|
import {
|
||||||
|
accountButton,
|
||||||
currentWorkspaceLabel,
|
currentWorkspaceLabel,
|
||||||
settingSlideBar,
|
settingSlideBar,
|
||||||
|
sidebarFooter,
|
||||||
sidebarItemsWrapper,
|
sidebarItemsWrapper,
|
||||||
sidebarSelectItem,
|
sidebarSelectItem,
|
||||||
sidebarSubtitle,
|
sidebarSubtitle,
|
||||||
sidebarTitle,
|
sidebarTitle,
|
||||||
} from './style.css';
|
} from './style.css';
|
||||||
|
|
||||||
interface SettingSidebarProps {
|
export type UserInfoProps = {
|
||||||
generalSettingList: GeneralSettingList;
|
|
||||||
onGeneralSettingClick: (key: GeneralSettingKeys) => void;
|
|
||||||
onWorkspaceSettingClick: (workspaceId: string) => void;
|
|
||||||
selectedWorkspaceId: string | null;
|
|
||||||
selectedGeneralKey: string | null;
|
|
||||||
onAccountSettingClick: () => void;
|
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 = ({
|
export const SettingSidebar = ({
|
||||||
generalSettingList,
|
generalSettingList,
|
||||||
@@ -43,9 +103,17 @@ export const SettingSidebar = ({
|
|||||||
onWorkspaceSettingClick,
|
onWorkspaceSettingClick,
|
||||||
selectedWorkspaceId,
|
selectedWorkspaceId,
|
||||||
selectedGeneralKey,
|
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 t = useAFFiNEI18N();
|
||||||
|
const loginStatus = useCurrenLoginStatus();
|
||||||
return (
|
return (
|
||||||
<div className={settingSlideBar} data-testid="settings-sidebar">
|
<div className={settingSlideBar} data-testid="settings-sidebar">
|
||||||
<div className={sidebarTitle}>{t['Settings']()}</div>
|
<div className={sidebarTitle}>{t['Settings']()}</div>
|
||||||
@@ -79,32 +147,43 @@ export const SettingSidebar = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className={clsx(sidebarItemsWrapper, 'scroll')}>
|
<div className={clsx(sidebarItemsWrapper, 'scroll')}>
|
||||||
<Suspense fallback={<WorkspaceListSkeleton />}>
|
<Suspense fallback={<WorkspaceListSkeleton />}>
|
||||||
<ScrollableContainer>
|
<WorkspaceList
|
||||||
<WorkspaceList
|
onWorkspaceSettingClick={onWorkspaceSettingClick}
|
||||||
onWorkspaceSettingClick={onWorkspaceSettingClick}
|
selectedWorkspaceId={selectedWorkspaceId}
|
||||||
selectedWorkspaceId={selectedWorkspaceId}
|
/>
|
||||||
/>
|
|
||||||
</ScrollableContainer>
|
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className={sidebarFooter}>
|
||||||
|
{runtimeConfig.enableCloud && loginStatus === 'unauthenticated' ? (
|
||||||
|
<SignInButton />
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{runtimeConfig.enableCloud && loginStatus === 'authenticated' ? (
|
||||||
|
<UserInfo onAccountSettingClick={onAccountSettingClick} />
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
interface WorkspaceListProps {
|
|
||||||
onWorkspaceSettingClick: (workspaceId: string) => void;
|
|
||||||
selectedWorkspaceId: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const WorkspaceList = ({
|
export const WorkspaceList = ({
|
||||||
onWorkspaceSettingClick,
|
onWorkspaceSettingClick,
|
||||||
selectedWorkspaceId,
|
selectedWorkspaceId,
|
||||||
}: WorkspaceListProps) => {
|
}: {
|
||||||
|
onWorkspaceSettingClick: (workspaceId: string) => void;
|
||||||
|
selectedWorkspaceId: string | null;
|
||||||
|
}) => {
|
||||||
const workspaces = useAtomValue(rootWorkspacesMetadataAtom);
|
const workspaces = useAtomValue(rootWorkspacesMetadataAtom);
|
||||||
const [currentWorkspace] = useCurrentWorkspace();
|
const [currentWorkspace] = useCurrentWorkspace();
|
||||||
|
const workspaceList = useMemo(() => {
|
||||||
|
return workspaces.filter(
|
||||||
|
({ flavour }) => flavour !== WorkspaceFlavour.AFFINE_PUBLIC
|
||||||
|
);
|
||||||
|
}, [workspaces]);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{workspaces.map(workspace => {
|
{workspaceList.map(workspace => {
|
||||||
return (
|
return (
|
||||||
<Suspense key={workspace.id} fallback={<WorkspaceListItemSkeleton />}>
|
<Suspense key={workspace.id} fallback={<WorkspaceListItemSkeleton />}>
|
||||||
<WorkspaceListItem
|
<WorkspaceListItem
|
||||||
@@ -122,19 +201,17 @@ export const WorkspaceList = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
interface WorkspaceListItemProps {
|
|
||||||
meta: RootWorkspaceMetadata;
|
|
||||||
onClick: () => void;
|
|
||||||
isCurrent: boolean;
|
|
||||||
isActive: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const WorkspaceListItem = ({
|
const WorkspaceListItem = ({
|
||||||
meta,
|
meta,
|
||||||
onClick,
|
onClick,
|
||||||
isCurrent,
|
isCurrent,
|
||||||
isActive,
|
isActive,
|
||||||
}: WorkspaceListItemProps) => {
|
}: {
|
||||||
|
meta: RootWorkspaceMetadata;
|
||||||
|
onClick: () => void;
|
||||||
|
isCurrent: boolean;
|
||||||
|
isActive: boolean;
|
||||||
|
}) => {
|
||||||
const workspace = useStaticBlockSuiteWorkspace(meta.id);
|
const workspace = useStaticBlockSuiteWorkspace(meta.id);
|
||||||
const [workspaceName] = useBlockSuiteWorkspaceName(workspace);
|
const [workspaceName] = useBlockSuiteWorkspaceName(workspace);
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export const sidebarItemsWrapper = style({
|
|||||||
selectors: {
|
selectors: {
|
||||||
'&.scroll': {
|
'&.scroll': {
|
||||||
flexGrow: 1,
|
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({
|
export const accountButton = style({
|
||||||
height: '42px',
|
height: '42px',
|
||||||
padding: '4px 8px',
|
padding: '4px 8px',
|
||||||
@@ -110,6 +112,20 @@ globalStyle(`${accountButton} .avatar`, {
|
|||||||
border: '1px solid',
|
border: '1px solid',
|
||||||
borderColor: 'var(--affine-white)',
|
borderColor: 'var(--affine-white)',
|
||||||
marginRight: '10px',
|
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`, {
|
globalStyle(`${accountButton} .content`, {
|
||||||
flexGrow: '1',
|
flexGrow: '1',
|
||||||
|
|||||||
@@ -3,16 +3,17 @@ import { globalStyle, style } from '@vanilla-extract/css';
|
|||||||
export const settingContent = style({
|
export const settingContent = style({
|
||||||
flexGrow: '1',
|
flexGrow: '1',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
padding: '40px 15px 20px',
|
padding: '40px 15px',
|
||||||
overflow: 'auto',
|
overflow: 'hidden',
|
||||||
});
|
});
|
||||||
|
|
||||||
globalStyle(`${settingContent} .wrapper`, {
|
globalStyle(`${settingContent} .wrapper`, {
|
||||||
width: '66%',
|
width: '60%',
|
||||||
minWidth: '450px',
|
padding: '0 15px',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
maxWidth: '560px',
|
minWidth: '560px',
|
||||||
margin: '0 auto',
|
margin: '0 auto',
|
||||||
|
overflowY: 'auto',
|
||||||
});
|
});
|
||||||
|
|
||||||
globalStyle(`${settingContent} .wrapper::-webkit-scrollbar`, {
|
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 { usePassiveWorkspaceEffect } from '@toeverything/infra/__internal__/react';
|
||||||
import { useSetAtom } from 'jotai';
|
import { useSetAtom } from 'jotai';
|
||||||
|
import { useAtomValue } from 'jotai';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import { getUIAdapter } from '../../../../adapters/workspace';
|
import { getUIAdapter } from '../../../../adapters/workspace';
|
||||||
import { openSettingModalAtom } from '../../../../atoms';
|
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 { useOnTransformWorkspace } from '../../../../hooks/root/use-on-transform-workspace';
|
||||||
import {
|
import {
|
||||||
RouteLogic,
|
RouteLogic,
|
||||||
@@ -13,28 +20,83 @@ import { useWorkspace } from '../../../../hooks/use-workspace';
|
|||||||
import { useAppHelper } from '../../../../hooks/use-workspaces';
|
import { useAppHelper } from '../../../../hooks/use-workspaces';
|
||||||
|
|
||||||
export const WorkspaceSetting = ({ workspaceId }: { workspaceId: string }) => {
|
export const WorkspaceSetting = ({ workspaceId }: { workspaceId: string }) => {
|
||||||
|
const t = useAFFiNEI18N();
|
||||||
|
|
||||||
|
const { jumpToSubPath, jumpToIndex } = useNavigateHelper();
|
||||||
|
const [currentWorkspace] = useCurrentWorkspace();
|
||||||
const workspace = useWorkspace(workspaceId);
|
const workspace = useWorkspace(workspaceId);
|
||||||
|
const workspaces = useAtomValue(rootWorkspacesMetadataAtom);
|
||||||
|
const pushNotification = useSetAtom(pushNotificationAtom);
|
||||||
|
|
||||||
|
const leaveWorkspace = useLeaveWorkspace();
|
||||||
usePassiveWorkspaceEffect(workspace.blockSuiteWorkspace);
|
usePassiveWorkspaceEffect(workspace.blockSuiteWorkspace);
|
||||||
const setSettingModal = useSetAtom(openSettingModalAtom);
|
const setSettingModal = useSetAtom(openSettingModalAtom);
|
||||||
const helper = useAppHelper();
|
const { deleteWorkspace } = useAppHelper();
|
||||||
const { jumpToIndex } = useNavigateHelper();
|
|
||||||
|
|
||||||
const { NewSettingsDetail } = getUIAdapter(workspace.flavour);
|
const { NewSettingsDetail } = getUIAdapter(workspace.flavour);
|
||||||
|
|
||||||
const onDeleteWorkspace = useCallback(
|
const closeAndJumpOut = useCallback(() => {
|
||||||
async (id: string) => {
|
setSettingModal(prev => ({ ...prev, open: false, workspaceId: null }));
|
||||||
await helper.deleteWorkspace(id);
|
|
||||||
setSettingModal(prev => ({ ...prev, open: false, workspaceId: null }));
|
if (currentWorkspace.id === workspaceId) {
|
||||||
jumpToIndex(RouteLogic.REPLACE);
|
const backWorkspace = workspaces.find(ws => ws.id !== workspaceId);
|
||||||
},
|
// TODO: if there is no workspace, jump to a new page(wait for design)
|
||||||
[helper, jumpToIndex, setSettingModal]
|
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 onTransformWorkspace = useOnTransformWorkspace();
|
||||||
|
// const handleDelete = useCallback(async () => {
|
||||||
|
// await onDeleteWorkspace();
|
||||||
|
// toast(t['Successfully deleted'](), {
|
||||||
|
// portal: document.body,
|
||||||
|
// });
|
||||||
|
// onClose();
|
||||||
|
// }, [onClose, onDeleteWorkspace, t, workspace.id]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NewSettingsDetail
|
<NewSettingsDetail
|
||||||
|
onDeleteCloudWorkspace={handleDeleteWorkspace}
|
||||||
|
onDeleteLocalWorkspace={handleDeleteWorkspace}
|
||||||
|
onLeaveWorkspace={handleLeaveWorkspace}
|
||||||
onTransformWorkspace={onTransformWorkspace}
|
onTransformWorkspace={onTransformWorkspace}
|
||||||
onDeleteWorkspace={onDeleteWorkspace}
|
|
||||||
currentWorkspaceId={workspaceId}
|
currentWorkspaceId={workspaceId}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
import {
|
import {
|
||||||
useBlockSuitePageMeta,
|
useBlockSuitePageMeta,
|
||||||
@@ -12,7 +13,6 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
import type { AffineOfficialWorkspace } from '../../../shared';
|
|
||||||
import { EditorModeSwitch } from '../block-suite-mode-switch';
|
import { EditorModeSwitch } from '../block-suite-mode-switch';
|
||||||
import { PageMenu } from './operation-menu';
|
import { PageMenu } from './operation-menu';
|
||||||
import * as styles from './styles.css';
|
import * as styles from './styles.css';
|
||||||
@@ -139,7 +139,7 @@ const BlockSuiteTitleWithRename = (props: BlockSuiteHeaderTitleProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const BlockSuiteHeaderTitle = (props: BlockSuiteHeaderTitleProps) => {
|
export const BlockSuiteHeaderTitle = (props: BlockSuiteHeaderTitleProps) => {
|
||||||
if (props.workspace.flavour === WorkspaceFlavour.PUBLIC) {
|
if (props.workspace.flavour === WorkspaceFlavour.AFFINE_PUBLIC) {
|
||||||
return <StableTitle {...props} />;
|
return <StableTitle {...props} />;
|
||||||
}
|
}
|
||||||
return <BlockSuiteTitleWithRename {...props} />;
|
return <BlockSuiteTitleWithRename {...props} />;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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 { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { CloudWorkspaceIcon } from '@blocksuite/icons';
|
import { CloudWorkspaceIcon } from '@blocksuite/icons';
|
||||||
import { Button } from '@toeverything/components/button';
|
import { signIn } from 'next-auth/react';
|
||||||
import { useSetAtom } from 'jotai';
|
import { type CSSProperties, type FC, forwardRef, useCallback } from 'react';
|
||||||
import { type CSSProperties, forwardRef } from 'react';
|
|
||||||
|
|
||||||
import { openDisableCloudAlertModalAtom } from '../../../atoms';
|
import { useCurrenLoginStatus } from '../../../hooks/affine/use-curren-login-status';
|
||||||
|
// import { openDisableCloudAlertModalAtom } from '../../../atoms';
|
||||||
import { stringToColour } from '../../../utils';
|
import { stringToColour } from '../../../utils';
|
||||||
import { StyledFooter } from './styles';
|
import { StyledFooter, StyledSignInButton } from './styles';
|
||||||
|
export const Footer: FC = () => {
|
||||||
export const Footer = () => {
|
const loginStatus = useCurrenLoginStatus();
|
||||||
const t = useAFFiNEI18N();
|
|
||||||
const setOpen = useSetAtom(openDisableCloudAlertModalAtom);
|
|
||||||
|
|
||||||
|
// const setOpen = useSetAtom(openDisableCloudAlertModalAtom);
|
||||||
return (
|
return (
|
||||||
<StyledFooter data-testid="workspace-list-modal-footer">
|
<StyledFooter data-testid="workspace-list-modal-footer">
|
||||||
<Button
|
{loginStatus === 'authenticated' ? null : <SignInButton />}
|
||||||
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>
|
|
||||||
</StyledFooter>
|
</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 {
|
interface WorkspaceAvatarProps {
|
||||||
size: number;
|
size: number;
|
||||||
name: string | undefined;
|
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')(() => {
|
export const StyleWorkspaceInfo = styled('div')(() => {
|
||||||
return {
|
return {
|
||||||
marginLeft: '15px',
|
marginLeft: '15px',
|
||||||
@@ -110,3 +125,28 @@ export const StyledModalHeader = styled('div')(() => {
|
|||||||
...displayFlex('space-between', 'center'),
|
...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 { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
|
import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
|
||||||
import {
|
import {
|
||||||
|
AccountIcon,
|
||||||
CloudWorkspaceIcon,
|
CloudWorkspaceIcon,
|
||||||
ImportIcon,
|
ImportIcon,
|
||||||
MoreHorizontalIcon,
|
MoreHorizontalIcon,
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
|
SignOutIcon,
|
||||||
} from '@blocksuite/icons';
|
} from '@blocksuite/icons';
|
||||||
import type { DragEndEvent } from '@dnd-kit/core';
|
import type { DragEndEvent } from '@dnd-kit/core';
|
||||||
import { Popover } from '@mui/material';
|
import { Popover } from '@mui/material';
|
||||||
import { IconButton } from '@toeverything/components/button';
|
import { IconButton } from '@toeverything/components/button';
|
||||||
import { Divider } from '@toeverything/components/divider';
|
import { Divider } from '@toeverything/components/divider';
|
||||||
import { useSetAtom } from 'jotai';
|
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 { useCallback } from 'react';
|
||||||
|
|
||||||
import { openDisableCloudAlertModalAtom } from '../../../atoms';
|
import {
|
||||||
|
authAtom,
|
||||||
|
openDisableCloudAlertModalAtom,
|
||||||
|
openSettingModalAtom,
|
||||||
|
} from '../../../atoms';
|
||||||
import type { AllWorkspace } from '../../../shared';
|
import type { AllWorkspace } from '../../../shared';
|
||||||
import {
|
import {
|
||||||
StyledCreateWorkspaceCardPill,
|
StyledCreateWorkspaceCardPill,
|
||||||
@@ -39,6 +47,7 @@ import {
|
|||||||
StyledSignInCardPillTextCotainer,
|
StyledSignInCardPillTextCotainer,
|
||||||
StyledSignInCardPillTextPrimary,
|
StyledSignInCardPillTextPrimary,
|
||||||
StyledSignInCardPillTextSecondary,
|
StyledSignInCardPillTextSecondary,
|
||||||
|
StyledWorkspaceFlavourTitle,
|
||||||
} from './styles';
|
} from './styles';
|
||||||
|
|
||||||
interface WorkspaceModalProps {
|
interface WorkspaceModalProps {
|
||||||
@@ -56,18 +65,31 @@ interface WorkspaceModalProps {
|
|||||||
|
|
||||||
const AccountMenu = () => {
|
const AccountMenu = () => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
|
const setOpen = useSetAtom(openSettingModalAtom);
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div>Unlimted</div>
|
{/* <div>Unlimted</div>
|
||||||
<Divider size="thinner" dividerColor="var(--affine-border-color)" />
|
<Divider size="thinner" dividerColor="var(--affine-border-color)" />
|
||||||
<MenuItem icon={<ImportIcon />} data-testid="editor-option-menu-import">
|
<MenuItem icon={<ImportIcon />} data-testid="editor-option-menu-import">
|
||||||
{t['com.affine.workspace.cloud.join']()}
|
{t['com.affine.workspace.cloud.join']()}
|
||||||
</MenuItem>
|
</MenuItem> */}
|
||||||
<MenuItem icon={<ImportIcon />} data-testid="editor-option-menu-import">
|
<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']()}
|
{t['com.affine.workspace.cloud.account.settings']()}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<Divider size="thinner" dividerColor="var(--affine-border-color)" />
|
<Divider />
|
||||||
<MenuItem icon={<ImportIcon />} data-testid="editor-option-menu-import">
|
<MenuItem
|
||||||
|
icon={<SignOutIcon />}
|
||||||
|
data-testid="editor-option-menu-import"
|
||||||
|
onClick={useCallback(() => {
|
||||||
|
signOut().catch(console.error);
|
||||||
|
}, [])}
|
||||||
|
>
|
||||||
{t['com.affine.workspace.cloud.account.logout']()}
|
{t['com.affine.workspace.cloud.account.logout']()}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</div>
|
</div>
|
||||||
@@ -89,31 +111,16 @@ const CloudWorkSpaceList = ({
|
|||||||
<StyledModalHeader>
|
<StyledModalHeader>
|
||||||
<StyledModalHeaderLeft>
|
<StyledModalHeaderLeft>
|
||||||
<StyledModalTitle>
|
<StyledModalTitle>
|
||||||
{t['com.affine.workspace.cloud.sync']()}
|
{t['com.affine.workspace.cloud']()}
|
||||||
</StyledModalTitle>
|
</StyledModalTitle>
|
||||||
</StyledModalHeaderLeft>
|
</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>
|
</StyledModalHeader>
|
||||||
<StyledModalContent>
|
<StyledModalContent>
|
||||||
<WorkspaceList
|
<WorkspaceList
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
items={
|
items={
|
||||||
workspaces.filter(
|
workspaces.filter(
|
||||||
({ flavour }) => flavour !== WorkspaceFlavour.PUBLIC
|
({ flavour }) => flavour === WorkspaceFlavour.AFFINE_CLOUD
|
||||||
) as (AffineCloudWorkspace | LocalWorkspace)[]
|
) as (AffineCloudWorkspace | LocalWorkspace)[]
|
||||||
}
|
}
|
||||||
currentWorkspaceId={currentWorkspaceId}
|
currentWorkspaceId={currentWorkspaceId}
|
||||||
@@ -129,7 +136,6 @@ const CloudWorkSpaceList = ({
|
|||||||
[onMoveWorkspace]
|
[onMoveWorkspace]
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Divider size="thinner" dividerColor="var(--affine-border-color)" />
|
|
||||||
</StyledModalContent>
|
</StyledModalContent>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -148,11 +154,18 @@ export const WorkspaceListModal = ({
|
|||||||
onMoveWorkspace,
|
onMoveWorkspace,
|
||||||
}: WorkspaceModalProps) => {
|
}: WorkspaceModalProps) => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const setOpen = useSetAtom(openDisableCloudAlertModalAtom);
|
const setOpen = useSetAtom(authAtom);
|
||||||
|
const setDisableCloudOpen = useSetAtom(openDisableCloudAlertModalAtom);
|
||||||
// TODO: AFFiNE Cloud support
|
// 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 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 (
|
return (
|
||||||
<Popover
|
<Popover
|
||||||
sx={{
|
sx={{
|
||||||
@@ -164,6 +177,7 @@ export const WorkspaceListModal = ({
|
|||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
boxShadow: 'var(--affine-shadow-2)',
|
boxShadow: 'var(--affine-shadow-2)',
|
||||||
backgroundColor: 'var(--affine-background-overlay-panel-color)',
|
backgroundColor: 'var(--affine-background-overlay-panel-color)',
|
||||||
|
padding: '16px 12px',
|
||||||
},
|
},
|
||||||
maxHeight: '90vh',
|
maxHeight: '90vh',
|
||||||
}}
|
}}
|
||||||
@@ -171,63 +185,92 @@ export const WorkspaceListModal = ({
|
|||||||
anchorEl={anchorEl}
|
anchorEl={anchorEl}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
>
|
>
|
||||||
<StyledModalHeaderContent>
|
{!isLoggedIn ? (
|
||||||
<StyledSignInCardPill>
|
<StyledModalHeaderContent>
|
||||||
<MenuItem
|
<StyledSignInCardPill>
|
||||||
style={{
|
<MenuItem
|
||||||
height: 'auto',
|
style={{
|
||||||
padding: '8px 12px',
|
height: 'auto',
|
||||||
}}
|
padding: '0px 12px',
|
||||||
onClick={async () => {
|
}}
|
||||||
if (!runtimeConfig.enableCloud) {
|
onClick={async () => {
|
||||||
setOpen(true);
|
if (!runtimeConfig.enableCloud) {
|
||||||
}
|
setDisableCloudOpen(true);
|
||||||
}}
|
} else {
|
||||||
data-testid="cloud-signin-button"
|
setOpen(state => ({
|
||||||
>
|
...state,
|
||||||
<StyledCreateWorkspaceCardPillContent>
|
openModal: true,
|
||||||
<StyledCreateWorkspaceCardPillIcon>
|
}));
|
||||||
<CloudWorkspaceIcon />
|
}
|
||||||
</StyledCreateWorkspaceCardPillIcon>
|
}}
|
||||||
<StyledSignInCardPillTextCotainer>
|
data-testid="cloud-signin-button"
|
||||||
<StyledSignInCardPillTextPrimary>
|
>
|
||||||
{t['com.affine.workspace.cloud.auth']()}
|
<StyledCreateWorkspaceCardPillContent>
|
||||||
</StyledSignInCardPillTextPrimary>
|
<StyledCreateWorkspaceCardPillIcon>
|
||||||
<StyledSignInCardPillTextSecondary>
|
<CloudWorkspaceIcon />
|
||||||
Sync with AFFiNE Cloud
|
</StyledCreateWorkspaceCardPillIcon>
|
||||||
</StyledSignInCardPillTextSecondary>
|
<StyledSignInCardPillTextCotainer>
|
||||||
</StyledSignInCardPillTextCotainer>
|
<StyledSignInCardPillTextPrimary>
|
||||||
</StyledCreateWorkspaceCardPillContent>
|
{t['com.affine.workspace.cloud.auth']()}
|
||||||
</MenuItem>
|
</StyledSignInCardPillTextPrimary>
|
||||||
</StyledSignInCardPill>
|
<StyledSignInCardPillTextSecondary>
|
||||||
<Divider size="thinner" dividerColor="var(--affine-border-color)" />
|
{t['com.affine.workspace.cloud.description']()}
|
||||||
</StyledModalHeaderContent>
|
</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>
|
<StyledModalBody>
|
||||||
{isLoggedIn ? (
|
{isLoggedIn && cloudWorkspaces.length !== 0 ? (
|
||||||
<CloudWorkSpaceList
|
<>
|
||||||
disabled={disabled}
|
<CloudWorkSpaceList
|
||||||
open={open}
|
disabled={disabled}
|
||||||
onClose={onClose}
|
open={open}
|
||||||
workspaces={workspaces}
|
onClose={onClose}
|
||||||
onClickWorkspace={onClickWorkspace}
|
workspaces={workspaces}
|
||||||
onClickWorkspaceSetting={onClickWorkspaceSetting}
|
onClickWorkspace={onClickWorkspace}
|
||||||
onNewWorkspace={onNewWorkspace}
|
onClickWorkspaceSetting={onClickWorkspaceSetting}
|
||||||
onAddWorkspace={onAddWorkspace}
|
onNewWorkspace={onNewWorkspace}
|
||||||
currentWorkspaceId={currentWorkspaceId}
|
onAddWorkspace={onAddWorkspace}
|
||||||
onMoveWorkspace={onMoveWorkspace}
|
currentWorkspaceId={currentWorkspaceId}
|
||||||
/>
|
onMoveWorkspace={onMoveWorkspace}
|
||||||
|
/>
|
||||||
|
<Divider style={{ margin: '12px 0px', minHeight: '1px' }} />
|
||||||
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
<StyledModalHeader>
|
<StyledModalHeader>
|
||||||
<StyledModalTitle>{t['Local Workspace']()}</StyledModalTitle>
|
<StyledWorkspaceFlavourTitle>
|
||||||
|
{t['com.affine.workspace.local']()}
|
||||||
|
</StyledWorkspaceFlavourTitle>
|
||||||
</StyledModalHeader>
|
</StyledModalHeader>
|
||||||
<StyledModalContent>
|
<StyledModalContent>
|
||||||
<WorkspaceList
|
<WorkspaceList
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
items={
|
items={localWorkspaces}
|
||||||
workspaces.filter(
|
|
||||||
({ flavour }) => flavour !== WorkspaceFlavour.PUBLIC
|
|
||||||
) as (AffineCloudWorkspace | LocalWorkspace)[]
|
|
||||||
}
|
|
||||||
currentWorkspaceId={currentWorkspaceId}
|
currentWorkspaceId={currentWorkspaceId}
|
||||||
onClick={onClickWorkspace}
|
onClick={onClickWorkspace}
|
||||||
onSettingClick={onClickWorkspaceSetting}
|
onSettingClick={onClickWorkspaceSetting}
|
||||||
|
|||||||
@@ -70,7 +70,6 @@ export const StyledCreateWorkspaceCard = styled('div')(() => {
|
|||||||
});
|
});
|
||||||
export const StyledCreateWorkspaceCardPillContainer = styled('div')(() => {
|
export const StyledCreateWorkspaceCardPillContainer = styled('div')(() => {
|
||||||
return {
|
return {
|
||||||
padding: '12px',
|
|
||||||
borderRadius: '10px',
|
borderRadius: '10px',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
margin: '-8px -4px',
|
margin: '-8px -4px',
|
||||||
@@ -173,6 +172,7 @@ export const StyledModalContent = styled('div')({
|
|||||||
flexWrap: 'wrap',
|
flexWrap: 'wrap',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
|
gap: '4px',
|
||||||
});
|
});
|
||||||
|
|
||||||
export const StyledModalFooterContent = styled('div')({
|
export const StyledModalFooterContent = styled('div')({
|
||||||
@@ -180,7 +180,7 @@ export const StyledModalFooterContent = styled('div')({
|
|||||||
flexWrap: 'wrap',
|
flexWrap: 'wrap',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
padding: '12px',
|
marginTop: '12px',
|
||||||
backgroundColor: 'var(--affine-background-overlay-panel-color)',
|
backgroundColor: 'var(--affine-background-overlay-panel-color)',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -189,7 +189,6 @@ export const StyledModalHeaderContent = styled('div')({
|
|||||||
flexWrap: 'wrap',
|
flexWrap: 'wrap',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
padding: '12px 12px 0px 12px',
|
|
||||||
backgroundColor: 'var(--affine-background-overlay-panel-color)',
|
backgroundColor: 'var(--affine-background-overlay-panel-color)',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -219,19 +218,27 @@ export const StyledModalHeader = styled('div')(() => {
|
|||||||
left: 0,
|
left: 0,
|
||||||
top: 0,
|
top: 0,
|
||||||
borderRadius: '24px 24px 0 0',
|
borderRadius: '24px 24px 0 0',
|
||||||
padding: '12px 14px',
|
padding: '0px 14px',
|
||||||
...displayFlex('space-between', 'center'),
|
...displayFlex('space-between', 'center'),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
export const StyledModalBody = styled('div')(() => {
|
export const StyledModalBody = styled('div')(() => {
|
||||||
return {
|
return {
|
||||||
padding: '0px 12px',
|
|
||||||
display: 'inline-flex',
|
display: 'inline-flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
alignItems: 'flex-start',
|
alignItems: 'flex-start',
|
||||||
gap: '12px',
|
gap: '4px',
|
||||||
flex: 1,
|
flex: 1,
|
||||||
overflowY: 'auto',
|
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';
|
} from '@affine/component/page-list';
|
||||||
import type { Collection } from '@affine/env/filter';
|
import type { Collection } from '@affine/env/filter';
|
||||||
import type { PropertiesMeta } from '@affine/env/filter';
|
import type { PropertiesMeta } from '@affine/env/filter';
|
||||||
import type {
|
import {
|
||||||
WorkspaceFlavour,
|
WorkspaceFlavour,
|
||||||
WorkspaceHeaderProps,
|
type WorkspaceHeaderProps,
|
||||||
} from '@affine/env/workspace';
|
} from '@affine/env/workspace';
|
||||||
import { WorkspaceSubPath } from '@affine/env/workspace';
|
import { WorkspaceSubPath } from '@affine/env/workspace';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import { useGetPageInfoById } from '../hooks/use-get-page-info';
|
import { useGetPageInfoById } from '../hooks/use-get-page-info';
|
||||||
import { useWorkspace } from '../hooks/use-workspace';
|
import { useWorkspace } from '../hooks/use-workspace';
|
||||||
|
import { SharePageModal } from './affine/share-page-modal';
|
||||||
import { BlockSuiteHeaderTitle } from './blocksuite/block-suite-header-title';
|
import { BlockSuiteHeaderTitle } from './blocksuite/block-suite-header-title';
|
||||||
import { filterContainerStyle } from './filter-container.css';
|
import { filterContainerStyle } from './filter-container.css';
|
||||||
import { Header } from './pure/header';
|
import { Header } from './pure/header';
|
||||||
@@ -77,7 +78,6 @@ export function WorkspaceHeader({
|
|||||||
const setting = useCollectionManager(currentWorkspaceId);
|
const setting = useCollectionManager(currentWorkspaceId);
|
||||||
|
|
||||||
const currentWorkspace = useWorkspace(currentWorkspaceId);
|
const currentWorkspace = useWorkspace(currentWorkspaceId);
|
||||||
|
|
||||||
const getPageInfoById = useGetPageInfoById(
|
const getPageInfoById = useGetPageInfoById(
|
||||||
currentWorkspace.blockSuiteWorkspace
|
currentWorkspace.blockSuiteWorkspace
|
||||||
);
|
);
|
||||||
@@ -117,6 +117,15 @@ export function WorkspaceHeader({
|
|||||||
|
|
||||||
// route in edit page
|
// route in edit page
|
||||||
if ('pageId' in currentEntry) {
|
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 (
|
return (
|
||||||
<Header
|
<Header
|
||||||
center={
|
center={
|
||||||
@@ -125,7 +134,12 @@ export function WorkspaceHeader({
|
|||||||
pageId={currentEntry.pageId}
|
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
Reference in New Issue
Block a user