mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-11 20:08:37 +00: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:
@@ -3,11 +3,15 @@ function testPackageName(regexp: RegExp): (module: any) => boolean {
|
||||
module.nameForCondition && regexp.test(module.nameForCondition());
|
||||
}
|
||||
|
||||
// https://hackernoon.com/the-100-correct-way-to-split-your-chunks-with-webpack-f8a9df5b7758
|
||||
export const productionCacheGroups = {
|
||||
asyncVendor: {
|
||||
test: /[\\/]node_modules[\\/]/,
|
||||
name(module: any) {
|
||||
// https://hackernoon.com/the-100-correct-way-to-split-your-chunks-with-webpack-f8a9df5b7758
|
||||
// monorepo linked in node_modules, so it's not a npm package
|
||||
if (!module.context.includes('node_modules')) {
|
||||
return `app-async`;
|
||||
}
|
||||
const name = module.context.match(
|
||||
/[\\/]node_modules[\\/](.*?)([\\/]|$)/
|
||||
)?.[1];
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { join, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { createRequire } from 'node:module';
|
||||
|
||||
import type { Configuration as DevServerConfiguration } from 'webpack-dev-server';
|
||||
import { PerfseePlugin } from '@perfsee/webpack';
|
||||
import { sentryWebpackPlugin } from '@sentry/webpack-plugin';
|
||||
@@ -10,13 +11,14 @@ import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin';
|
||||
import TerserPlugin from 'terser-webpack-plugin';
|
||||
import webpack from 'webpack';
|
||||
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
|
||||
import { compact } from 'lodash-es';
|
||||
|
||||
import { productionCacheGroups } from './cache-group.js';
|
||||
import type { BuildFlags } from '@affine/cli/config';
|
||||
import { projectRoot } from '@affine/cli/config';
|
||||
import { VanillaExtractPlugin } from '@vanilla-extract/webpack-plugin';
|
||||
import { computeCacheKey } from './utils.js';
|
||||
import type { RuntimeConfig } from '@affine/env/global';
|
||||
import { WebpackS3Plugin, gitShortHash } from './s3-plugin.js';
|
||||
|
||||
const IN_CI = !!process.env.CI;
|
||||
|
||||
@@ -67,16 +69,26 @@ const OptimizeOptionOptions: (
|
||||
},
|
||||
});
|
||||
|
||||
export const publicPath = (function () {
|
||||
const { BUILD_TYPE } = process.env;
|
||||
const publicPath = process.env.PUBLIC_PATH ?? '/';
|
||||
if (process.env.COVERAGE) {
|
||||
return publicPath;
|
||||
}
|
||||
if (BUILD_TYPE === 'canary') {
|
||||
return `https://dev.affineassets.com/${gitShortHash()}/`;
|
||||
} else if (BUILD_TYPE === 'beta' || BUILD_TYPE === 'stable') {
|
||||
return `https://prod.affineassets.com/${gitShortHash()}/`;
|
||||
}
|
||||
return publicPath;
|
||||
})();
|
||||
|
||||
export const createConfiguration: (
|
||||
buildFlags: BuildFlags,
|
||||
runtimeConfig: RuntimeConfig
|
||||
) => webpack.Configuration = (buildFlags, runtimeConfig) => {
|
||||
let publicPath = process.env.PUBLIC_PATH ?? '/';
|
||||
|
||||
const blocksuiteBaseDir = buildFlags.localBlockSuite;
|
||||
|
||||
const cacheKey = computeCacheKey(buildFlags);
|
||||
|
||||
const config = {
|
||||
name: 'affine',
|
||||
// to set a correct base path for the source map
|
||||
@@ -96,8 +108,11 @@ export const createConfiguration: (
|
||||
? 'js/[name]-[contenthash:8].js'
|
||||
: 'js/[name].js',
|
||||
// In some cases webpack will emit files starts with "_" which is reserved in web extension.
|
||||
chunkFilename: 'js/chunk.[name].js',
|
||||
assetModuleFilename: 'assets/[contenthash:8][ext][query]',
|
||||
chunkFilename:
|
||||
buildFlags.mode === 'production'
|
||||
? 'js/chunk.[name]-[contenthash:8].js'
|
||||
: 'js/chunk.[name].js',
|
||||
assetModuleFilename: 'assets/[name]-[contenthash:8][ext][query]',
|
||||
devtoolModuleFilenameTemplate: 'webpack://[namespace]/[resource-path]',
|
||||
hotUpdateChunkFilename: 'hot/[id].[fullhash].js',
|
||||
hotUpdateMainFilename: 'hot/[runtime].[fullhash].json',
|
||||
@@ -192,14 +207,6 @@ export const createConfiguration: (
|
||||
},
|
||||
},
|
||||
|
||||
cache: {
|
||||
type: 'filesystem',
|
||||
buildDependencies: {
|
||||
config: [fileURLToPath(import.meta.url)],
|
||||
},
|
||||
version: cacheKey,
|
||||
},
|
||||
|
||||
module: {
|
||||
parser: {
|
||||
javascript: {
|
||||
@@ -308,7 +315,7 @@ export const createConfiguration: (
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
url: false,
|
||||
url: true,
|
||||
sourceMap: false,
|
||||
modules: false,
|
||||
import: true,
|
||||
@@ -333,22 +340,23 @@ export const createConfiguration: (
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
plugins: [
|
||||
...(IN_CI ? [] : [new webpack.ProgressPlugin({ percentBy: 'entries' })]),
|
||||
...(buildFlags.mode === 'development'
|
||||
? [new ReactRefreshWebpackPlugin({ overlay: false, esModule: true })]
|
||||
: [
|
||||
new MiniCssExtractPlugin({
|
||||
filename: `[name].[contenthash:8].css`,
|
||||
ignoreOrder: true,
|
||||
}),
|
||||
]),
|
||||
plugins: compact([
|
||||
IN_CI ? null : new webpack.ProgressPlugin({ percentBy: 'entries' }),
|
||||
buildFlags.mode === 'development'
|
||||
? new ReactRefreshWebpackPlugin({ overlay: false, esModule: true })
|
||||
: new MiniCssExtractPlugin({
|
||||
filename: `[name].[contenthash:8].css`,
|
||||
ignoreOrder: true,
|
||||
}),
|
||||
new VanillaExtractPlugin(),
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': JSON.stringify({}),
|
||||
'process.env.COVERAGE': JSON.stringify(!!buildFlags.coverage),
|
||||
'process.env.NODE_ENV': JSON.stringify(buildFlags.mode),
|
||||
'process.env.SHOULD_REPORT_TRACE': `${Boolean(
|
||||
process.env.SHOULD_REPORT_TRACE
|
||||
)}`,
|
||||
'process.env.TRACE_REPORT_ENDPOINT': `"${process.env.TRACE_REPORT_ENDPOINT}"`,
|
||||
runtimeConfig: JSON.stringify(runtimeConfig),
|
||||
}),
|
||||
new CopyPlugin({
|
||||
@@ -359,7 +367,10 @@ export const createConfiguration: (
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
buildFlags.mode === 'production' && process.env.R2_SECRET_ACCESS_KEY
|
||||
? new WebpackS3Plugin()
|
||||
: null,
|
||||
]),
|
||||
|
||||
optimization: OptimizeOptionOptions(buildFlags),
|
||||
|
||||
@@ -373,6 +384,14 @@ export const createConfiguration: (
|
||||
publicPath: '/',
|
||||
watch: true,
|
||||
},
|
||||
proxy: {
|
||||
'/api': 'http://localhost:3010',
|
||||
'/socket.io': {
|
||||
target: 'http://localhost:3010',
|
||||
ws: true,
|
||||
},
|
||||
'/graphql': 'http://localhost:3010',
|
||||
},
|
||||
} as DevServerConfiguration,
|
||||
} satisfies webpack.Configuration;
|
||||
|
||||
|
||||
@@ -24,9 +24,9 @@ const editorFlags: BlockSuiteFeatureFlags = {
|
||||
};
|
||||
|
||||
export function getRuntimeConfig(buildFlags: BuildFlags): RuntimeConfig {
|
||||
const buildPreset: Record<string, RuntimeConfig> = {
|
||||
const buildPreset: Record<BuildFlags['channel'], RuntimeConfig> = {
|
||||
stable: {
|
||||
enablePlugin: false,
|
||||
enablePlugin: true,
|
||||
enableTestProperties: false,
|
||||
enableBroadcastChannelProvider: true,
|
||||
enableDebugPage: true,
|
||||
@@ -37,13 +37,26 @@ export function getRuntimeConfig(buildFlags: BuildFlags): RuntimeConfig {
|
||||
enableNewSettingUnstableApi: false,
|
||||
enableSQLiteProvider: true,
|
||||
enableMoveDatabase: false,
|
||||
enableNotificationCenter: false,
|
||||
enableCloud: false,
|
||||
serverAPI: 'https://localhost:3010',
|
||||
enableNotificationCenter: true,
|
||||
enableCloud: true,
|
||||
enableEnhanceShareMode: false,
|
||||
serverUrlPrefix: 'https://app.affine.pro',
|
||||
editorFlags,
|
||||
appVersion: packageJson.version,
|
||||
editorVersion: packageJson.dependencies['@blocksuite/editor'],
|
||||
},
|
||||
get beta() {
|
||||
return {
|
||||
...this.stable,
|
||||
serverUrlPrefix: 'https://ambassador.affine.pro',
|
||||
};
|
||||
},
|
||||
get internal() {
|
||||
return {
|
||||
...this.stable,
|
||||
serverUrlPrefix: 'https://affine.fail',
|
||||
};
|
||||
},
|
||||
// canary will be aggressive and enable all features
|
||||
canary: {
|
||||
enablePlugin: true,
|
||||
@@ -58,18 +71,15 @@ export function getRuntimeConfig(buildFlags: BuildFlags): RuntimeConfig {
|
||||
enableSQLiteProvider: true,
|
||||
enableMoveDatabase: false,
|
||||
enableNotificationCenter: true,
|
||||
enableCloud: false,
|
||||
serverAPI: 'https://localhost:3010',
|
||||
enableCloud: true,
|
||||
enableEnhanceShareMode: false,
|
||||
serverUrlPrefix: 'https://affine.fail',
|
||||
editorFlags,
|
||||
appVersion: packageJson.version,
|
||||
editorVersion: packageJson.dependencies['@blocksuite/editor'],
|
||||
},
|
||||
};
|
||||
|
||||
// beta and internal versions are the same as stable
|
||||
buildPreset.beta = buildPreset.stable;
|
||||
buildPreset.internal = buildPreset.stable;
|
||||
|
||||
const currentBuild = buildFlags.channel;
|
||||
|
||||
if (!(currentBuild in buildPreset)) {
|
||||
@@ -107,11 +117,18 @@ export function getRuntimeConfig(buildFlags: BuildFlags): RuntimeConfig {
|
||||
enableCloud: process.env.ENABLE_CLOUD
|
||||
? process.env.ENABLE_CLOUD === 'true'
|
||||
: currentBuildPreset.enableCloud,
|
||||
enableEnhanceShareMode: process.env.ENABLE_ENHANCE_SHARE_MODE
|
||||
? process.env.ENABLE_ENHANCE_SHARE_MODE === 'true'
|
||||
: currentBuildPreset.enableEnhanceShareMode,
|
||||
enableMoveDatabase: process.env.ENABLE_MOVE_DATABASE
|
||||
? process.env.ENABLE_MOVE_DATABASE === 'true'
|
||||
: currentBuildPreset.enableMoveDatabase,
|
||||
};
|
||||
|
||||
if (buildFlags.mode === 'development') {
|
||||
currentBuildPreset.serverUrlPrefix = 'http://localhost:8080';
|
||||
}
|
||||
|
||||
return {
|
||||
...currentBuildPreset,
|
||||
// environment preset will overwrite current build preset
|
||||
|
||||
58
apps/core/.webpack/s3-plugin.ts
Normal file
58
apps/core/.webpack/s3-plugin.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { join } from 'node:path';
|
||||
import { execSync } from 'node:child_process';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
|
||||
import type { PutObjectCommandInput } from '@aws-sdk/client-s3';
|
||||
import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
|
||||
import { once } from 'lodash-es';
|
||||
import { lookup } from 'mime-types';
|
||||
import type { Compiler, WebpackPluginInstance } from 'webpack';
|
||||
|
||||
export const gitShortHash = once(() => {
|
||||
const { GITHUB_SHA } = process.env;
|
||||
if (GITHUB_SHA) {
|
||||
return GITHUB_SHA.substring(0, 9);
|
||||
}
|
||||
const sha = execSync(`git rev-parse --short HEAD`, {
|
||||
encoding: 'utf-8',
|
||||
}).trim();
|
||||
return sha;
|
||||
});
|
||||
|
||||
export const R2_BUCKET =
|
||||
process.env.R2_BUCKET! ??
|
||||
(process.env.BUILD_TYPE === 'canary' ? 'assets-dev' : 'assets-prod');
|
||||
|
||||
export class WebpackS3Plugin implements WebpackPluginInstance {
|
||||
private readonly s3 = new S3Client({
|
||||
region: 'auto',
|
||||
endpoint: `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
|
||||
credentials: {
|
||||
accessKeyId: process.env.R2_ACCESS_KEY_ID!,
|
||||
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
|
||||
},
|
||||
});
|
||||
|
||||
apply(compiler: Compiler) {
|
||||
compiler.hooks.assetEmitted.tapPromise(
|
||||
'WebpackS3Plugin',
|
||||
async (asset, { outputPath }) => {
|
||||
if (asset === 'index.html') {
|
||||
return;
|
||||
}
|
||||
const assetPath = join(outputPath, asset);
|
||||
const assetSource = await readFile(assetPath);
|
||||
const putObjectCommandOptions: PutObjectCommandInput = {
|
||||
Body: assetSource,
|
||||
Bucket: R2_BUCKET,
|
||||
Key: join(gitShortHash(), asset),
|
||||
};
|
||||
const contentType = lookup(asset);
|
||||
if (contentType) {
|
||||
putObjectCommandOptions.ContentType = contentType;
|
||||
}
|
||||
await this.s3.send(new PutObjectCommand(putObjectCommandOptions));
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -17,14 +17,14 @@
|
||||
<meta name="twitter:url" content="https://app.affine.pro/" />
|
||||
<meta
|
||||
name="twitter:title"
|
||||
content="AFFiNE:There can be more than Notion and Miro."
|
||||
content="AFFiNE: There can be more than Notion and Miro."
|
||||
/>
|
||||
<meta name="twitter:description" content="{description}" />
|
||||
<meta name="twitter:site" content="@AffineOfficial" />
|
||||
<meta name="twitter:image" content="https://affine.pro/og.jpeg" />
|
||||
<meta
|
||||
property="og:title"
|
||||
content="AFFiNE:There can be more than Notion and Miro."
|
||||
content="AFFiNE: There can be more than Notion and Miro."
|
||||
/>
|
||||
<meta property="og:type" content="website" />
|
||||
<meta
|
||||
@@ -39,7 +39,8 @@
|
||||
href="https://affine.pro/favicon.ico"
|
||||
/>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<div id="app" data-version="<%= GIT_SHORT_SHA %>"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { createConfiguration, rootPath } from './config.js';
|
||||
import { createConfiguration, rootPath, publicPath } from './config.js';
|
||||
import { merge } from 'webpack-merge';
|
||||
import { join, resolve } from 'node:path';
|
||||
import type { BuildFlags } from '@affine/cli/config';
|
||||
import { getRuntimeConfig } from './runtime-config.js';
|
||||
import HTMLPlugin from 'html-webpack-plugin';
|
||||
|
||||
import { gitShortHash } from './s3-plugin.js';
|
||||
|
||||
export default async function (cli_env: any, _: any) {
|
||||
const flags: BuildFlags = JSON.parse(
|
||||
Buffer.from(cli_env.flags, 'hex').toString('utf-8')
|
||||
@@ -44,12 +46,16 @@ export default async function (cli_env: any, _: any) {
|
||||
minify: false,
|
||||
chunks: ['app', 'plugin', 'polyfill/intl-segmenter', 'polyfill/ses'],
|
||||
filename: 'index.html',
|
||||
templateParameters: {
|
||||
GIT_SHORT_SHA: gitShortHash(),
|
||||
},
|
||||
}),
|
||||
new HTMLPlugin({
|
||||
template: join(rootPath, '.webpack', 'template.html'),
|
||||
inject: 'body',
|
||||
scriptLoading: 'module',
|
||||
minify: false,
|
||||
publicPath,
|
||||
chunks: [
|
||||
'_plugin/index.test',
|
||||
'plugin',
|
||||
@@ -57,6 +63,9 @@ export default async function (cli_env: any, _: any) {
|
||||
'polyfill/ses',
|
||||
],
|
||||
filename: '_plugin/index.html',
|
||||
templateParameters: {
|
||||
GIT_SHORT_SHA: gitShortHash(),
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"@emotion/server": "^11.11.0",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@mui/material": "^5.14.6",
|
||||
"@radix-ui/react-select": "^1.2.2",
|
||||
"@react-hookz/web": "^23.1.0",
|
||||
"@toeverything/components": "^0.0.19",
|
||||
"async-call-rpc": "^6.3.1",
|
||||
@@ -52,6 +53,7 @@
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lottie-web": "^5.12.2",
|
||||
"mini-css-extract-plugin": "^2.7.6",
|
||||
"next-auth": "^4.22.1",
|
||||
"next-themes": "^0.2.1",
|
||||
"postcss-loader": "^7.3.3",
|
||||
"react": "18.2.0",
|
||||
@@ -62,22 +64,27 @@
|
||||
"rxjs": "^7.8.1",
|
||||
"ses": "^0.18.7",
|
||||
"swr": "2.2.1",
|
||||
"valtio": "^1.10.6",
|
||||
"y-protocols": "^1.0.5",
|
||||
"yjs": "^13.6.7",
|
||||
"zod": "^3.22.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aws-sdk/client-s3": "3.400.0",
|
||||
"@perfsee/webpack": "^1.8.4",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.11",
|
||||
"@sentry/webpack-plugin": "^2.7.0",
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@swc/core": "^1.3.80",
|
||||
"@types/lodash-es": "^4.17.8",
|
||||
"@types/lodash.debounce": "^4.0.7",
|
||||
"@types/webpack-env": "^1.18.1",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"css-loader": "^6.8.1",
|
||||
"express": "^4.18.2",
|
||||
"html-webpack-plugin": "^5.5.3",
|
||||
"lodash-es": "^4.17.21",
|
||||
"mime-types": "^2.1.35",
|
||||
"raw-loader": "^4.0.2",
|
||||
"source-map-loader": "^4.0.1",
|
||||
"style-loader": "^3.3.3",
|
||||
|
||||
@@ -10,20 +10,18 @@
|
||||
"target": "build",
|
||||
"params": "ignore"
|
||||
},
|
||||
{
|
||||
"projects": ["tag:infra"],
|
||||
"target": "build",
|
||||
"params": "ignore"
|
||||
},
|
||||
"^build"
|
||||
],
|
||||
"inputs": [
|
||||
"{projectRoot}/.webpack/**/*",
|
||||
"{projectRoot}/**/*",
|
||||
"{projectRoot}/public/**/*",
|
||||
"{workspaceRoot}/packages/env/src/**/*",
|
||||
"{workspaceRoot}/packages/component/src/**/*",
|
||||
"{workspaceRoot}/packages/debug/src/**/*",
|
||||
"{workspaceRoot}/packages/graphql/src/**/*",
|
||||
"{workspaceRoot}/packages/hooks/src/**/*",
|
||||
"{workspaceRoot}/packages/jotai/src/**/*",
|
||||
"{workspaceRoot}/packages/templates/src/**/*",
|
||||
"{workspaceRoot}/packages/workspace/src/**/*",
|
||||
"{workspaceRoot}/packages/**/*",
|
||||
{
|
||||
"env": "BUILD_TYPE"
|
||||
},
|
||||
|
||||
Binary file not shown.
@@ -1,93 +0,0 @@
|
||||
Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,93 +0,0 @@
|
||||
Copyright (c) 2014, Indian Type Foundry (info@indiantypefoundry.com).
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -1,93 +0,0 @@
|
||||
Copyright 2010, 2012 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries.
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,91 +0,0 @@
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,93 +0,0 @@
|
||||
Copyright 2016 Google Inc. All Rights Reserved.
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
164
apps/core/src/adapters/cloud/crud.ts
Normal file
164
apps/core/src/adapters/cloud/crud.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import type {
|
||||
AffineCloudWorkspace,
|
||||
WorkspaceCRUD,
|
||||
} from '@affine/env/workspace';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import {
|
||||
createWorkspaceMutation,
|
||||
deleteWorkspaceMutation,
|
||||
getWorkspaceQuery,
|
||||
getWorkspacesQuery,
|
||||
} from '@affine/graphql';
|
||||
import { fetcher } from '@affine/workspace/affine/gql';
|
||||
import { getOrCreateWorkspace } from '@affine/workspace/manager';
|
||||
import { createIndexeddbStorage, Workspace } from '@blocksuite/store';
|
||||
import { migrateLocalBlobStorage } from '@toeverything/infra/blocksuite';
|
||||
import {
|
||||
createIndexedDBProvider,
|
||||
DEFAULT_DB_NAME,
|
||||
} from '@toeverything/y-indexeddb';
|
||||
import { getSession } from 'next-auth/react';
|
||||
import { proxy } from 'valtio/vanilla';
|
||||
|
||||
const Y = Workspace.Y;
|
||||
|
||||
async function deleteLocalBlobStorage(id: string) {
|
||||
const storage = createIndexeddbStorage(id);
|
||||
const keys = await storage.crud.list();
|
||||
for (const key of keys) {
|
||||
await storage.crud.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
// we don't need to persistence the state into local storage
|
||||
// because if a user clicks create multiple time and nothing happened
|
||||
// because of the server delay or something, he/she will wait.
|
||||
// and also the user journey of creating workspace is long.
|
||||
const createdWorkspaces = proxy<string[]>([]);
|
||||
|
||||
export const CRUD: WorkspaceCRUD<WorkspaceFlavour.AFFINE_CLOUD> = {
|
||||
create: async blockSuiteWorkspace => {
|
||||
if (createdWorkspaces.some(id => id === blockSuiteWorkspace.id)) {
|
||||
throw new Error('workspace already created');
|
||||
}
|
||||
const { createWorkspace } = await fetcher({
|
||||
query: createWorkspaceMutation,
|
||||
variables: {
|
||||
init: new File(
|
||||
[Y.encodeStateAsUpdate(blockSuiteWorkspace.doc)],
|
||||
'initBinary.yDoc'
|
||||
),
|
||||
},
|
||||
});
|
||||
createdWorkspaces.push(blockSuiteWorkspace.id);
|
||||
const newBLockSuiteWorkspace = getOrCreateWorkspace(
|
||||
createWorkspace.id,
|
||||
WorkspaceFlavour.AFFINE_CLOUD
|
||||
);
|
||||
|
||||
Y.applyUpdate(
|
||||
newBLockSuiteWorkspace.doc,
|
||||
Y.encodeStateAsUpdate(blockSuiteWorkspace.doc)
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
[...blockSuiteWorkspace.doc.subdocs].map(async subdoc => {
|
||||
subdoc.load();
|
||||
return subdoc.whenLoaded.then(() => {
|
||||
newBLockSuiteWorkspace.doc.subdocs.forEach(newSubdoc => {
|
||||
if (newSubdoc.guid === subdoc.guid) {
|
||||
Y.applyUpdate(newSubdoc, Y.encodeStateAsUpdate(subdoc));
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
const provider = createIndexedDBProvider(
|
||||
newBLockSuiteWorkspace.doc,
|
||||
DEFAULT_DB_NAME
|
||||
);
|
||||
provider.connect();
|
||||
migrateLocalBlobStorage(blockSuiteWorkspace.id, createWorkspace.id)
|
||||
.then(() => deleteLocalBlobStorage(blockSuiteWorkspace.id))
|
||||
.catch(e => {
|
||||
console.error('error when moving blob storage:', e);
|
||||
});
|
||||
// todo(himself65): delete old workspace in the future
|
||||
return createWorkspace.id;
|
||||
},
|
||||
delete: async workspace => {
|
||||
await fetcher({
|
||||
query: deleteWorkspaceMutation,
|
||||
variables: {
|
||||
id: workspace.id,
|
||||
},
|
||||
});
|
||||
},
|
||||
get: async id => {
|
||||
if (!environment.isServer && !navigator.onLine) {
|
||||
// no network
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
!(await getSession()
|
||||
.then(() => true)
|
||||
.catch(() => false))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
await fetcher({
|
||||
query: getWorkspaceQuery,
|
||||
variables: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
return {
|
||||
id,
|
||||
flavour: WorkspaceFlavour.AFFINE_CLOUD,
|
||||
blockSuiteWorkspace: getOrCreateWorkspace(
|
||||
id,
|
||||
WorkspaceFlavour.AFFINE_CLOUD
|
||||
),
|
||||
} satisfies AffineCloudWorkspace;
|
||||
} catch (e) {
|
||||
console.error('error when fetching cloud workspace:', e);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
list: async () => {
|
||||
if (!environment.isServer && !navigator.onLine) {
|
||||
// no network
|
||||
return [];
|
||||
}
|
||||
if (
|
||||
!(await getSession()
|
||||
.then(() => true)
|
||||
.catch(() => false))
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const { workspaces } = await fetcher({
|
||||
query: getWorkspacesQuery,
|
||||
});
|
||||
const ids = workspaces.map(({ id }) => id);
|
||||
|
||||
return ids.map(
|
||||
id =>
|
||||
({
|
||||
id,
|
||||
flavour: WorkspaceFlavour.AFFINE_CLOUD,
|
||||
blockSuiteWorkspace: getOrCreateWorkspace(
|
||||
id,
|
||||
WorkspaceFlavour.AFFINE_CLOUD
|
||||
),
|
||||
}) satisfies AffineCloudWorkspace
|
||||
);
|
||||
} catch (e) {
|
||||
console.error('error when fetching cloud workspaces:', e);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
};
|
||||
75
apps/core/src/adapters/cloud/ui.tsx
Normal file
75
apps/core/src/adapters/cloud/ui.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import { initEmptyPage } from '@affine/env/blocksuite';
|
||||
import { PageNotFoundError } from '@affine/env/constant';
|
||||
import type {
|
||||
WorkspaceFlavour,
|
||||
WorkspaceUISchema,
|
||||
} from '@affine/env/workspace';
|
||||
import { lazy, useCallback } from 'react';
|
||||
|
||||
import { useIsWorkspaceOwner } from '../../hooks/affine/use-is-workspace-owner';
|
||||
import { useWorkspace } from '../../hooks/use-workspace';
|
||||
import {
|
||||
BlockSuitePageList,
|
||||
NewWorkspaceSettingDetail,
|
||||
PageDetailEditor,
|
||||
Provider,
|
||||
WorkspaceHeader,
|
||||
} from '../shared';
|
||||
|
||||
const LoginCard = lazy(() =>
|
||||
import('../../components/cloud/login-card').then(({ LoginCard }) => ({
|
||||
default: LoginCard,
|
||||
}))
|
||||
);
|
||||
|
||||
export const UI = {
|
||||
Provider,
|
||||
LoginCard,
|
||||
Header: WorkspaceHeader,
|
||||
PageDetail: ({ currentWorkspaceId, currentPageId, onLoadEditor }) => {
|
||||
const workspace = useWorkspace(currentWorkspaceId);
|
||||
const page = workspace.blockSuiteWorkspace.getPage(currentPageId);
|
||||
if (!page) {
|
||||
throw new PageNotFoundError(workspace.blockSuiteWorkspace, currentPageId);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<PageDetailEditor
|
||||
pageId={currentPageId}
|
||||
onInit={useCallback(async page => initEmptyPage(page), [])}
|
||||
onLoad={onLoadEditor}
|
||||
workspace={workspace.blockSuiteWorkspace}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
},
|
||||
PageList: ({ blockSuiteWorkspace, onOpenPage, collection }) => {
|
||||
return (
|
||||
<BlockSuitePageList
|
||||
listType="all"
|
||||
collection={collection}
|
||||
onOpenPage={onOpenPage}
|
||||
blockSuiteWorkspace={blockSuiteWorkspace}
|
||||
/>
|
||||
);
|
||||
},
|
||||
NewSettingsDetail: ({
|
||||
currentWorkspaceId,
|
||||
onTransformWorkspace,
|
||||
onDeleteLocalWorkspace,
|
||||
onDeleteCloudWorkspace,
|
||||
onLeaveWorkspace,
|
||||
}) => {
|
||||
const isOwner = useIsWorkspaceOwner(currentWorkspaceId);
|
||||
return (
|
||||
<NewWorkspaceSettingDetail
|
||||
onDeleteLocalWorkspace={onDeleteLocalWorkspace}
|
||||
onDeleteCloudWorkspace={onDeleteCloudWorkspace}
|
||||
onLeaveWorkspace={onLeaveWorkspace}
|
||||
workspaceId={currentWorkspaceId}
|
||||
onTransferWorkspace={onTransformWorkspace}
|
||||
isOwner={isOwner}
|
||||
/>
|
||||
);
|
||||
},
|
||||
} satisfies WorkspaceUISchema<WorkspaceFlavour.AFFINE_CLOUD>;
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
BlockSuitePageList,
|
||||
NewWorkspaceSettingDetail,
|
||||
PageDetailEditor,
|
||||
Provider,
|
||||
WorkspaceHeader,
|
||||
} from '../shared';
|
||||
|
||||
@@ -39,6 +40,7 @@ export const LocalAdapter: WorkspaceAdapter<WorkspaceFlavour.LOCAL> = {
|
||||
flavour: WorkspaceFlavour.LOCAL,
|
||||
loadPriority: LoadPriority.LOW,
|
||||
Events: {
|
||||
'app:access': async () => true,
|
||||
'app:init': () => {
|
||||
const blockSuiteWorkspace = getOrCreateWorkspace(
|
||||
nanoid(),
|
||||
@@ -79,9 +81,7 @@ export const LocalAdapter: WorkspaceAdapter<WorkspaceFlavour.LOCAL> = {
|
||||
CRUD,
|
||||
UI: {
|
||||
Header: WorkspaceHeader,
|
||||
Provider: ({ children }) => {
|
||||
return <>{children}</>;
|
||||
},
|
||||
Provider,
|
||||
PageDetail: ({ currentWorkspaceId, currentPageId, onLoadEditor }) => {
|
||||
const workspace = useStaticBlockSuiteWorkspace(currentWorkspaceId);
|
||||
const page = workspace.getPage(currentPageId);
|
||||
@@ -111,14 +111,19 @@ export const LocalAdapter: WorkspaceAdapter<WorkspaceFlavour.LOCAL> = {
|
||||
},
|
||||
NewSettingsDetail: ({
|
||||
currentWorkspaceId,
|
||||
onDeleteWorkspace,
|
||||
onTransformWorkspace,
|
||||
onDeleteLocalWorkspace,
|
||||
onDeleteCloudWorkspace,
|
||||
onLeaveWorkspace,
|
||||
}) => {
|
||||
return (
|
||||
<NewWorkspaceSettingDetail
|
||||
onDeleteWorkspace={onDeleteWorkspace}
|
||||
onDeleteLocalWorkspace={onDeleteLocalWorkspace}
|
||||
onDeleteCloudWorkspace={onDeleteCloudWorkspace}
|
||||
onLeaveWorkspace={onLeaveWorkspace}
|
||||
workspaceId={currentWorkspaceId}
|
||||
onTransferWorkspace={onTransformWorkspace}
|
||||
isOwner={true}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
45
apps/core/src/adapters/public-cloud/ui.tsx
Normal file
45
apps/core/src/adapters/public-cloud/ui.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { initEmptyPage } from '@affine/env/blocksuite';
|
||||
import { PageNotFoundError } from '@affine/env/constant';
|
||||
import type { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { type WorkspaceUISchema } from '@affine/env/workspace';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useWorkspace } from '../../hooks/use-workspace';
|
||||
import { BlockSuitePageList, PageDetailEditor, Provider } from '../shared';
|
||||
|
||||
export const UI = {
|
||||
Provider,
|
||||
Header: () => {
|
||||
return null;
|
||||
},
|
||||
PageDetail: ({ currentWorkspaceId, currentPageId, onLoadEditor }) => {
|
||||
const workspace = useWorkspace(currentWorkspaceId);
|
||||
const page = workspace.blockSuiteWorkspace.getPage(currentPageId);
|
||||
if (!page) {
|
||||
throw new PageNotFoundError(workspace.blockSuiteWorkspace, currentPageId);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<PageDetailEditor
|
||||
pageId={currentPageId}
|
||||
onInit={useCallback(async page => initEmptyPage(page), [])}
|
||||
onLoad={onLoadEditor}
|
||||
workspace={workspace.blockSuiteWorkspace}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
},
|
||||
PageList: ({ blockSuiteWorkspace, onOpenPage, collection }) => {
|
||||
return (
|
||||
<BlockSuitePageList
|
||||
listType="all"
|
||||
collection={collection}
|
||||
onOpenPage={onOpenPage}
|
||||
blockSuiteWorkspace={blockSuiteWorkspace}
|
||||
/>
|
||||
);
|
||||
},
|
||||
NewSettingsDetail: () => {
|
||||
throw new Error('Not implemented');
|
||||
},
|
||||
} satisfies WorkspaceUISchema<WorkspaceFlavour.AFFINE_PUBLIC>;
|
||||
@@ -1,5 +1,11 @@
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const Provider = lazy(() =>
|
||||
import('../components/cloud/provider').then(({ Provider }) => ({
|
||||
default: Provider,
|
||||
}))
|
||||
);
|
||||
|
||||
export const NewWorkspaceSettingDetail = lazy(() =>
|
||||
import('../components/affine/new-workspace-setting-detail').then(
|
||||
({ WorkspaceSettingDetail }) => ({
|
||||
|
||||
@@ -10,7 +10,10 @@ import {
|
||||
WorkspaceFlavour,
|
||||
} from '@affine/env/workspace';
|
||||
|
||||
import { CRUD as CloudCRUD } from './cloud/crud';
|
||||
import { UI as CloudUI } from './cloud/ui';
|
||||
import { LocalAdapter } from './local';
|
||||
import { UI as PublicCloudUI } from './public-cloud/ui';
|
||||
|
||||
const unimplemented = () => {
|
||||
throw new Error('Not implemented');
|
||||
@@ -26,26 +29,24 @@ export const WorkspaceAdapters = {
|
||||
releaseType: ReleaseType.UNRELEASED,
|
||||
flavour: WorkspaceFlavour.AFFINE_CLOUD,
|
||||
loadPriority: LoadPriority.HIGH,
|
||||
Events: {} as Partial<AppEvents>,
|
||||
// todo: implement this
|
||||
CRUD: {
|
||||
get: unimplemented,
|
||||
list: bypassList,
|
||||
delete: unimplemented,
|
||||
create: unimplemented,
|
||||
},
|
||||
// todo: implement this
|
||||
UI: {
|
||||
Provider: unimplemented,
|
||||
Header: unimplemented,
|
||||
PageDetail: unimplemented,
|
||||
PageList: unimplemented,
|
||||
NewSettingsDetail: unimplemented,
|
||||
},
|
||||
Events: {
|
||||
'app:access': async () => {
|
||||
try {
|
||||
const { getSession } = await import('next-auth/react');
|
||||
const session = await getSession();
|
||||
return !!session;
|
||||
} catch (e) {
|
||||
console.error('failed to get session', e);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
} as Partial<AppEvents>,
|
||||
CRUD: CloudCRUD,
|
||||
UI: CloudUI,
|
||||
},
|
||||
[WorkspaceFlavour.PUBLIC]: {
|
||||
[WorkspaceFlavour.AFFINE_PUBLIC]: {
|
||||
releaseType: ReleaseType.UNRELEASED,
|
||||
flavour: WorkspaceFlavour.PUBLIC,
|
||||
flavour: WorkspaceFlavour.AFFINE_PUBLIC,
|
||||
loadPriority: LoadPriority.LOW,
|
||||
Events: {} as Partial<AppEvents>,
|
||||
// todo: implement this
|
||||
@@ -55,14 +56,7 @@ export const WorkspaceAdapters = {
|
||||
delete: unimplemented,
|
||||
create: unimplemented,
|
||||
},
|
||||
// todo: implement this
|
||||
UI: {
|
||||
Provider: unimplemented,
|
||||
Header: unimplemented,
|
||||
PageDetail: unimplemented,
|
||||
PageList: unimplemented,
|
||||
NewSettingsDetail: unimplemented,
|
||||
},
|
||||
UI: PublicCloudUI,
|
||||
},
|
||||
} satisfies {
|
||||
[Key in WorkspaceFlavour]: WorkspaceAdapter<Key>;
|
||||
|
||||
@@ -7,6 +7,7 @@ import { WorkspaceFallback } from '@affine/component/workspace';
|
||||
import { CacheProvider } from '@emotion/react';
|
||||
import { getCurrentStore } from '@toeverything/infra/atom';
|
||||
import { use } from 'foxact/use';
|
||||
import { SessionProvider } from 'next-auth/react';
|
||||
import type { PropsWithChildren, ReactElement } from 'react';
|
||||
import { lazy, memo, Suspense } from 'react';
|
||||
import { RouterProvider } from 'react-router-dom';
|
||||
@@ -47,16 +48,18 @@ const languageLoadingPromise = loadLanguage().catch(console.error);
|
||||
export const App = memo(function App() {
|
||||
use(languageLoadingPromise);
|
||||
return (
|
||||
<CacheProvider value={cache}>
|
||||
<AffineContext store={getCurrentStore()}>
|
||||
<DebugProvider>
|
||||
<RouterProvider
|
||||
fallbackElement={<WorkspaceFallback key="RouterFallback" />}
|
||||
router={router}
|
||||
future={future}
|
||||
/>
|
||||
</DebugProvider>
|
||||
</AffineContext>
|
||||
</CacheProvider>
|
||||
<SessionProvider refetchOnWindowFocus>
|
||||
<CacheProvider value={cache}>
|
||||
<AffineContext store={getCurrentStore()}>
|
||||
<DebugProvider>
|
||||
<RouterProvider
|
||||
fallbackElement={<WorkspaceFallback key="RouterFallback" />}
|
||||
router={router}
|
||||
future={future}
|
||||
/>
|
||||
</DebugProvider>
|
||||
</AffineContext>
|
||||
</CacheProvider>
|
||||
</SessionProvider>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -3,9 +3,9 @@ import { atom } from 'jotai';
|
||||
import { atomFamily, atomWithStorage } from 'jotai/utils';
|
||||
import type { AtomFamily } from 'jotai/vanilla/utils/atomFamily';
|
||||
|
||||
import type { AuthProps } from '../components/affine/auth';
|
||||
import type { CreateWorkspaceMode } from '../components/affine/create-workspace-modal';
|
||||
import type { SettingProps } from '../components/affine/setting-modal';
|
||||
|
||||
// modal atoms
|
||||
export const openWorkspacesModalAtom = atom(false);
|
||||
export const openCreateWorkspaceModalAtom = atom<CreateWorkspaceMode>(false);
|
||||
@@ -22,6 +22,22 @@ export const openSettingModalAtom = atom<SettingAtom>({
|
||||
open: false,
|
||||
});
|
||||
|
||||
export type AuthAtom = {
|
||||
openModal: boolean;
|
||||
state: AuthProps['state'];
|
||||
email?: string;
|
||||
emailType?: AuthProps['emailType'];
|
||||
// Only used for sign in page callback, after called, it will be set to undefined
|
||||
onceSignedIn?: () => void;
|
||||
};
|
||||
|
||||
export const authAtom = atom<AuthAtom>({
|
||||
openModal: false,
|
||||
state: 'signIn',
|
||||
email: '',
|
||||
emailType: 'changeEmail',
|
||||
});
|
||||
|
||||
export const openDisableCloudAlertModalAtom = atom(false);
|
||||
|
||||
type PageMode = 'page' | 'edgeless';
|
||||
|
||||
13
apps/core/src/components/adapter-worksapce-wrapper.tsx
Normal file
13
apps/core/src/components/adapter-worksapce-wrapper.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import type { FC, PropsWithChildren } from 'react';
|
||||
|
||||
import { WorkspaceAdapters } from '../adapters/workspace';
|
||||
import { useCurrentWorkspace } from '../hooks/current/use-current-workspace';
|
||||
|
||||
export const AdapterProviderWrapper: FC<PropsWithChildren> = ({ children }) => {
|
||||
const [currentWorkspace] = useCurrentWorkspace();
|
||||
|
||||
const Provider = WorkspaceAdapters[currentWorkspace.flavour].UI.Provider;
|
||||
assertExists(Provider);
|
||||
return <Provider>{children}</Provider>;
|
||||
};
|
||||
11
apps/core/src/components/affine/any-error-boundary/index.tsx
Normal file
11
apps/core/src/components/affine/any-error-boundary/index.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { ReactElement } from 'react';
|
||||
import type { FallbackProps } from 'react-error-boundary';
|
||||
|
||||
export const AnyErrorBoundary = (props: FallbackProps): ReactElement => {
|
||||
return (
|
||||
<div>
|
||||
<p>Something went wrong:</p>
|
||||
<p>{props.error.toString()}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
import {
|
||||
AuthContent,
|
||||
BackButton,
|
||||
ModalHeader,
|
||||
ResendButton,
|
||||
} from '@affine/component/auth-components';
|
||||
import { Trans } from '@affine/i18n';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { signIn } from 'next-auth/react';
|
||||
import { type FC, useCallback } from 'react';
|
||||
|
||||
import { buildCallbackUrl } from './callback-url';
|
||||
import type { AuthPanelProps } from './index';
|
||||
import * as style from './style.css';
|
||||
|
||||
export const AfterSignInSendEmail: FC<AuthPanelProps> = ({
|
||||
setAuthState,
|
||||
email,
|
||||
}) => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
return (
|
||||
<>
|
||||
<ModalHeader
|
||||
title={t['com.affine.auth.sign.in']()}
|
||||
subTitle={t['com.affine.auth.sign.in.sent.email.subtitle']()}
|
||||
/>
|
||||
<AuthContent style={{ height: 162 }}>
|
||||
{t['com.affine.auth.sign.sent.email.message.start']()}
|
||||
<a href={`mailto:${email}`}>{email}</a>
|
||||
{t['com.affine.auth.sign.sent.email.message.end']()}
|
||||
</AuthContent>
|
||||
|
||||
<ResendButton
|
||||
onClick={useCallback(() => {
|
||||
signIn('email', {
|
||||
email,
|
||||
callbackUrl: buildCallbackUrl('signIn'),
|
||||
redirect: true,
|
||||
}).catch(console.error);
|
||||
}, [email])}
|
||||
/>
|
||||
|
||||
<div className={style.authMessage} style={{ marginTop: 20 }}>
|
||||
{/*prettier-ignore*/}
|
||||
<Trans i18nKey="com.affine.auth.sign.auth.code.message.password">
|
||||
If you haven't received the email, please check your spam folder.
|
||||
Or <span
|
||||
className="link"
|
||||
data-testid='sign-in-with-password'
|
||||
onClick={useCallback(() => {
|
||||
setAuthState('signInWithPassword');
|
||||
}, [setAuthState])}
|
||||
>
|
||||
sign in with password
|
||||
</span> instead.
|
||||
</Trans>
|
||||
</div>
|
||||
|
||||
<BackButton
|
||||
onClick={useCallback(() => {
|
||||
setAuthState('signIn');
|
||||
}, [setAuthState])}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
import {
|
||||
AuthContent,
|
||||
BackButton,
|
||||
ModalHeader,
|
||||
ResendButton,
|
||||
} from '@affine/component/auth-components';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { signIn } from 'next-auth/react';
|
||||
import { type FC, useCallback } from 'react';
|
||||
|
||||
import { buildCallbackUrl } from './callback-url';
|
||||
import type { AuthPanelProps } from './index';
|
||||
import * as style from './style.css';
|
||||
|
||||
export const AfterSignUpSendEmail: FC<AuthPanelProps> = ({
|
||||
setAuthState,
|
||||
email,
|
||||
}) => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
return (
|
||||
<>
|
||||
<ModalHeader
|
||||
title={t['com.affine.auth.sign.up']()}
|
||||
subTitle={t['com.affine.auth.sign.up.sent.email.subtitle']()}
|
||||
/>
|
||||
<AuthContent style={{ height: 162 }}>
|
||||
{t['com.affine.auth.sign.sent.email.message.start']()}
|
||||
<a href={`mailto:${email}`}>{email}</a>
|
||||
{t['com.affine.auth.sign.sent.email.message.end']()}
|
||||
</AuthContent>
|
||||
|
||||
<ResendButton
|
||||
onClick={useCallback(() => {
|
||||
signIn('email', {
|
||||
email: email,
|
||||
callbackUrl: buildCallbackUrl('signUp'),
|
||||
redirect: true,
|
||||
}).catch(console.error);
|
||||
}, [email])}
|
||||
/>
|
||||
|
||||
<div className={style.authMessage} style={{ marginTop: 20 }}>
|
||||
{t['com.affine.auth.sign.auth.code.message']()}
|
||||
</div>
|
||||
|
||||
<BackButton
|
||||
onClick={useCallback(() => {
|
||||
setAuthState('signIn');
|
||||
}, [setAuthState])}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
16
apps/core/src/components/affine/auth/callback-url.ts
Normal file
16
apps/core/src/components/affine/auth/callback-url.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { isDesktop } from '@affine/env/constant';
|
||||
|
||||
type Action = 'signUp' | 'changePassword' | 'signIn' | 'signUp';
|
||||
|
||||
export function buildCallbackUrl(action: Action) {
|
||||
const callbackUrl = `/auth/${action}`;
|
||||
const params: string[][] = [];
|
||||
if (isDesktop && window.appInfo.schema) {
|
||||
params.push(['schema', window.appInfo.schema]);
|
||||
}
|
||||
const query =
|
||||
params.length > 0
|
||||
? '?' + params.map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join('&')
|
||||
: '';
|
||||
return callbackUrl + query;
|
||||
}
|
||||
155
apps/core/src/components/affine/auth/index.tsx
Normal file
155
apps/core/src/components/affine/auth/index.tsx
Normal file
@@ -0,0 +1,155 @@
|
||||
import {
|
||||
AuthModal as AuthModalBase,
|
||||
type AuthModalProps as AuthModalBaseProps,
|
||||
} from '@affine/component/auth-components';
|
||||
import { isDesktop } from '@affine/env/constant';
|
||||
import { atom, useAtom } from 'jotai';
|
||||
import { type FC, useCallback, useEffect, useMemo } from 'react';
|
||||
|
||||
import { AfterSignInSendEmail } from './after-sign-in-send-email';
|
||||
import { AfterSignUpSendEmail } from './after-sign-up-send-email';
|
||||
import { SendEmail } from './send-email';
|
||||
import { SignIn } from './sign-in';
|
||||
import { SignInWithPassword } from './sign-in-with-password';
|
||||
|
||||
export type AuthProps = {
|
||||
state:
|
||||
| 'signIn'
|
||||
| 'afterSignUpSendEmail'
|
||||
| 'afterSignInSendEmail'
|
||||
// throw away
|
||||
| 'signInWithPassword'
|
||||
| 'sendEmail';
|
||||
setAuthState: (state: AuthProps['state']) => void;
|
||||
setAuthEmail: (state: AuthProps['email']) => void;
|
||||
setEmailType: (state: AuthProps['emailType']) => void;
|
||||
email: string;
|
||||
emailType: 'setPassword' | 'changePassword' | 'changeEmail';
|
||||
onSignedIn?: () => void;
|
||||
};
|
||||
|
||||
export type AuthPanelProps = {
|
||||
email: string;
|
||||
setAuthState: AuthProps['setAuthState'];
|
||||
setAuthEmail: AuthProps['setAuthEmail'];
|
||||
setEmailType: AuthProps['setEmailType'];
|
||||
emailType: AuthProps['emailType'];
|
||||
onSignedIn?: () => void;
|
||||
authStore: AuthStoreAtom;
|
||||
setAuthStore: (data: Partial<AuthStoreAtom>) => void;
|
||||
};
|
||||
|
||||
const config: {
|
||||
[k in AuthProps['state']]: FC<AuthPanelProps>;
|
||||
} = {
|
||||
signIn: SignIn,
|
||||
afterSignUpSendEmail: AfterSignUpSendEmail,
|
||||
afterSignInSendEmail: AfterSignInSendEmail,
|
||||
signInWithPassword: SignInWithPassword,
|
||||
sendEmail: SendEmail,
|
||||
};
|
||||
|
||||
type AuthStoreAtom = {
|
||||
hasSentEmail: boolean;
|
||||
resendCountDown: number;
|
||||
};
|
||||
export const authStoreAtom = atom<AuthStoreAtom>({
|
||||
hasSentEmail: false,
|
||||
resendCountDown: 60,
|
||||
});
|
||||
|
||||
export const AuthModal: FC<AuthModalBaseProps & AuthProps> = ({
|
||||
open,
|
||||
state,
|
||||
setOpen,
|
||||
email,
|
||||
setAuthEmail,
|
||||
setAuthState,
|
||||
setEmailType,
|
||||
emailType,
|
||||
}) => {
|
||||
const [, setAuthStore] = useAtom(authStoreAtom);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setAuthStore({
|
||||
hasSentEmail: false,
|
||||
resendCountDown: 60,
|
||||
});
|
||||
setAuthEmail('');
|
||||
}
|
||||
}, [open, setAuthEmail, setAuthStore]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isDesktop) {
|
||||
return window.events?.ui.onFinishLogin(() => {
|
||||
setOpen(false);
|
||||
});
|
||||
}
|
||||
return;
|
||||
}, [setOpen]);
|
||||
|
||||
const onSignedIn = useCallback(() => {
|
||||
setOpen(false);
|
||||
}, [setOpen]);
|
||||
|
||||
return (
|
||||
<AuthModalBase open={open} setOpen={setOpen}>
|
||||
<AuthPanel
|
||||
state={state}
|
||||
email={email}
|
||||
setAuthEmail={setAuthEmail}
|
||||
setAuthState={setAuthState}
|
||||
setEmailType={setEmailType}
|
||||
emailType={emailType}
|
||||
onSignedIn={onSignedIn}
|
||||
/>
|
||||
</AuthModalBase>
|
||||
);
|
||||
};
|
||||
|
||||
export const AuthPanel: FC<AuthProps> = ({
|
||||
state,
|
||||
email,
|
||||
setAuthEmail,
|
||||
setAuthState,
|
||||
setEmailType,
|
||||
emailType,
|
||||
onSignedIn,
|
||||
}) => {
|
||||
const [authStore, setAuthStore] = useAtom(authStoreAtom);
|
||||
|
||||
const CurrentPanel = useMemo(() => {
|
||||
return config[state];
|
||||
}, [state]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
setAuthStore({
|
||||
hasSentEmail: false,
|
||||
resendCountDown: 60,
|
||||
});
|
||||
};
|
||||
}, [setAuthEmail, setAuthStore]);
|
||||
|
||||
return (
|
||||
<CurrentPanel
|
||||
email={email}
|
||||
setAuthState={setAuthState}
|
||||
setAuthEmail={setAuthEmail}
|
||||
setEmailType={setEmailType}
|
||||
authStore={authStore}
|
||||
emailType={emailType}
|
||||
onSignedIn={onSignedIn}
|
||||
setAuthStore={useCallback(
|
||||
(data: Partial<AuthStoreAtom>) => {
|
||||
setAuthStore(prev => ({
|
||||
...prev,
|
||||
...data,
|
||||
}));
|
||||
},
|
||||
[setAuthStore]
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
185
apps/core/src/components/affine/auth/send-email.tsx
Normal file
185
apps/core/src/components/affine/auth/send-email.tsx
Normal file
@@ -0,0 +1,185 @@
|
||||
import { Wrapper } from '@affine/component';
|
||||
import {
|
||||
AuthContent,
|
||||
AuthInput,
|
||||
BackButton,
|
||||
ModalHeader,
|
||||
} from '@affine/component/auth-components';
|
||||
import { pushNotificationAtom } from '@affine/component/notification-center';
|
||||
import { isDesktop } from '@affine/env/constant';
|
||||
import {
|
||||
sendChangeEmailMutation,
|
||||
sendChangePasswordEmailMutation,
|
||||
sendSetPasswordEmailMutation,
|
||||
} from '@affine/graphql';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useMutation } from '@affine/workspace/affine/gql';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import { useSetAtom } from 'jotai/react';
|
||||
import { type FC, useCallback } from 'react';
|
||||
|
||||
import type { AuthPanelProps } from './index';
|
||||
|
||||
const useEmailTitle = (emailType: AuthPanelProps['emailType']) => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
switch (emailType) {
|
||||
case 'setPassword':
|
||||
return t['com.affine.auth.set.password']();
|
||||
case 'changePassword':
|
||||
return t['com.affine.auth.reset.password']();
|
||||
case 'changeEmail':
|
||||
return t['com.affine.settings.email.action']();
|
||||
}
|
||||
};
|
||||
|
||||
const useNotificationHint = (emailType: AuthPanelProps['emailType']) => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
switch (emailType) {
|
||||
case 'setPassword':
|
||||
return t['com.affine.auth.sent.set.password.hint']();
|
||||
case 'changePassword':
|
||||
return t['com.affine.auth.sent.change.password.hint']();
|
||||
case 'changeEmail':
|
||||
return t['com.affine.auth.sent.change.email.hint']();
|
||||
}
|
||||
};
|
||||
const useButtonContent = (emailType: AuthPanelProps['emailType']) => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
switch (emailType) {
|
||||
case 'setPassword':
|
||||
return t['com.affine.auth.send.set.password.link']();
|
||||
case 'changePassword':
|
||||
return t['com.affine.auth.send.reset.password.link']();
|
||||
case 'changeEmail':
|
||||
return t['com.affine.auth.send.change.email.link']();
|
||||
}
|
||||
};
|
||||
|
||||
const useSendEmail = (emailType: AuthPanelProps['emailType']) => {
|
||||
const {
|
||||
trigger: sendChangePasswordEmail,
|
||||
isMutating: isChangePasswordMutating,
|
||||
} = useMutation({
|
||||
mutation: sendChangePasswordEmailMutation,
|
||||
});
|
||||
const { trigger: sendSetPasswordEmail, isMutating: isSetPasswordMutating } =
|
||||
useMutation({
|
||||
mutation: sendSetPasswordEmailMutation,
|
||||
});
|
||||
const { trigger: sendChangeEmail, isMutating: isChangeEmailMutating } =
|
||||
useMutation({
|
||||
mutation: sendChangeEmailMutation,
|
||||
});
|
||||
|
||||
return {
|
||||
loading:
|
||||
isChangePasswordMutating ||
|
||||
isSetPasswordMutating ||
|
||||
isChangeEmailMutating,
|
||||
sendEmail: useCallback(
|
||||
(email: string) => {
|
||||
let trigger: (args: {
|
||||
email: string;
|
||||
callbackUrl: string;
|
||||
}) => Promise<unknown>;
|
||||
let callbackUrl;
|
||||
switch (emailType) {
|
||||
case 'setPassword':
|
||||
trigger = sendSetPasswordEmail;
|
||||
callbackUrl = 'setPassword';
|
||||
break;
|
||||
case 'changePassword':
|
||||
trigger = sendChangePasswordEmail;
|
||||
callbackUrl = 'changePassword';
|
||||
break;
|
||||
case 'changeEmail':
|
||||
trigger = sendChangeEmail;
|
||||
callbackUrl = 'changeEmail';
|
||||
break;
|
||||
}
|
||||
// TODO: add error handler
|
||||
return trigger({
|
||||
email,
|
||||
callbackUrl: `/auth/${callbackUrl}?isClient=${
|
||||
isDesktop ? 'true' : 'false'
|
||||
}`,
|
||||
});
|
||||
},
|
||||
[
|
||||
emailType,
|
||||
sendChangeEmail,
|
||||
sendChangePasswordEmail,
|
||||
sendSetPasswordEmail,
|
||||
]
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
export const SendEmail: FC<AuthPanelProps> = ({
|
||||
setAuthState,
|
||||
setAuthStore,
|
||||
email,
|
||||
authStore: { hasSentEmail },
|
||||
emailType,
|
||||
}) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const pushNotification = useSetAtom(pushNotificationAtom);
|
||||
|
||||
const title = useEmailTitle(emailType);
|
||||
const hint = useNotificationHint(emailType);
|
||||
const buttonContent = useButtonContent(emailType);
|
||||
const { loading, sendEmail } = useSendEmail(emailType);
|
||||
|
||||
const onSendEmail = useCallback(async () => {
|
||||
// TODO: add error handler
|
||||
await sendEmail(email);
|
||||
|
||||
pushNotification({
|
||||
title: hint,
|
||||
message: '',
|
||||
key: Date.now().toString(),
|
||||
type: 'success',
|
||||
});
|
||||
setAuthStore({ hasSentEmail: true });
|
||||
}, [email, hint, pushNotification, sendEmail, setAuthStore]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ModalHeader title={t['AFFiNE Cloud']()} subTitle={title} />
|
||||
<AuthContent>{t['com.affine.auth.reset.password.message']()}</AuthContent>
|
||||
|
||||
<Wrapper
|
||||
marginTop={30}
|
||||
marginBottom={50}
|
||||
style={{
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<AuthInput
|
||||
label={t['com.affine.settings.email']()}
|
||||
disabled={true}
|
||||
value={email}
|
||||
/>
|
||||
</Wrapper>
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
size="extraLarge"
|
||||
style={{ width: '100%' }}
|
||||
disabled={hasSentEmail}
|
||||
loading={loading}
|
||||
onClick={onSendEmail}
|
||||
>
|
||||
{hasSentEmail ? t['com.affine.auth.sent']() : buttonContent}
|
||||
</Button>
|
||||
<BackButton
|
||||
onClick={useCallback(() => {
|
||||
setAuthState('signIn');
|
||||
}, [setAuthState])}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
111
apps/core/src/components/affine/auth/sign-in-with-password.tsx
Normal file
111
apps/core/src/components/affine/auth/sign-in-with-password.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import { Wrapper } from '@affine/component';
|
||||
import {
|
||||
AuthInput,
|
||||
BackButton,
|
||||
ModalHeader,
|
||||
} from '@affine/component/auth-components';
|
||||
import { pushNotificationAtom } from '@affine/component/notification-center';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import { useSetAtom } from 'jotai';
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
import { signIn, useSession } from 'next-auth/react';
|
||||
import type { FC } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import type { AuthPanelProps } from './index';
|
||||
import { forgetPasswordButton } from './style.css';
|
||||
|
||||
export const SignInWithPassword: FC<AuthPanelProps> = ({
|
||||
setAuthState,
|
||||
email,
|
||||
onSignedIn,
|
||||
}) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const { update } = useSession();
|
||||
|
||||
const pushNotification = useSetAtom(pushNotificationAtom);
|
||||
|
||||
const [password, setPassword] = useState('');
|
||||
const [passwordError, setPasswordError] = useState(false);
|
||||
|
||||
const onSignIn = useCallback(async () => {
|
||||
const res = await signIn('credentials', {
|
||||
redirect: false,
|
||||
email,
|
||||
password,
|
||||
}).catch(console.error);
|
||||
|
||||
if (!res?.ok) {
|
||||
return setPasswordError(true);
|
||||
}
|
||||
|
||||
await update();
|
||||
onSignedIn?.();
|
||||
pushNotification({
|
||||
title: `${email}${t['com.affine.auth.has.signed']()}`,
|
||||
message: '',
|
||||
key: Date.now().toString(),
|
||||
type: 'success',
|
||||
});
|
||||
}, [email, password, pushNotification, onSignedIn, t, update]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ModalHeader
|
||||
title={t['com.affine.auth.sign.in']()}
|
||||
subTitle={t['AFFiNE Cloud']()}
|
||||
/>
|
||||
|
||||
<Wrapper
|
||||
marginTop={30}
|
||||
marginBottom={50}
|
||||
style={{
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<AuthInput
|
||||
label={t['com.affine.settings.email']()}
|
||||
disabled={true}
|
||||
value={email}
|
||||
/>
|
||||
<AuthInput
|
||||
data-testid="password-input"
|
||||
label={t['com.affine.auth.password']()}
|
||||
value={password}
|
||||
type="password"
|
||||
onChange={useCallback((value: string) => {
|
||||
setPassword(value);
|
||||
}, [])}
|
||||
error={passwordError}
|
||||
errorHint={t['com.affine.auth.password.error']()}
|
||||
onEnter={onSignIn}
|
||||
/>
|
||||
<span></span>
|
||||
<button
|
||||
className={forgetPasswordButton}
|
||||
// onClick={useCallback(() => {
|
||||
// setAuthState('sendPasswordEmail');
|
||||
// }, [setAuthState])}
|
||||
>
|
||||
{t['com.affine.auth.forget']()}
|
||||
</button>
|
||||
</Wrapper>
|
||||
<Button
|
||||
data-testid="sign-in-button"
|
||||
type="primary"
|
||||
size="extraLarge"
|
||||
style={{ width: '100%' }}
|
||||
onClick={onSignIn}
|
||||
>
|
||||
{t['com.affine.auth.sign.in']()}
|
||||
</Button>
|
||||
|
||||
<BackButton
|
||||
onClick={useCallback(() => {
|
||||
setAuthState('afterSignInSendEmail');
|
||||
}, [setAuthState])}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
151
apps/core/src/components/affine/auth/sign-in.tsx
Normal file
151
apps/core/src/components/affine/auth/sign-in.tsx
Normal file
@@ -0,0 +1,151 @@
|
||||
import { AuthInput, ModalHeader } from '@affine/component/auth-components';
|
||||
import { pushNotificationAtom } from '@affine/component/notification-center';
|
||||
import type { Notification } from '@affine/component/notification-center/index.jotai';
|
||||
import { getUserQuery } from '@affine/graphql';
|
||||
import { Trans } from '@affine/i18n';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useMutation } from '@affine/workspace/affine/gql';
|
||||
import { ArrowDownBigIcon, GoogleDuotoneIcon } from '@blocksuite/icons';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { signIn, type SignInResponse } from 'next-auth/react';
|
||||
import { type FC, useState } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { emailRegex } from '../../../utils/email-regex';
|
||||
import { buildCallbackUrl } from './callback-url';
|
||||
import type { AuthPanelProps } from './index';
|
||||
import * as style from './style.css';
|
||||
|
||||
function validateEmail(email: string) {
|
||||
return emailRegex.test(email);
|
||||
}
|
||||
|
||||
function handleSendEmailError(
|
||||
res: SignInResponse | undefined,
|
||||
pushNotification: (notification: Notification) => void
|
||||
) {
|
||||
if (res?.error) {
|
||||
pushNotification({
|
||||
title: 'Send email error',
|
||||
message: 'Please back to home and try again',
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const SignIn: FC<AuthPanelProps> = ({
|
||||
setAuthState,
|
||||
setAuthEmail,
|
||||
email,
|
||||
}) => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const { trigger: verifyUser, isMutating } = useMutation({
|
||||
mutation: getUserQuery,
|
||||
});
|
||||
const [isValidEmail, setIsValidEmail] = useState(true);
|
||||
const pushNotification = useSetAtom(pushNotificationAtom);
|
||||
const onContinue = useCallback(async () => {
|
||||
if (!validateEmail(email)) {
|
||||
setIsValidEmail(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsValidEmail(true);
|
||||
const { user } = await verifyUser({ email });
|
||||
|
||||
setAuthEmail(email);
|
||||
if (user) {
|
||||
signIn('email', {
|
||||
email: email,
|
||||
callbackUrl: buildCallbackUrl('signIn'),
|
||||
redirect: false,
|
||||
})
|
||||
.then(res => handleSendEmailError(res, pushNotification))
|
||||
.catch(console.error);
|
||||
setAuthState('afterSignInSendEmail');
|
||||
} else {
|
||||
signIn('email', {
|
||||
email: email,
|
||||
callbackUrl: buildCallbackUrl('signUp'),
|
||||
redirect: false,
|
||||
})
|
||||
.then(res => handleSendEmailError(res, pushNotification))
|
||||
.catch(console.error);
|
||||
|
||||
setAuthState('afterSignUpSendEmail');
|
||||
}
|
||||
}, [email, setAuthEmail, setAuthState, verifyUser, pushNotification]);
|
||||
return (
|
||||
<>
|
||||
<ModalHeader
|
||||
title={t['com.affine.auth.sign.in']()}
|
||||
subTitle={t['AFFiNE Cloud']()}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
block
|
||||
size="extraLarge"
|
||||
style={{
|
||||
marginTop: 30,
|
||||
}}
|
||||
icon={<GoogleDuotoneIcon />}
|
||||
onClick={useCallback(() => {
|
||||
signIn('google').catch(console.error);
|
||||
}, [])}
|
||||
>
|
||||
{t['Continue with Google']()}
|
||||
</Button>
|
||||
|
||||
<div className={style.authModalContent}>
|
||||
<AuthInput
|
||||
label={t['com.affine.settings.email']()}
|
||||
placeholder={t['com.affine.auth.sign.email.placeholder']()}
|
||||
value={email}
|
||||
onChange={useCallback(
|
||||
(value: string) => {
|
||||
setAuthEmail(value);
|
||||
},
|
||||
[setAuthEmail]
|
||||
)}
|
||||
error={!isValidEmail}
|
||||
errorHint={
|
||||
isValidEmail ? '' : t['com.affine.auth.sign.email.error']()
|
||||
}
|
||||
onEnter={onContinue}
|
||||
/>
|
||||
|
||||
<Button
|
||||
size="extraLarge"
|
||||
data-testid="continue-login-button"
|
||||
block
|
||||
loading={isMutating}
|
||||
icon={
|
||||
<ArrowDownBigIcon
|
||||
width={20}
|
||||
height={20}
|
||||
style={{
|
||||
transform: 'rotate(-90deg)',
|
||||
color: 'var(--affine-blue)',
|
||||
}}
|
||||
/>
|
||||
}
|
||||
iconPosition="end"
|
||||
onClick={onContinue}
|
||||
>
|
||||
{t['com.affine.auth.sign.email.continue']()}
|
||||
</Button>
|
||||
|
||||
<div className={style.authMessage}>
|
||||
{/*prettier-ignore*/}
|
||||
<Trans i18nKey="com.affine.auth.sign.message">
|
||||
By clicking "Continue with Google/Email" above, you acknowledge that
|
||||
you agree to AFFiNE's <a href="https://affine.pro/terms" target="_blank" rel="noreferrer">Terms of Conditions</a> and <a href="https://affine.pro/privacy" target="_blank" rel="noreferrer">Privacy Policy</a>.
|
||||
</Trans>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
28
apps/core/src/components/affine/auth/style.css.ts
Normal file
28
apps/core/src/components/affine/auth/style.css.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { globalStyle, style } from '@vanilla-extract/css';
|
||||
|
||||
export const authModalContent = style({
|
||||
marginTop: '30px',
|
||||
});
|
||||
|
||||
export const authMessage = style({
|
||||
marginTop: '30px',
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
fontSize: 'var(--affine-font-xs)',
|
||||
lineHeight: 1.5,
|
||||
});
|
||||
globalStyle(`${authMessage} a`, {
|
||||
color: 'var(--affine-link-color)',
|
||||
});
|
||||
globalStyle(`${authMessage} .link`, {
|
||||
cursor: 'pointer',
|
||||
color: 'var(--affine-link-color)',
|
||||
});
|
||||
|
||||
export const forgetPasswordButton = style({
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
display: 'none',
|
||||
});
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Modal, ModalWrapper, Wrapper } from '@affine/component';
|
||||
import { Modal, ModalWrapper } from '@affine/component';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { CloseIcon } from '@blocksuite/icons';
|
||||
import { Button, IconButton } from '@toeverything/components/button';
|
||||
|
||||
import { Content, ContentTitle, Header, StyleTips } from './style';
|
||||
import { ButtonContainer, Content, Header, StyleTips, Title } from './style';
|
||||
|
||||
interface EnableAffineCloudModalProps {
|
||||
open: boolean;
|
||||
@@ -20,32 +20,32 @@ export const EnableAffineCloudModal = ({
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose} data-testid="logout-modal">
|
||||
<ModalWrapper width={560} height={292}>
|
||||
<ModalWrapper width={480}>
|
||||
<Header>
|
||||
<Title>{t['Enable AFFiNE Cloud']()}</Title>
|
||||
<IconButton onClick={onClose}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</Header>
|
||||
<Content>
|
||||
<ContentTitle>{t['Enable AFFiNE Cloud']()}?</ContentTitle>
|
||||
<StyleTips>{t['Enable AFFiNE Cloud Description']()}</StyleTips>
|
||||
{/* <StyleTips>{t('Retain cached cloud data')}</StyleTips> */}
|
||||
<Wrapper width={284} margin="auto">
|
||||
<Button
|
||||
data-testid="confirm-enable-affine-cloud-button"
|
||||
type="primary"
|
||||
block
|
||||
onClick={onConfirm}
|
||||
style={{
|
||||
marginBottom: '16px',
|
||||
}}
|
||||
>
|
||||
{t['Sign in and Enable']()}
|
||||
</Button>
|
||||
<Button onClick={onClose} block>
|
||||
{t['Not now']()}
|
||||
</Button>
|
||||
</Wrapper>
|
||||
<ButtonContainer>
|
||||
<div>
|
||||
<Button onClick={onClose} block>
|
||||
{t['Cancel']()}
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
data-testid="confirm-enable-affine-cloud-button"
|
||||
type="primary"
|
||||
block
|
||||
onClick={onConfirm}
|
||||
>
|
||||
{t['Sign in and Enable']()}
|
||||
</Button>
|
||||
</div>
|
||||
</ButtonContainer>
|
||||
</Content>
|
||||
</ModalWrapper>
|
||||
</Modal>
|
||||
|
||||
@@ -1,31 +1,35 @@
|
||||
import { styled } from '@affine/component';
|
||||
|
||||
export const Header = styled('div')({
|
||||
height: '44px',
|
||||
display: 'flex',
|
||||
flexDirection: 'row-reverse',
|
||||
paddingRight: '10px',
|
||||
paddingTop: '10px',
|
||||
flexShrink: 0,
|
||||
justifyContent: 'space-between',
|
||||
paddingRight: '20px',
|
||||
paddingTop: '20px',
|
||||
paddingLeft: '24px',
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
export const Content = styled('div')({
|
||||
textAlign: 'center',
|
||||
padding: '12px 24px 20px 24px',
|
||||
});
|
||||
|
||||
export const ContentTitle = styled('h1')({
|
||||
fontSize: '20px',
|
||||
lineHeight: '28px',
|
||||
export const Title = styled('div')({
|
||||
fontSize: 'var(--affine-font-h6)',
|
||||
lineHeight: '26px',
|
||||
fontWeight: 600,
|
||||
textAlign: 'center',
|
||||
});
|
||||
|
||||
export const StyleTips = styled('div')(() => {
|
||||
return {
|
||||
userSelect: 'none',
|
||||
width: '400px',
|
||||
margin: 'auto',
|
||||
marginBottom: '32px',
|
||||
marginTop: '12px',
|
||||
marginBottom: '20px',
|
||||
};
|
||||
});
|
||||
export const ButtonContainer = styled('div')(() => {
|
||||
return {
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
gap: '20px',
|
||||
paddingTop: '20px',
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { Input, Modal, ModalCloseButton } from '@affine/component';
|
||||
import type { AffineOfficialWorkspace } from '@affine/env/workspace';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { Trans } from '@affine/i18n';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import type { AffineOfficialWorkspace } from '../../../../../shared';
|
||||
import { toast } from '../../../../../utils';
|
||||
import {
|
||||
StyledButtonContent,
|
||||
StyledInputContent,
|
||||
@@ -21,14 +20,14 @@ interface WorkspaceDeleteProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
workspace: AffineOfficialWorkspace;
|
||||
onDeleteWorkspace: (id: string) => Promise<void>;
|
||||
onConfirm: () => void;
|
||||
}
|
||||
|
||||
export const WorkspaceDeleteModal = ({
|
||||
open,
|
||||
onClose,
|
||||
onConfirm,
|
||||
workspace,
|
||||
onDeleteWorkspace,
|
||||
}: WorkspaceDeleteProps) => {
|
||||
const [workspaceName] = useBlockSuiteWorkspaceName(
|
||||
workspace.blockSuiteWorkspace
|
||||
@@ -37,19 +36,6 @@ export const WorkspaceDeleteModal = ({
|
||||
const allowDelete = deleteStr === workspaceName;
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const handleDelete = useCallback(() => {
|
||||
onDeleteWorkspace(workspace.id)
|
||||
.then(() => {
|
||||
toast(t['Successfully deleted'](), {
|
||||
portal: document.body,
|
||||
});
|
||||
onClose();
|
||||
})
|
||||
.catch(() => {
|
||||
// ignore error
|
||||
});
|
||||
}, [onClose, onDeleteWorkspace, t, workspace.id]);
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<StyledModalWrapper>
|
||||
@@ -99,7 +85,7 @@ export const WorkspaceDeleteModal = ({
|
||||
<Button
|
||||
data-testid="delete-workspace-confirm-button"
|
||||
disabled={!allowDelete}
|
||||
onClick={handleDelete}
|
||||
onClick={onConfirm}
|
||||
size="large"
|
||||
type="error"
|
||||
style={{ marginLeft: '24px' }}
|
||||
|
||||
@@ -1,29 +1,55 @@
|
||||
import { ConfirmModal } from '@affine/component';
|
||||
import { SettingRow } from '@affine/component/setting-components';
|
||||
import type { AffineOfficialWorkspace } from '@affine/env/workspace';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { ArrowRightSmallIcon } from '@blocksuite/icons';
|
||||
import { useState } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import type { AffineOfficialWorkspace } from '../../../../shared';
|
||||
import type { WorkspaceSettingDetailProps } from '../index';
|
||||
import { WorkspaceDeleteModal } from './delete';
|
||||
import { WorkspaceLeave } from './leave';
|
||||
|
||||
interface DeleteLeaveWorkspaceProps {
|
||||
export interface DeleteLeaveWorkspaceProps extends WorkspaceSettingDetailProps {
|
||||
workspace: AffineOfficialWorkspace;
|
||||
onDeleteWorkspace: WorkspaceSettingDetailProps['onDeleteWorkspace'];
|
||||
}
|
||||
|
||||
export const DeleteLeaveWorkspace = ({
|
||||
workspace,
|
||||
onDeleteWorkspace,
|
||||
onDeleteCloudWorkspace,
|
||||
onDeleteLocalWorkspace,
|
||||
onLeaveWorkspace,
|
||||
isOwner,
|
||||
}: DeleteLeaveWorkspaceProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
// fixme: cloud regression
|
||||
const isOwner = true;
|
||||
|
||||
const [showDelete, setShowDelete] = useState(false);
|
||||
const [showLeave, setShowLeave] = useState(false);
|
||||
|
||||
const onLeaveOrDelete = useCallback(() => {
|
||||
if (isOwner) {
|
||||
setShowDelete(true);
|
||||
} else {
|
||||
setShowLeave(true);
|
||||
}
|
||||
}, [isOwner]);
|
||||
|
||||
const onCloseLeaveModal = useCallback(() => {
|
||||
setShowLeave(false);
|
||||
}, []);
|
||||
|
||||
const onLeaveConfirm = useCallback(() => {
|
||||
return onLeaveWorkspace();
|
||||
}, [onLeaveWorkspace]);
|
||||
|
||||
const onDeleteConfirm = useCallback(() => {
|
||||
if (workspace.flavour === WorkspaceFlavour.LOCAL) {
|
||||
return onDeleteLocalWorkspace();
|
||||
}
|
||||
if (workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD) {
|
||||
return onDeleteCloudWorkspace();
|
||||
}
|
||||
}, [onDeleteCloudWorkspace, onDeleteLocalWorkspace, workspace.flavour]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingRow
|
||||
@@ -36,16 +62,14 @@ export const DeleteLeaveWorkspace = ({
|
||||
}
|
||||
desc={t['com.affine.settings.remove-workspace-description']()}
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
setShowDelete(true);
|
||||
}}
|
||||
onClick={onLeaveOrDelete}
|
||||
data-testid="delete-workspace-button"
|
||||
>
|
||||
<ArrowRightSmallIcon />
|
||||
</SettingRow>
|
||||
{isOwner ? (
|
||||
<WorkspaceDeleteModal
|
||||
onDeleteWorkspace={onDeleteWorkspace}
|
||||
onConfirm={onDeleteConfirm}
|
||||
open={showDelete}
|
||||
onClose={() => {
|
||||
setShowDelete(false);
|
||||
@@ -53,11 +77,15 @@ export const DeleteLeaveWorkspace = ({
|
||||
workspace={workspace}
|
||||
/>
|
||||
) : (
|
||||
<WorkspaceLeave
|
||||
<ConfirmModal
|
||||
open={showLeave}
|
||||
onClose={() => {
|
||||
setShowLeave(false);
|
||||
}}
|
||||
onConfirm={onLeaveConfirm}
|
||||
onCancel={onCloseLeaveModal}
|
||||
onClose={onCloseLeaveModal}
|
||||
title={`${t['Leave Workspace']()}?`}
|
||||
content={t['Leave Workspace hint']()}
|
||||
confirmType="warning"
|
||||
confirmText={t['Leave']()}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
import { Modal } from '@affine/component';
|
||||
import { ModalCloseButton } from '@affine/component';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
|
||||
import {
|
||||
StyledButtonContent,
|
||||
StyledModalHeader,
|
||||
StyledModalWrapper,
|
||||
StyledTextContent,
|
||||
} from './style';
|
||||
|
||||
interface WorkspaceDeleteProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const WorkspaceLeave = ({ open, onClose }: WorkspaceDeleteProps) => {
|
||||
// const { leaveWorkSpace } = useWorkspaceHelper();
|
||||
const t = useAFFiNEI18N();
|
||||
const handleLeave = async () => {
|
||||
// await leaveWorkSpace();
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<StyledModalWrapper>
|
||||
<ModalCloseButton onClick={onClose} />
|
||||
<StyledModalHeader>{t['Leave Workspace']()}</StyledModalHeader>
|
||||
<StyledTextContent>
|
||||
{t['Leave Workspace Description']()}
|
||||
</StyledTextContent>
|
||||
<StyledButtonContent>
|
||||
<Button shape="circle" onClick={onClose}>
|
||||
{t['Cancel']()}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleLeave}
|
||||
type="error"
|
||||
style={{ marginLeft: '24px' }}
|
||||
>
|
||||
{t['Leave']()}
|
||||
</Button>
|
||||
</StyledButtonContent>
|
||||
</StyledModalWrapper>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -1,44 +0,0 @@
|
||||
import { styled } from '@affine/component';
|
||||
|
||||
export const StyledModalWrapper = styled('div')(() => {
|
||||
return {
|
||||
position: 'relative',
|
||||
padding: '0px',
|
||||
width: '460px',
|
||||
background: 'var(--affine-white)',
|
||||
borderRadius: '12px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledModalHeader = styled('div')(() => {
|
||||
return {
|
||||
margin: '44px 0px 12px 0px',
|
||||
width: '460px',
|
||||
fontWeight: '600',
|
||||
fontSize: '20px;',
|
||||
textAlign: 'center',
|
||||
};
|
||||
});
|
||||
|
||||
// export const StyledModalContent = styled('div')(({ theme }) => {});
|
||||
|
||||
export const StyledTextContent = styled('div')(() => {
|
||||
return {
|
||||
margin: 'auto',
|
||||
width: '425px',
|
||||
fontStyle: 'normal',
|
||||
fontWeight: '400',
|
||||
fontSize: '18px',
|
||||
lineHeight: '26px',
|
||||
textAlign: 'center',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledButtonContent = styled('div')(() => {
|
||||
return {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
margin: '0px 0 32px 0',
|
||||
};
|
||||
});
|
||||
@@ -1,13 +1,12 @@
|
||||
import { toast } from '@affine/component';
|
||||
import { SettingRow } from '@affine/component/setting-components';
|
||||
import { isDesktop } from '@affine/env/constant';
|
||||
import type { AffineOfficialWorkspace } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import type { SaveDBFileResult } from '@toeverything/infra/type';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import type { AffineOfficialWorkspace } from '../../../shared';
|
||||
|
||||
async function syncBlobsToSqliteDb(workspace: AffineOfficialWorkspace) {
|
||||
if (window.apis && isDesktop) {
|
||||
const bs = workspace.blockSuiteWorkspace.blobs;
|
||||
@@ -41,7 +40,7 @@ export const ExportPanel = ({ workspace }: ExportPanelProps) => {
|
||||
const result: SaveDBFileResult =
|
||||
await window.apis?.dialog.saveDBFileAs(workspaceId);
|
||||
if (result?.error) {
|
||||
toast(t[result.error]());
|
||||
toast(result.error);
|
||||
} else if (!result?.canceled) {
|
||||
toast(t['Export success']());
|
||||
}
|
||||
|
||||
@@ -14,13 +14,17 @@ import { useMemo } from 'react';
|
||||
import { useWorkspace } from '../../../hooks/use-workspace';
|
||||
import { DeleteLeaveWorkspace } from './delete-leave-workspace';
|
||||
import { ExportPanel } from './export';
|
||||
import { MembersPanel } from './members';
|
||||
import { ProfilePanel } from './profile';
|
||||
import { PublishPanel } from './publish';
|
||||
import { StoragePanel } from './storage';
|
||||
|
||||
export interface WorkspaceSettingDetailProps {
|
||||
workspaceId: string;
|
||||
onDeleteWorkspace: (id: string) => Promise<void>;
|
||||
isOwner: boolean;
|
||||
onDeleteLocalWorkspace: () => void;
|
||||
onDeleteCloudWorkspace: () => void;
|
||||
onLeaveWorkspace: () => void;
|
||||
onTransferWorkspace: <
|
||||
From extends WorkspaceFlavour,
|
||||
To extends WorkspaceFlavour,
|
||||
@@ -31,11 +35,8 @@ export interface WorkspaceSettingDetailProps {
|
||||
) => void;
|
||||
}
|
||||
|
||||
export const WorkspaceSettingDetail = ({
|
||||
workspaceId,
|
||||
onDeleteWorkspace,
|
||||
...props
|
||||
}: WorkspaceSettingDetailProps) => {
|
||||
export const WorkspaceSettingDetail = (props: WorkspaceSettingDetailProps) => {
|
||||
const { workspaceId } = props;
|
||||
const t = useAFFiNEI18N();
|
||||
const workspace = useWorkspace(workspaceId);
|
||||
const [name] = useBlockSuiteWorkspaceName(workspace.blockSuiteWorkspace);
|
||||
@@ -67,22 +68,16 @@ export const WorkspaceSettingDetail = ({
|
||||
desc={t['com.affine.settings.workspace.not-owner']()}
|
||||
spreadCol={false}
|
||||
>
|
||||
<ProfilePanel workspace={workspace} />
|
||||
<ProfilePanel workspace={workspace} {...props} />
|
||||
</SettingRow>
|
||||
</SettingWrapper>
|
||||
<SettingWrapper title={t['AFFiNE Cloud']()}>
|
||||
<PublishPanel
|
||||
workspace={workspace}
|
||||
onDeleteWorkspace={onDeleteWorkspace}
|
||||
{...props}
|
||||
/>
|
||||
<PublishPanel workspace={workspace} {...props} />
|
||||
<MembersPanel workspace={workspace} {...props} />
|
||||
</SettingWrapper>
|
||||
{storageAndExportSetting}
|
||||
<SettingWrapper>
|
||||
<DeleteLeaveWorkspace
|
||||
workspace={workspace}
|
||||
onDeleteWorkspace={onDeleteWorkspace}
|
||||
/>
|
||||
<DeleteLeaveWorkspace workspace={workspace} {...props} />
|
||||
</SettingWrapper>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
import { Menu, MenuItem } from '@affine/component';
|
||||
import {
|
||||
InviteModal,
|
||||
type InviteModalProps,
|
||||
} from '@affine/component/member-components';
|
||||
import { pushNotificationAtom } from '@affine/component/notification-center';
|
||||
import { SettingRow } from '@affine/component/setting-components';
|
||||
import type { AffineOfficialWorkspace } from '@affine/env/workspace';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { Permission } from '@affine/graphql';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { MoreVerticalIcon } from '@blocksuite/icons';
|
||||
import { Avatar } from '@toeverything/components/avatar';
|
||||
import { Button, IconButton } from '@toeverything/components/button';
|
||||
import { Tooltip } from '@toeverything/components/tooltip';
|
||||
import clsx from 'clsx';
|
||||
import { useSetAtom } from 'jotai/react';
|
||||
import type { ReactElement } from 'react';
|
||||
import { Suspense, useCallback, useMemo, useState } from 'react';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
|
||||
import type { CheckedUser } from '../../../hooks/affine/use-current-user';
|
||||
import { useCurrentUser } from '../../../hooks/affine/use-current-user';
|
||||
import { useInviteMember } from '../../../hooks/affine/use-invite-member';
|
||||
import { type Member, useMembers } from '../../../hooks/affine/use-members';
|
||||
import { useRevokeMemberPermission } from '../../../hooks/affine/use-revoke-member-permission';
|
||||
import { AnyErrorBoundary } from '../any-error-boundary';
|
||||
import { type WorkspaceSettingDetailProps } from './index';
|
||||
import * as style from './style.css';
|
||||
|
||||
export interface MembersPanelProps extends WorkspaceSettingDetailProps {
|
||||
workspace: AffineOfficialWorkspace;
|
||||
}
|
||||
|
||||
const MembersPanelLocal = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
return (
|
||||
<Tooltip content={t['com.affine.settings.member-tooltip']()}>
|
||||
<div className={style.fakeWrapper}>
|
||||
<SettingRow name={`${t['Members']()} (0)`} desc={t['Members hint']()}>
|
||||
<Button size="large">{t['Invite Members']()}</Button>
|
||||
</SettingRow>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export const CloudWorkspaceMembersPanel = ({
|
||||
workspace,
|
||||
isOwner,
|
||||
}: MembersPanelProps): ReactElement => {
|
||||
const workspaceId = workspace.id;
|
||||
const members = useMembers(workspaceId);
|
||||
const t = useAFFiNEI18N();
|
||||
const currentUser = useCurrentUser();
|
||||
const { invite, isMutating } = useInviteMember(workspaceId);
|
||||
const [open, setOpen] = useState(false);
|
||||
const pushNotification = useSetAtom(pushNotificationAtom);
|
||||
const revokeMemberPermission = useRevokeMemberPermission(workspaceId);
|
||||
|
||||
const memberCount = members.length;
|
||||
const memberList = useMemo(
|
||||
() =>
|
||||
members.sort((a, b) => {
|
||||
if (
|
||||
a.permission === Permission.Owner &&
|
||||
b.permission !== Permission.Owner
|
||||
) {
|
||||
return -1;
|
||||
}
|
||||
if (
|
||||
a.permission !== Permission.Owner &&
|
||||
b.permission === Permission.Owner
|
||||
) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}),
|
||||
[members]
|
||||
);
|
||||
|
||||
const openModal = useCallback(() => {
|
||||
setOpen(true);
|
||||
}, []);
|
||||
|
||||
const onInviteConfirm = useCallback<InviteModalProps['onConfirm']>(
|
||||
async ({ email, permission }) => {
|
||||
const success = await invite(
|
||||
email,
|
||||
permission,
|
||||
// send invite email
|
||||
true
|
||||
);
|
||||
if (success) {
|
||||
pushNotification({
|
||||
title: t['Invitation sent'](),
|
||||
message: t['Invitation sent hint'](),
|
||||
type: 'success',
|
||||
});
|
||||
setOpen(false);
|
||||
}
|
||||
},
|
||||
[invite, pushNotification, t]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingRow
|
||||
name={`${t['Members']()} (${memberCount})`}
|
||||
desc={t['Members hint']()}
|
||||
>
|
||||
{isOwner ? (
|
||||
<>
|
||||
<Button onClick={openModal}>{t['Invite Members']()}</Button>
|
||||
<InviteModal
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
onConfirm={onInviteConfirm}
|
||||
isMutating={isMutating}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</SettingRow>
|
||||
<div className={style.membersList}>
|
||||
{memberList.map(member => (
|
||||
<MemberItem
|
||||
key={member.id}
|
||||
member={member}
|
||||
isOwner={isOwner}
|
||||
currentUser={currentUser}
|
||||
onRevoke={revokeMemberPermission}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const MemberItem = ({
|
||||
member,
|
||||
isOwner,
|
||||
currentUser,
|
||||
onRevoke,
|
||||
}: {
|
||||
member: Member;
|
||||
isOwner: boolean;
|
||||
currentUser: CheckedUser;
|
||||
onRevoke: (memberId: string) => void;
|
||||
}) => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const handleRevoke = useCallback(() => {
|
||||
onRevoke(member.id);
|
||||
}, [onRevoke, member.id]);
|
||||
|
||||
const operationButtonInfo = useMemo(() => {
|
||||
return {
|
||||
show: isOwner && currentUser.id !== member.id,
|
||||
leaveOrRevokeText: t['Remove from workspace'](),
|
||||
};
|
||||
}, [currentUser.id, isOwner, member.id, t]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div key={member.id} className={style.listItem}>
|
||||
<Avatar
|
||||
size={36}
|
||||
url={member.avatarUrl}
|
||||
name={(member.emailVerified ? member.name : member.email) as string}
|
||||
/>
|
||||
<div className={style.memberContainer}>
|
||||
{member.emailVerified ? (
|
||||
<>
|
||||
<div className={style.memberName}>{member.name}</div>
|
||||
<div className={style.memberEmail}>{member.email}</div>
|
||||
</>
|
||||
) : (
|
||||
<div className={style.memberName}>{member.email}</div>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={clsx(style.roleOrStatus, {
|
||||
pending: !member.accepted,
|
||||
})}
|
||||
>
|
||||
{member.accepted
|
||||
? member.permission === Permission.Owner
|
||||
? 'Workspace Owner'
|
||||
: 'Member'
|
||||
: 'Pending'}
|
||||
</div>
|
||||
<Menu
|
||||
content={
|
||||
<MenuItem data-member-id={member.id} onClick={handleRevoke}>
|
||||
{operationButtonInfo.leaveOrRevokeText}
|
||||
</MenuItem>
|
||||
}
|
||||
placement="bottom"
|
||||
disablePortal={true}
|
||||
trigger="click"
|
||||
>
|
||||
<IconButton
|
||||
disabled={!operationButtonInfo.show}
|
||||
style={{
|
||||
visibility: operationButtonInfo.show ? 'visible' : 'hidden',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<MoreVerticalIcon />
|
||||
</IconButton>
|
||||
</Menu>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const MembersPanel = (props: MembersPanelProps): ReactElement | null => {
|
||||
if (props.workspace.flavour === WorkspaceFlavour.LOCAL) {
|
||||
return <MembersPanelLocal />;
|
||||
}
|
||||
return (
|
||||
<ErrorBoundary FallbackComponent={AnyErrorBoundary}>
|
||||
<Suspense>
|
||||
<CloudWorkspaceMembersPanel {...props} />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
@@ -1,21 +1,23 @@
|
||||
import { FlexWrapper, Input, toast, Wrapper } from '@affine/component';
|
||||
import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
|
||||
import type { AffineOfficialWorkspace } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { CameraIcon, DoneIcon } from '@blocksuite/icons';
|
||||
import { IconButton } from '@toeverything/components/button';
|
||||
import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-block-suite-workspace-avatar-url';
|
||||
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
|
||||
import clsx from 'clsx';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import type { AffineOfficialWorkspace } from '../../../shared';
|
||||
import { Upload } from '../../pure/file-upload';
|
||||
import { type WorkspaceSettingDetailProps } from './index';
|
||||
import * as style from './style.css';
|
||||
|
||||
interface ProfilePanelProps {
|
||||
export interface ProfilePanelProps extends WorkspaceSettingDetailProps {
|
||||
workspace: AffineOfficialWorkspace;
|
||||
}
|
||||
|
||||
export const ProfilePanel = ({ workspace }: ProfilePanelProps) => {
|
||||
export const ProfilePanel = ({ workspace, isOwner }: ProfilePanelProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const [, update] = useBlockSuiteWorkspaceAvatarUrl(
|
||||
@@ -38,7 +40,7 @@ export const ProfilePanel = ({ workspace }: ProfilePanelProps) => {
|
||||
|
||||
return (
|
||||
<div className={style.profileWrapper}>
|
||||
<div className={style.avatarWrapper}>
|
||||
<div className={clsx(style.avatarWrapper, { disable: !isOwner })}>
|
||||
<Upload
|
||||
accept="image/gif,image/jpeg,image/jpg,image/png,image/svg"
|
||||
fileChange={update}
|
||||
@@ -59,6 +61,7 @@ export const ProfilePanel = ({ workspace }: ProfilePanelProps) => {
|
||||
<div className={style.label}>{t['Workspace Name']()}</div>
|
||||
<FlexWrapper alignItems="center" flexGrow="1">
|
||||
<Input
|
||||
disabled={!isOwner}
|
||||
width={280}
|
||||
height={32}
|
||||
defaultValue={input}
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import { FlexWrapper, Switch } from '@affine/component';
|
||||
import { FlexWrapper, Input, Switch } from '@affine/component';
|
||||
import { SettingRow } from '@affine/component/setting-components';
|
||||
import { Unreachable } from '@affine/env/constant';
|
||||
import type {
|
||||
AffineCloudWorkspace,
|
||||
AffinePublicWorkspace,
|
||||
LocalWorkspace,
|
||||
} from '@affine/env/workspace';
|
||||
import type { AffineOfficialWorkspace } from '@affine/env/workspace';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import { Tooltip } from '@toeverything/components/tooltip';
|
||||
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import type { AffineOfficialWorkspace } from '../../../shared';
|
||||
import { toast } from '../../../utils';
|
||||
import { EnableAffineCloudModal } from '../enable-affine-cloud-modal';
|
||||
import { TmpDisableAffineCloudModal } from '../tmp-disable-affine-cloud-modal';
|
||||
@@ -29,14 +30,16 @@ export interface PublishPanelLocalProps
|
||||
}
|
||||
export interface PublishPanelAffineProps
|
||||
extends Omit<WorkspaceSettingDetailProps, 'workspaceId'> {
|
||||
workspace: AffineCloudWorkspace;
|
||||
workspace: AffineCloudWorkspace | AffinePublicWorkspace;
|
||||
}
|
||||
|
||||
const PublishPanelAffine = (props: PublishPanelAffineProps) => {
|
||||
const { workspace } = props;
|
||||
const t = useAFFiNEI18N();
|
||||
// const toggleWorkspacePublish = useToggleWorkspacePublish(workspace);
|
||||
|
||||
const isPublic = useMemo(() => {
|
||||
return workspace.flavour === WorkspaceFlavour.AFFINE_PUBLIC;
|
||||
}, [workspace]);
|
||||
const [origin, setOrigin] = useState('');
|
||||
const shareUrl = origin + '/public-workspace/' + workspace.id;
|
||||
|
||||
@@ -54,35 +57,35 @@ const PublishPanelAffine = (props: PublishPanelAffineProps) => {
|
||||
}, [shareUrl, t]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ display: 'none' }}>
|
||||
<SettingRow
|
||||
name={t['Publish']()}
|
||||
desc={
|
||||
// workspace.public ? t['Unpublished hint']() : t['Published hint']()
|
||||
'UNFINISHED'
|
||||
}
|
||||
desc={isPublic ? t['Unpublished hint']() : t['Published hint']()}
|
||||
style={{
|
||||
marginBottom: isPublic ? '12px' : '25px',
|
||||
}}
|
||||
>
|
||||
{/* <Switch
|
||||
checked={workspace.public}
|
||||
onChange={checked => toggleWorkspacePublish(checked)}
|
||||
/> */}
|
||||
<Switch
|
||||
checked={isPublic}
|
||||
// onChange={useCallback(value => {
|
||||
// console.log('onChange', value);
|
||||
// }, [])}
|
||||
/>
|
||||
</SettingRow>
|
||||
<FlexWrapper justifyContent="space-between">
|
||||
<Button
|
||||
className={style.urlButton}
|
||||
size="large"
|
||||
onClick={useCallback(() => {
|
||||
window.open(shareUrl, '_blank');
|
||||
}, [shareUrl])}
|
||||
title={shareUrl}
|
||||
>
|
||||
{shareUrl}
|
||||
</Button>
|
||||
<Button size="large" onClick={copyUrl}>
|
||||
{t['Copy']()}
|
||||
</Button>
|
||||
</FlexWrapper>
|
||||
</>
|
||||
{isPublic ? (
|
||||
<FlexWrapper justifyContent="space-between" marginBottom={25}>
|
||||
<Input value={shareUrl} disabled />
|
||||
<Button
|
||||
onClick={copyUrl}
|
||||
style={{
|
||||
marginLeft: '20px',
|
||||
}}
|
||||
>
|
||||
{t['Copy']()}
|
||||
</Button>
|
||||
</FlexWrapper>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -164,7 +167,10 @@ const PublishPanelLocal = ({
|
||||
};
|
||||
|
||||
export const PublishPanel = (props: PublishPanelProps) => {
|
||||
if (props.workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD) {
|
||||
if (
|
||||
props.workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD ||
|
||||
props.workspace.flavour === WorkspaceFlavour.AFFINE_PUBLIC
|
||||
) {
|
||||
return <PublishPanelAffine {...props} workspace={props.workspace} />;
|
||||
} else if (props.workspace.flavour === WorkspaceFlavour.LOCAL) {
|
||||
return <PublishPanelLocal {...props} workspace={props.workspace} />;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { FlexWrapper, toast } from '@affine/component';
|
||||
import { SettingRow } from '@affine/component/setting-components';
|
||||
import type { AffineOfficialWorkspace } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import { Tooltip } from '@toeverything/components/tooltip';
|
||||
@@ -7,9 +8,6 @@ import type { MoveDBFileResult } from '@toeverything/infra/type';
|
||||
import { useMemo } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import type { AffineOfficialWorkspace } from '../../../shared';
|
||||
import * as style from './style.css';
|
||||
|
||||
const useDBFileSecondaryPath = (workspaceId: string) => {
|
||||
const [path, setPath] = useState<string | undefined>(undefined);
|
||||
useEffect(() => {
|
||||
@@ -83,7 +81,7 @@ export const StoragePanel = ({ workspace }: StoragePanelProps) => {
|
||||
>
|
||||
<Button
|
||||
data-testid="move-folder"
|
||||
className={style.urlButton}
|
||||
// className={style.urlButton}
|
||||
size="large"
|
||||
onClick={handleMoveTo}
|
||||
>
|
||||
|
||||
@@ -5,6 +5,12 @@ export const profileWrapper = style({
|
||||
alignItems: 'flex-end',
|
||||
marginTop: '12px',
|
||||
});
|
||||
export const profileHandlerWrapper = style({
|
||||
flexGrow: '1',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
marginLeft: '20px',
|
||||
});
|
||||
|
||||
export const avatarWrapper = style({
|
||||
width: '56px',
|
||||
@@ -24,6 +30,9 @@ export const avatarWrapper = style({
|
||||
globalStyle(`${avatarWrapper}:hover .camera-icon-wrapper`, {
|
||||
display: 'flex',
|
||||
});
|
||||
globalStyle(`${avatarWrapper}:hover .camera-icon-wrapper`, {
|
||||
display: 'flex',
|
||||
});
|
||||
globalStyle(`${avatarWrapper} .camera-icon-wrapper`, {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
@@ -68,6 +77,70 @@ export const fakeWrapper = style({
|
||||
},
|
||||
});
|
||||
|
||||
export const membersList = style({
|
||||
marginTop: '24px',
|
||||
padding: '4px',
|
||||
borderRadius: '12px',
|
||||
background: 'var(--affine-background-primary-color)',
|
||||
maxHeight: '464px',
|
||||
overflow: 'hidden',
|
||||
});
|
||||
|
||||
export const listItem = style({
|
||||
padding: '0 4px 0 16px',
|
||||
height: '58px',
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
':hover': {
|
||||
background: 'var(--affine-hover-color)',
|
||||
borderRadius: '8px',
|
||||
},
|
||||
});
|
||||
export const memberContainer = style({
|
||||
width: '250px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flexShrink: 0,
|
||||
marginLeft: '12px',
|
||||
marginRight: '20px',
|
||||
});
|
||||
export const roleOrStatus = style({
|
||||
// width: '20%',
|
||||
flexGrow: 1,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
selectors: {
|
||||
'&.pending': {
|
||||
color: 'var(--affine-primary-color)',
|
||||
},
|
||||
},
|
||||
});
|
||||
export const memberName = style({
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
lineHeight: '22px',
|
||||
});
|
||||
export const memberEmail = style({
|
||||
fontSize: 'var(--affine-font-xs)',
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
lineHeight: '20px',
|
||||
});
|
||||
export const iconButton = style({});
|
||||
|
||||
globalStyle(`${listItem}:hover ${iconButton}`, {
|
||||
opacity: 1,
|
||||
pointerEvents: 'all',
|
||||
});
|
||||
|
||||
export const label = style({
|
||||
fontSize: 'var(--affine-font-xs)',
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
|
||||
@@ -1,3 +1,185 @@
|
||||
export const AccountSetting = () => {
|
||||
return <div>AccountSetting</div>;
|
||||
import { FlexWrapper, Input } from '@affine/component';
|
||||
import {
|
||||
SettingHeader,
|
||||
SettingRow,
|
||||
} from '@affine/component/setting-components';
|
||||
import { UserAvatar } from '@affine/component/user-avatar';
|
||||
import { uploadAvatarMutation } from '@affine/graphql';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useMutation } from '@affine/workspace/affine/gql';
|
||||
import { ArrowRightSmallIcon, CameraIcon, DoneIcon } from '@blocksuite/icons';
|
||||
import { Button, IconButton } from '@toeverything/components/button';
|
||||
import { useAtom } from 'jotai/index';
|
||||
import { signOut } from 'next-auth/react';
|
||||
import { type FC, useCallback, useState } from 'react';
|
||||
|
||||
import { authAtom } from '../../../../atoms';
|
||||
import { useCurrentUser } from '../../../../hooks/affine/use-current-user';
|
||||
import { Upload } from '../../../pure/file-upload';
|
||||
import * as style from './style.css';
|
||||
|
||||
export const AvatarAndName = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const user = useCurrentUser();
|
||||
|
||||
const [input, setInput] = useState<string>(user.name);
|
||||
|
||||
const { trigger: avatarTrigger } = useMutation({
|
||||
mutation: uploadAvatarMutation,
|
||||
});
|
||||
|
||||
const handleUpdateUserName = useCallback(
|
||||
(newName: string) => {
|
||||
user.update({ name: newName }).catch(console.error);
|
||||
},
|
||||
[user]
|
||||
);
|
||||
|
||||
const handleUpdateUserAvatar = useCallback(
|
||||
async (file: File) => {
|
||||
await avatarTrigger({
|
||||
id: user.id,
|
||||
avatar: file,
|
||||
});
|
||||
// XXX: This is a hack to force the user to update, since next-auth can not only use update function without params
|
||||
user.update({ name: user.name }).catch(console.error);
|
||||
},
|
||||
[avatarTrigger, user]
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<SettingRow
|
||||
name={t['com.affine.settings.profile']()}
|
||||
desc={t['com.affine.settings.profile.message']()}
|
||||
spreadCol={false}
|
||||
>
|
||||
<FlexWrapper style={{ margin: '12px 0 24px 0' }} alignItems="center">
|
||||
<div className={style.avatarWrapper}>
|
||||
<Upload
|
||||
accept="image/gif,image/jpeg,image/jpg,image/png,image/svg"
|
||||
fileChange={handleUpdateUserAvatar}
|
||||
data-testid="upload-user-avatar"
|
||||
>
|
||||
<>
|
||||
<div className="camera-icon-wrapper">
|
||||
<CameraIcon />
|
||||
</div>
|
||||
<UserAvatar
|
||||
size={56}
|
||||
name={user.name}
|
||||
url={user.image}
|
||||
className="avatar"
|
||||
/>
|
||||
</>
|
||||
</Upload>
|
||||
</div>
|
||||
|
||||
<div className={style.profileInputWrapper}>
|
||||
<label>{t['com.affine.settings.profile.name']()}</label>
|
||||
<FlexWrapper alignItems="center">
|
||||
<Input
|
||||
defaultValue={input}
|
||||
data-testid="user-name-input"
|
||||
placeholder={t['com.affine.settings.profile.placeholder']()}
|
||||
maxLength={64}
|
||||
minLength={0}
|
||||
width={280}
|
||||
height={28}
|
||||
onChange={setInput}
|
||||
/>
|
||||
{input && input === user.name ? null : (
|
||||
<IconButton
|
||||
data-testid="save-user-name"
|
||||
onClick={() => {
|
||||
handleUpdateUserName(input);
|
||||
}}
|
||||
style={{
|
||||
color: 'var(--affine-primary-color)',
|
||||
marginLeft: '12px',
|
||||
}}
|
||||
>
|
||||
<DoneIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
</FlexWrapper>
|
||||
</div>
|
||||
</FlexWrapper>
|
||||
</SettingRow>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const AccountSetting: FC = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const user = useCurrentUser();
|
||||
const [, setAuthModal] = useAtom(authAtom);
|
||||
|
||||
const onChangeEmail = useCallback(() => {
|
||||
setAuthModal({
|
||||
openModal: true,
|
||||
state: 'sendEmail',
|
||||
email: user.email,
|
||||
emailType: 'changeEmail',
|
||||
});
|
||||
}, [setAuthModal, user.email]);
|
||||
const onChangePassword = useCallback(() => {
|
||||
setAuthModal({
|
||||
openModal: true,
|
||||
state: 'sendEmail',
|
||||
email: user.email,
|
||||
emailType: 'changePassword',
|
||||
});
|
||||
}, [setAuthModal, user.email]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingHeader
|
||||
title={t['com.affine.setting.account']()}
|
||||
subtitle={t['com.affine.setting.account.message']()}
|
||||
data-testid="account-title"
|
||||
/>
|
||||
<AvatarAndName />
|
||||
<SettingRow name={t['com.affine.settings.email']()} desc={user.email}>
|
||||
<Button onClick={onChangeEmail}>
|
||||
{t['com.affine.settings.email.action']()}
|
||||
</Button>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name={t['com.affine.settings.password']()}
|
||||
desc={t['com.affine.settings.password.message']()}
|
||||
>
|
||||
<Button onClick={onChangePassword}>
|
||||
{user.hasPassword
|
||||
? t['com.affine.settings.password.action.change']()
|
||||
: t['com.affine.settings.password.action.set']()}
|
||||
</Button>
|
||||
</SettingRow>
|
||||
|
||||
<SettingRow
|
||||
name={t[`Sign out`]()}
|
||||
desc={t['com.affine.setting.sign.out.message']()}
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={useCallback(() => {
|
||||
signOut().catch(console.error);
|
||||
}, [])}
|
||||
>
|
||||
<ArrowRightSmallIcon />
|
||||
</SettingRow>
|
||||
{/*<SettingRow*/}
|
||||
{/* name={*/}
|
||||
{/* <span style={{ color: 'var(--affine-warning-color)' }}>*/}
|
||||
{/* {t['com.affine.setting.account.delete']()}*/}
|
||||
{/* </span>*/}
|
||||
{/* }*/}
|
||||
{/* desc={t['com.affine.setting.account.delete.message']()}*/}
|
||||
{/* style={{ cursor: 'pointer' }}*/}
|
||||
{/* onClick={useCallback(() => {*/}
|
||||
{/* toast('Function coming soon');*/}
|
||||
{/* }, [])}*/}
|
||||
{/* testId="delete-account-button"*/}
|
||||
{/*>*/}
|
||||
{/* <ArrowRightSmallIcon />*/}
|
||||
{/*</SettingRow>*/}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { globalStyle, style } from '@vanilla-extract/css';
|
||||
export const profileInputWrapper = style({
|
||||
marginLeft: '20px',
|
||||
});
|
||||
globalStyle(`${profileInputWrapper} label`, {
|
||||
display: 'block',
|
||||
fontSize: 'var(--affine-font-xs)',
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
marginBottom: '4px',
|
||||
});
|
||||
|
||||
export const avatarWrapper = style({
|
||||
width: '56px',
|
||||
height: '56px',
|
||||
borderRadius: '50%',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
cursor: 'pointer',
|
||||
flexShrink: '0',
|
||||
selectors: {
|
||||
'&.disable': {
|
||||
cursor: 'default',
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
},
|
||||
});
|
||||
globalStyle(`${avatarWrapper}:hover .camera-icon-wrapper`, {
|
||||
display: 'flex',
|
||||
});
|
||||
globalStyle(`${avatarWrapper} .camera-icon-wrapper`, {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
display: 'none',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'rgba(60, 61, 63, 0.5)',
|
||||
zIndex: '1',
|
||||
color: 'var(--affine-white)',
|
||||
fontSize: 'var(--affine-font-h-4)',
|
||||
});
|
||||
@@ -7,6 +7,7 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { ContactWithUsIcon } from '@blocksuite/icons';
|
||||
import { Suspense, useCallback } from 'react';
|
||||
|
||||
import { useCurrenLoginStatus } from '../../../hooks/affine/use-curren-login-status';
|
||||
import { AccountSetting } from './account-setting';
|
||||
import {
|
||||
GeneralSetting,
|
||||
@@ -38,6 +39,7 @@ export const SettingModal = ({
|
||||
onSettingClick,
|
||||
}: SettingModalProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const loginStatus = useCurrenLoginStatus();
|
||||
|
||||
const generalSettingList = useGeneralSettingList();
|
||||
|
||||
@@ -85,7 +87,9 @@ export const SettingModal = ({
|
||||
{generalSettingList.find(v => v.key === activeTab) ? (
|
||||
<GeneralSetting generalKey={activeTab as GeneralSettingKeys} />
|
||||
) : null}
|
||||
{activeTab === 'account' ? <AccountSetting /> : null}
|
||||
{activeTab === 'account' && loginStatus === 'authenticated' ? (
|
||||
<AccountSetting />
|
||||
) : null}
|
||||
</div>
|
||||
<div className="footer">
|
||||
<div className={footerIconWrapper}>
|
||||
|
||||
@@ -1,41 +1,101 @@
|
||||
import { ScrollableContainer } from '@affine/component';
|
||||
import {
|
||||
WorkspaceListItemSkeleton,
|
||||
WorkspaceListSkeleton,
|
||||
} from '@affine/component/setting-components';
|
||||
import { UserAvatar } from '@affine/component/user-avatar';
|
||||
import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
|
||||
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
||||
import { Logo1Icon } from '@blocksuite/icons';
|
||||
import { Tooltip } from '@toeverything/components/tooltip';
|
||||
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
|
||||
import { useStaticBlockSuiteWorkspace } from '@toeverything/infra/__internal__/react';
|
||||
import clsx from 'clsx';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { Suspense, useRef } from 'react';
|
||||
import { useAtom, useAtomValue } from 'jotai/react';
|
||||
import {
|
||||
type ReactElement,
|
||||
Suspense,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react';
|
||||
|
||||
import { authAtom } from '../../../../atoms';
|
||||
import { useCurrenLoginStatus } from '../../../../hooks/affine/use-curren-login-status';
|
||||
import { useCurrentUser } from '../../../../hooks/affine/use-current-user';
|
||||
import { useCurrentWorkspace } from '../../../../hooks/current/use-current-workspace';
|
||||
import type {
|
||||
GeneralSettingKeys,
|
||||
GeneralSettingList,
|
||||
} from '../general-setting';
|
||||
import {
|
||||
accountButton,
|
||||
currentWorkspaceLabel,
|
||||
settingSlideBar,
|
||||
sidebarFooter,
|
||||
sidebarItemsWrapper,
|
||||
sidebarSelectItem,
|
||||
sidebarSubtitle,
|
||||
sidebarTitle,
|
||||
} from './style.css';
|
||||
|
||||
interface SettingSidebarProps {
|
||||
generalSettingList: GeneralSettingList;
|
||||
onGeneralSettingClick: (key: GeneralSettingKeys) => void;
|
||||
onWorkspaceSettingClick: (workspaceId: string) => void;
|
||||
selectedWorkspaceId: string | null;
|
||||
selectedGeneralKey: string | null;
|
||||
export type UserInfoProps = {
|
||||
onAccountSettingClick: () => void;
|
||||
}
|
||||
};
|
||||
|
||||
export const UserInfo = ({
|
||||
onAccountSettingClick,
|
||||
}: UserInfoProps): ReactElement => {
|
||||
const user = useCurrentUser();
|
||||
return (
|
||||
<div className={accountButton} onClick={onAccountSettingClick}>
|
||||
<UserAvatar
|
||||
size={28}
|
||||
name={user.name}
|
||||
url={user.image}
|
||||
className="avatar"
|
||||
/>
|
||||
|
||||
<div className="content">
|
||||
<div className="name" title="xxx">
|
||||
{user.name}
|
||||
</div>
|
||||
<div className="email" title="xxx">
|
||||
{user.email}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const SignInButton = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const [, setAuthModal] = useAtom(authAtom);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={accountButton}
|
||||
onClick={useCallback(() => {
|
||||
setAuthModal({ openModal: true, state: 'signIn' });
|
||||
}, [setAuthModal])}
|
||||
>
|
||||
<div className="avatar not-sign">
|
||||
<Logo1Icon />
|
||||
</div>
|
||||
|
||||
<div className="content">
|
||||
<div className="name" title={t['com.affine.settings.sign']()}>
|
||||
{t['com.affine.settings.sign']()}
|
||||
</div>
|
||||
<div className="email" title={t['com.affine.setting.sign.message']()}>
|
||||
{t['com.affine.setting.sign.message']()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const SettingSidebar = ({
|
||||
generalSettingList,
|
||||
@@ -43,9 +103,17 @@ export const SettingSidebar = ({
|
||||
onWorkspaceSettingClick,
|
||||
selectedWorkspaceId,
|
||||
selectedGeneralKey,
|
||||
}: SettingSidebarProps) => {
|
||||
onAccountSettingClick,
|
||||
}: {
|
||||
generalSettingList: GeneralSettingList;
|
||||
onGeneralSettingClick: (key: GeneralSettingKeys) => void;
|
||||
onWorkspaceSettingClick: (workspaceId: string) => void;
|
||||
selectedWorkspaceId: string | null;
|
||||
selectedGeneralKey: string | null;
|
||||
onAccountSettingClick: () => void;
|
||||
}) => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const loginStatus = useCurrenLoginStatus();
|
||||
return (
|
||||
<div className={settingSlideBar} data-testid="settings-sidebar">
|
||||
<div className={sidebarTitle}>{t['Settings']()}</div>
|
||||
@@ -79,32 +147,43 @@ export const SettingSidebar = ({
|
||||
</div>
|
||||
<div className={clsx(sidebarItemsWrapper, 'scroll')}>
|
||||
<Suspense fallback={<WorkspaceListSkeleton />}>
|
||||
<ScrollableContainer>
|
||||
<WorkspaceList
|
||||
onWorkspaceSettingClick={onWorkspaceSettingClick}
|
||||
selectedWorkspaceId={selectedWorkspaceId}
|
||||
/>
|
||||
</ScrollableContainer>
|
||||
<WorkspaceList
|
||||
onWorkspaceSettingClick={onWorkspaceSettingClick}
|
||||
selectedWorkspaceId={selectedWorkspaceId}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
|
||||
<div className={sidebarFooter}>
|
||||
{runtimeConfig.enableCloud && loginStatus === 'unauthenticated' ? (
|
||||
<SignInButton />
|
||||
) : null}
|
||||
|
||||
{runtimeConfig.enableCloud && loginStatus === 'authenticated' ? (
|
||||
<UserInfo onAccountSettingClick={onAccountSettingClick} />
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface WorkspaceListProps {
|
||||
onWorkspaceSettingClick: (workspaceId: string) => void;
|
||||
selectedWorkspaceId: string | null;
|
||||
}
|
||||
|
||||
export const WorkspaceList = ({
|
||||
onWorkspaceSettingClick,
|
||||
selectedWorkspaceId,
|
||||
}: WorkspaceListProps) => {
|
||||
}: {
|
||||
onWorkspaceSettingClick: (workspaceId: string) => void;
|
||||
selectedWorkspaceId: string | null;
|
||||
}) => {
|
||||
const workspaces = useAtomValue(rootWorkspacesMetadataAtom);
|
||||
const [currentWorkspace] = useCurrentWorkspace();
|
||||
const workspaceList = useMemo(() => {
|
||||
return workspaces.filter(
|
||||
({ flavour }) => flavour !== WorkspaceFlavour.AFFINE_PUBLIC
|
||||
);
|
||||
}, [workspaces]);
|
||||
return (
|
||||
<>
|
||||
{workspaces.map(workspace => {
|
||||
{workspaceList.map(workspace => {
|
||||
return (
|
||||
<Suspense key={workspace.id} fallback={<WorkspaceListItemSkeleton />}>
|
||||
<WorkspaceListItem
|
||||
@@ -122,19 +201,17 @@ export const WorkspaceList = ({
|
||||
);
|
||||
};
|
||||
|
||||
interface WorkspaceListItemProps {
|
||||
meta: RootWorkspaceMetadata;
|
||||
onClick: () => void;
|
||||
isCurrent: boolean;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
const WorkspaceListItem = ({
|
||||
meta,
|
||||
onClick,
|
||||
isCurrent,
|
||||
isActive,
|
||||
}: WorkspaceListItemProps) => {
|
||||
}: {
|
||||
meta: RootWorkspaceMetadata;
|
||||
onClick: () => void;
|
||||
isCurrent: boolean;
|
||||
isActive: boolean;
|
||||
}) => {
|
||||
const workspace = useStaticBlockSuiteWorkspace(meta.id);
|
||||
const [workspaceName] = useBlockSuiteWorkspaceName(workspace);
|
||||
const ref = useRef(null);
|
||||
|
||||
@@ -34,7 +34,7 @@ export const sidebarItemsWrapper = style({
|
||||
selectors: {
|
||||
'&.scroll': {
|
||||
flexGrow: 1,
|
||||
overflowY: 'hidden',
|
||||
overflowY: 'auto',
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -92,6 +92,8 @@ export const currentWorkspaceLabel = style({
|
||||
},
|
||||
});
|
||||
|
||||
export const sidebarFooter = style({ padding: '0 16px' });
|
||||
|
||||
export const accountButton = style({
|
||||
height: '42px',
|
||||
padding: '4px 8px',
|
||||
@@ -110,6 +112,20 @@ globalStyle(`${accountButton} .avatar`, {
|
||||
border: '1px solid',
|
||||
borderColor: 'var(--affine-white)',
|
||||
marginRight: '10px',
|
||||
flexShrink: 0,
|
||||
});
|
||||
|
||||
globalStyle(`${accountButton} .avatar.not-sign`, {
|
||||
width: '28px',
|
||||
height: '28px',
|
||||
borderRadius: '50%',
|
||||
fontSize: '22px',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderColor: 'var(--affine-border-color)',
|
||||
color: 'var(--affine-border-color)',
|
||||
background: 'var(--affine-white)',
|
||||
});
|
||||
globalStyle(`${accountButton} .content`, {
|
||||
flexGrow: '1',
|
||||
|
||||
@@ -3,16 +3,17 @@ import { globalStyle, style } from '@vanilla-extract/css';
|
||||
export const settingContent = style({
|
||||
flexGrow: '1',
|
||||
height: '100%',
|
||||
padding: '40px 15px 20px',
|
||||
overflow: 'auto',
|
||||
padding: '40px 15px',
|
||||
overflow: 'hidden',
|
||||
});
|
||||
|
||||
globalStyle(`${settingContent} .wrapper`, {
|
||||
width: '66%',
|
||||
minWidth: '450px',
|
||||
width: '60%',
|
||||
padding: '0 15px',
|
||||
height: '100%',
|
||||
maxWidth: '560px',
|
||||
minWidth: '560px',
|
||||
margin: '0 auto',
|
||||
overflowY: 'auto',
|
||||
});
|
||||
|
||||
globalStyle(`${settingContent} .wrapper::-webkit-scrollbar`, {
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
import { pushNotificationAtom } from '@affine/component/notification-center';
|
||||
import { WorkspaceSubPath } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
||||
import { usePassiveWorkspaceEffect } from '@toeverything/infra/__internal__/react';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { getUIAdapter } from '../../../../adapters/workspace';
|
||||
import { openSettingModalAtom } from '../../../../atoms';
|
||||
import { useLeaveWorkspace } from '../../../../hooks/affine/use-leave-workspace';
|
||||
import { useCurrentWorkspace } from '../../../../hooks/current/use-current-workspace';
|
||||
import { useOnTransformWorkspace } from '../../../../hooks/root/use-on-transform-workspace';
|
||||
import {
|
||||
RouteLogic,
|
||||
@@ -13,28 +20,83 @@ import { useWorkspace } from '../../../../hooks/use-workspace';
|
||||
import { useAppHelper } from '../../../../hooks/use-workspaces';
|
||||
|
||||
export const WorkspaceSetting = ({ workspaceId }: { workspaceId: string }) => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const { jumpToSubPath, jumpToIndex } = useNavigateHelper();
|
||||
const [currentWorkspace] = useCurrentWorkspace();
|
||||
const workspace = useWorkspace(workspaceId);
|
||||
const workspaces = useAtomValue(rootWorkspacesMetadataAtom);
|
||||
const pushNotification = useSetAtom(pushNotificationAtom);
|
||||
|
||||
const leaveWorkspace = useLeaveWorkspace();
|
||||
usePassiveWorkspaceEffect(workspace.blockSuiteWorkspace);
|
||||
const setSettingModal = useSetAtom(openSettingModalAtom);
|
||||
const helper = useAppHelper();
|
||||
const { jumpToIndex } = useNavigateHelper();
|
||||
const { deleteWorkspace } = useAppHelper();
|
||||
|
||||
const { NewSettingsDetail } = getUIAdapter(workspace.flavour);
|
||||
|
||||
const onDeleteWorkspace = useCallback(
|
||||
async (id: string) => {
|
||||
await helper.deleteWorkspace(id);
|
||||
setSettingModal(prev => ({ ...prev, open: false, workspaceId: null }));
|
||||
jumpToIndex(RouteLogic.REPLACE);
|
||||
},
|
||||
[helper, jumpToIndex, setSettingModal]
|
||||
);
|
||||
const closeAndJumpOut = useCallback(() => {
|
||||
setSettingModal(prev => ({ ...prev, open: false, workspaceId: null }));
|
||||
|
||||
if (currentWorkspace.id === workspaceId) {
|
||||
const backWorkspace = workspaces.find(ws => ws.id !== workspaceId);
|
||||
// TODO: if there is no workspace, jump to a new page(wait for design)
|
||||
if (backWorkspace) {
|
||||
jumpToSubPath(
|
||||
backWorkspace?.id || '',
|
||||
WorkspaceSubPath.ALL,
|
||||
RouteLogic.REPLACE
|
||||
);
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
jumpToIndex(RouteLogic.REPLACE);
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
currentWorkspace.id,
|
||||
jumpToIndex,
|
||||
jumpToSubPath,
|
||||
setSettingModal,
|
||||
workspaceId,
|
||||
workspaces,
|
||||
]);
|
||||
|
||||
const handleDeleteWorkspace = useCallback(async () => {
|
||||
closeAndJumpOut();
|
||||
await deleteWorkspace(workspaceId);
|
||||
|
||||
pushNotification({
|
||||
title: t['Successfully deleted'](),
|
||||
type: 'success',
|
||||
});
|
||||
}, [closeAndJumpOut, deleteWorkspace, pushNotification, t, workspaceId]);
|
||||
|
||||
const handleLeaveWorkspace = useCallback(async () => {
|
||||
closeAndJumpOut();
|
||||
await leaveWorkspace(workspaceId);
|
||||
|
||||
pushNotification({
|
||||
title: 'Successfully leave',
|
||||
type: 'success',
|
||||
});
|
||||
}, [closeAndJumpOut, leaveWorkspace, pushNotification, workspaceId]);
|
||||
|
||||
const onTransformWorkspace = useOnTransformWorkspace();
|
||||
// const handleDelete = useCallback(async () => {
|
||||
// await onDeleteWorkspace();
|
||||
// toast(t['Successfully deleted'](), {
|
||||
// portal: document.body,
|
||||
// });
|
||||
// onClose();
|
||||
// }, [onClose, onDeleteWorkspace, t, workspace.id]);
|
||||
|
||||
return (
|
||||
<NewSettingsDetail
|
||||
onDeleteCloudWorkspace={handleDeleteWorkspace}
|
||||
onDeleteLocalWorkspace={handleDeleteWorkspace}
|
||||
onLeaveWorkspace={handleLeaveWorkspace}
|
||||
onTransformWorkspace={onTransformWorkspace}
|
||||
onDeleteWorkspace={onDeleteWorkspace}
|
||||
currentWorkspaceId={workspaceId}
|
||||
/>
|
||||
);
|
||||
|
||||
48
apps/core/src/components/affine/share-page-modal/index.tsx
Normal file
48
apps/core/src/components/affine/share-page-modal/index.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { ShareMenu } from '@affine/component/share-menu';
|
||||
import {
|
||||
type AffineOfficialWorkspace,
|
||||
WorkspaceFlavour,
|
||||
} from '@affine/env/workspace';
|
||||
import type { Page } from '@blocksuite/store';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { useIsSharedPage } from '../../../hooks/affine/use-is-shared-page';
|
||||
import { useOnTransformWorkspace } from '../../../hooks/root/use-on-transform-workspace';
|
||||
import { EnableAffineCloudModal } from '../enable-affine-cloud-modal';
|
||||
|
||||
type SharePageModalProps = {
|
||||
workspace: AffineOfficialWorkspace;
|
||||
page: Page;
|
||||
};
|
||||
|
||||
export const SharePageModal = ({ workspace, page }: SharePageModalProps) => {
|
||||
const onTransformWorkspace = useOnTransformWorkspace();
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<ShareMenu
|
||||
workspace={workspace}
|
||||
currentPage={page}
|
||||
useIsSharedPage={useIsSharedPage}
|
||||
onEnableAffineCloud={() => setOpen(true)}
|
||||
togglePagePublic={async () => {}}
|
||||
/>
|
||||
{workspace.flavour === WorkspaceFlavour.LOCAL ? (
|
||||
<EnableAffineCloudModal
|
||||
open={open}
|
||||
onClose={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
onConfirm={() => {
|
||||
onTransformWorkspace(
|
||||
WorkspaceFlavour.LOCAL,
|
||||
WorkspaceFlavour.AFFINE_CLOUD,
|
||||
workspace
|
||||
);
|
||||
setOpen(false);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { AffineOfficialWorkspace } from '@affine/env/workspace';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import {
|
||||
useBlockSuitePageMeta,
|
||||
@@ -12,7 +13,6 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import type { AffineOfficialWorkspace } from '../../../shared';
|
||||
import { EditorModeSwitch } from '../block-suite-mode-switch';
|
||||
import { PageMenu } from './operation-menu';
|
||||
import * as styles from './styles.css';
|
||||
@@ -139,7 +139,7 @@ const BlockSuiteTitleWithRename = (props: BlockSuiteHeaderTitleProps) => {
|
||||
};
|
||||
|
||||
export const BlockSuiteHeaderTitle = (props: BlockSuiteHeaderTitleProps) => {
|
||||
if (props.workspace.flavour === WorkspaceFlavour.PUBLIC) {
|
||||
if (props.workspace.flavour === WorkspaceFlavour.AFFINE_PUBLIC) {
|
||||
return <StableTitle {...props} />;
|
||||
}
|
||||
return <BlockSuiteTitleWithRename {...props} />;
|
||||
|
||||
53
apps/core/src/components/cloud/login-card.tsx
Normal file
53
apps/core/src/components/cloud/login-card.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import { UserAvatar } from '@affine/component/user-avatar';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { CloudWorkspaceIcon } from '@blocksuite/icons';
|
||||
import { signIn } from 'next-auth/react';
|
||||
|
||||
import { useCurrenLoginStatus } from '../../hooks/affine/use-curren-login-status';
|
||||
import { useCurrentUser } from '../../hooks/affine/use-current-user';
|
||||
import { StyledSignInButton } from '../pure/footer/styles';
|
||||
|
||||
export const LoginCard = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const loginStatus = useCurrenLoginStatus();
|
||||
if (loginStatus === 'authenticated') {
|
||||
return <UserCard />;
|
||||
}
|
||||
return (
|
||||
<StyledSignInButton
|
||||
data-testid="sign-in-button"
|
||||
onClick={async () => {
|
||||
// jump to login page
|
||||
signIn().catch(console.error);
|
||||
}}
|
||||
>
|
||||
<div className="circle">
|
||||
<CloudWorkspaceIcon />
|
||||
</div>{' '}
|
||||
{t['Sign in']()}
|
||||
</StyledSignInButton>
|
||||
);
|
||||
};
|
||||
|
||||
const UserCard = () => {
|
||||
const user = useCurrentUser();
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<UserAvatar
|
||||
size={28}
|
||||
name={user.name}
|
||||
url={user.image}
|
||||
className="avatar"
|
||||
/>
|
||||
<div style={{ marginLeft: '15px' }}>
|
||||
<div>{user.name}</div>
|
||||
<div>{user.email}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
58
apps/core/src/components/cloud/provider.tsx
Normal file
58
apps/core/src/components/cloud/provider.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import { pushNotificationAtom } from '@affine/component/notification-center';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import { GraphQLError } from 'graphql';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import type { PropsWithChildren, ReactElement } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import type { SWRConfiguration } from 'swr';
|
||||
import { SWRConfig } from 'swr';
|
||||
|
||||
const cloudConfig: SWRConfiguration = {
|
||||
suspense: true,
|
||||
use: [
|
||||
useSWRNext => (key, fetcher, config) => {
|
||||
const pushNotification = useSetAtom(pushNotificationAtom);
|
||||
const fetcherWrapper = useCallback(
|
||||
async (...args: any[]) => {
|
||||
assertExists(fetcher);
|
||||
const d = fetcher(...args);
|
||||
if (d instanceof Promise) {
|
||||
return d.catch(e => {
|
||||
if (
|
||||
e instanceof GraphQLError ||
|
||||
(Array.isArray(e) && e[0] instanceof GraphQLError)
|
||||
) {
|
||||
const graphQLError = e instanceof GraphQLError ? e : e[0];
|
||||
pushNotification({
|
||||
title: 'GraphQL Error',
|
||||
message: graphQLError.toString(),
|
||||
key: Date.now().toString(),
|
||||
type: 'error',
|
||||
});
|
||||
} else {
|
||||
pushNotification({
|
||||
title: 'Error',
|
||||
message: e.toString(),
|
||||
key: Date.now().toString(),
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
return d;
|
||||
},
|
||||
[fetcher, pushNotification]
|
||||
);
|
||||
return useSWRNext(key, fetcher ? fetcherWrapper : fetcher, config);
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const Provider = (props: PropsWithChildren): ReactElement => {
|
||||
if (!runtimeConfig.enableCloud) {
|
||||
return <>{props.children}</>;
|
||||
}
|
||||
|
||||
return <SWRConfig value={cloudConfig}>{props.children}</SWRConfig>;
|
||||
};
|
||||
@@ -1,39 +1,42 @@
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { CloudWorkspaceIcon } from '@blocksuite/icons';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { type CSSProperties, forwardRef } from 'react';
|
||||
import { signIn } from 'next-auth/react';
|
||||
import { type CSSProperties, type FC, forwardRef, useCallback } from 'react';
|
||||
|
||||
import { openDisableCloudAlertModalAtom } from '../../../atoms';
|
||||
import { useCurrenLoginStatus } from '../../../hooks/affine/use-curren-login-status';
|
||||
// import { openDisableCloudAlertModalAtom } from '../../../atoms';
|
||||
import { stringToColour } from '../../../utils';
|
||||
import { StyledFooter } from './styles';
|
||||
|
||||
export const Footer = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const setOpen = useSetAtom(openDisableCloudAlertModalAtom);
|
||||
import { StyledFooter, StyledSignInButton } from './styles';
|
||||
export const Footer: FC = () => {
|
||||
const loginStatus = useCurrenLoginStatus();
|
||||
|
||||
// const setOpen = useSetAtom(openDisableCloudAlertModalAtom);
|
||||
return (
|
||||
<StyledFooter data-testid="workspace-list-modal-footer">
|
||||
<Button
|
||||
data-testid="sign-in-button"
|
||||
type="plain"
|
||||
icon={
|
||||
<CloudWorkspaceIcon
|
||||
style={{ color: 'var(--affine-primary-color)' }}
|
||||
/>
|
||||
}
|
||||
onClick={async () => {
|
||||
if (!runtimeConfig.enableCloud) {
|
||||
setOpen(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t['Sign in']()}
|
||||
</Button>
|
||||
{loginStatus === 'authenticated' ? null : <SignInButton />}
|
||||
</StyledFooter>
|
||||
);
|
||||
};
|
||||
|
||||
const SignInButton = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
return (
|
||||
<StyledSignInButton
|
||||
data-testid="sign-in-button"
|
||||
onClick={useCallback(() => {
|
||||
signIn().catch(console.error);
|
||||
}, [])}
|
||||
>
|
||||
<div className="circle">
|
||||
<CloudWorkspaceIcon />
|
||||
</div>
|
||||
|
||||
{t['Sign in']()}
|
||||
</StyledSignInButton>
|
||||
);
|
||||
};
|
||||
|
||||
interface WorkspaceAvatarProps {
|
||||
size: number;
|
||||
name: string | undefined;
|
||||
|
||||
@@ -1,4 +1,19 @@
|
||||
import { displayFlex, styled, textEllipsis } from '@affine/component';
|
||||
import {
|
||||
displayFlex,
|
||||
displayInlineFlex,
|
||||
styled,
|
||||
textEllipsis,
|
||||
} from '@affine/component';
|
||||
|
||||
export const StyledSplitLine = styled('div')(() => {
|
||||
return {
|
||||
width: '1px',
|
||||
height: '20px',
|
||||
background: 'var(--affine-border-color)',
|
||||
marginRight: '24px',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyleWorkspaceInfo = styled('div')(() => {
|
||||
return {
|
||||
marginLeft: '15px',
|
||||
@@ -110,3 +125,28 @@ export const StyledModalHeader = styled('div')(() => {
|
||||
...displayFlex('space-between', 'center'),
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledSignInButton = styled('button')(() => {
|
||||
return {
|
||||
fontWeight: 600,
|
||||
paddingLeft: 0,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingRight: '15px',
|
||||
borderRadius: '8px',
|
||||
'&:hover': {
|
||||
backgroundColor: 'var(--affine-hover-color)',
|
||||
},
|
||||
'.circle': {
|
||||
width: '40px',
|
||||
height: '40px',
|
||||
borderRadius: '20px',
|
||||
color: 'var(--affine-primary-color)',
|
||||
fontSize: '24px',
|
||||
flexShrink: 0,
|
||||
marginRight: '16px',
|
||||
...displayInlineFlex('center', 'center'),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -8,19 +8,27 @@ import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
|
||||
import {
|
||||
AccountIcon,
|
||||
CloudWorkspaceIcon,
|
||||
ImportIcon,
|
||||
MoreHorizontalIcon,
|
||||
PlusIcon,
|
||||
SignOutIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import type { DragEndEvent } from '@dnd-kit/core';
|
||||
import { Popover } from '@mui/material';
|
||||
import { IconButton } from '@toeverything/components/button';
|
||||
import { Divider } from '@toeverything/components/divider';
|
||||
import { useSetAtom } from 'jotai';
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
import { signOut, useSession } from 'next-auth/react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { openDisableCloudAlertModalAtom } from '../../../atoms';
|
||||
import {
|
||||
authAtom,
|
||||
openDisableCloudAlertModalAtom,
|
||||
openSettingModalAtom,
|
||||
} from '../../../atoms';
|
||||
import type { AllWorkspace } from '../../../shared';
|
||||
import {
|
||||
StyledCreateWorkspaceCardPill,
|
||||
@@ -39,6 +47,7 @@ import {
|
||||
StyledSignInCardPillTextCotainer,
|
||||
StyledSignInCardPillTextPrimary,
|
||||
StyledSignInCardPillTextSecondary,
|
||||
StyledWorkspaceFlavourTitle,
|
||||
} from './styles';
|
||||
|
||||
interface WorkspaceModalProps {
|
||||
@@ -56,18 +65,31 @@ interface WorkspaceModalProps {
|
||||
|
||||
const AccountMenu = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const setOpen = useSetAtom(openSettingModalAtom);
|
||||
return (
|
||||
<div>
|
||||
<div>Unlimted</div>
|
||||
{/* <div>Unlimted</div>
|
||||
<Divider size="thinner" dividerColor="var(--affine-border-color)" />
|
||||
<MenuItem icon={<ImportIcon />} data-testid="editor-option-menu-import">
|
||||
{t['com.affine.workspace.cloud.join']()}
|
||||
</MenuItem>
|
||||
<MenuItem icon={<ImportIcon />} data-testid="editor-option-menu-import">
|
||||
</MenuItem> */}
|
||||
<MenuItem
|
||||
icon={<AccountIcon />}
|
||||
data-testid="editor-option-menu-import"
|
||||
onClick={useCallback(() => {
|
||||
setOpen(prev => ({ ...prev, open: true, activeTab: 'account' }));
|
||||
}, [setOpen])}
|
||||
>
|
||||
{t['com.affine.workspace.cloud.account.settings']()}
|
||||
</MenuItem>
|
||||
<Divider size="thinner" dividerColor="var(--affine-border-color)" />
|
||||
<MenuItem icon={<ImportIcon />} data-testid="editor-option-menu-import">
|
||||
<Divider />
|
||||
<MenuItem
|
||||
icon={<SignOutIcon />}
|
||||
data-testid="editor-option-menu-import"
|
||||
onClick={useCallback(() => {
|
||||
signOut().catch(console.error);
|
||||
}, [])}
|
||||
>
|
||||
{t['com.affine.workspace.cloud.account.logout']()}
|
||||
</MenuItem>
|
||||
</div>
|
||||
@@ -89,31 +111,16 @@ const CloudWorkSpaceList = ({
|
||||
<StyledModalHeader>
|
||||
<StyledModalHeaderLeft>
|
||||
<StyledModalTitle>
|
||||
{t['com.affine.workspace.cloud.sync']()}
|
||||
{t['com.affine.workspace.cloud']()}
|
||||
</StyledModalTitle>
|
||||
</StyledModalHeaderLeft>
|
||||
|
||||
<StyledOperationWrapper>
|
||||
<Menu
|
||||
placement="bottom-end"
|
||||
trigger={['click']}
|
||||
content={<AccountMenu />}
|
||||
zIndex={1000}
|
||||
>
|
||||
<IconButton
|
||||
data-testid="previous-image-button"
|
||||
icon={<MoreHorizontalIcon />}
|
||||
type="plain"
|
||||
/>
|
||||
</Menu>
|
||||
</StyledOperationWrapper>
|
||||
</StyledModalHeader>
|
||||
<StyledModalContent>
|
||||
<WorkspaceList
|
||||
disabled={disabled}
|
||||
items={
|
||||
workspaces.filter(
|
||||
({ flavour }) => flavour !== WorkspaceFlavour.PUBLIC
|
||||
({ flavour }) => flavour === WorkspaceFlavour.AFFINE_CLOUD
|
||||
) as (AffineCloudWorkspace | LocalWorkspace)[]
|
||||
}
|
||||
currentWorkspaceId={currentWorkspaceId}
|
||||
@@ -129,7 +136,6 @@ const CloudWorkSpaceList = ({
|
||||
[onMoveWorkspace]
|
||||
)}
|
||||
/>
|
||||
<Divider size="thinner" dividerColor="var(--affine-border-color)" />
|
||||
</StyledModalContent>
|
||||
</>
|
||||
);
|
||||
@@ -148,11 +154,18 @@ export const WorkspaceListModal = ({
|
||||
onMoveWorkspace,
|
||||
}: WorkspaceModalProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const setOpen = useSetAtom(openDisableCloudAlertModalAtom);
|
||||
const setOpen = useSetAtom(authAtom);
|
||||
const setDisableCloudOpen = useSetAtom(openDisableCloudAlertModalAtom);
|
||||
// TODO: AFFiNE Cloud support
|
||||
const isLoggedIn = false;
|
||||
const { data: session, status } = useSession();
|
||||
const isLoggedIn = status === 'authenticated' ? true : false;
|
||||
const anchorEl = document.getElementById('current-workspace');
|
||||
|
||||
const cloudWorkspaces = workspaces.filter(
|
||||
({ flavour }) => flavour === WorkspaceFlavour.AFFINE_CLOUD
|
||||
) as (AffineCloudWorkspace | LocalWorkspace)[];
|
||||
const localWorkspaces = workspaces.filter(
|
||||
({ flavour }) => flavour === WorkspaceFlavour.LOCAL
|
||||
) as (AffineCloudWorkspace | LocalWorkspace)[];
|
||||
return (
|
||||
<Popover
|
||||
sx={{
|
||||
@@ -164,6 +177,7 @@ export const WorkspaceListModal = ({
|
||||
flexDirection: 'column',
|
||||
boxShadow: 'var(--affine-shadow-2)',
|
||||
backgroundColor: 'var(--affine-background-overlay-panel-color)',
|
||||
padding: '16px 12px',
|
||||
},
|
||||
maxHeight: '90vh',
|
||||
}}
|
||||
@@ -171,63 +185,92 @@ export const WorkspaceListModal = ({
|
||||
anchorEl={anchorEl}
|
||||
onClose={onClose}
|
||||
>
|
||||
<StyledModalHeaderContent>
|
||||
<StyledSignInCardPill>
|
||||
<MenuItem
|
||||
style={{
|
||||
height: 'auto',
|
||||
padding: '8px 12px',
|
||||
}}
|
||||
onClick={async () => {
|
||||
if (!runtimeConfig.enableCloud) {
|
||||
setOpen(true);
|
||||
}
|
||||
}}
|
||||
data-testid="cloud-signin-button"
|
||||
>
|
||||
<StyledCreateWorkspaceCardPillContent>
|
||||
<StyledCreateWorkspaceCardPillIcon>
|
||||
<CloudWorkspaceIcon />
|
||||
</StyledCreateWorkspaceCardPillIcon>
|
||||
<StyledSignInCardPillTextCotainer>
|
||||
<StyledSignInCardPillTextPrimary>
|
||||
{t['com.affine.workspace.cloud.auth']()}
|
||||
</StyledSignInCardPillTextPrimary>
|
||||
<StyledSignInCardPillTextSecondary>
|
||||
Sync with AFFiNE Cloud
|
||||
</StyledSignInCardPillTextSecondary>
|
||||
</StyledSignInCardPillTextCotainer>
|
||||
</StyledCreateWorkspaceCardPillContent>
|
||||
</MenuItem>
|
||||
</StyledSignInCardPill>
|
||||
<Divider size="thinner" dividerColor="var(--affine-border-color)" />
|
||||
</StyledModalHeaderContent>
|
||||
{!isLoggedIn ? (
|
||||
<StyledModalHeaderContent>
|
||||
<StyledSignInCardPill>
|
||||
<MenuItem
|
||||
style={{
|
||||
height: 'auto',
|
||||
padding: '0px 12px',
|
||||
}}
|
||||
onClick={async () => {
|
||||
if (!runtimeConfig.enableCloud) {
|
||||
setDisableCloudOpen(true);
|
||||
} else {
|
||||
setOpen(state => ({
|
||||
...state,
|
||||
openModal: true,
|
||||
}));
|
||||
}
|
||||
}}
|
||||
data-testid="cloud-signin-button"
|
||||
>
|
||||
<StyledCreateWorkspaceCardPillContent>
|
||||
<StyledCreateWorkspaceCardPillIcon>
|
||||
<CloudWorkspaceIcon />
|
||||
</StyledCreateWorkspaceCardPillIcon>
|
||||
<StyledSignInCardPillTextCotainer>
|
||||
<StyledSignInCardPillTextPrimary>
|
||||
{t['com.affine.workspace.cloud.auth']()}
|
||||
</StyledSignInCardPillTextPrimary>
|
||||
<StyledSignInCardPillTextSecondary>
|
||||
{t['com.affine.workspace.cloud.description']()}
|
||||
</StyledSignInCardPillTextSecondary>
|
||||
</StyledSignInCardPillTextCotainer>
|
||||
</StyledCreateWorkspaceCardPillContent>
|
||||
</MenuItem>
|
||||
</StyledSignInCardPill>
|
||||
<Divider style={{ margin: '12px 0px' }} />
|
||||
</StyledModalHeaderContent>
|
||||
) : (
|
||||
<StyledModalHeaderContent>
|
||||
<StyledModalHeader>
|
||||
<StyledModalTitle>{session?.user.email}</StyledModalTitle>
|
||||
<StyledOperationWrapper>
|
||||
<Menu
|
||||
placement="bottom-end"
|
||||
trigger={['click']}
|
||||
content={<AccountMenu />}
|
||||
zIndex={1000}
|
||||
>
|
||||
<IconButton
|
||||
data-testid="more-button"
|
||||
icon={<MoreHorizontalIcon />}
|
||||
type="plain"
|
||||
/>
|
||||
</Menu>
|
||||
</StyledOperationWrapper>
|
||||
</StyledModalHeader>
|
||||
<Divider style={{ margin: '12px 0px' }} />
|
||||
</StyledModalHeaderContent>
|
||||
)}
|
||||
<StyledModalBody>
|
||||
{isLoggedIn ? (
|
||||
<CloudWorkSpaceList
|
||||
disabled={disabled}
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
workspaces={workspaces}
|
||||
onClickWorkspace={onClickWorkspace}
|
||||
onClickWorkspaceSetting={onClickWorkspaceSetting}
|
||||
onNewWorkspace={onNewWorkspace}
|
||||
onAddWorkspace={onAddWorkspace}
|
||||
currentWorkspaceId={currentWorkspaceId}
|
||||
onMoveWorkspace={onMoveWorkspace}
|
||||
/>
|
||||
{isLoggedIn && cloudWorkspaces.length !== 0 ? (
|
||||
<>
|
||||
<CloudWorkSpaceList
|
||||
disabled={disabled}
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
workspaces={workspaces}
|
||||
onClickWorkspace={onClickWorkspace}
|
||||
onClickWorkspaceSetting={onClickWorkspaceSetting}
|
||||
onNewWorkspace={onNewWorkspace}
|
||||
onAddWorkspace={onAddWorkspace}
|
||||
currentWorkspaceId={currentWorkspaceId}
|
||||
onMoveWorkspace={onMoveWorkspace}
|
||||
/>
|
||||
<Divider style={{ margin: '12px 0px', minHeight: '1px' }} />
|
||||
</>
|
||||
) : null}
|
||||
<StyledModalHeader>
|
||||
<StyledModalTitle>{t['Local Workspace']()}</StyledModalTitle>
|
||||
<StyledWorkspaceFlavourTitle>
|
||||
{t['com.affine.workspace.local']()}
|
||||
</StyledWorkspaceFlavourTitle>
|
||||
</StyledModalHeader>
|
||||
<StyledModalContent>
|
||||
<WorkspaceList
|
||||
disabled={disabled}
|
||||
items={
|
||||
workspaces.filter(
|
||||
({ flavour }) => flavour !== WorkspaceFlavour.PUBLIC
|
||||
) as (AffineCloudWorkspace | LocalWorkspace)[]
|
||||
}
|
||||
items={localWorkspaces}
|
||||
currentWorkspaceId={currentWorkspaceId}
|
||||
onClick={onClickWorkspace}
|
||||
onSettingClick={onClickWorkspaceSetting}
|
||||
|
||||
@@ -70,7 +70,6 @@ export const StyledCreateWorkspaceCard = styled('div')(() => {
|
||||
});
|
||||
export const StyledCreateWorkspaceCardPillContainer = styled('div')(() => {
|
||||
return {
|
||||
padding: '12px',
|
||||
borderRadius: '10px',
|
||||
display: 'flex',
|
||||
margin: '-8px -4px',
|
||||
@@ -173,6 +172,7 @@ export const StyledModalContent = styled('div')({
|
||||
flexWrap: 'wrap',
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
gap: '4px',
|
||||
});
|
||||
|
||||
export const StyledModalFooterContent = styled('div')({
|
||||
@@ -180,7 +180,7 @@ export const StyledModalFooterContent = styled('div')({
|
||||
flexWrap: 'wrap',
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
padding: '12px',
|
||||
marginTop: '12px',
|
||||
backgroundColor: 'var(--affine-background-overlay-panel-color)',
|
||||
});
|
||||
|
||||
@@ -189,7 +189,6 @@ export const StyledModalHeaderContent = styled('div')({
|
||||
flexWrap: 'wrap',
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
padding: '12px 12px 0px 12px',
|
||||
backgroundColor: 'var(--affine-background-overlay-panel-color)',
|
||||
});
|
||||
|
||||
@@ -219,19 +218,27 @@ export const StyledModalHeader = styled('div')(() => {
|
||||
left: 0,
|
||||
top: 0,
|
||||
borderRadius: '24px 24px 0 0',
|
||||
padding: '12px 14px',
|
||||
padding: '0px 14px',
|
||||
...displayFlex('space-between', 'center'),
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledModalBody = styled('div')(() => {
|
||||
return {
|
||||
padding: '0px 12px',
|
||||
display: 'inline-flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start',
|
||||
gap: '12px',
|
||||
gap: '4px',
|
||||
flex: 1,
|
||||
overflowY: 'auto',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledWorkspaceFlavourTitle = styled('div')(() => {
|
||||
return {
|
||||
fontSize: '12px',
|
||||
fontWeight: 600,
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
lineHeight: '20px',
|
||||
};
|
||||
});
|
||||
|
||||
@@ -6,15 +6,16 @@ import {
|
||||
} from '@affine/component/page-list';
|
||||
import type { Collection } from '@affine/env/filter';
|
||||
import type { PropertiesMeta } from '@affine/env/filter';
|
||||
import type {
|
||||
import {
|
||||
WorkspaceFlavour,
|
||||
WorkspaceHeaderProps,
|
||||
type WorkspaceHeaderProps,
|
||||
} from '@affine/env/workspace';
|
||||
import { WorkspaceSubPath } from '@affine/env/workspace';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useGetPageInfoById } from '../hooks/use-get-page-info';
|
||||
import { useWorkspace } from '../hooks/use-workspace';
|
||||
import { SharePageModal } from './affine/share-page-modal';
|
||||
import { BlockSuiteHeaderTitle } from './blocksuite/block-suite-header-title';
|
||||
import { filterContainerStyle } from './filter-container.css';
|
||||
import { Header } from './pure/header';
|
||||
@@ -77,7 +78,6 @@ export function WorkspaceHeader({
|
||||
const setting = useCollectionManager(currentWorkspaceId);
|
||||
|
||||
const currentWorkspace = useWorkspace(currentWorkspaceId);
|
||||
|
||||
const getPageInfoById = useGetPageInfoById(
|
||||
currentWorkspace.blockSuiteWorkspace
|
||||
);
|
||||
@@ -117,6 +117,15 @@ export function WorkspaceHeader({
|
||||
|
||||
// route in edit page
|
||||
if ('pageId' in currentEntry) {
|
||||
const isCloudWorkspace =
|
||||
currentWorkspace.flavour === WorkspaceFlavour.AFFINE_CLOUD;
|
||||
const currentPage = currentWorkspace.blockSuiteWorkspace.getPage(
|
||||
currentEntry.pageId
|
||||
);
|
||||
const sharePageModal =
|
||||
isCloudWorkspace && currentPage ? (
|
||||
<SharePageModal workspace={currentWorkspace} page={currentPage} />
|
||||
) : null;
|
||||
return (
|
||||
<Header
|
||||
center={
|
||||
@@ -125,7 +134,12 @@ export function WorkspaceHeader({
|
||||
pageId={currentEntry.pageId}
|
||||
/>
|
||||
}
|
||||
right={<PluginHeader />}
|
||||
right={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
{sharePageModal}
|
||||
<PluginHeader />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
10
apps/core/src/hooks/affine/use-curren-login-status.ts
Normal file
10
apps/core/src/hooks/affine/use-curren-login-status.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
import { useSession } from 'next-auth/react';
|
||||
|
||||
export function useCurrenLoginStatus():
|
||||
| 'authenticated'
|
||||
| 'unauthenticated'
|
||||
| 'loading' {
|
||||
const session = useSession();
|
||||
return session.status;
|
||||
}
|
||||
45
apps/core/src/hooks/affine/use-current-user.ts
Normal file
45
apps/core/src/hooks/affine/use-current-user.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import type { DefaultSession } from 'next-auth';
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
import { useSession } from 'next-auth/react';
|
||||
export type CheckedUser = {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
image: string;
|
||||
hasPassword: boolean;
|
||||
update: ReturnType<typeof useSession>['update'];
|
||||
};
|
||||
|
||||
// FIXME: Should this namespace be here?
|
||||
declare module 'next-auth' {
|
||||
interface Session {
|
||||
user: {
|
||||
id: string;
|
||||
hasPassword: boolean;
|
||||
} & DefaultSession['user'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This hook checks if the user is logged in.
|
||||
* If not, it will throw an error.
|
||||
*/
|
||||
export function useCurrentUser(): CheckedUser {
|
||||
const { data: session, status, update } = useSession();
|
||||
// If you are seeing this error, it means that you are not logged in.
|
||||
// This should be prohibited in the development environment, please re-write your component logic.
|
||||
if (status === 'unauthenticated') {
|
||||
throw new Error('session.status should be authenticated');
|
||||
}
|
||||
|
||||
const user = session?.user;
|
||||
|
||||
return {
|
||||
id: user?.id ?? 'REPLACE_ME_DEFAULT_ID',
|
||||
name: user?.name ?? 'REPLACE_ME_DEFAULT_NAME',
|
||||
email: user?.email ?? 'REPLACE_ME_DEFAULT_EMAIL',
|
||||
image: user?.image ?? 'REPLACE_ME_DEFAULT_URL',
|
||||
hasPassword: user?.hasPassword ?? false,
|
||||
update,
|
||||
};
|
||||
}
|
||||
30
apps/core/src/hooks/affine/use-invite-member.ts
Normal file
30
apps/core/src/hooks/affine/use-invite-member.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import type { Permission } from '@affine/graphql';
|
||||
import { inviteByEmailMutation } from '@affine/graphql';
|
||||
import { useMutation } from '@affine/workspace/affine/gql';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useMutateCloud } from './use-mutate-cloud';
|
||||
|
||||
export function useInviteMember(workspaceId: string) {
|
||||
const { trigger, isMutating } = useMutation({
|
||||
mutation: inviteByEmailMutation,
|
||||
});
|
||||
const mutate = useMutateCloud();
|
||||
return {
|
||||
invite: useCallback(
|
||||
async (email: string, permission: Permission, sendInviteMail = false) => {
|
||||
const res = await trigger({
|
||||
workspaceId,
|
||||
email,
|
||||
permission,
|
||||
sendInviteMail,
|
||||
});
|
||||
await mutate();
|
||||
// return is successful
|
||||
return res?.invite;
|
||||
},
|
||||
[mutate, trigger, workspaceId]
|
||||
),
|
||||
isMutating,
|
||||
};
|
||||
}
|
||||
57
apps/core/src/hooks/affine/use-is-shared-page.ts
Normal file
57
apps/core/src/hooks/affine/use-is-shared-page.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import {
|
||||
getWorkspaceSharedPagesQuery,
|
||||
revokePageMutation,
|
||||
sharePageMutation,
|
||||
} from '@affine/graphql';
|
||||
import { useMutation, useQuery } from '@affine/workspace/affine/gql';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
export function useIsSharedPage(
|
||||
workspaceId: string,
|
||||
pageId: string
|
||||
): [isSharedPage: boolean, setSharedPage: (enable: boolean) => void] {
|
||||
const { data, mutate } = useQuery({
|
||||
query: getWorkspaceSharedPagesQuery,
|
||||
variables: {
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
const { trigger: enableSharePage } = useMutation({
|
||||
mutation: sharePageMutation,
|
||||
});
|
||||
const { trigger: disableSharePage } = useMutation({
|
||||
mutation: revokePageMutation,
|
||||
});
|
||||
return [
|
||||
useMemo(
|
||||
() => data.workspace.sharedPages.some(id => id === pageId),
|
||||
[data.workspace.sharedPages, pageId]
|
||||
),
|
||||
useCallback(
|
||||
(enable: boolean) => {
|
||||
// todo: push notification
|
||||
if (enable) {
|
||||
enableSharePage({
|
||||
workspaceId,
|
||||
pageId,
|
||||
})
|
||||
.then(() => {
|
||||
return mutate();
|
||||
})
|
||||
.catch(console.error);
|
||||
} else {
|
||||
disableSharePage({
|
||||
workspaceId,
|
||||
pageId,
|
||||
})
|
||||
.then(() => {
|
||||
return mutate();
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
mutate().catch(console.error);
|
||||
},
|
||||
[disableSharePage, enableSharePage, mutate, pageId, workspaceId]
|
||||
),
|
||||
];
|
||||
}
|
||||
13
apps/core/src/hooks/affine/use-is-workspace-owner.ts
Normal file
13
apps/core/src/hooks/affine/use-is-workspace-owner.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { getIsOwnerQuery } from '@affine/graphql';
|
||||
import { useQuery } from '@affine/workspace/affine/gql';
|
||||
|
||||
export function useIsWorkspaceOwner(workspaceId: string) {
|
||||
const { data } = useQuery({
|
||||
query: getIsOwnerQuery,
|
||||
variables: {
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
return data.isOwner;
|
||||
}
|
||||
23
apps/core/src/hooks/affine/use-leave-workspace.ts
Normal file
23
apps/core/src/hooks/affine/use-leave-workspace.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { leaveWorkspaceMutation } from '@affine/graphql';
|
||||
import { useMutation } from '@affine/workspace/affine/gql';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useAppHelper } from '../use-workspaces';
|
||||
|
||||
export function useLeaveWorkspace() {
|
||||
const { deleteWorkspaceMeta } = useAppHelper();
|
||||
|
||||
const { trigger: leaveWorkspace } = useMutation({
|
||||
mutation: leaveWorkspaceMutation,
|
||||
});
|
||||
|
||||
return useCallback(
|
||||
async (workspaceId: string) => {
|
||||
deleteWorkspaceMeta(workspaceId);
|
||||
await leaveWorkspace({
|
||||
workspaceId,
|
||||
});
|
||||
},
|
||||
[deleteWorkspaceMeta, leaveWorkspace]
|
||||
);
|
||||
}
|
||||
19
apps/core/src/hooks/affine/use-members.ts
Normal file
19
apps/core/src/hooks/affine/use-members.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import {
|
||||
type GetMembersByWorkspaceIdQuery,
|
||||
getMembersByWorkspaceIdQuery,
|
||||
} from '@affine/graphql';
|
||||
import { useQuery } from '@affine/workspace/affine/gql';
|
||||
|
||||
export type Member = Omit<
|
||||
GetMembersByWorkspaceIdQuery['workspace']['members'][number],
|
||||
'__typename'
|
||||
>;
|
||||
export function useMembers(workspaceId: string) {
|
||||
const { data } = useQuery({
|
||||
query: getMembersByWorkspaceIdQuery,
|
||||
variables: {
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
return data.workspace.members;
|
||||
}
|
||||
14
apps/core/src/hooks/affine/use-mutate-cloud.ts
Normal file
14
apps/core/src/hooks/affine/use-mutate-cloud.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useSWRConfig } from 'swr';
|
||||
|
||||
export function useMutateCloud() {
|
||||
const { mutate } = useSWRConfig();
|
||||
return useCallback(async () => {
|
||||
return mutate(key => {
|
||||
if (Array.isArray(key)) {
|
||||
return key[0] === 'cloud';
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}, [mutate]);
|
||||
}
|
||||
23
apps/core/src/hooks/affine/use-revoke-member-permission.ts
Normal file
23
apps/core/src/hooks/affine/use-revoke-member-permission.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { revokeMemberPermissionMutation } from '@affine/graphql';
|
||||
import { useMutation } from '@affine/workspace/affine/gql';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useMutateCloud } from './use-mutate-cloud';
|
||||
|
||||
export function useRevokeMemberPermission(workspaceId: string) {
|
||||
const mutate = useMutateCloud();
|
||||
const { trigger } = useMutation({
|
||||
mutation: revokeMemberPermissionMutation,
|
||||
});
|
||||
|
||||
return useCallback(
|
||||
async (userId: string) => {
|
||||
await trigger({
|
||||
workspaceId,
|
||||
userId,
|
||||
});
|
||||
await mutate();
|
||||
},
|
||||
[mutate, trigger, workspaceId]
|
||||
);
|
||||
}
|
||||
15
apps/core/src/hooks/affine/use-share-link.ts
Normal file
15
apps/core/src/hooks/affine/use-share-link.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
'use client';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export function useShareLink(workspaceId: string): string {
|
||||
return useMemo(() => {
|
||||
if (environment.isServer) {
|
||||
throw new Error('useShareLink is not available on server side');
|
||||
}
|
||||
if (environment.isDesktop) {
|
||||
return '???';
|
||||
} else {
|
||||
return origin + '/share/' + workspaceId;
|
||||
}
|
||||
}, [workspaceId]);
|
||||
}
|
||||
22
apps/core/src/hooks/affine/use-toggle-cloud-public.ts
Normal file
22
apps/core/src/hooks/affine/use-toggle-cloud-public.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { setWorkspacePublicByIdMutation } from '@affine/graphql';
|
||||
import { useMutation } from '@affine/workspace/affine/gql';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useMutateCloud } from './use-mutate-cloud';
|
||||
|
||||
export function useToggleCloudPublic(workspaceId: string) {
|
||||
const mutate = useMutateCloud();
|
||||
const { trigger } = useMutation({
|
||||
mutation: setWorkspacePublicByIdMutation,
|
||||
});
|
||||
return useCallback(
|
||||
async (isPublic: boolean) => {
|
||||
await trigger({
|
||||
id: workspaceId,
|
||||
public: isPublic,
|
||||
});
|
||||
await mutate();
|
||||
},
|
||||
[mutate, trigger, workspaceId]
|
||||
);
|
||||
}
|
||||
@@ -1,34 +1,62 @@
|
||||
import type { WorkspaceRegistry } from '@affine/env/workspace';
|
||||
import type { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { WorkspaceSubPath } from '@affine/env/workspace';
|
||||
import {
|
||||
rootWorkspacesMetadataAtom,
|
||||
workspaceAdaptersAtom,
|
||||
} from '@affine/workspace/atom';
|
||||
import { currentPageIdAtom } from '@toeverything/infra/atom';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { WorkspaceVersion } from '@toeverything/infra/blocksuite';
|
||||
import { useAtomValue, useSetAtom } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useTransformWorkspace } from '../use-transform-workspace';
|
||||
import { openSettingModalAtom } from '../../atoms';
|
||||
import { useNavigateHelper } from '../use-navigate-helper';
|
||||
|
||||
export function useOnTransformWorkspace() {
|
||||
const transformWorkspace = useTransformWorkspace();
|
||||
const setWorkspaceId = useSetAtom(currentPageIdAtom);
|
||||
const setSettingModal = useSetAtom(openSettingModalAtom);
|
||||
const WorkspaceAdapters = useAtomValue(workspaceAdaptersAtom);
|
||||
const setMetadata = useSetAtom(rootWorkspacesMetadataAtom);
|
||||
const { openPage } = useNavigateHelper();
|
||||
const currentPageId = useAtomValue(currentPageIdAtom);
|
||||
return useCallback(
|
||||
async <From extends WorkspaceFlavour, To extends WorkspaceFlavour>(
|
||||
from: From,
|
||||
to: To,
|
||||
workspace: WorkspaceRegistry[From]
|
||||
): Promise<void> => {
|
||||
const workspaceId = await transformWorkspace(from, to, workspace);
|
||||
// create first, then delete, in case of failure
|
||||
const newId = await WorkspaceAdapters[to].CRUD.create(
|
||||
workspace.blockSuiteWorkspace
|
||||
);
|
||||
await WorkspaceAdapters[from].CRUD.delete(workspace.blockSuiteWorkspace);
|
||||
setMetadata(workspaces => {
|
||||
const idx = workspaces.findIndex(ws => ws.id === workspace.id);
|
||||
workspaces.splice(idx, 1, {
|
||||
id: newId,
|
||||
flavour: to,
|
||||
version: WorkspaceVersion.SubDoc,
|
||||
});
|
||||
return [...workspaces];
|
||||
}, newId);
|
||||
// fixme(himself65): setting modal could still open and open the non-exist workspace
|
||||
setSettingModal(settings => ({
|
||||
...settings,
|
||||
open: false,
|
||||
}));
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('affine-workspace:transform', {
|
||||
detail: {
|
||||
from,
|
||||
to,
|
||||
oldId: workspace.id,
|
||||
newId: workspaceId,
|
||||
newId: newId,
|
||||
},
|
||||
})
|
||||
);
|
||||
setWorkspaceId(workspaceId);
|
||||
openPage(newId, currentPageId ?? WorkspaceSubPath.ALL);
|
||||
},
|
||||
[setWorkspaceId, transformWorkspace]
|
||||
[WorkspaceAdapters, setMetadata, setSettingModal, openPage, currentPageId]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import type { WorkspaceSubPath } from '@affine/env/workspace';
|
||||
import { useCallback } from 'react';
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
type NavigateOptions,
|
||||
useLocation,
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
useNavigate,
|
||||
} from 'react-router-dom';
|
||||
|
||||
export enum RouteLogic {
|
||||
REPLACE = 'replace',
|
||||
@@ -78,6 +82,26 @@ export function useNavigateHelper() {
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
const jumpToExpired = useCallback(
|
||||
(logic: RouteLogic = RouteLogic.PUSH) => {
|
||||
return navigate('/expired', {
|
||||
replace: logic === RouteLogic.REPLACE,
|
||||
});
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
const jumpToSignIn = useCallback(
|
||||
(
|
||||
logic: RouteLogic = RouteLogic.PUSH,
|
||||
otherOptions?: Omit<NavigateOptions, 'replace'>
|
||||
) => {
|
||||
return navigate('/signIn', {
|
||||
replace: logic === RouteLogic.REPLACE,
|
||||
...otherOptions,
|
||||
});
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
|
||||
return {
|
||||
jumpToPage,
|
||||
@@ -86,5 +110,7 @@ export function useNavigateHelper() {
|
||||
jumpToIndex,
|
||||
jumpTo404,
|
||||
openPage,
|
||||
jumpToExpired,
|
||||
jumpToSignIn,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import type { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import type { WorkspaceRegistry } from '@affine/env/workspace';
|
||||
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
||||
import { WorkspaceVersion } from '@toeverything/infra/blocksuite';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { WorkspaceAdapters } from '../adapters/workspace';
|
||||
|
||||
/**
|
||||
* Transform workspace from one flavor to another
|
||||
*
|
||||
* The logic here is to delete the old workspace and create a new one.
|
||||
*/
|
||||
export function useTransformWorkspace() {
|
||||
const set = useSetAtom(rootWorkspacesMetadataAtom);
|
||||
return useCallback(
|
||||
async <From extends WorkspaceFlavour, To extends WorkspaceFlavour>(
|
||||
from: From,
|
||||
to: To,
|
||||
workspace: WorkspaceRegistry[From]
|
||||
): Promise<string> => {
|
||||
// create first, then delete, in case of failure
|
||||
const newId = await WorkspaceAdapters[to].CRUD.create(
|
||||
workspace.blockSuiteWorkspace
|
||||
);
|
||||
await WorkspaceAdapters[from].CRUD.delete(workspace as any);
|
||||
set(workspaces => {
|
||||
const idx = workspaces.findIndex(ws => ws.id === workspace.id);
|
||||
workspaces.splice(idx, 1, {
|
||||
id: newId,
|
||||
flavour: to,
|
||||
version: WorkspaceVersion.DatabaseV3,
|
||||
});
|
||||
return [...workspaces];
|
||||
});
|
||||
return newId;
|
||||
},
|
||||
[set]
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { AffineOfficialWorkspace } from '@affine/env/workspace';
|
||||
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import type { Workspace } from '@blocksuite/store';
|
||||
@@ -5,8 +6,6 @@ import { useStaticBlockSuiteWorkspace } from '@toeverything/infra/__internal__/r
|
||||
import type { Atom } from 'jotai';
|
||||
import { atom, useAtomValue } from 'jotai';
|
||||
|
||||
import type { AffineOfficialWorkspace } from '../shared';
|
||||
|
||||
const workspaceWeakMap = new WeakMap<
|
||||
Workspace,
|
||||
Atom<Promise<AffineOfficialWorkspace>>
|
||||
@@ -18,7 +17,7 @@ export function useWorkspace(workspaceId: string): AffineOfficialWorkspace {
|
||||
const baseAtom = atom(async get => {
|
||||
const metadata = await get(rootWorkspacesMetadataAtom);
|
||||
const flavour = metadata.find(({ id }) => id === workspaceId)?.flavour;
|
||||
assertExists(flavour);
|
||||
assertExists(flavour, 'workspace flavour not found');
|
||||
return {
|
||||
id: workspaceId,
|
||||
flavour,
|
||||
|
||||
@@ -43,6 +43,21 @@ export function useAppHelper() {
|
||||
},
|
||||
[set]
|
||||
),
|
||||
addCloudWorkspace: useCallback(
|
||||
(workspaceId: string) => {
|
||||
getOrCreateWorkspace(workspaceId, WorkspaceFlavour.AFFINE_CLOUD);
|
||||
set(workspaces => [
|
||||
...workspaces,
|
||||
{
|
||||
id: workspaceId,
|
||||
flavour: WorkspaceFlavour.AFFINE_CLOUD,
|
||||
version: WorkspaceVersion.DatabaseV3,
|
||||
},
|
||||
]);
|
||||
logger.debug('imported cloud workspace', workspaceId);
|
||||
},
|
||||
[set]
|
||||
),
|
||||
createLocalWorkspace: useCallback(
|
||||
async (name: string): Promise<string> => {
|
||||
const blockSuiteWorkspace = getOrCreateWorkspace(
|
||||
@@ -97,5 +112,11 @@ export function useAppHelper() {
|
||||
},
|
||||
[jotaiWorkspaces, set]
|
||||
),
|
||||
deleteWorkspaceMeta: useCallback(
|
||||
(workspaceId: string) => {
|
||||
set(workspaces => workspaces.filter(ws => ws.id !== workspaceId));
|
||||
},
|
||||
[set]
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -33,16 +33,16 @@ import { usePassiveWorkspaceEffect } from '@toeverything/infra/__internal__/reac
|
||||
import { currentWorkspaceIdAtom } from '@toeverything/infra/atom';
|
||||
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
|
||||
import type { PropsWithChildren, ReactElement } from 'react';
|
||||
import { lazy, Suspense, useCallback, useMemo } from 'react';
|
||||
import { lazy, Suspense, useCallback } from 'react';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
|
||||
import { WorkspaceAdapters } from '../adapters/workspace';
|
||||
import {
|
||||
openQuickSearchModalAtom,
|
||||
openSettingModalAtom,
|
||||
openWorkspacesModalAtom,
|
||||
} from '../atoms';
|
||||
import { useAppSetting } from '../atoms/settings';
|
||||
import { AdapterProviderWrapper } from '../components/adapter-worksapce-wrapper';
|
||||
import { AppContainer } from '../components/affine/app-container';
|
||||
import { usePageHelper } from '../components/blocksuite/block-suite-page-list/utils';
|
||||
import type { IslandItemNames } from '../components/pure/help-island';
|
||||
@@ -68,10 +68,6 @@ const QuickSearchModal = lazy(() =>
|
||||
}))
|
||||
);
|
||||
|
||||
function DefaultProvider({ children }: PropsWithChildren) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
export const QuickSearch = () => {
|
||||
const [currentWorkspace] = useCurrentWorkspace();
|
||||
const [openQuickSearchModal, setOpenQuickSearchModalAtom] = useAtom(
|
||||
@@ -117,32 +113,19 @@ export const CurrentWorkspaceContext = ({
|
||||
export const WorkspaceLayout = function WorkspacesSuspense({
|
||||
children,
|
||||
}: PropsWithChildren) {
|
||||
const currentWorkspaceId = useAtomValue(currentWorkspaceIdAtom);
|
||||
const jotaiWorkspaces = useAtomValue(rootWorkspacesMetadataAtom);
|
||||
const meta = useMemo(
|
||||
() => jotaiWorkspaces.find(x => x.id === currentWorkspaceId),
|
||||
[currentWorkspaceId, jotaiWorkspaces]
|
||||
);
|
||||
|
||||
const Provider =
|
||||
(meta && WorkspaceAdapters[meta.flavour].UI.Provider) ?? DefaultProvider;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* load all workspaces is costly, do not block the whole UI */}
|
||||
<Suspense fallback={null}>
|
||||
<AllWorkspaceModals />
|
||||
<CurrentWorkspaceContext>
|
||||
{/* fixme(himself65): don't re-render whole modals */}
|
||||
<CurrentWorkspaceModals key={currentWorkspaceId} />
|
||||
</CurrentWorkspaceContext>
|
||||
</Suspense>
|
||||
<AdapterProviderWrapper>
|
||||
<CurrentWorkspaceContext>
|
||||
<Provider>
|
||||
{/* load all workspaces is costly, do not block the whole UI */}
|
||||
<Suspense>
|
||||
<AllWorkspaceModals />
|
||||
<CurrentWorkspaceModals />
|
||||
</Suspense>
|
||||
<Suspense fallback={<WorkspaceFallback />}>
|
||||
<WorkspaceLayoutInner>{children}</WorkspaceLayoutInner>
|
||||
</Provider>
|
||||
</Suspense>
|
||||
</CurrentWorkspaceContext>
|
||||
</>
|
||||
</AdapterProviderWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
133
apps/core/src/pages/auth.tsx
Normal file
133
apps/core/src/pages/auth.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
import {
|
||||
ChangeEmailPage,
|
||||
ChangePasswordPage,
|
||||
SetPasswordPage,
|
||||
SignInSuccessPage,
|
||||
SignUpPage,
|
||||
} from '@affine/component/auth-components';
|
||||
import { isDesktop } from '@affine/env/constant';
|
||||
import { changeEmailMutation, changePasswordMutation } from '@affine/graphql';
|
||||
import { useMutation } from '@affine/workspace/affine/gql';
|
||||
import type { ReactElement } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { type LoaderFunction, redirect, useParams } from 'react-router-dom';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { useCurrenLoginStatus } from '../hooks/affine/use-curren-login-status';
|
||||
import { useCurrentUser } from '../hooks/affine/use-current-user';
|
||||
import { RouteLogic, useNavigateHelper } from '../hooks/use-navigate-helper';
|
||||
|
||||
const authTypeSchema = z.enum([
|
||||
'setPassword',
|
||||
'signIn',
|
||||
'changePassword',
|
||||
'signUp',
|
||||
'changeEmail',
|
||||
]);
|
||||
|
||||
export const AuthPage = (): ReactElement | null => {
|
||||
const user = useCurrentUser();
|
||||
const { authType } = useParams();
|
||||
const { trigger: changePassword } = useMutation({
|
||||
mutation: changePasswordMutation,
|
||||
});
|
||||
|
||||
const { trigger: changeEmail } = useMutation({
|
||||
mutation: changeEmailMutation,
|
||||
});
|
||||
const { jumpToIndex } = useNavigateHelper();
|
||||
|
||||
const onChangeEmail = useCallback(
|
||||
async (email: string) => {
|
||||
const res = await changeEmail({
|
||||
id: user.id,
|
||||
newEmail: email,
|
||||
});
|
||||
return !!res?.changeEmail;
|
||||
},
|
||||
[changeEmail, user.id]
|
||||
);
|
||||
|
||||
const onSetPassword = useCallback(
|
||||
(password: string) => {
|
||||
changePassword({
|
||||
id: user.id,
|
||||
newPassword: password,
|
||||
}).catch(console.error);
|
||||
},
|
||||
[changePassword, user.id]
|
||||
);
|
||||
const onOpenAffine = useCallback(() => {
|
||||
if (isDesktop) {
|
||||
window.apis.ui.handleFinishLogin();
|
||||
} else {
|
||||
jumpToIndex(RouteLogic.REPLACE);
|
||||
}
|
||||
}, [jumpToIndex]);
|
||||
|
||||
switch (authType) {
|
||||
case 'signUp': {
|
||||
return (
|
||||
<SignUpPage
|
||||
user={user}
|
||||
onSetPassword={onSetPassword}
|
||||
onOpenAffine={onOpenAffine}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'signIn': {
|
||||
return <SignInSuccessPage onOpenAffine={onOpenAffine} />;
|
||||
}
|
||||
case 'changePassword': {
|
||||
return (
|
||||
<ChangePasswordPage
|
||||
user={user}
|
||||
onSetPassword={onSetPassword}
|
||||
onOpenAffine={onOpenAffine}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'setPassword': {
|
||||
return (
|
||||
<SetPasswordPage
|
||||
user={user}
|
||||
onSetPassword={onSetPassword}
|
||||
onOpenAffine={onOpenAffine}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'changeEmail': {
|
||||
return (
|
||||
<ChangeEmailPage
|
||||
user={user}
|
||||
onChangeEmail={onChangeEmail}
|
||||
onOpenAffine={onOpenAffine}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const loader: LoaderFunction = async args => {
|
||||
if (!args.params.authType) {
|
||||
return redirect('/404');
|
||||
}
|
||||
if (!authTypeSchema.safeParse(args.params.authType).success) {
|
||||
return redirect('/404');
|
||||
}
|
||||
return null;
|
||||
};
|
||||
export const Component = () => {
|
||||
const loginStatus = useCurrenLoginStatus();
|
||||
const { jumpToExpired } = useNavigateHelper();
|
||||
|
||||
if (loginStatus === 'unauthenticated') {
|
||||
jumpToExpired(RouteLogic.REPLACE);
|
||||
}
|
||||
|
||||
if (loginStatus === 'authenticated') {
|
||||
return <AuthPage />;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user