mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-06 00:41:39 +08:00
Merge branch 'canary' into stable
This commit is contained in:
@@ -20,12 +20,6 @@ export const productionCacheGroups = {
|
||||
priority: Number.MAX_SAFE_INTEGER,
|
||||
chunks: 'async' as const,
|
||||
},
|
||||
mui: {
|
||||
name: `npm-mui`,
|
||||
test: testPackageName(/[\\/]node_modules[\\/](mui|@mui)[\\/]/),
|
||||
priority: 200,
|
||||
enforce: true,
|
||||
},
|
||||
blocksuite: {
|
||||
name: `npm-blocksuite`,
|
||||
test: testPackageName(/[\\/]node_modules[\\/](@blocksuite)[\\/]/),
|
||||
|
||||
@@ -186,9 +186,6 @@ export const createConfiguration: (
|
||||
'global',
|
||||
'dist'
|
||||
),
|
||||
'@blocksuite/lit': blocksuiteBaseDir
|
||||
? join(blocksuiteBaseDir, 'packages', 'lit', 'src')
|
||||
: join(workspaceRoot, 'node_modules', '@blocksuite', 'lit', 'dist'),
|
||||
'@blocksuite/store/providers/broadcast-channel': blocksuiteBaseDir
|
||||
? join(
|
||||
blocksuiteBaseDir,
|
||||
|
||||
@@ -129,8 +129,8 @@ export function getRuntimeConfig(buildFlags: BuildFlags): RuntimeConfig {
|
||||
enableCaptcha: process.env.ENABLE_CAPTCHA
|
||||
? process.env.ENABLE_CAPTCHA === 'true'
|
||||
: buildFlags.mode === 'development'
|
||||
? false
|
||||
: currentBuildPreset.enableCaptcha,
|
||||
? false
|
||||
: currentBuildPreset.enableCaptcha,
|
||||
enableEnhanceShareMode: process.env.ENABLE_ENHANCE_SHARE_MODE
|
||||
? process.env.ENABLE_ENHANCE_SHARE_MODE === 'true'
|
||||
: currentBuildPreset.enableEnhanceShareMode,
|
||||
@@ -140,8 +140,8 @@ export function getRuntimeConfig(buildFlags: BuildFlags): RuntimeConfig {
|
||||
enablePayment: process.env.ENABLE_PAYMENT
|
||||
? process.env.ENABLE_PAYMENT !== 'false'
|
||||
: buildFlags.mode === 'development'
|
||||
? true
|
||||
: currentBuildPreset.enablePayment,
|
||||
? true
|
||||
: currentBuildPreset.enablePayment,
|
||||
};
|
||||
|
||||
if (buildFlags.mode === 'development') {
|
||||
|
||||
@@ -19,26 +19,8 @@ export default async function (cli_env: any, _: any) {
|
||||
const config = createConfiguration(flags, runtimeConfig);
|
||||
return merge(config, {
|
||||
entry: {
|
||||
'polyfill/intl-segmenter': {
|
||||
import: resolve(rootPath, 'src/polyfill/intl-segmenter.ts'),
|
||||
},
|
||||
'polyfill/ses': {
|
||||
import: resolve(rootPath, 'src/polyfill/ses.ts'),
|
||||
},
|
||||
plugin: {
|
||||
dependOn: ['polyfill/intl-segmenter', 'polyfill/ses'],
|
||||
import: resolve(rootPath, 'src/bootstrap/register-plugins.ts'),
|
||||
},
|
||||
app: {
|
||||
chunkLoading: 'import',
|
||||
dependOn: ['polyfill/intl-segmenter', 'polyfill/ses', 'plugin'],
|
||||
import: resolve(rootPath, 'src/index.tsx'),
|
||||
},
|
||||
'_plugin/index.test': {
|
||||
chunkLoading: 'import',
|
||||
dependOn: ['polyfill/intl-segmenter', 'polyfill/ses', 'plugin'],
|
||||
import: resolve(rootPath, 'src/_plugin/index.test.tsx'),
|
||||
},
|
||||
app: resolve(rootPath, 'src/index.tsx'),
|
||||
'_plugin/index.test': resolve(rootPath, 'src/_plugin/index.test.tsx'),
|
||||
},
|
||||
plugins: [
|
||||
new HTMLPlugin({
|
||||
@@ -46,7 +28,7 @@ export default async function (cli_env: any, _: any) {
|
||||
inject: 'body',
|
||||
scriptLoading: 'module',
|
||||
minify: false,
|
||||
chunks: ['app', 'plugin', 'polyfill/intl-segmenter', 'polyfill/ses'],
|
||||
chunks: ['app'],
|
||||
filename: 'index.html',
|
||||
templateParameters: {
|
||||
GIT_SHORT_SHA: gitShortHash(),
|
||||
@@ -59,12 +41,7 @@ export default async function (cli_env: any, _: any) {
|
||||
scriptLoading: 'module',
|
||||
minify: false,
|
||||
publicPath: getPublicPath(flags),
|
||||
chunks: [
|
||||
'_plugin/index.test',
|
||||
'plugin',
|
||||
'polyfill/intl-segmenter',
|
||||
'polyfill/ses',
|
||||
],
|
||||
chunks: ['_plugin/index.test'],
|
||||
filename: '_plugin/index.html',
|
||||
templateParameters: {
|
||||
GIT_SHORT_SHA: gitShortHash(),
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@affine/core",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"version": "0.10.2",
|
||||
"version": "0.10.3-canary.2",
|
||||
"scripts": {
|
||||
"build": "yarn -T run build-core",
|
||||
"dev": "yarn -T run dev-core",
|
||||
@@ -17,6 +17,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@affine-test/fixtures": "workspace:*",
|
||||
"@affine/cmdk": "workspace:*",
|
||||
"@affine/component": "workspace:*",
|
||||
"@affine/debug": "workspace:*",
|
||||
"@affine/env": "workspace:*",
|
||||
@@ -24,49 +25,62 @@
|
||||
"@affine/i18n": "workspace:*",
|
||||
"@affine/templates": "workspace:*",
|
||||
"@affine/workspace": "workspace:*",
|
||||
"@blocksuite/block-std": "0.0.0-20231116023037-31273bb7-nightly",
|
||||
"@blocksuite/blocks": "0.0.0-20231116023037-31273bb7-nightly",
|
||||
"@blocksuite/editor": "0.0.0-20231116023037-31273bb7-nightly",
|
||||
"@blocksuite/global": "0.0.0-20231116023037-31273bb7-nightly",
|
||||
"@blocksuite/icons": "2.1.35",
|
||||
"@blocksuite/lit": "0.0.0-20231116023037-31273bb7-nightly",
|
||||
"@blocksuite/store": "0.0.0-20231116023037-31273bb7-nightly",
|
||||
"@blocksuite/block-std": "0.0.0-20231122113751-6bf81eb3-nightly",
|
||||
"@blocksuite/blocks": "0.0.0-20231122113751-6bf81eb3-nightly",
|
||||
"@blocksuite/editor": "0.0.0-20231122113751-6bf81eb3-nightly",
|
||||
"@blocksuite/global": "0.0.0-20231122113751-6bf81eb3-nightly",
|
||||
"@blocksuite/icons": "2.1.36",
|
||||
"@blocksuite/lit": "0.0.0-20231122113751-6bf81eb3-nightly",
|
||||
"@blocksuite/store": "0.0.0-20231122113751-6bf81eb3-nightly",
|
||||
"@blocksuite/virgo": "0.0.0-20231122113751-6bf81eb3-nightly",
|
||||
"@dnd-kit/core": "^6.0.8",
|
||||
"@dnd-kit/sortable": "^7.0.2",
|
||||
"@dnd-kit/sortable": "^8.0.0",
|
||||
"@emotion/cache": "^11.11.0",
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/server": "^11.11.0",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@marsidev/react-turnstile": "^0.3.1",
|
||||
"@mui/material": "^5.14.14",
|
||||
"@radix-ui/react-collapsible": "^1.0.3",
|
||||
"@radix-ui/react-dialog": "^1.0.4",
|
||||
"@radix-ui/react-scroll-area": "^1.0.5",
|
||||
"@radix-ui/react-select": "^2.0.0",
|
||||
"@react-hookz/web": "^23.1.0",
|
||||
"@toeverything/components": "^0.0.46",
|
||||
"@toeverything/theme": "^0.7.20",
|
||||
"@vanilla-extract/css": "^1.13.0",
|
||||
"@vanilla-extract/dynamic": "^2.0.3",
|
||||
"async-call-rpc": "^6.3.1",
|
||||
"bytes": "^3.1.2",
|
||||
"clsx": "^2.0.0",
|
||||
"css-spring": "^4.1.0",
|
||||
"cssnano": "^6.0.1",
|
||||
"dayjs": "^1.11.10",
|
||||
"foxact": "^0.2.20",
|
||||
"graphql": "^16.8.1",
|
||||
"idb": "^7.1.1",
|
||||
"intl-segmenter-polyfill-rs": "^0.1.6",
|
||||
"jotai": "^2.4.3",
|
||||
"jotai": "^2.5.1",
|
||||
"jotai-devtools": "^0.7.0",
|
||||
"lit": "^3.0.2",
|
||||
"lottie-web": "^5.12.2",
|
||||
"mini-css-extract-plugin": "^2.7.6",
|
||||
"next-auth": "^4.23.2",
|
||||
"nanoid": "^5.0.3",
|
||||
"next-auth": "^4.24.5",
|
||||
"next-themes": "^0.2.1",
|
||||
"postcss-loader": "^7.3.3",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-error-boundary": "^4.0.11",
|
||||
"react-is": "18.2.0",
|
||||
"react-resizable-panels": "^0.0.55",
|
||||
"react-resizable-panels": "^0.0.61",
|
||||
"react-router-dom": "^6.16.0",
|
||||
"rxjs": "^7.8.1",
|
||||
"ses": "^0.18.8",
|
||||
"swr": "2.2.4",
|
||||
"uuid": "^9.0.1",
|
||||
"valtio": "^1.11.2",
|
||||
"y-protocols": "^1.0.6",
|
||||
"yjs": "^13.6.8",
|
||||
"yjs": "^13.6.10",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -76,12 +90,15 @@
|
||||
"@sentry/webpack-plugin": "^2.8.0",
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
"@swc/core": "^1.3.93",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@types/bytes": "^3.1.3",
|
||||
"@types/lodash-es": "^4.17.9",
|
||||
"@types/uuid": "^9.0.5",
|
||||
"@types/webpack-env": "^1.18.2",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"css-loader": "^6.8.1",
|
||||
"express": "^4.18.2",
|
||||
"fake-indexeddb": "^5.0.0",
|
||||
"html-webpack-plugin": "^5.5.3",
|
||||
"lodash-es": "^4.17.21",
|
||||
"mime-types": "^2.1.35",
|
||||
@@ -91,6 +108,7 @@
|
||||
"swc-loader": "^0.2.3",
|
||||
"swc-plugin-coverage-instrument": "^0.0.20",
|
||||
"thread-loader": "^4.0.2",
|
||||
"vitest": "0.34.6",
|
||||
"webpack": "^5.89.0",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"webpack-dev-server": "^4.15.1",
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import '../polyfill/ses-lockdown';
|
||||
import '../polyfill/intl-segmenter';
|
||||
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import {
|
||||
getCurrentStore,
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
DEFAULT_WORKSPACE_NAME,
|
||||
PageNotFoundError,
|
||||
} from '@affine/env/constant';
|
||||
import type { LocalIndexedDBDownloadProvider } from '@affine/env/workspace';
|
||||
import type { WorkspaceAdapter } from '@affine/env/workspace';
|
||||
import {
|
||||
LoadPriority,
|
||||
@@ -14,11 +13,7 @@ import {
|
||||
CRUD,
|
||||
saveWorkspaceToLocalStorage,
|
||||
} from '@affine/workspace/local/crud';
|
||||
import {
|
||||
getOrCreateWorkspace,
|
||||
globalBlockSuiteSchema,
|
||||
} from '@affine/workspace/manager';
|
||||
import { createIndexedDBDownloadProvider } from '@affine/workspace/providers';
|
||||
import { getOrCreateWorkspace } from '@affine/workspace/manager';
|
||||
import { getBlockSuiteWorkspaceAtom } from '@toeverything/infra/__internal__/workspace';
|
||||
import { getCurrentStore } from '@toeverything/infra/atom';
|
||||
import { initEmptyPage } from '@toeverything/infra/blocksuite';
|
||||
@@ -49,7 +44,6 @@ export const LocalAdapter: WorkspaceAdapter<WorkspaceFlavour.LOCAL> = {
|
||||
blockSuiteWorkspace.meta.setName(DEFAULT_WORKSPACE_NAME);
|
||||
if (runtimeConfig.enablePreloading) {
|
||||
buildShowcaseWorkspace(blockSuiteWorkspace, {
|
||||
schema: globalBlockSuiteSchema,
|
||||
store: getCurrentStore(),
|
||||
atoms: {
|
||||
pageMode: setPageModeAtom,
|
||||
@@ -66,15 +60,6 @@ export const LocalAdapter: WorkspaceAdapter<WorkspaceFlavour.LOCAL> = {
|
||||
logger.error('init page with empty failed', error);
|
||||
});
|
||||
}
|
||||
const provider = createIndexedDBDownloadProvider(
|
||||
blockSuiteWorkspace.id,
|
||||
blockSuiteWorkspace.doc,
|
||||
{
|
||||
awareness: blockSuiteWorkspace.awarenessStore.awareness,
|
||||
}
|
||||
) as LocalIndexedDBDownloadProvider;
|
||||
provider.sync();
|
||||
provider.whenReady.catch(console.error);
|
||||
saveWorkspaceToLocalStorage(blockSuiteWorkspace.id);
|
||||
logger.debug('create first workspace');
|
||||
return [blockSuiteWorkspace.id];
|
||||
|
||||
@@ -130,7 +130,9 @@ export const pageCollectionBaseAtom =
|
||||
await userSetting.loaded;
|
||||
const view = userSetting.view;
|
||||
if (view) {
|
||||
const collections: DeprecatedCollection[] = [...view.values()];
|
||||
const collections: Omit<DeprecatedCollection, 'workspaceId'>[] = [
|
||||
...view.values(),
|
||||
];
|
||||
//delete collections
|
||||
view.clear();
|
||||
return collections.map(v => {
|
||||
@@ -147,36 +149,49 @@ export const pageCollectionBaseAtom =
|
||||
|
||||
return new Observable<BaseCollectionsDataType>(subscriber => {
|
||||
const group = new DisposableGroup();
|
||||
currentWorkspacePromise.then(async currentWorkspace => {
|
||||
const workspaceSetting = getWorkspaceSetting(currentWorkspace);
|
||||
migrateCollectionsFromIdbData(currentWorkspace).then(collections => {
|
||||
if (collections.length) {
|
||||
workspaceSetting.addCollection(...collections);
|
||||
}
|
||||
});
|
||||
migrateCollectionsFromUserData(currentWorkspace).then(collections => {
|
||||
if (collections.length) {
|
||||
workspaceSetting.addCollection(...collections);
|
||||
}
|
||||
});
|
||||
subscriber.next({
|
||||
loading: false,
|
||||
collections: workspaceSetting.collections,
|
||||
});
|
||||
if (group.disposed) {
|
||||
return;
|
||||
}
|
||||
const fn = () => {
|
||||
currentWorkspacePromise
|
||||
.then(async currentWorkspace => {
|
||||
const workspaceSetting = getWorkspaceSetting(currentWorkspace);
|
||||
migrateCollectionsFromIdbData(currentWorkspace)
|
||||
.then(collections => {
|
||||
if (collections.length) {
|
||||
workspaceSetting.addCollection(...collections);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
});
|
||||
migrateCollectionsFromUserData(currentWorkspace)
|
||||
.then(collections => {
|
||||
if (collections.length) {
|
||||
workspaceSetting.addCollection(...collections);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
});
|
||||
subscriber.next({
|
||||
loading: false,
|
||||
collections: workspaceSetting.collections,
|
||||
});
|
||||
};
|
||||
workspaceSetting.collectionsYArray.observe(fn);
|
||||
group.add(() => {
|
||||
workspaceSetting.collectionsYArray.unobserve(fn);
|
||||
if (group.disposed) {
|
||||
return;
|
||||
}
|
||||
const fn = () => {
|
||||
subscriber.next({
|
||||
loading: false,
|
||||
collections: workspaceSetting.collections,
|
||||
});
|
||||
};
|
||||
workspaceSetting.collectionsYArray.observe(fn);
|
||||
group.add(() => {
|
||||
workspaceSetting.collectionsYArray.unobserve(fn);
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
subscriber.error(error);
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
group.dispose();
|
||||
};
|
||||
|
||||
@@ -40,7 +40,7 @@ export const authAtom = atom<AuthAtom>({
|
||||
|
||||
export const openDisableCloudAlertModalAtom = atom(false);
|
||||
|
||||
type PageMode = 'page' | 'edgeless';
|
||||
export type PageMode = 'page' | 'edgeless';
|
||||
type PageLocalSetting = {
|
||||
mode: PageMode;
|
||||
};
|
||||
|
||||
@@ -1,129 +1,18 @@
|
||||
import { setupGlobal } from '@affine/env/global';
|
||||
import type { WorkspaceAdapter } from '@affine/env/workspace';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
|
||||
import type { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import {
|
||||
type RootWorkspaceMetadataV2,
|
||||
rootWorkspacesMetadataAtom,
|
||||
workspaceAdaptersAtom,
|
||||
} from '@affine/workspace/atom';
|
||||
import {
|
||||
getOrCreateWorkspace,
|
||||
globalBlockSuiteSchema,
|
||||
} from '@affine/workspace/manager';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import {
|
||||
migrateLocalBlobStorage,
|
||||
migrateWorkspace,
|
||||
WorkspaceVersion,
|
||||
} from '@toeverything/infra/blocksuite';
|
||||
import { downloadBinary, overwriteBinary } from '@toeverything/y-indexeddb';
|
||||
import type { createStore } from 'jotai/vanilla';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { applyUpdate, Doc as YDoc, encodeStateAsUpdate } from 'yjs';
|
||||
|
||||
import { WorkspaceAdapters } from '../adapters/workspace';
|
||||
import { performanceLogger } from '../shared';
|
||||
|
||||
const performanceSetupLogger = performanceLogger.namespace('setup');
|
||||
|
||||
async function tryMigration() {
|
||||
const value = localStorage.getItem('jotai-workspaces');
|
||||
if (value) {
|
||||
try {
|
||||
const metadata = JSON.parse(value) as RootWorkspaceMetadata[];
|
||||
const promises: Promise<void>[] = [];
|
||||
const newMetadata = [...metadata];
|
||||
metadata.forEach(oldMeta => {
|
||||
if (oldMeta.flavour === WorkspaceFlavour.LOCAL) {
|
||||
let doc: YDoc;
|
||||
const options = {
|
||||
getCurrentRootDoc: async () => {
|
||||
doc = new YDoc({
|
||||
guid: oldMeta.id,
|
||||
});
|
||||
const downloadWorkspace = async (doc: YDoc): Promise<void> => {
|
||||
const binary = await downloadBinary(doc.guid);
|
||||
if (binary) {
|
||||
applyUpdate(doc, binary);
|
||||
}
|
||||
await Promise.all(
|
||||
[...doc.subdocs.values()].map(subdoc =>
|
||||
downloadWorkspace(subdoc)
|
||||
)
|
||||
);
|
||||
};
|
||||
await downloadWorkspace(doc);
|
||||
return doc;
|
||||
},
|
||||
createWorkspace: async () =>
|
||||
getOrCreateWorkspace(nanoid(), WorkspaceFlavour.LOCAL),
|
||||
getSchema: () => globalBlockSuiteSchema,
|
||||
};
|
||||
promises.push(
|
||||
migrateWorkspace(
|
||||
'version' in oldMeta ? oldMeta.version : undefined,
|
||||
options
|
||||
).then(async status => {
|
||||
if (typeof status !== 'boolean') {
|
||||
const adapter = WorkspaceAdapters[oldMeta.flavour];
|
||||
const oldWorkspace = await adapter.CRUD.get(oldMeta.id);
|
||||
const newId = await adapter.CRUD.create(status);
|
||||
assertExists(
|
||||
oldWorkspace,
|
||||
'workspace should exist after migrate'
|
||||
);
|
||||
await adapter.CRUD.delete(oldWorkspace.blockSuiteWorkspace);
|
||||
const index = newMetadata.findIndex(
|
||||
meta => meta.id === oldMeta.id
|
||||
);
|
||||
newMetadata[index] = {
|
||||
...oldMeta,
|
||||
id: newId,
|
||||
version: WorkspaceVersion.Surface,
|
||||
};
|
||||
await migrateLocalBlobStorage(status.id, newId);
|
||||
console.log('workspace migrated', oldMeta.id, newId);
|
||||
} else if (status) {
|
||||
const index = newMetadata.findIndex(
|
||||
meta => meta.id === oldMeta.id
|
||||
);
|
||||
newMetadata[index] = {
|
||||
...oldMeta,
|
||||
version: WorkspaceVersion.Surface,
|
||||
};
|
||||
const overWrite = async (doc: YDoc): Promise<void> => {
|
||||
await overwriteBinary(doc.guid, encodeStateAsUpdate(doc));
|
||||
return Promise.all(
|
||||
[...doc.subdocs.values()].map(subdoc => overWrite(subdoc))
|
||||
).then();
|
||||
};
|
||||
await overWrite(doc);
|
||||
console.log('workspace migrated', oldMeta.id);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(promises)
|
||||
.then(() => {
|
||||
console.log('migration done');
|
||||
})
|
||||
.catch(e => {
|
||||
console.error('migration failed', e);
|
||||
})
|
||||
.finally(() => {
|
||||
localStorage.setItem('jotai-workspaces', JSON.stringify(newMetadata));
|
||||
window.dispatchEvent(new CustomEvent('migration-done'));
|
||||
window.$migrationDone = true;
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('error when migrating data', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function createFirstAppData(store: ReturnType<typeof createStore>) {
|
||||
const createFirst = (): RootWorkspaceMetadataV2[] => {
|
||||
const Plugins = Object.values(WorkspaceAdapters).sort(
|
||||
@@ -136,7 +25,6 @@ export function createFirstAppData(store: ReturnType<typeof createStore>) {
|
||||
<RootWorkspaceMetadataV2>{
|
||||
id,
|
||||
flavour: Plugin.flavour,
|
||||
version: WorkspaceVersion.DatabaseV3,
|
||||
}
|
||||
);
|
||||
}).filter((ids): ids is RootWorkspaceMetadataV2 => !!ids);
|
||||
@@ -163,9 +51,6 @@ export async function setup(store: ReturnType<typeof createStore>) {
|
||||
performanceSetupLogger.info('setup global');
|
||||
setupGlobal();
|
||||
|
||||
performanceSetupLogger.info('try migration');
|
||||
await tryMigration();
|
||||
|
||||
performanceSetupLogger.info('get root workspace meta');
|
||||
// do not read `rootWorkspacesMetadataAtom` before migration
|
||||
await store.get(rootWorkspacesMetadataAtom);
|
||||
|
||||
@@ -15,7 +15,7 @@ export const errorDetailStyle = style({
|
||||
});
|
||||
|
||||
export const errorTitle = style({
|
||||
fontSize: '36px',
|
||||
fontSize: '32px',
|
||||
lineHeight: '44px',
|
||||
fontWeight: 700,
|
||||
});
|
||||
|
||||
@@ -20,7 +20,7 @@ import { useLocation, useParams } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
RecoverableError,
|
||||
SessionFetchErrorRightAfterLoginOrSignUp,
|
||||
type SessionFetchErrorRightAfterLoginOrSignUp,
|
||||
} from '../../unexpected-application-state/errors';
|
||||
import {
|
||||
errorDescription,
|
||||
@@ -33,7 +33,9 @@ import {
|
||||
} from './affine-error-boundary.css';
|
||||
import errorBackground from './error-status.assets.svg';
|
||||
|
||||
export type AffineErrorBoundaryProps = React.PropsWithChildren;
|
||||
export type AffineErrorBoundaryProps = React.PropsWithChildren & {
|
||||
height?: number | string;
|
||||
};
|
||||
|
||||
type AffineError =
|
||||
| QueryParamError
|
||||
@@ -81,7 +83,7 @@ export class AffineErrorBoundary extends Component<
|
||||
if (this.state.error.canRetry()) {
|
||||
this.state.error.retry();
|
||||
this.setState({
|
||||
error: this.state.error,
|
||||
error: null,
|
||||
canRetryRecoveredError: this.state.error.canRetry(),
|
||||
});
|
||||
} else {
|
||||
@@ -90,6 +92,10 @@ export class AffineErrorBoundary extends Component<
|
||||
}
|
||||
};
|
||||
|
||||
private readonly handleRefresh = () => {
|
||||
this.setState({ error: null });
|
||||
};
|
||||
|
||||
static getDerivedStateFromError(
|
||||
error: AffineError
|
||||
): AffineErrorBoundaryState {
|
||||
@@ -121,14 +127,14 @@ export class AffineErrorBoundary extends Component<
|
||||
</>
|
||||
</>
|
||||
);
|
||||
} else if (error instanceof SessionFetchErrorRightAfterLoginOrSignUp) {
|
||||
} else if (error instanceof RecoverableError) {
|
||||
const retryButtonDesc = this.state.canRetryRecoveredError
|
||||
? 'Refetch'
|
||||
: 'Reload';
|
||||
errorDetail = (
|
||||
<>
|
||||
<h1 className={errorTitle}>Sorry.. there was an error</h1>
|
||||
<span className={errorDescription}> Fetching session failed </span>
|
||||
<span className={errorDescription}> {error.message} </span>
|
||||
<span className={errorDescription}>
|
||||
If you are still experiencing this issue, please{' '}
|
||||
<a
|
||||
@@ -151,13 +157,22 @@ export class AffineErrorBoundary extends Component<
|
||||
} else {
|
||||
errorDetail = (
|
||||
<>
|
||||
<h1>Sorry.. there was an error</h1>
|
||||
{error.message ?? error.toString()}
|
||||
<h1 className={errorTitle}>Sorry.. there was an error</h1>
|
||||
<code className={errorDescription}>
|
||||
{error.message ?? error.toString()}
|
||||
</code>
|
||||
<Button
|
||||
onClick={this.handleRefresh}
|
||||
className={errorRetryButton}
|
||||
type="primary"
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className={errorLayout}>
|
||||
<div className={errorLayout} style={{ height: this.props.height }}>
|
||||
<div className={errorDetailStyle}>{errorDetail}</div>
|
||||
<span className={errorDivider} />
|
||||
<div
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
import { Trans } from '@affine/i18n';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import { useCurrentLoginStatus } from '../../../hooks/affine/use-current-login-status';
|
||||
@@ -31,7 +32,7 @@ export const AfterSignInSendEmail = ({
|
||||
onSignedIn?.();
|
||||
}
|
||||
|
||||
const onResendClick = useCallback(async () => {
|
||||
const onResendClick = useAsyncCallback(async () => {
|
||||
if (verifyToken) {
|
||||
await signIn(email, verifyToken, challenge);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
} from '@affine/component/auth-components';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||
import { type FC, useCallback } from 'react';
|
||||
|
||||
import { useCurrentLoginStatus } from '../../../hooks/affine/use-current-login-status';
|
||||
@@ -29,7 +30,7 @@ export const AfterSignUpSendEmail: FC<AuthPanelProps> = ({
|
||||
onSignedIn?.();
|
||||
}
|
||||
|
||||
const onResendClick = useCallback(async () => {
|
||||
const onResendClick = useAsyncCallback(async () => {
|
||||
if (verifyToken) {
|
||||
await signUp(email, verifyToken, challenge);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useMutation } from '@affine/workspace/affine/gql';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||
import { useSetAtom } from 'jotai/react';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
@@ -146,7 +147,7 @@ export const SendEmail = ({
|
||||
const buttonContent = useButtonContent(emailType);
|
||||
const { loading, sendEmail } = useSendEmail(emailType);
|
||||
|
||||
const onSendEmail = useCallback(async () => {
|
||||
const onSendEmail = useAsyncCallback(async () => {
|
||||
// TODO: add error handler
|
||||
await sendEmail(email);
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
} from '@affine/component/auth-components';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
import { useSession } from 'next-auth/react';
|
||||
import type { FC } from 'react';
|
||||
@@ -26,7 +27,7 @@ export const SignInWithPassword: FC<AuthPanelProps> = ({
|
||||
const [password, setPassword] = useState('');
|
||||
const [passwordError, setPasswordError] = useState(false);
|
||||
|
||||
const onSignIn = useCallback(async () => {
|
||||
const onSignIn = useAsyncCallback(async () => {
|
||||
const res = await signInCloud('credentials', {
|
||||
redirect: false,
|
||||
email,
|
||||
|
||||
@@ -9,6 +9,7 @@ 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 { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||
import { GraphQLError } from 'graphql';
|
||||
import { type FC, useState } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
@@ -52,7 +53,7 @@ export const SignIn: FC<AuthPanelProps> = ({
|
||||
onSignedIn?.();
|
||||
}
|
||||
|
||||
const onContinue = useCallback(async () => {
|
||||
const onContinue = useAsyncCallback(async () => {
|
||||
if (!validateEmail(email)) {
|
||||
setIsValidEmail(false);
|
||||
return;
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useMutation, useQuery } from '@affine/workspace/affine/gql';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import { Loading } from '@toeverything/components/loading';
|
||||
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { Suspense, useCallback, useEffect, useMemo } from 'react';
|
||||
|
||||
@@ -33,12 +34,12 @@ const usePaymentRedirect = () => {
|
||||
mutation: checkoutMutation,
|
||||
});
|
||||
|
||||
return useCallback(() => {
|
||||
checkoutSubscription({ recurring, idempotencyKey })
|
||||
.then(({ checkout }) => {
|
||||
window.open(checkout, '_self', 'norefferer');
|
||||
})
|
||||
.catch(e => console.error(e));
|
||||
return useAsyncCallback(async () => {
|
||||
const { checkout } = await checkoutSubscription({
|
||||
recurring,
|
||||
idempotencyKey,
|
||||
});
|
||||
window.open(checkout, '_self', 'norefferer');
|
||||
}, [recurring, idempotencyKey, checkoutSubscription]);
|
||||
};
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
Modal,
|
||||
} from '@toeverything/components/modal';
|
||||
import { Tooltip } from '@toeverything/components/tooltip';
|
||||
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||
import type {
|
||||
LoadDBFileResult,
|
||||
SelectDBFileLocationResult,
|
||||
@@ -330,14 +331,13 @@ export const CreateWorkspaceModal = ({
|
||||
]
|
||||
);
|
||||
|
||||
const onConfirmName = useCallback(
|
||||
(name: string) => {
|
||||
const onConfirmName = useAsyncCallback(
|
||||
async (name: string) => {
|
||||
setWorkspaceName(name);
|
||||
// this will be the last step for web for now
|
||||
// fix me later
|
||||
createLocalWorkspace(name).then(id => {
|
||||
onCreate(id);
|
||||
});
|
||||
const id = await createLocalWorkspace(name);
|
||||
onCreate(id);
|
||||
},
|
||||
[createLocalWorkspace, onCreate]
|
||||
);
|
||||
|
||||
@@ -19,7 +19,7 @@ export const EnableAffineCloudModal = ({
|
||||
const setAuthAtom = useSetAtom(authAtom);
|
||||
const setOnceSignedInEvent = useSetAtom(setOnceSignedInEventAtom);
|
||||
|
||||
const confirm = useCallback(async () => {
|
||||
const confirm = useCallback(() => {
|
||||
return propsOnConfirm?.();
|
||||
}, [propsOnConfirm]);
|
||||
|
||||
|
||||
+5
-5
@@ -1,17 +1,17 @@
|
||||
import { pushNotificationAtom } from '@affine/component/notification-center';
|
||||
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 { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||
import type { SaveDBFileResult } from '@toeverything/infra/type';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import type { Doc } from 'yjs';
|
||||
import { encodeStateAsUpdate } from 'yjs';
|
||||
|
||||
async function syncBlobsToSqliteDb(workspace: AffineOfficialWorkspace) {
|
||||
if (window.apis && isDesktop) {
|
||||
if (window.apis && environment.isDesktop) {
|
||||
const bs = workspace.blockSuiteWorkspace.blob;
|
||||
const blobsInDb = await window.apis.db.getBlobKeys(workspace.id);
|
||||
const blobsInStorage = await bs.list();
|
||||
@@ -32,7 +32,7 @@ async function syncBlobsToSqliteDb(workspace: AffineOfficialWorkspace) {
|
||||
}
|
||||
|
||||
async function syncDocsToSqliteDb(workspace: AffineOfficialWorkspace) {
|
||||
if (window.apis && isDesktop) {
|
||||
if (window.apis && environment.isDesktop) {
|
||||
const workspaceId = workspace.blockSuiteWorkspace.doc.guid;
|
||||
const syncDoc = async (doc: Doc) => {
|
||||
await window.apis.db.applyDocUpdate(
|
||||
@@ -56,7 +56,7 @@ export const ExportPanel = ({ workspace }: ExportPanelProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const [syncing, setSyncing] = useState(false);
|
||||
const pushNotification = useSetAtom(pushNotificationAtom);
|
||||
const onExport = useCallback(async () => {
|
||||
const onExport = useAsyncCallback(async () => {
|
||||
if (syncing) {
|
||||
return;
|
||||
}
|
||||
|
||||
+3
-1
@@ -167,7 +167,9 @@ export const CloudWorkspaceMembersPanel = ({
|
||||
})}
|
||||
,
|
||||
<div className={style.goUpgradeWrapper} onClick={handleUpgrade}>
|
||||
<span className={style.goUpgrade}>go upgrade</span>
|
||||
<span className={style.goUpgrade}>
|
||||
{t['com.affine.payment.member.description.go-upgrade']()}
|
||||
</span>
|
||||
<ArrowRightBigIcon className={style.arrowRight} />
|
||||
</div>
|
||||
</span>
|
||||
|
||||
+2
-2
@@ -77,8 +77,8 @@ export const ProfilePanel = ({ workspace, isOwner }: ProfilePanelProps) => {
|
||||
);
|
||||
|
||||
const handleUploadAvatar = useCallback(
|
||||
async (file: File) => {
|
||||
await update(file)
|
||||
(file: File) => {
|
||||
update(file)
|
||||
.then(() => {
|
||||
pushNotification({
|
||||
title: 'Update workspace avatar success',
|
||||
|
||||
+3
-2
@@ -11,9 +11,10 @@ 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 { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
|
||||
import { noop } from 'foxact/noop';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { toast } from '../../../utils';
|
||||
import { EnableAffineCloudModal } from '../enable-affine-cloud-modal';
|
||||
@@ -52,7 +53,7 @@ const PublishPanelAffine = (props: PublishPanelAffineProps) => {
|
||||
);
|
||||
}, []);
|
||||
|
||||
const copyUrl = useCallback(async () => {
|
||||
const copyUrl = useAsyncCallback(async () => {
|
||||
await navigator.clipboard.writeText(shareUrl);
|
||||
toast(t['Copied link to clipboard']());
|
||||
}, [shareUrl, t]);
|
||||
|
||||
+2
-1
@@ -16,6 +16,7 @@ import { useMutation, useQuery } from '@affine/workspace/affine/gql';
|
||||
import { ArrowRightSmallIcon, CameraIcon } from '@blocksuite/icons';
|
||||
import { Avatar } from '@toeverything/components/avatar';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||
import { validateAndReduceImage } from '@toeverything/hooks/use-block-suite-workspace-avatar-url';
|
||||
import bytes from 'bytes';
|
||||
import { useSetAtom } from 'jotai';
|
||||
@@ -50,7 +51,7 @@ export const UserAvatar = () => {
|
||||
mutation: removeAvatarMutation,
|
||||
});
|
||||
|
||||
const handleUpdateUserAvatar = useCallback(
|
||||
const handleUpdateUserAvatar = useAsyncCallback(
|
||||
async (file: File) => {
|
||||
try {
|
||||
const reducedFile = await validateAndReduceImage(file);
|
||||
|
||||
+8
-7
@@ -1,3 +1,4 @@
|
||||
import { Skeleton } from '@affine/component';
|
||||
import { Pagination } from '@affine/component/member-components';
|
||||
import {
|
||||
SettingHeader,
|
||||
@@ -19,9 +20,9 @@ import { Trans } from '@affine/i18n';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useMutation, useQuery } from '@affine/workspace/affine/gql';
|
||||
import { ArrowRightSmallIcon } from '@blocksuite/icons';
|
||||
import { Skeleton } from '@mui/material';
|
||||
import { Button, IconButton } from '@toeverything/components/button';
|
||||
import { Loading } from '@toeverything/components/loading';
|
||||
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { Suspense, useCallback, useMemo, useState } from 'react';
|
||||
|
||||
@@ -105,10 +106,10 @@ const SubscriptionSettings = () => {
|
||||
plan === SubscriptionPlan.Free
|
||||
? '0'
|
||||
: price
|
||||
? recurring === SubscriptionRecurring.Monthly
|
||||
? String(price.amount / 100)
|
||||
: String(price.yearlyAmount / 100)
|
||||
: '?';
|
||||
? recurring === SubscriptionRecurring.Monthly
|
||||
? String(price.amount / 100)
|
||||
: String(price.yearlyAmount / 100)
|
||||
: '?';
|
||||
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
@@ -255,8 +256,8 @@ const PaymentMethodUpdater = () => {
|
||||
});
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const update = useCallback(() => {
|
||||
trigger(null, {
|
||||
const update = useAsyncCallback(async () => {
|
||||
await trigger(null, {
|
||||
onSuccess: data => {
|
||||
window.open(data.createCustomerPortal, '_blank', 'noopener noreferrer');
|
||||
},
|
||||
|
||||
+6
-5
@@ -4,9 +4,10 @@ import {
|
||||
resumeSubscriptionMutation,
|
||||
} from '@affine/graphql';
|
||||
import { useMutation } from '@affine/workspace/affine/gql';
|
||||
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||
import { nanoid } from 'nanoid';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { ConfirmLoadingModal, DowngradeModal } from './modals';
|
||||
|
||||
@@ -30,8 +31,8 @@ export const CancelAction = ({
|
||||
mutation: cancelSubscriptionMutation,
|
||||
});
|
||||
|
||||
const downgrade = useCallback(() => {
|
||||
trigger(
|
||||
const downgrade = useAsyncCallback(async () => {
|
||||
await trigger(
|
||||
{ idempotencyKey },
|
||||
{
|
||||
onSuccess: data => {
|
||||
@@ -78,8 +79,8 @@ export const ResumeAction = ({
|
||||
mutation: resumeSubscriptionMutation,
|
||||
});
|
||||
|
||||
const resume = useCallback(() => {
|
||||
trigger(
|
||||
const resume = useAsyncCallback(async () => {
|
||||
await trigger(
|
||||
{ idempotencyKey },
|
||||
{
|
||||
onSuccess: data => {
|
||||
|
||||
+4
@@ -13,6 +13,10 @@ export const scrollArea = style({
|
||||
overflowX: 'auto',
|
||||
scrollSnapType: 'x mandatory',
|
||||
paddingBottom: '21px',
|
||||
|
||||
/** Avoid box-shadow clipping */
|
||||
paddingTop: '21px',
|
||||
marginTop: '-21px',
|
||||
});
|
||||
|
||||
export const scrollBar = style({
|
||||
|
||||
+15
-8
@@ -15,6 +15,7 @@ import { useMutation } from '@affine/workspace/affine/gql';
|
||||
import { DoneIcon } from '@blocksuite/icons';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import { Tooltip } from '@toeverything/components/tooltip';
|
||||
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useAtom } from 'jotai';
|
||||
import { nanoid } from 'nanoid';
|
||||
@@ -127,21 +128,25 @@ export function getPlanDetail(t: ReturnType<typeof useAFFiNEI18N>) {
|
||||
}
|
||||
|
||||
export const PlanCard = (props: PlanCardProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const { detail, subscription, recurring } = props;
|
||||
const loggedIn = useCurrentLoginStatus() === 'authenticated';
|
||||
const currentPlan = subscription?.plan ?? SubscriptionPlan.Free;
|
||||
|
||||
const isCurrent = loggedIn && detail.plan === currentPlan;
|
||||
const isPro = detail.plan === SubscriptionPlan.Pro;
|
||||
|
||||
return (
|
||||
<div
|
||||
data-current={isCurrent}
|
||||
key={detail.plan}
|
||||
className={isCurrent ? styles.currentPlanCard : styles.planCard}
|
||||
className={isPro ? styles.proPlanCard : styles.planCard}
|
||||
>
|
||||
<div className={styles.planTitle}>
|
||||
<p>
|
||||
{detail.plan}{' '}
|
||||
<span className={isCurrent ? styles.proPlanTitle : ''}>
|
||||
{detail.plan}
|
||||
</span>{' '}
|
||||
{'discount' in detail &&
|
||||
recurring === SubscriptionRecurring.Yearly && (
|
||||
<span className={styles.discountLabel}>
|
||||
@@ -161,7 +166,9 @@ export const PlanCard = (props: PlanCardProps) => {
|
||||
? detail.price
|
||||
: detail.yearlyPrice}
|
||||
</span>
|
||||
<span className={styles.planPriceDesc}>per month</span>
|
||||
<span className={styles.planPriceDesc}>
|
||||
{t['com.affine.payment.price-description.per-month']()}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
@@ -364,7 +371,7 @@ const Upgrade = ({
|
||||
}, [onSubscriptionUpdate]);
|
||||
|
||||
const [, openPaymentDisableModal] = useAtom(openPaymentDisableAtom);
|
||||
const upgrade = useCallback(() => {
|
||||
const upgrade = useAsyncCallback(async () => {
|
||||
if (!runtimeConfig.enablePayment) {
|
||||
openPaymentDisableModal(true);
|
||||
return;
|
||||
@@ -373,7 +380,7 @@ const Upgrade = ({
|
||||
if (newTabRef.current) {
|
||||
newTabRef.current.focus();
|
||||
} else {
|
||||
trigger(
|
||||
await trigger(
|
||||
{ recurring, idempotencyKey },
|
||||
{
|
||||
onSuccess: data => {
|
||||
@@ -439,8 +446,8 @@ const ChangeRecurring = ({
|
||||
mutation: updateSubscriptionMutation,
|
||||
});
|
||||
|
||||
const change = useCallback(() => {
|
||||
trigger(
|
||||
const change = useAsyncCallback(async () => {
|
||||
await trigger(
|
||||
{ recurring: to, idempotencyKey },
|
||||
{
|
||||
onSuccess: data => {
|
||||
@@ -492,7 +499,7 @@ const ChangeRecurring = ({
|
||||
const SignUpAction = ({ children }: PropsWithChildren) => {
|
||||
const setOpen = useSetAtom(authAtom);
|
||||
|
||||
const onClickSignIn = useCallback(async () => {
|
||||
const onClickSignIn = useCallback(() => {
|
||||
setOpen(state => ({
|
||||
...state,
|
||||
openModal: true,
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { Skeleton } from '@mui/material';
|
||||
import { Skeleton } from '@affine/component';
|
||||
|
||||
import { PlanLayout } from './layout';
|
||||
import * as styles from './skeleton.css';
|
||||
|
||||
+25
-7
@@ -9,7 +9,7 @@ export const recurringRadioGroup = style({
|
||||
|
||||
export const radioButtonDiscount = style({
|
||||
marginLeft: '4px',
|
||||
color: 'var(--affine-primary-color)',
|
||||
color: 'var(--affine-brand-color)',
|
||||
fontWeight: 400,
|
||||
});
|
||||
export const radioButtonText = style({
|
||||
@@ -46,23 +46,41 @@ export const planCard = style({
|
||||
},
|
||||
});
|
||||
|
||||
export const currentPlanCard = style([
|
||||
export const proPlanCard = style([
|
||||
planCard,
|
||||
{
|
||||
borderWidth: '2px',
|
||||
borderColor: 'var(--affine-primary-color)',
|
||||
borderWidth: '1px',
|
||||
borderColor: 'var(--affine-brand-color)',
|
||||
boxShadow: 'var(--affine-shadow-2)',
|
||||
position: 'relative',
|
||||
'::after': {
|
||||
content: '',
|
||||
position: 'absolute',
|
||||
inset: '-1px',
|
||||
borderRadius: 'inherit',
|
||||
boxShadow: '0px 0px 0px 2px var(--affine-brand-color)',
|
||||
opacity: 0.3,
|
||||
zIndex: 1,
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
export const proPlanTitle = style({
|
||||
backgroundColor: 'var(--affine-brand-color)',
|
||||
color: 'var(--affine-white)',
|
||||
padding: '0px 6px',
|
||||
borderRadius: '4px',
|
||||
height: '24px',
|
||||
display: 'inline-block',
|
||||
});
|
||||
|
||||
export const discountLabel = style({
|
||||
color: 'var(--affine-primary-color)',
|
||||
color: 'var(--affine-text-emphasis-color)',
|
||||
marginLeft: '8px',
|
||||
lineHeight: '20px',
|
||||
fontSize: 'var(--affine-font-xs)',
|
||||
fontWeight: 500,
|
||||
padding: '0 4px',
|
||||
backgroundColor: 'var(--affine-blue-50)',
|
||||
borderRadius: '4px',
|
||||
display: 'inline-block',
|
||||
height: '100%',
|
||||
|
||||
+3
-2
@@ -2,6 +2,7 @@ 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 { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useAtomValue } from 'jotai';
|
||||
@@ -65,7 +66,7 @@ export const WorkspaceSetting = ({ workspaceId }: { workspaceId: string }) => {
|
||||
workspaces,
|
||||
]);
|
||||
|
||||
const handleDeleteWorkspace = useCallback(async () => {
|
||||
const handleDeleteWorkspace = useAsyncCallback(async () => {
|
||||
closeAndJumpOut();
|
||||
await deleteWorkspace(workspaceId);
|
||||
|
||||
@@ -75,7 +76,7 @@ export const WorkspaceSetting = ({ workspaceId }: { workspaceId: string }) => {
|
||||
});
|
||||
}, [closeAndJumpOut, deleteWorkspace, pushNotification, t, workspaceId]);
|
||||
|
||||
const handleLeaveWorkspace = useCallback(async () => {
|
||||
const handleLeaveWorkspace = useAsyncCallback(async () => {
|
||||
closeAndJumpOut();
|
||||
await leaveWorkspace(workspaceId, workspaceName);
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { ShareMenu } from '@affine/component/share-menu';
|
||||
import {
|
||||
type AffineOfficialWorkspace,
|
||||
WorkspaceFlavour,
|
||||
@@ -6,10 +5,9 @@ import {
|
||||
import type { Page } from '@blocksuite/store';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { useExportPage } from '../../../hooks/affine/use-export-page';
|
||||
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';
|
||||
import { ShareMenu } from './share-menu';
|
||||
|
||||
type SharePageModalProps = {
|
||||
workspace: AffineOfficialWorkspace;
|
||||
@@ -19,7 +17,7 @@ type SharePageModalProps = {
|
||||
export const SharePageModal = ({ workspace, page }: SharePageModalProps) => {
|
||||
const onTransformWorkspace = useOnTransformWorkspace();
|
||||
const [open, setOpen] = useState(false);
|
||||
const exportHandler = useExportPage(page);
|
||||
|
||||
const handleConfirm = useCallback(() => {
|
||||
if (workspace.flavour !== WorkspaceFlavour.LOCAL) {
|
||||
return;
|
||||
@@ -31,15 +29,13 @@ export const SharePageModal = ({ workspace, page }: SharePageModalProps) => {
|
||||
);
|
||||
setOpen(false);
|
||||
}, [onTransformWorkspace, workspace]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ShareMenu
|
||||
workspace={workspace}
|
||||
currentPage={page}
|
||||
useIsSharedPage={useIsSharedPage}
|
||||
onEnableAffineCloud={() => setOpen(true)}
|
||||
togglePagePublic={async () => {}}
|
||||
exportHandler={exportHandler}
|
||||
/>
|
||||
{workspace.flavour === WorkspaceFlavour.LOCAL ? (
|
||||
<EnableAffineCloudModal
|
||||
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
import { globalStyle, style } from '@vanilla-extract/css';
|
||||
|
||||
export const headerStyle = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
fontWeight: 600,
|
||||
lineHeight: '22px',
|
||||
padding: '0 4px',
|
||||
gap: '4px',
|
||||
});
|
||||
|
||||
export const menuStyle = style({
|
||||
width: '410px',
|
||||
height: 'auto',
|
||||
padding: '12px',
|
||||
transform: 'translateX(-10px)',
|
||||
});
|
||||
|
||||
export const menuItemStyle = style({
|
||||
padding: '4px',
|
||||
transition: 'all 0.3s',
|
||||
});
|
||||
|
||||
export const descriptionStyle = style({
|
||||
wordWrap: 'break-word',
|
||||
// wordBreak: 'break-all',
|
||||
fontSize: 'var(--affine-font-xs)',
|
||||
lineHeight: '20px',
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
textAlign: 'left',
|
||||
padding: '0 6px',
|
||||
});
|
||||
|
||||
export const buttonStyle = style({
|
||||
marginTop: '18px',
|
||||
// todo: new color scheme should be used
|
||||
});
|
||||
|
||||
export const actionsStyle = style({
|
||||
display: 'flex',
|
||||
gap: '9px',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'flex-start',
|
||||
});
|
||||
|
||||
export const containerStyle = style({
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
flexDirection: 'column',
|
||||
gap: '8px',
|
||||
});
|
||||
|
||||
export const indicatorContainerStyle = style({
|
||||
position: 'relative',
|
||||
});
|
||||
|
||||
export const inputButtonRowStyle = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginTop: '16px',
|
||||
});
|
||||
|
||||
export const titleContainerStyle = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
fontWeight: 500,
|
||||
lineHeight: '22px',
|
||||
padding: '0 4px',
|
||||
});
|
||||
|
||||
export const subTitleStyle = style({
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
fontWeight: 500,
|
||||
lineHeight: '22px',
|
||||
});
|
||||
|
||||
export const columnContainerStyle = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
width: '100%',
|
||||
gap: '8px',
|
||||
});
|
||||
|
||||
export const rowContainerStyle = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
gap: '12px',
|
||||
padding: '4px',
|
||||
});
|
||||
|
||||
export const radioButtonGroup = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
padding: '2px',
|
||||
minWidth: '154px',
|
||||
maxWidth: '250px',
|
||||
});
|
||||
|
||||
export const radioButton = style({
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
selectors: {
|
||||
'&[data-state="checked"]': {
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
},
|
||||
},
|
||||
});
|
||||
export const spanStyle = style({
|
||||
padding: '4px 20px',
|
||||
});
|
||||
|
||||
export const disableSharePage = style({
|
||||
color: 'var(--affine-error-color)',
|
||||
});
|
||||
|
||||
export const localSharePage = style({
|
||||
padding: '12px 8px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
borderRadius: '8px',
|
||||
backgroundColor: 'var(--affine-background-secondary-color)',
|
||||
minHeight: '84px',
|
||||
position: 'relative',
|
||||
});
|
||||
|
||||
export const cloudSvgContainer = style({
|
||||
width: '146px',
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center',
|
||||
position: 'absolute',
|
||||
bottom: '0',
|
||||
right: '0',
|
||||
});
|
||||
export const shareIconStyle = style({
|
||||
fontSize: '16px',
|
||||
color: 'var(--affine-icon-color)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
export const shareLinkStyle = style({
|
||||
padding: '4px',
|
||||
fontSize: 'var(--affine-font-xs)',
|
||||
fontWeight: 500,
|
||||
lineHeight: '20px',
|
||||
transform: 'translateX(-4px)',
|
||||
gap: '4px',
|
||||
});
|
||||
globalStyle(`${shareLinkStyle} > span`, {
|
||||
color: 'var(--affine-link-color)',
|
||||
});
|
||||
globalStyle(`${shareLinkStyle} > div > svg`, {
|
||||
color: 'var(--affine-link-color)',
|
||||
});
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
import { atom } from 'jotai/vanilla';
|
||||
|
||||
export const enableShareMenuAtom = atom(false);
|
||||
@@ -0,0 +1 @@
|
||||
export * from './share-menu';
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
import { ExportMenuItems } from '@affine/component/page-list';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { LinkIcon } from '@blocksuite/icons';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import { Divider } from '@toeverything/components/divider';
|
||||
|
||||
import { useExportPage } from '../../../../hooks/affine/use-export-page';
|
||||
import * as styles from './index.css';
|
||||
import type { ShareMenuProps } from './share-menu';
|
||||
import { useSharingUrl } from './use-share-url';
|
||||
|
||||
export const ShareExport = ({ workspace, currentPage }: ShareMenuProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const workspaceId = workspace.id;
|
||||
const pageId = currentPage.id;
|
||||
const { onClickCopyLink } = useSharingUrl({
|
||||
workspaceId,
|
||||
pageId,
|
||||
urlType: 'workspace',
|
||||
});
|
||||
const exportHandler = useExportPage(currentPage);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.titleContainerStyle}>
|
||||
{t['com.affine.share-menu.ShareViaExport']()}
|
||||
</div>
|
||||
<div className={styles.descriptionStyle}>
|
||||
{t['com.affine.share-menu.ShareViaExportDescription']()}
|
||||
</div>
|
||||
<div>
|
||||
<ExportMenuItems
|
||||
exportHandler={exportHandler}
|
||||
className={styles.menuItemStyle}
|
||||
/>
|
||||
</div>
|
||||
{workspace.flavour !== WorkspaceFlavour.LOCAL ? (
|
||||
<div className={styles.columnContainerStyle}>
|
||||
<Divider size="thinner" />
|
||||
<div className={styles.titleContainerStyle}>
|
||||
{t['com.affine.share-menu.share-privately']()}
|
||||
</div>
|
||||
<div className={styles.descriptionStyle}>
|
||||
{t['com.affine.share-menu.share-privately.description']()}
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
className={styles.shareLinkStyle}
|
||||
onClick={onClickCopyLink}
|
||||
icon={<LinkIcon />}
|
||||
type="plain"
|
||||
>
|
||||
{t['com.affine.share-menu.copy-private-link']()}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
import {
|
||||
type AffineCloudWorkspace,
|
||||
type AffineOfficialWorkspace,
|
||||
type AffinePublicWorkspace,
|
||||
type LocalWorkspace,
|
||||
WorkspaceFlavour,
|
||||
} from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { WebIcon } from '@blocksuite/icons';
|
||||
import type { Page } from '@blocksuite/store';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import { Divider } from '@toeverything/components/divider';
|
||||
import { Menu } from '@toeverything/components/menu';
|
||||
|
||||
import { useIsSharedPage } from '../../../../hooks/affine/use-is-shared-page';
|
||||
import * as styles from './index.css';
|
||||
import { ShareExport } from './share-export';
|
||||
import { SharePage } from './share-page';
|
||||
|
||||
export interface ShareMenuProps<
|
||||
Workspace extends AffineOfficialWorkspace =
|
||||
| AffineCloudWorkspace
|
||||
| LocalWorkspace
|
||||
| AffinePublicWorkspace,
|
||||
> {
|
||||
workspace: Workspace;
|
||||
currentPage: Page;
|
||||
onEnableAffineCloud: () => void;
|
||||
}
|
||||
|
||||
const ShareMenuContent = (props: ShareMenuProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
return (
|
||||
<div className={styles.containerStyle}>
|
||||
<div className={styles.headerStyle}>
|
||||
<div className={styles.shareIconStyle}>
|
||||
<WebIcon />
|
||||
</div>
|
||||
{t['com.affine.share-menu.SharePage']()}
|
||||
</div>
|
||||
<SharePage {...props} />
|
||||
<div className={styles.columnContainerStyle}>
|
||||
<Divider size="thinner" />
|
||||
</div>
|
||||
<ShareExport {...props} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const LocalShareMenu = (props: ShareMenuProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
return (
|
||||
<Menu
|
||||
items={<ShareMenuContent {...props} />}
|
||||
contentOptions={{
|
||||
className: styles.menuStyle,
|
||||
['data-testid' as string]: 'local-share-menu',
|
||||
}}
|
||||
rootOptions={{
|
||||
modal: false,
|
||||
}}
|
||||
>
|
||||
<Button data-testid="local-share-menu-button" type="plain">
|
||||
{t['com.affine.share-menu.shareButton']()}
|
||||
</Button>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
const CloudShareMenu = (props: ShareMenuProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const {
|
||||
workspace: { id: workspaceId },
|
||||
currentPage,
|
||||
} = props;
|
||||
const { isSharedPage } = useIsSharedPage(
|
||||
workspaceId,
|
||||
currentPage.spaceDoc.guid
|
||||
);
|
||||
|
||||
return (
|
||||
<Menu
|
||||
items={<ShareMenuContent {...props} />}
|
||||
contentOptions={{
|
||||
className: styles.menuStyle,
|
||||
['data-testid' as string]: 'cloud-share-menu',
|
||||
}}
|
||||
rootOptions={{
|
||||
modal: false,
|
||||
}}
|
||||
>
|
||||
<Button data-testid="cloud-share-menu-button" type="plain">
|
||||
<div
|
||||
style={{
|
||||
color: isSharedPage
|
||||
? 'var(--affine-link-color)'
|
||||
: 'var(--affine-text-primary-color)',
|
||||
}}
|
||||
>
|
||||
{isSharedPage
|
||||
? t['com.affine.share-menu.sharedButton']()
|
||||
: t['com.affine.share-menu.shareButton']()}
|
||||
</div>
|
||||
</Button>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
export const ShareMenu = (props: ShareMenuProps) => {
|
||||
const { workspace } = props;
|
||||
|
||||
if (workspace.flavour === WorkspaceFlavour.LOCAL) {
|
||||
return <LocalShareMenu {...props} />;
|
||||
}
|
||||
return <CloudShareMenu {...props} />;
|
||||
};
|
||||
+256
@@ -0,0 +1,256 @@
|
||||
import {
|
||||
Input,
|
||||
RadioButton,
|
||||
RadioButtonGroup,
|
||||
Switch,
|
||||
toast,
|
||||
} from '@affine/component';
|
||||
import { PublicLinkDisableModal } from '@affine/component/disable-public-link';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { ArrowRightSmallIcon } from '@blocksuite/icons';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import { Menu, MenuItem, MenuTrigger } from '@toeverything/components/menu';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import type { PageMode } from '../../../../atoms';
|
||||
import { currentModeAtom } from '../../../../atoms/mode';
|
||||
import { useIsSharedPage } from '../../../../hooks/affine/use-is-shared-page';
|
||||
import * as styles from './index.css';
|
||||
import type { ShareMenuProps } from './share-menu';
|
||||
import { useSharingUrl } from './use-share-url';
|
||||
|
||||
const CloudSvg = () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="146"
|
||||
height="84"
|
||||
viewBox="0 0 146 84"
|
||||
fill="none"
|
||||
>
|
||||
<g opacity="0.1">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M66.9181 15.9788C52.6393 15.9788 41.064 27.5541 41.064 41.8329C41.064 43.7879 41.2801 45.687 41.6881 47.5094C42.2383 49.9676 40.6923 52.4066 38.2344 52.9579C29.4068 54.938 22.814 62.8293 22.814 72.2496C22.814 83.1687 31.6657 92.0204 42.5848 92.0204H97.3348C111.614 92.0204 123.189 80.4451 123.189 66.1663C123.189 51.8874 111.614 40.3121 97.3348 40.3121C97.1618 40.3121 96.9892 40.3138 96.8169 40.3172C94.6134 40.3603 92.6941 38.8222 92.2561 36.6623C89.8629 24.8606 79.4226 15.9788 66.9181 15.9788ZM31.939 41.8329C31.939 22.5145 47.5997 6.85376 66.9181 6.85376C82.573 6.85376 95.8181 17.1339 100.285 31.3098C118.223 32.808 132.314 47.8415 132.314 66.1663C132.314 85.4847 116.653 101.145 97.3348 101.145H42.5848C26.6261 101.145 13.689 88.2083 13.689 72.2496C13.689 59.9818 21.3304 49.5073 32.1102 45.3122C31.9969 44.1668 31.939 43.0061 31.939 41.8329Z"
|
||||
fill="var(--affine-icon-color)"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const LocalSharePage = (props: ShareMenuProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
return (
|
||||
<div className={styles.localSharePage}>
|
||||
<div className={styles.columnContainerStyle} style={{ gap: '12px' }}>
|
||||
<div className={styles.descriptionStyle} style={{ maxWidth: '230px' }}>
|
||||
{t['com.affine.share-menu.EnableCloudDescription']()}
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
onClick={props.onEnableAffineCloud}
|
||||
type="primary"
|
||||
data-testid="share-menu-enable-affine-cloud-button"
|
||||
>
|
||||
{t['Enable AFFiNE Cloud']()}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.cloudSvgContainer}>
|
||||
<CloudSvg />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const AffineSharePage = (props: ShareMenuProps) => {
|
||||
const {
|
||||
workspace: { id: workspaceId },
|
||||
currentPage,
|
||||
} = props;
|
||||
const pageId = currentPage.id;
|
||||
const [showDisable, setShowDisable] = useState(false);
|
||||
const {
|
||||
isSharedPage,
|
||||
enableShare,
|
||||
changeShare,
|
||||
currentShareMode,
|
||||
disableShare,
|
||||
} = useIsSharedPage(workspaceId, currentPage.spaceDoc.guid);
|
||||
const currentPageMode = useAtomValue(currentModeAtom);
|
||||
|
||||
const defaultMode = useMemo(() => {
|
||||
if (isSharedPage) {
|
||||
// if it's a shared page, use the share mode
|
||||
return currentShareMode;
|
||||
}
|
||||
// default to current page mode
|
||||
return currentPageMode;
|
||||
}, [currentPageMode, currentShareMode, isSharedPage]);
|
||||
const [mode, setMode] = useState<PageMode>(defaultMode);
|
||||
|
||||
const { sharingUrl, onClickCopyLink } = useSharingUrl({
|
||||
workspaceId,
|
||||
pageId,
|
||||
urlType: 'share',
|
||||
});
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const onClickCreateLink = useCallback(() => {
|
||||
enableShare(mode);
|
||||
}, [enableShare, mode]);
|
||||
|
||||
const onDisablePublic = useCallback(() => {
|
||||
disableShare();
|
||||
toast('Successfully disabled', {
|
||||
portal: document.body,
|
||||
});
|
||||
setShowDisable(false);
|
||||
}, [disableShare]);
|
||||
|
||||
const onShareModeChange = useCallback(
|
||||
(value: PageMode) => {
|
||||
setMode(value);
|
||||
if (isSharedPage) {
|
||||
changeShare(value);
|
||||
}
|
||||
},
|
||||
[changeShare, isSharedPage]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.titleContainerStyle}>
|
||||
{t['com.affine.share-menu.publish-to-web']()}
|
||||
</div>
|
||||
<div className={styles.columnContainerStyle}>
|
||||
<div className={styles.descriptionStyle}>
|
||||
{t['com.affine.share-menu.publish-to-web.description']()}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.rowContainerStyle}>
|
||||
<Input
|
||||
inputStyle={{
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
fontSize: 'var(--affine-font-xs)',
|
||||
lineHeight: '20px',
|
||||
}}
|
||||
value={
|
||||
isSharedPage ? sharingUrl : `${runtimeConfig.serverUrlPrefix}/...`
|
||||
}
|
||||
readOnly
|
||||
/>
|
||||
{isSharedPage ? (
|
||||
<Button
|
||||
onClick={onClickCopyLink}
|
||||
data-testid="share-menu-copy-link-button"
|
||||
style={{ padding: '4px 12px', whiteSpace: 'nowrap' }}
|
||||
>
|
||||
{t.Copy()}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
onClick={onClickCreateLink}
|
||||
type="primary"
|
||||
data-testid="share-menu-create-link-button"
|
||||
style={{ padding: '4px 12px', whiteSpace: 'nowrap' }}
|
||||
>
|
||||
{t.Create()}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.rowContainerStyle}>
|
||||
<div className={styles.subTitleStyle}>
|
||||
{t['com.affine.share-menu.ShareMode']()}
|
||||
</div>
|
||||
<div>
|
||||
<RadioButtonGroup
|
||||
className={styles.radioButtonGroup}
|
||||
defaultValue={defaultMode}
|
||||
value={mode}
|
||||
onValueChange={onShareModeChange}
|
||||
>
|
||||
<RadioButton
|
||||
className={styles.radioButton}
|
||||
value={'page'}
|
||||
spanStyle={styles.spanStyle}
|
||||
>
|
||||
{t['com.affine.pageMode.page']()}
|
||||
</RadioButton>
|
||||
<RadioButton
|
||||
className={styles.radioButton}
|
||||
value={'edgeless'}
|
||||
spanStyle={styles.spanStyle}
|
||||
>
|
||||
{t['com.affine.pageMode.edgeless']()}
|
||||
</RadioButton>
|
||||
</RadioButtonGroup>
|
||||
</div>
|
||||
</div>
|
||||
{isSharedPage ? (
|
||||
<>
|
||||
{runtimeConfig.enableEnhanceShareMode && (
|
||||
<>
|
||||
<div className={styles.rowContainerStyle}>
|
||||
<div className={styles.subTitleStyle}>Link expires</div>
|
||||
<div>
|
||||
<Menu items={<MenuItem>Never</MenuItem>}>
|
||||
<MenuTrigger>Never</MenuTrigger>
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.rowContainerStyle}>
|
||||
<div className={styles.subTitleStyle}>
|
||||
{'Show "Created with AFFiNE"'}
|
||||
</div>
|
||||
<div>
|
||||
<Switch />
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.rowContainerStyle}>
|
||||
<div className={styles.subTitleStyle}>
|
||||
Search engine indexing
|
||||
</div>
|
||||
<div>
|
||||
<Switch />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<MenuItem
|
||||
endFix={<ArrowRightSmallIcon />}
|
||||
block
|
||||
type="danger"
|
||||
className={styles.menuItemStyle}
|
||||
onSelect={e => {
|
||||
e.preventDefault();
|
||||
setShowDisable(true);
|
||||
}}
|
||||
>
|
||||
<div className={styles.disableSharePage}>
|
||||
{t['Disable Public Link']()}
|
||||
</div>
|
||||
</MenuItem>
|
||||
<PublicLinkDisableModal
|
||||
open={showDisable}
|
||||
onConfirm={onDisablePublic}
|
||||
onOpenChange={setShowDisable}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const SharePage = (props: ShareMenuProps) => {
|
||||
if (props.workspace.flavour === WorkspaceFlavour.LOCAL) {
|
||||
return <LocalSharePage {...props} />;
|
||||
} else if (props.workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD) {
|
||||
return <AffineSharePage {...props} />;
|
||||
}
|
||||
throw new Error('Unreachable');
|
||||
};
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import { toast } from '@affine/component';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
type UrlType = 'share' | 'workspace';
|
||||
|
||||
type UseSharingUrl = {
|
||||
workspaceId: string;
|
||||
pageId: string;
|
||||
urlType: UrlType;
|
||||
};
|
||||
|
||||
export const generateUrl = ({
|
||||
workspaceId,
|
||||
pageId,
|
||||
urlType,
|
||||
}: UseSharingUrl) => {
|
||||
// to generate a private url like https://affine.app/workspace/123/456
|
||||
// to generate a public url like https://affine.app/share/123/456
|
||||
// or https://affine.app/share/123/456?mode=edgeless
|
||||
|
||||
const url = new URL(
|
||||
`${runtimeConfig.serverUrlPrefix}/${urlType}/${workspaceId}/${pageId}`
|
||||
);
|
||||
return url.toString();
|
||||
};
|
||||
|
||||
export const useSharingUrl = ({
|
||||
workspaceId,
|
||||
pageId,
|
||||
urlType,
|
||||
}: UseSharingUrl) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const sharingUrl = useMemo(
|
||||
() => generateUrl({ workspaceId, pageId, urlType }),
|
||||
[workspaceId, pageId, urlType]
|
||||
);
|
||||
|
||||
const onClickCopyLink = useCallback(() => {
|
||||
navigator.clipboard
|
||||
.writeText(sharingUrl)
|
||||
.then(() => {
|
||||
toast(t['Copied link to clipboard']());
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}, [sharingUrl, t]);
|
||||
|
||||
return {
|
||||
sharingUrl,
|
||||
onClickCopyLink,
|
||||
};
|
||||
};
|
||||
+16
-4
@@ -1,5 +1,4 @@
|
||||
import type { AffineOfficialWorkspace } from '@affine/env/workspace';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import {
|
||||
useBlockSuitePageMeta,
|
||||
usePageMetaHelper,
|
||||
@@ -13,6 +12,7 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import type { PageMode } from '../../../atoms';
|
||||
import { EditorModeSwitch } from '../block-suite-mode-switch';
|
||||
import { PageMenu } from './operation-menu';
|
||||
import * as styles from './styles.css';
|
||||
@@ -20,6 +20,8 @@ import * as styles from './styles.css';
|
||||
export interface BlockSuiteHeaderTitleProps {
|
||||
workspace: AffineOfficialWorkspace;
|
||||
pageId: string;
|
||||
isPublic?: boolean;
|
||||
publicMode?: PageMode;
|
||||
}
|
||||
|
||||
const EditableTitle = ({
|
||||
@@ -54,6 +56,8 @@ const StableTitle = ({
|
||||
workspace,
|
||||
pageId,
|
||||
onRename,
|
||||
isPublic,
|
||||
publicMode,
|
||||
}: BlockSuiteHeaderTitleProps & {
|
||||
onRename?: () => void;
|
||||
}) => {
|
||||
@@ -64,11 +68,19 @@ const StableTitle = ({
|
||||
|
||||
const title = pageMeta?.title;
|
||||
|
||||
const handleRename = useCallback(() => {
|
||||
if (!isPublic && onRename) {
|
||||
onRename();
|
||||
}
|
||||
}, [isPublic, onRename]);
|
||||
|
||||
return (
|
||||
<div className={styles.headerTitleContainer}>
|
||||
<EditorModeSwitch
|
||||
blockSuiteWorkspace={workspace.blockSuiteWorkspace}
|
||||
pageId={pageId}
|
||||
isPublic={isPublic}
|
||||
publicMode={publicMode}
|
||||
style={{
|
||||
marginRight: '12px',
|
||||
}}
|
||||
@@ -76,11 +88,11 @@ const StableTitle = ({
|
||||
<span
|
||||
data-testid="title-edit-button"
|
||||
className={styles.titleEditButton}
|
||||
onDoubleClick={onRename}
|
||||
onDoubleClick={handleRename}
|
||||
>
|
||||
{title || 'Untitled'}
|
||||
</span>
|
||||
<PageMenu rename={onRename} pageId={pageId} />
|
||||
{isPublic ? null : <PageMenu rename={onRename} pageId={pageId} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -139,7 +151,7 @@ const BlockSuiteTitleWithRename = (props: BlockSuiteHeaderTitleProps) => {
|
||||
};
|
||||
|
||||
export const BlockSuiteHeaderTitle = (props: BlockSuiteHeaderTitleProps) => {
|
||||
if (props.workspace.flavour === WorkspaceFlavour.AFFINE_PUBLIC) {
|
||||
if (props.isPublic) {
|
||||
return <StableTitle {...props} />;
|
||||
}
|
||||
return <BlockSuiteTitleWithRename {...props} />;
|
||||
|
||||
+2
-1
@@ -18,6 +18,7 @@ import {
|
||||
MenuItem,
|
||||
MenuSeparator,
|
||||
} from '@toeverything/components/menu';
|
||||
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||
import {
|
||||
useBlockSuitePageMeta,
|
||||
usePageMetaHelper,
|
||||
@@ -99,7 +100,7 @@ export const PageMenu = ({ rename, pageId }: PageMenuProps) => {
|
||||
const exportHandler = useExportPage(currentPage);
|
||||
const setPageMode = useSetAtom(setPageModeAtom);
|
||||
|
||||
const duplicate = useCallback(async () => {
|
||||
const duplicate = useAsyncCallback(async () => {
|
||||
const currentPageMeta = currentPage.meta;
|
||||
const newPage = createPage();
|
||||
await newPage.waitForLoaded();
|
||||
|
||||
+34
-12
@@ -6,6 +6,7 @@ import { useAtomValue } from 'jotai';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
import type { PageMode } from '../../../atoms';
|
||||
import { currentModeAtom } from '../../../atoms/mode';
|
||||
import { useBlockSuiteMetaHelper } from '../../../hooks/affine/use-block-suite-meta-helper';
|
||||
import type { BlockSuiteWorkspace } from '../../../shared';
|
||||
@@ -18,6 +19,8 @@ export type EditorModeSwitchProps = {
|
||||
blockSuiteWorkspace: BlockSuiteWorkspace;
|
||||
pageId: string;
|
||||
style?: CSSProperties;
|
||||
isPublic?: boolean;
|
||||
publicMode?: PageMode;
|
||||
};
|
||||
const TooltipContent = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
@@ -34,6 +37,8 @@ export const EditorModeSwitch = ({
|
||||
style,
|
||||
blockSuiteWorkspace,
|
||||
pageId,
|
||||
isPublic,
|
||||
publicMode,
|
||||
}: EditorModeSwitchProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
const pageMeta = useBlockSuitePageMeta(blockSuiteWorkspace).find(
|
||||
@@ -47,7 +52,7 @@ export const EditorModeSwitch = ({
|
||||
const currentMode = useAtomValue(currentModeAtom);
|
||||
|
||||
useEffect(() => {
|
||||
if (trash) {
|
||||
if (trash || isPublic) {
|
||||
return;
|
||||
}
|
||||
const keydown = (e: KeyboardEvent) => {
|
||||
@@ -64,41 +69,58 @@ export const EditorModeSwitch = ({
|
||||
document.addEventListener('keydown', keydown, { capture: true });
|
||||
return () =>
|
||||
document.removeEventListener('keydown', keydown, { capture: true });
|
||||
}, [currentMode, pageId, t, togglePageMode, trash]);
|
||||
}, [currentMode, isPublic, pageId, t, togglePageMode, trash]);
|
||||
|
||||
const onSwitchToPageMode = useCallback(() => {
|
||||
if (currentMode === 'page') {
|
||||
if (currentMode === 'page' || isPublic) {
|
||||
return;
|
||||
}
|
||||
switchToPageMode(pageId);
|
||||
toast(t['com.affine.toastMessage.pageMode']());
|
||||
}, [currentMode, pageId, switchToPageMode, t]);
|
||||
}, [currentMode, isPublic, pageId, switchToPageMode, t]);
|
||||
|
||||
const onSwitchToEdgelessMode = useCallback(() => {
|
||||
if (currentMode === 'edgeless') {
|
||||
if (currentMode === 'edgeless' || isPublic) {
|
||||
return;
|
||||
}
|
||||
switchToEdgelessMode(pageId);
|
||||
toast(t['com.affine.toastMessage.edgelessMode']());
|
||||
}, [currentMode, pageId, switchToEdgelessMode, t]);
|
||||
}, [currentMode, isPublic, pageId, switchToEdgelessMode, t]);
|
||||
|
||||
const shouldHide = useCallback(
|
||||
(mode: PageMode) =>
|
||||
(trash && currentMode !== mode) || (isPublic && publicMode !== mode),
|
||||
[currentMode, isPublic, publicMode, trash]
|
||||
);
|
||||
|
||||
const shouldActive = useCallback(
|
||||
(mode: PageMode) => (isPublic ? false : currentMode === mode),
|
||||
[currentMode, isPublic]
|
||||
);
|
||||
|
||||
return (
|
||||
<Tooltip content={<TooltipContent />}>
|
||||
<Tooltip
|
||||
content={<TooltipContent />}
|
||||
options={{
|
||||
hidden: isPublic || trash,
|
||||
}}
|
||||
>
|
||||
<StyledEditorModeSwitch
|
||||
style={style}
|
||||
switchLeft={currentMode === 'page'}
|
||||
showAlone={trash}
|
||||
showAlone={trash || isPublic}
|
||||
>
|
||||
<PageSwitchItem
|
||||
data-testid="switch-page-mode-button"
|
||||
active={currentMode === 'page'}
|
||||
hide={trash && currentMode !== 'page'}
|
||||
active={shouldActive('page')}
|
||||
hide={shouldHide('page')}
|
||||
trash={trash}
|
||||
onClick={onSwitchToPageMode}
|
||||
/>
|
||||
<EdgelessSwitchItem
|
||||
data-testid="switch-edgeless-mode-button"
|
||||
active={currentMode === 'edgeless'}
|
||||
hide={trash && currentMode !== 'edgeless'}
|
||||
active={shouldActive('edgeless')}
|
||||
hide={shouldHide('edgeless')}
|
||||
trash={trash}
|
||||
onClick={onSwitchToEdgelessMode}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { toast } from '@affine/component';
|
||||
import { WorkspaceSubPath } from '@affine/env/workspace';
|
||||
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||
import { useBlockSuiteWorkspaceHelper } from '@toeverything/hooks/use-block-suite-workspace-helper';
|
||||
import { initEmptyPage } from '@toeverything/infra/blocksuite';
|
||||
import { useAtomValue, useSetAtom } from 'jotai';
|
||||
@@ -33,7 +34,7 @@ export const usePageHelper = (blockSuiteWorkspace: BlockSuiteWorkspace) => {
|
||||
const createEdgelessAndOpen = useCallback(() => {
|
||||
return createPageAndOpen('edgeless');
|
||||
}, [createPageAndOpen]);
|
||||
const importFileAndOpen = useCallback(async () => {
|
||||
const importFileAndOpen = useAsyncCallback(async () => {
|
||||
const { showImportModal } = await import('@blocksuite/blocks');
|
||||
const onSuccess = (pageIds: string[], isWorkspaceFile: boolean) => {
|
||||
toast(
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { MenuItem, PureMenu } from '@affine/component';
|
||||
import { MuiClickAwayListener } from '@affine/component';
|
||||
import type { SerializedBlock } from '@blocksuite/blocks';
|
||||
import type { BaseBlockModel } from '@blocksuite/store';
|
||||
import type { Page } from '@blocksuite/store';
|
||||
import type { VEditor } from '@blocksuite/virgo';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Menu, MenuItem } from '@toeverything/components/menu';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
type ShortcutMap = {
|
||||
[key: string]: (e: KeyboardEvent, page: Page) => void;
|
||||
@@ -121,60 +121,6 @@ export type BookmarkProps = {
|
||||
|
||||
export const Bookmark = ({ page }: BookmarkProps) => {
|
||||
const [anchor, setAnchor] = useState<Range | null>(null);
|
||||
const [selectedOption, setSelectedOption] = useState<string>(
|
||||
menuOptions[0].id
|
||||
);
|
||||
const shortcutMap = useMemo<ShortcutMap>(
|
||||
() => ({
|
||||
ArrowUp: () => {
|
||||
const curIndex = menuOptions.findIndex(
|
||||
({ id }) => id === selectedOption
|
||||
);
|
||||
if (menuOptions[curIndex - 1]) {
|
||||
setSelectedOption(menuOptions[curIndex - 1].id);
|
||||
} else if (curIndex === -1) {
|
||||
setSelectedOption(menuOptions[0].id);
|
||||
} else {
|
||||
setSelectedOption(menuOptions[menuOptions.length - 1].id);
|
||||
}
|
||||
},
|
||||
ArrowDown: () => {
|
||||
const curIndex = menuOptions.findIndex(
|
||||
({ id }) => id === selectedOption
|
||||
);
|
||||
if (curIndex !== -1 && menuOptions[curIndex + 1]) {
|
||||
setSelectedOption(menuOptions[curIndex + 1].id);
|
||||
} else {
|
||||
setSelectedOption(menuOptions[0].id);
|
||||
}
|
||||
},
|
||||
Enter: () =>
|
||||
handleEnter({
|
||||
page,
|
||||
selectedOption,
|
||||
callback: () => {
|
||||
setAnchor(null);
|
||||
},
|
||||
}),
|
||||
Escape: () => {
|
||||
setAnchor(null);
|
||||
},
|
||||
}),
|
||||
[page, selectedOption]
|
||||
);
|
||||
const onKeydown = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
const shortcut = shortcutMap[e.key];
|
||||
if (shortcut) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
shortcut(e, page);
|
||||
} else {
|
||||
setAnchor(null);
|
||||
}
|
||||
},
|
||||
[page, shortcutMap]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const disposer = page.slots.pasted.on(pastedBlocks => {
|
||||
@@ -189,56 +135,35 @@ export const Bookmark = ({ page }: BookmarkProps) => {
|
||||
return () => {
|
||||
disposer.dispose();
|
||||
};
|
||||
}, [onKeydown, page, shortcutMap]);
|
||||
}, [page]);
|
||||
|
||||
useEffect(() => {
|
||||
if (anchor) {
|
||||
document.addEventListener('keydown', onKeydown, { capture: true });
|
||||
} else {
|
||||
// reset status and remove event
|
||||
setSelectedOption(menuOptions[0].id);
|
||||
document.removeEventListener('keydown', onKeydown, { capture: true });
|
||||
}
|
||||
const portalContainer = anchor?.startContainer.parentElement;
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', onKeydown, { capture: true });
|
||||
};
|
||||
}, [anchor, onKeydown]);
|
||||
|
||||
return anchor ? (
|
||||
<MuiClickAwayListener
|
||||
onClickAway={() => {
|
||||
setAnchor(null);
|
||||
setSelectedOption('');
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<PureMenu open={!!anchor} anchorEl={anchor} placement="bottom-start">
|
||||
{menuOptions.map(({ id, label }) => {
|
||||
return (
|
||||
<MenuItem
|
||||
key={id}
|
||||
active={selectedOption === id}
|
||||
onClick={() => {
|
||||
handleEnter({
|
||||
page,
|
||||
selectedOption: id,
|
||||
callback: () => {
|
||||
setAnchor(null);
|
||||
},
|
||||
});
|
||||
}}
|
||||
disableHover={true}
|
||||
onMouseEnter={() => {
|
||||
setSelectedOption(id);
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</PureMenu>
|
||||
</div>
|
||||
</MuiClickAwayListener>
|
||||
) : null;
|
||||
return anchor && portalContainer
|
||||
? createPortal(
|
||||
<Menu
|
||||
rootOptions={{
|
||||
defaultOpen: true,
|
||||
onOpenChange: (e: boolean) => !e && setAnchor(null),
|
||||
}}
|
||||
items={menuOptions.map(({ id, label }) => (
|
||||
<MenuItem
|
||||
key={id}
|
||||
onClick={() =>
|
||||
handleEnter({
|
||||
page,
|
||||
selectedOption: id,
|
||||
callback: () => setAnchor(null),
|
||||
})
|
||||
}
|
||||
>
|
||||
{label}
|
||||
</MenuItem>
|
||||
))}
|
||||
>
|
||||
<span></span>
|
||||
</Menu>,
|
||||
portalContainer
|
||||
)
|
||||
: null;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { CloudWorkspaceIcon } from '@blocksuite/icons';
|
||||
import { Avatar } from '@toeverything/components/avatar';
|
||||
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||
|
||||
import { useCurrentLoginStatus } from '../../hooks/affine/use-current-login-status';
|
||||
import { useCurrentUser } from '../../hooks/affine/use-current-user';
|
||||
@@ -10,16 +11,16 @@ import { StyledSignInButton } from '../pure/footer/styles';
|
||||
export const LoginCard = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const loginStatus = useCurrentLoginStatus();
|
||||
|
||||
const onSignInClick = useAsyncCallback(async () => {
|
||||
await signInCloud();
|
||||
}, []);
|
||||
|
||||
if (loginStatus === 'authenticated') {
|
||||
return <UserCard />;
|
||||
}
|
||||
return (
|
||||
<StyledSignInButton
|
||||
data-testid="sign-in-button"
|
||||
onClick={async () => {
|
||||
signInCloud().catch(console.error);
|
||||
}}
|
||||
>
|
||||
<StyledSignInButton data-testid="sign-in-button" onClick={onSignInClick}>
|
||||
<div className="circle">
|
||||
<CloudWorkspaceIcon />
|
||||
</div>{' '}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Logo1Icon } from '@blocksuite/icons';
|
||||
|
||||
import { useCurrentLoginStatus } from '../../../hooks/affine/use-current-login-status';
|
||||
import * as styles from './styles.css';
|
||||
import { PublishPageUserAvatar } from './user-avatar';
|
||||
|
||||
const ShareHeaderLeftItem = () => {
|
||||
const loginStatus = useCurrentLoginStatus();
|
||||
if (loginStatus === 'authenticated') {
|
||||
return <PublishPageUserAvatar />;
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
href="https://affine.pro/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={styles.iconWrapper}
|
||||
data-testid="share-page-logo"
|
||||
>
|
||||
<Logo1Icon />
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShareHeaderLeftItem;
|
||||
@@ -0,0 +1,15 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const iconWrapper = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
fontSize: '24px',
|
||||
cursor: 'pointer',
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
selectors: {
|
||||
'&:visited': {
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { SignOutIcon } from '@blocksuite/icons';
|
||||
import { Avatar } from '@toeverything/components/avatar';
|
||||
import { Menu, MenuIcon, MenuItem } from '@toeverything/components/menu';
|
||||
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||
import { useMemo } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { useCurrentUser } from '../../../hooks/affine/use-current-user';
|
||||
import { signOutCloud } from '../../../utils/cloud-utils';
|
||||
import * as styles from './styles.css';
|
||||
|
||||
export const PublishPageUserAvatar = () => {
|
||||
const user = useCurrentUser();
|
||||
const t = useAFFiNEI18N();
|
||||
const location = useLocation();
|
||||
|
||||
const handleSignOut = useAsyncCallback(async () => {
|
||||
await signOutCloud({ callbackUrl: location.pathname });
|
||||
}, [location.pathname]);
|
||||
|
||||
const menuItem = useMemo(() => {
|
||||
return (
|
||||
<MenuItem
|
||||
preFix={
|
||||
<MenuIcon>
|
||||
<SignOutIcon />
|
||||
</MenuIcon>
|
||||
}
|
||||
data-testid="share-page-sign-out-option"
|
||||
onClick={handleSignOut}
|
||||
>
|
||||
{t['com.affine.workspace.cloud.account.logout']()}
|
||||
</MenuItem>
|
||||
);
|
||||
}, [handleSignOut, t]);
|
||||
|
||||
return (
|
||||
<Menu items={menuItem}>
|
||||
<div className={styles.iconWrapper} data-testid="share-page-user-avatar">
|
||||
<Avatar size={24} url={user.image} name={user.name} />
|
||||
</div>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
|
||||
import { useCurrentUser } from '../../../hooks/affine/use-current-user';
|
||||
import { useMembers } from '../../../hooks/affine/use-members';
|
||||
import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
|
||||
import type { ShareHeaderRightItemProps } from '.';
|
||||
|
||||
export const AuthenticatedItem = ({ ...props }: ShareHeaderRightItemProps) => {
|
||||
const { workspaceId, pageId } = props;
|
||||
const user = useCurrentUser();
|
||||
const members = useMembers(workspaceId, 0);
|
||||
const isMember = members.some(m => m.id === user.id);
|
||||
const t = useAFFiNEI18N();
|
||||
const { jumpToPage } = useNavigateHelper();
|
||||
|
||||
if (isMember) {
|
||||
return (
|
||||
<Button
|
||||
type="plain"
|
||||
onClick={() => jumpToPage(workspaceId, pageId)}
|
||||
data-testid="share-page-edit-button"
|
||||
>
|
||||
{t['Edit']()}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import { useCurrentLoginStatus } from '../../../hooks/affine/use-current-login-status';
|
||||
import { AuthenticatedItem } from './authenticated-item';
|
||||
|
||||
export type ShareHeaderRightItemProps = {
|
||||
workspaceId: string;
|
||||
pageId: string;
|
||||
};
|
||||
|
||||
const ShareHeaderRightItem = ({ ...props }: ShareHeaderRightItemProps) => {
|
||||
const loginStatus = useCurrentLoginStatus();
|
||||
if (loginStatus === 'authenticated') {
|
||||
return <AuthenticatedItem {...props} />;
|
||||
}
|
||||
// TODO: Add TOC
|
||||
return null;
|
||||
};
|
||||
|
||||
export default ShareHeaderRightItem;
|
||||
@@ -0,0 +1,15 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const iconWrapper = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
fontSize: '24px',
|
||||
cursor: 'pointer',
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
selectors: {
|
||||
'&:visited': {
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -31,7 +31,7 @@ import {
|
||||
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { pageSettingFamily } from '../atoms';
|
||||
import { type PageMode, pageSettingFamily } from '../atoms';
|
||||
import { fontStyleOptions } from '../atoms/settings';
|
||||
import { useAppSettingHelper } from '../hooks/affine/use-app-setting-helper';
|
||||
import { useBlockSuiteMetaHelper } from '../hooks/affine/use-block-suite-meta-helper';
|
||||
@@ -50,6 +50,7 @@ export type OnLoadEditor = (page: Page, editor: EditorContainer) => () => void;
|
||||
|
||||
export interface PageDetailEditorProps {
|
||||
isPublic?: boolean;
|
||||
publishMode?: PageMode;
|
||||
workspace: Workspace;
|
||||
pageId: string;
|
||||
onLoad?: OnLoadEditor;
|
||||
@@ -91,6 +92,7 @@ const EditorWrapper = memo(function EditorWrapper({
|
||||
pageId,
|
||||
onLoad,
|
||||
isPublic,
|
||||
publishMode,
|
||||
}: PageDetailEditorProps) {
|
||||
const page = useBlockSuiteWorkspacePage(workspace, pageId);
|
||||
if (!page) {
|
||||
@@ -105,7 +107,16 @@ const EditorWrapper = memo(function EditorWrapper({
|
||||
|
||||
const pageSettingAtom = pageSettingFamily(pageId);
|
||||
const pageSetting = useAtomValue(pageSettingAtom);
|
||||
const currentMode = pageSetting?.mode ?? 'page';
|
||||
|
||||
const mode = useMemo(() => {
|
||||
const currentMode = pageSetting.mode;
|
||||
const shareMode = publishMode || currentMode;
|
||||
|
||||
if (isPublic) {
|
||||
return shareMode;
|
||||
}
|
||||
return currentMode;
|
||||
}, [isPublic, publishMode, pageSetting.mode]);
|
||||
|
||||
const { appSettings } = useAppSettingHelper();
|
||||
|
||||
@@ -120,13 +131,16 @@ const EditorWrapper = memo(function EditorWrapper({
|
||||
|
||||
const setEditorMode = useCallback(
|
||||
(mode: 'page' | 'edgeless') => {
|
||||
if (isPublic) {
|
||||
return;
|
||||
}
|
||||
if (mode === 'edgeless') {
|
||||
switchToEdgelessMode(pageId);
|
||||
} else {
|
||||
switchToPageMode(pageId);
|
||||
}
|
||||
},
|
||||
[switchToEdgelessMode, switchToPageMode, pageId]
|
||||
[isPublic, switchToEdgelessMode, pageId, switchToPageMode]
|
||||
);
|
||||
|
||||
const [editor, setEditor] = useState<EditorContainer>();
|
||||
@@ -191,7 +205,7 @@ const EditorWrapper = memo(function EditorWrapper({
|
||||
'--affine-font-family': value,
|
||||
} as CSSProperties
|
||||
}
|
||||
mode={isPublic ? 'page' : currentMode}
|
||||
mode={mode}
|
||||
page={page}
|
||||
onModeChange={setEditorMode}
|
||||
defaultSelectedBlockId={blockId}
|
||||
@@ -275,7 +289,7 @@ const LayoutPanel = memo(function LayoutPanel(
|
||||
className={depth === 0 ? editorContainer : undefined}
|
||||
>
|
||||
<Panel
|
||||
defaultSize={node.splitPercentage}
|
||||
defaultSizePercentage={node.splitPercentage}
|
||||
style={{
|
||||
maxWidth: node.maxWidth?.[0],
|
||||
}}
|
||||
@@ -290,7 +304,7 @@ const LayoutPanel = memo(function LayoutPanel(
|
||||
</Panel>
|
||||
<PanelResizeHandle />
|
||||
<Panel
|
||||
defaultSize={100 - node.splitPercentage}
|
||||
defaultSizePercentage={100 - node.splitPercentage}
|
||||
style={{
|
||||
overflow: 'scroll',
|
||||
maxWidth: node.maxWidth?.[1],
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
PreconditionStrategy,
|
||||
} from '@toeverything/infra/command';
|
||||
import { atom, useAtomValue } from 'jotai';
|
||||
import groupBy from 'lodash/groupBy';
|
||||
import { groupBy } from 'lodash-es';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import {
|
||||
@@ -44,6 +44,10 @@ interface SearchResultsValue {
|
||||
content: string;
|
||||
}
|
||||
|
||||
export function removeDoubleQuotes(str?: string): string | undefined {
|
||||
return str?.replace(/"/g, '');
|
||||
}
|
||||
|
||||
export const cmdkQueryAtom = atom('');
|
||||
export const cmdkValueAtom = atom('');
|
||||
|
||||
@@ -423,7 +427,10 @@ export const customCommandFilter = (value: string, search: string) => {
|
||||
label = label.replace(contentMatchedWithoutSubtitle, '');
|
||||
}
|
||||
|
||||
const originalScore = commandScore(label, search);
|
||||
// use to remove double quotes from a string until this issue is fixed
|
||||
// https://github.com/pacocoursey/cmdk/issues/189
|
||||
const escapedSearch = removeDoubleQuotes(search) || '';
|
||||
const originalScore = commandScore(label, escapedSearch);
|
||||
|
||||
// hack to make the page title result always before the content result
|
||||
// if the command has matched the title but not the subtitle,
|
||||
|
||||
@@ -129,6 +129,7 @@ globalStyle(`${root} [cmdk-list]`, {
|
||||
height: 'min(330px, calc(var(--cmdk-list-height) + 8px))',
|
||||
padding: '0 0 8px 6px',
|
||||
scrollbarGutter: 'stable',
|
||||
scrollPaddingBlock: '12px',
|
||||
});
|
||||
|
||||
globalStyle(`${root} [cmdk-list]:not([data-opening])`, {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Command } from '@affine/cmdk';
|
||||
import { formatDate } from '@affine/component/page-list';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import type { PageMeta } from '@blocksuite/store';
|
||||
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||
import type { CommandCategory } from '@toeverything/infra/command';
|
||||
import clsx from 'clsx';
|
||||
import { useAtom } from 'jotai';
|
||||
@@ -11,6 +12,7 @@ import {
|
||||
cmdkQueryAtom,
|
||||
cmdkValueAtom,
|
||||
customCommandFilter,
|
||||
removeDoubleQuotes,
|
||||
useCMDKCommandGroups,
|
||||
} from './data';
|
||||
import { HighlightLabel } from './highlight';
|
||||
@@ -55,6 +57,19 @@ const QuickSearchGroup = ({
|
||||
const t = useAFFiNEI18N();
|
||||
const i18nkey = categoryToI18nKey[category];
|
||||
const [query, setQuery] = useAtom(cmdkQueryAtom);
|
||||
|
||||
const onCommendSelect = useAsyncCallback(
|
||||
async (command: CMDKCommand) => {
|
||||
try {
|
||||
await command.run();
|
||||
} finally {
|
||||
setQuery('');
|
||||
onOpenChange?.(false);
|
||||
}
|
||||
},
|
||||
[setQuery, onOpenChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<Command.Group key={category} heading={t[i18nkey]()}>
|
||||
{commands.map(command => {
|
||||
@@ -64,15 +79,15 @@ const QuickSearchGroup = ({
|
||||
title: command.label,
|
||||
}
|
||||
: command.label;
|
||||
|
||||
// use to remove double quotes from a string until this issue is fixed
|
||||
// https://github.com/pacocoursey/cmdk/issues/189
|
||||
const escapeValue = removeDoubleQuotes(command.value);
|
||||
return (
|
||||
<Command.Item
|
||||
key={command.id}
|
||||
onSelect={() => {
|
||||
command.run();
|
||||
setQuery('');
|
||||
onOpenChange?.(false);
|
||||
}}
|
||||
value={command.value}
|
||||
onSelect={() => onCommendSelect(command)}
|
||||
value={escapeValue}
|
||||
data-is-danger={
|
||||
command.id === 'editor:page-move-to-trash' ||
|
||||
command.id === 'editor:edgeless-move-to-trash'
|
||||
|
||||
@@ -10,7 +10,6 @@ import type { ReactNode } from 'react';
|
||||
import { forwardRef, useRef } from 'react';
|
||||
|
||||
import * as style from './style.css';
|
||||
import { TopTip } from './top-tip';
|
||||
import { WindowsAppControls } from './windows-app-controls';
|
||||
|
||||
interface HeaderPros {
|
||||
@@ -49,61 +48,58 @@ export const Header = forwardRef<HTMLDivElement, HeaderPros>(function Header(
|
||||
const open = useAtomValue(appSidebarOpenAtom);
|
||||
const appSidebarFloating = useAtomValue(appSidebarFloatingAtom);
|
||||
return (
|
||||
<>
|
||||
<TopTip />
|
||||
<div
|
||||
className={clsx(style.header, bottomBorder && style.bottomBorder)}
|
||||
// data-has-warning={showWarning}
|
||||
data-open={open}
|
||||
data-sidebar-floating={appSidebarFloating}
|
||||
data-testid="header"
|
||||
ref={ref}
|
||||
>
|
||||
<div
|
||||
className={clsx(style.header, bottomBorder && style.bottomBorder)}
|
||||
// data-has-warning={showWarning}
|
||||
data-open={open}
|
||||
data-sidebar-floating={appSidebarFloating}
|
||||
data-testid="header"
|
||||
ref={ref}
|
||||
className={clsx(style.headerSideContainer, {
|
||||
block: isTinyScreen,
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={clsx(style.headerSideContainer, {
|
||||
block: isTinyScreen,
|
||||
})}
|
||||
className={clsx(
|
||||
style.headerItem,
|
||||
'top-item',
|
||||
!open ? 'top-item-visible' : ''
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
style.headerItem,
|
||||
'top-item',
|
||||
!open ? 'top-item-visible' : ''
|
||||
)}
|
||||
>
|
||||
<div ref={sidebarSwitchRef}>
|
||||
<SidebarSwitch show={!open} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={clsx(style.headerItem, 'left')}>
|
||||
<div ref={leftSlotRef}>{left}</div>
|
||||
<div ref={sidebarSwitchRef}>
|
||||
<SidebarSwitch show={!open} />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={clsx({
|
||||
[style.headerCenter]: center,
|
||||
'is-window': isWindowsDesktop,
|
||||
})}
|
||||
ref={centerSlotRef}
|
||||
>
|
||||
{center}
|
||||
</div>
|
||||
<div
|
||||
className={clsx(style.headerSideContainer, 'right', {
|
||||
block: isTinyScreen,
|
||||
})}
|
||||
>
|
||||
<div className={clsx(style.headerItem, 'top-item')}>
|
||||
<div ref={windowControlsRef}>
|
||||
{isWindowsDesktop ? <WindowsAppControls /> : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className={clsx(style.headerItem, 'right')}>
|
||||
<div ref={rightSlotRef}>{right}</div>
|
||||
</div>
|
||||
<div className={clsx(style.headerItem, 'left')}>
|
||||
<div ref={leftSlotRef}>{left}</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
<div
|
||||
className={clsx({
|
||||
[style.headerCenter]: center,
|
||||
'is-window': isWindowsDesktop,
|
||||
})}
|
||||
ref={centerSlotRef}
|
||||
>
|
||||
{center}
|
||||
</div>
|
||||
<div
|
||||
className={clsx(style.headerSideContainer, 'right', {
|
||||
block: isTinyScreen,
|
||||
})}
|
||||
>
|
||||
<div className={clsx(style.headerItem, 'top-item')}>
|
||||
<div ref={windowControlsRef}>
|
||||
{isWindowsDesktop ? <WindowsAppControls /> : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className={clsx(style.headerItem, 'right')}>
|
||||
<div ref={rightSlotRef}>{right}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
import { BrowserWarning } from '@affine/component/affine-banner';
|
||||
import { DownloadTips } from '@affine/component/affine-banner';
|
||||
import { Trans } from '@affine/i18n';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useAtom } from 'jotai';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { guideDownloadClientTipAtom } from '../../../atoms/guide';
|
||||
|
||||
const minimumChromeVersion = 102;
|
||||
|
||||
const shouldShowWarning = () => {
|
||||
if (environment.isDesktop) {
|
||||
// even though desktop has compatibility issues,
|
||||
// we don't want to show the warning
|
||||
return false;
|
||||
}
|
||||
if (!environment.isBrowser) {
|
||||
// disable in SSR
|
||||
return false;
|
||||
}
|
||||
if (environment.isChrome) {
|
||||
return environment.chromeVersion < minimumChromeVersion;
|
||||
} else {
|
||||
return !environment.isMobile;
|
||||
}
|
||||
};
|
||||
|
||||
const OSWarningMessage = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const [notChrome, setNotChrome] = useState(false);
|
||||
const [notGoodVersion, setNotGoodVersion] = useState(false);
|
||||
useEffect(() => {
|
||||
setNotChrome(environment.isBrowser && !environment.isChrome);
|
||||
setNotGoodVersion(
|
||||
environment.isBrowser &&
|
||||
environment.isChrome &&
|
||||
environment.chromeVersion < minimumChromeVersion
|
||||
);
|
||||
}, []);
|
||||
|
||||
if (notChrome) {
|
||||
return (
|
||||
<span>
|
||||
<Trans i18nKey="recommendBrowser">
|
||||
We recommend the <strong>Chrome</strong> browser for an optimal
|
||||
experience.
|
||||
</Trans>
|
||||
</span>
|
||||
);
|
||||
} else if (notGoodVersion) {
|
||||
return <span>{t['upgradeBrowser']()}</span>;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
export const TopTip = () => {
|
||||
const [showWarning, setShowWarning] = useState(false);
|
||||
const [showDownloadTip, setShowDownloadTip] = useAtom(
|
||||
guideDownloadClientTipAtom
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setShowWarning(shouldShowWarning());
|
||||
}, []);
|
||||
|
||||
if (showDownloadTip && environment.isDesktop) {
|
||||
return (
|
||||
<DownloadTips
|
||||
onClose={() => {
|
||||
setShowDownloadTip(false);
|
||||
localStorage.setItem('affine-is-dt-hide', '1');
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<BrowserWarning
|
||||
show={showWarning}
|
||||
message={<OSWarningMessage />}
|
||||
onClose={() => {
|
||||
setShowWarning(false);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,3 @@
|
||||
import { MuiFade } from '@affine/component';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { CloseIcon, NewIcon, UserGuideIcon } from '@blocksuite/icons';
|
||||
import { Tooltip } from '@toeverything/components/tooltip';
|
||||
@@ -8,7 +7,7 @@ import { useCallback, useState } from 'react';
|
||||
|
||||
import { openOnboardingModalAtom, openSettingModalAtom } from '../../../atoms';
|
||||
import { currentModeAtom } from '../../../atoms/mode';
|
||||
import { ShortcutsModal } from '../shortcuts-modal';
|
||||
import type { SettingProps } from '../../affine/setting-modal';
|
||||
import { ContactIcon, HelpIcon, KeyboardIcon } from './icons';
|
||||
import {
|
||||
StyledAnimateWrapper,
|
||||
@@ -36,113 +35,106 @@ export const HelpIsland = ({
|
||||
const [spread, setShowSpread] = useState(false);
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const [openShortCut, setOpenShortCut] = useState(false);
|
||||
const openSettingModal = useCallback(
|
||||
(tab: SettingProps['activeTab']) => {
|
||||
setShowSpread(false);
|
||||
|
||||
const openAbout = useCallback(() => {
|
||||
setShowSpread(false);
|
||||
|
||||
setOpenSettingModalAtom({
|
||||
open: true,
|
||||
activeTab: 'about',
|
||||
workspaceId: null,
|
||||
});
|
||||
}, [setOpenSettingModalAtom]);
|
||||
setOpenSettingModalAtom({
|
||||
open: true,
|
||||
activeTab: tab,
|
||||
workspaceId: null,
|
||||
});
|
||||
},
|
||||
[setOpenSettingModalAtom]
|
||||
);
|
||||
const openAbout = useCallback(
|
||||
() => openSettingModal('about'),
|
||||
[openSettingModal]
|
||||
);
|
||||
const openShortcuts = useCallback(
|
||||
() => openSettingModal('shortcuts'),
|
||||
[openSettingModal]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledIsland
|
||||
spread={spread}
|
||||
data-testid="help-island"
|
||||
onClick={() => {
|
||||
setShowSpread(!spread);
|
||||
}}
|
||||
inEdgelessPage={mode === 'edgeless'}
|
||||
<StyledIsland
|
||||
spread={spread}
|
||||
data-testid="help-island"
|
||||
onClick={() => {
|
||||
setShowSpread(!spread);
|
||||
}}
|
||||
inEdgelessPage={mode === 'edgeless'}
|
||||
>
|
||||
<StyledAnimateWrapper
|
||||
style={{ height: spread ? `${showList.length * 40 + 4}px` : 0 }}
|
||||
>
|
||||
<StyledAnimateWrapper
|
||||
style={{ height: spread ? `${showList.length * 40 + 4}px` : 0 }}
|
||||
>
|
||||
{showList.includes('whatNew') && (
|
||||
<Tooltip
|
||||
content={t['com.affine.appUpdater.whatsNew']()}
|
||||
side="left"
|
||||
{showList.includes('whatNew') && (
|
||||
<Tooltip content={t['com.affine.appUpdater.whatsNew']()} side="left">
|
||||
<StyledIconWrapper
|
||||
data-testid="right-bottom-change-log-icon"
|
||||
onClick={() => {
|
||||
window.open(runtimeConfig.changelogUrl, '_blank');
|
||||
}}
|
||||
>
|
||||
<StyledIconWrapper
|
||||
data-testid="right-bottom-change-log-icon"
|
||||
onClick={() => {
|
||||
window.open(runtimeConfig.changelogUrl, '_blank');
|
||||
}}
|
||||
>
|
||||
<NewIcon />
|
||||
</StyledIconWrapper>
|
||||
</Tooltip>
|
||||
)}
|
||||
{showList.includes('contact') && (
|
||||
<Tooltip
|
||||
content={t['com.affine.helpIsland.contactUs']()}
|
||||
side="left"
|
||||
<NewIcon />
|
||||
</StyledIconWrapper>
|
||||
</Tooltip>
|
||||
)}
|
||||
{showList.includes('contact') && (
|
||||
<Tooltip content={t['com.affine.helpIsland.contactUs']()} side="left">
|
||||
<StyledIconWrapper
|
||||
data-testid="right-bottom-contact-us-icon"
|
||||
onClick={openAbout}
|
||||
>
|
||||
<StyledIconWrapper
|
||||
data-testid="right-bottom-contact-us-icon"
|
||||
onClick={openAbout}
|
||||
>
|
||||
<ContactIcon />
|
||||
</StyledIconWrapper>
|
||||
</Tooltip>
|
||||
)}
|
||||
{showList.includes('shortcuts') && (
|
||||
<Tooltip
|
||||
content={t['com.affine.keyboardShortcuts.title']()}
|
||||
side="left"
|
||||
<ContactIcon />
|
||||
</StyledIconWrapper>
|
||||
</Tooltip>
|
||||
)}
|
||||
{showList.includes('shortcuts') && (
|
||||
<Tooltip
|
||||
content={t['com.affine.keyboardShortcuts.title']()}
|
||||
side="left"
|
||||
>
|
||||
<StyledIconWrapper
|
||||
data-testid="shortcuts-icon"
|
||||
onClick={openShortcuts}
|
||||
>
|
||||
<StyledIconWrapper
|
||||
data-testid="shortcuts-icon"
|
||||
onClick={() => {
|
||||
setShowSpread(false);
|
||||
setOpenShortCut(true);
|
||||
}}
|
||||
>
|
||||
<KeyboardIcon />
|
||||
</StyledIconWrapper>
|
||||
</Tooltip>
|
||||
)}
|
||||
{showList.includes('guide') && (
|
||||
<Tooltip
|
||||
content={t['com.affine.helpIsland.gettingStarted']()}
|
||||
side="left"
|
||||
<KeyboardIcon />
|
||||
</StyledIconWrapper>
|
||||
</Tooltip>
|
||||
)}
|
||||
{showList.includes('guide') && (
|
||||
<Tooltip
|
||||
content={t['com.affine.helpIsland.gettingStarted']()}
|
||||
side="left"
|
||||
>
|
||||
<StyledIconWrapper
|
||||
data-testid="easy-guide"
|
||||
onClick={() => {
|
||||
setShowSpread(false);
|
||||
setOpenOnboarding(true);
|
||||
}}
|
||||
>
|
||||
<StyledIconWrapper
|
||||
data-testid="easy-guide"
|
||||
onClick={() => {
|
||||
setShowSpread(false);
|
||||
setOpenOnboarding(true);
|
||||
}}
|
||||
>
|
||||
<UserGuideIcon />
|
||||
</StyledIconWrapper>
|
||||
</Tooltip>
|
||||
)}
|
||||
</StyledAnimateWrapper>
|
||||
<UserGuideIcon />
|
||||
</StyledIconWrapper>
|
||||
</Tooltip>
|
||||
)}
|
||||
</StyledAnimateWrapper>
|
||||
|
||||
{spread ? (
|
||||
<StyledTriggerWrapper spread>
|
||||
<CloseIcon />
|
||||
</StyledTriggerWrapper>
|
||||
) : (
|
||||
<Tooltip
|
||||
content={t['com.affine.helpIsland.helpAndFeedback']()}
|
||||
side="left"
|
||||
>
|
||||
<MuiFade in={!spread} data-testid="faq-icon">
|
||||
<StyledTriggerWrapper>
|
||||
<HelpIcon />
|
||||
</StyledTriggerWrapper>
|
||||
</MuiFade>
|
||||
</Tooltip>
|
||||
<MuiFade in={spread}>
|
||||
<StyledTriggerWrapper spread>
|
||||
<CloseIcon />
|
||||
<StyledTriggerWrapper data-testid="faq-icon">
|
||||
<HelpIcon />
|
||||
</StyledTriggerWrapper>
|
||||
</MuiFade>
|
||||
</StyledIsland>
|
||||
<ShortcutsModal
|
||||
open={openShortCut}
|
||||
onClose={() => setOpenShortCut(false)}
|
||||
/>
|
||||
</>
|
||||
</Tooltip>
|
||||
)}
|
||||
</StyledIsland>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -11,16 +11,16 @@ export const StyledIsland = styled('div')<{
|
||||
boxShadow: spread
|
||||
? 'var(--affine-menu-shadow)'
|
||||
: inEdgelessPage
|
||||
? 'var(--affine-menu-shadow)'
|
||||
: 'unset',
|
||||
? 'var(--affine-menu-shadow)'
|
||||
: 'unset',
|
||||
padding: '0 4px 44px',
|
||||
borderRadius: '10px',
|
||||
background: spread
|
||||
? 'var(--affine-background-overlay-panel-color)'
|
||||
: 'var(--affine-background-primary-color)',
|
||||
':hover': {
|
||||
background: spread ? null : 'var(--affine-white)',
|
||||
boxShadow: spread ? null : 'var(--affine-menu-shadow)',
|
||||
background: spread ? undefined : 'var(--affine-white)',
|
||||
boxShadow: spread ? undefined : 'var(--affine-menu-shadow)',
|
||||
},
|
||||
'::after': {
|
||||
content: '""',
|
||||
@@ -73,7 +73,7 @@ export const StyledTriggerWrapper = styled('div')<{
|
||||
...displayFlex('center', 'center'),
|
||||
...positionAbsolute({ left: '4px', bottom: '4px' }),
|
||||
':hover': {
|
||||
backgroundColor: spread ? 'var(--affine-hover-color)' : null,
|
||||
backgroundColor: spread ? 'var(--affine-hover-color)' : undefined,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
export const CloseIcon = () => {
|
||||
return (
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 14 14"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M7.94 7.00014L13.4667 1.47348C13.5759 1.34594 13.633 1.18189 13.6265 1.01411C13.62 0.846324 13.5504 0.687165 13.4317 0.568435C13.313 0.449706 13.1538 0.38015 12.986 0.37367C12.8183 0.367189 12.6542 0.42426 12.5267 0.533477L7 6.06014L1.47334 0.526811C1.3478 0.401275 1.17754 0.33075 1 0.33075C0.822468 0.33075 0.652205 0.401275 0.526669 0.526811C0.401133 0.652346 0.330608 0.82261 0.330608 1.00014C0.330608 1.17768 0.401133 1.34794 0.526669 1.47348L6.06 7.00014L0.526669 12.5268C0.456881 12.5866 0.400201 12.6601 0.360186 12.7428C0.32017 12.8255 0.297683 12.9156 0.294137 13.0074C0.290591 13.0993 0.306061 13.1908 0.339577 13.2764C0.373094 13.3619 0.423932 13.4396 0.488902 13.5046C0.553872 13.5695 0.63157 13.6204 0.71712 13.6539C0.80267 13.6874 0.894225 13.7029 0.986038 13.6993C1.07785 13.6958 1.16794 13.6733 1.25065 13.6333C1.33336 13.5933 1.4069 13.5366 1.46667 13.4668L7 7.94014L12.5267 13.4668C12.6542 13.576 12.8183 13.6331 12.986 13.6266C13.1538 13.6201 13.313 13.5506 13.4317 13.4319C13.5504 13.3131 13.62 13.154 13.6265 12.9862C13.633 12.8184 13.5759 12.6543 13.4667 12.5268L7.94 7.00014Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const KeyboardIcon = () => {
|
||||
return (
|
||||
<svg
|
||||
width="20"
|
||||
height="15"
|
||||
viewBox="0 0 20 15"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M17.745 0C18.3417 0 18.914 0.237053 19.336 0.65901C19.7579 1.08097 19.995 1.65326 19.995 2.25V11.755C19.995 12.3517 19.7579 12.924 19.336 13.346C18.914 13.7679 18.3417 14.005 17.745 14.005H2.25C1.95453 14.005 1.66194 13.9468 1.38896 13.8337C1.11598 13.7207 0.867941 13.5549 0.65901 13.346C0.450078 13.1371 0.284344 12.889 0.171271 12.616C0.058198 12.3431 0 12.0505 0 11.755V2.25C0 1.65326 0.237053 1.08097 0.65901 0.65901C1.08097 0.237053 1.65326 0 2.25 0H17.745ZM17.745 1.5H2.25C2.05109 1.5 1.86032 1.57902 1.71967 1.71967C1.57902 1.86032 1.5 2.05109 1.5 2.25V11.755C1.5 12.169 1.836 12.505 2.25 12.505H17.745C17.9439 12.505 18.1347 12.426 18.2753 12.2853C18.416 12.1447 18.495 11.9539 18.495 11.755V2.25C18.495 2.05109 18.416 1.86032 18.2753 1.71967C18.1347 1.57902 17.9439 1.5 17.745 1.5ZM4.75 9.5H15.25C15.44 9.50006 15.6229 9.57224 15.7618 9.70197C15.9006 9.8317 15.9851 10.0093 15.998 10.1989C16.011 10.3885 15.9515 10.5759 15.8316 10.7233C15.7117 10.8707 15.5402 10.9671 15.352 10.993L15.25 11H4.75C4.55998 10.9999 4.37706 10.9278 4.23821 10.798C4.09936 10.6683 4.01493 10.4907 4.00197 10.3011C3.98902 10.1115 4.04852 9.92411 4.16843 9.7767C4.28835 9.62929 4.45975 9.5329 4.648 9.507L4.75 9.5H15.25H4.75ZM14.5 6C14.7652 6 15.0196 6.10536 15.2071 6.29289C15.3946 6.48043 15.5 6.73478 15.5 7C15.5 7.26522 15.3946 7.51957 15.2071 7.70711C15.0196 7.89464 14.7652 8 14.5 8C14.2348 8 13.9804 7.89464 13.7929 7.70711C13.6054 7.51957 13.5 7.26522 13.5 7C13.5 6.73478 13.6054 6.48043 13.7929 6.29289C13.9804 6.10536 14.2348 6 14.5 6ZM8.505 6C8.77022 6 9.02457 6.10536 9.21211 6.29289C9.39964 6.48043 9.505 6.73478 9.505 7C9.505 7.26522 9.39964 7.51957 9.21211 7.70711C9.02457 7.89464 8.77022 8 8.505 8C8.23978 8 7.98543 7.89464 7.79789 7.70711C7.61036 7.51957 7.505 7.26522 7.505 7C7.505 6.73478 7.61036 6.48043 7.79789 6.29289C7.98543 6.10536 8.23978 6 8.505 6ZM5.505 6C5.77022 6 6.02457 6.10536 6.21211 6.29289C6.39964 6.48043 6.505 6.73478 6.505 7C6.505 7.26522 6.39964 7.51957 6.21211 7.70711C6.02457 7.89464 5.77022 8 5.505 8C5.23978 8 4.98543 7.89464 4.79789 7.70711C4.61036 7.51957 4.505 7.26522 4.505 7C4.505 6.73478 4.61036 6.48043 4.79789 6.29289C4.98543 6.10536 5.23978 6 5.505 6ZM11.505 6C11.7702 6 12.0246 6.10536 12.2121 6.29289C12.3996 6.48043 12.505 6.73478 12.505 7C12.505 7.26522 12.3996 7.51957 12.2121 7.70711C12.0246 7.89464 11.7702 8 11.505 8C11.2398 8 10.9854 7.89464 10.7979 7.70711C10.6104 7.51957 10.505 7.26522 10.505 7C10.505 6.73478 10.6104 6.48043 10.7979 6.29289C10.9854 6.10536 11.2398 6 11.505 6ZM4 3C4.26522 3 4.51957 3.10536 4.70711 3.29289C4.89464 3.48043 5 3.73478 5 4C5 4.26522 4.89464 4.51957 4.70711 4.70711C4.51957 4.89464 4.26522 5 4 5C3.73478 5 3.48043 4.89464 3.29289 4.70711C3.10536 4.51957 3 4.26522 3 4C3 3.73478 3.10536 3.48043 3.29289 3.29289C3.48043 3.10536 3.73478 3 4 3ZM6.995 3C7.26022 3 7.51457 3.10536 7.70211 3.29289C7.88964 3.48043 7.995 3.73478 7.995 4C7.995 4.26522 7.88964 4.51957 7.70211 4.70711C7.51457 4.89464 7.26022 5 6.995 5C6.72978 5 6.47543 4.89464 6.28789 4.70711C6.10036 4.51957 5.995 4.26522 5.995 4C5.995 3.73478 6.10036 3.48043 6.28789 3.29289C6.47543 3.10536 6.72978 3 6.995 3ZM9.995 3C10.2602 3 10.5146 3.10536 10.7021 3.29289C10.8896 3.48043 10.995 3.73478 10.995 4C10.995 4.26522 10.8896 4.51957 10.7021 4.70711C10.5146 4.89464 10.2602 5 9.995 5C9.72978 5 9.47543 4.89464 9.28789 4.70711C9.10036 4.51957 8.995 4.26522 8.995 4C8.995 3.73478 9.10036 3.48043 9.28789 3.29289C9.47543 3.10536 9.72978 3 9.995 3ZM12.995 3C13.2602 3 13.5146 3.10536 13.7021 3.29289C13.8896 3.48043 13.995 3.73478 13.995 4C13.995 4.26522 13.8896 4.51957 13.7021 4.70711C13.5146 4.89464 13.2602 5 12.995 5C12.7298 5 12.4754 4.89464 12.2879 4.70711C12.1004 4.51957 11.995 4.26522 11.995 4C11.995 3.73478 12.1004 3.48043 12.2879 3.29289C12.4754 3.10536 12.7298 3 12.995 3ZM15.995 3C16.2602 3 16.5146 3.10536 16.7021 3.29289C16.8896 3.48043 16.995 3.73478 16.995 4C16.995 4.26522 16.8896 4.51957 16.7021 4.70711C16.5146 4.89464 16.2602 5 15.995 5C15.7298 5 15.4754 4.89464 15.2879 4.70711C15.1004 4.51957 14.995 4.26522 14.995 4C14.995 3.73478 15.1004 3.48043 15.2879 3.29289C15.4754 3.10536 15.7298 3 15.995 3Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -1,97 +0,0 @@
|
||||
import { MuiClickAwayListener, MuiSlide } from '@affine/component';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { CloseIcon } from '@blocksuite/icons';
|
||||
import { IconButton } from '@toeverything/components/button';
|
||||
|
||||
import {
|
||||
type ShortcutsInfo,
|
||||
useEdgelessShortcuts,
|
||||
useGeneralShortcuts,
|
||||
useMarkdownShortcuts,
|
||||
usePageShortcuts,
|
||||
} from '../../../hooks/affine/use-shortcuts';
|
||||
import { KeyboardIcon } from './icons';
|
||||
import * as styles from './style.css';
|
||||
|
||||
type ModalProps = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const ShortcutsPanel = ({
|
||||
shortcutsInfo,
|
||||
}: {
|
||||
shortcutsInfo: ShortcutsInfo;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<div className={styles.subtitle}>{shortcutsInfo.title}</div>
|
||||
|
||||
{Object.entries(shortcutsInfo.shortcuts).map(([title, shortcuts]) => {
|
||||
return (
|
||||
<div className={styles.listItem} key={title}>
|
||||
<span>{title}</span>
|
||||
<div className={styles.keyContainer}>
|
||||
{shortcuts.map(key => {
|
||||
return (
|
||||
<span className={styles.key} key={key}>
|
||||
{key}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const ShortcutsModal = ({ open, onClose }: ModalProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const markdownShortcutsInfo = useMarkdownShortcuts();
|
||||
const pageShortcutsInfo = usePageShortcuts();
|
||||
const edgelessShortcutsInfo = useEdgelessShortcuts();
|
||||
const generalShortcutsInfo = useGeneralShortcuts();
|
||||
|
||||
return (
|
||||
<MuiSlide direction="left" in={open} mountOnEnter unmountOnExit>
|
||||
<div className={styles.shortcutsModal} data-testid="shortcuts-modal">
|
||||
<MuiClickAwayListener
|
||||
onClickAway={() => {
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className={styles.modalHeader}
|
||||
style={{ marginBottom: '-28px' }}
|
||||
>
|
||||
<div className={styles.title}>
|
||||
<KeyboardIcon />
|
||||
{t['Shortcuts']()}
|
||||
</div>
|
||||
|
||||
<IconButton
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: 6,
|
||||
top: 6,
|
||||
}}
|
||||
onClick={() => {
|
||||
onClose();
|
||||
}}
|
||||
icon={<CloseIcon />}
|
||||
/>
|
||||
</div>
|
||||
<ShortcutsPanel shortcutsInfo={generalShortcutsInfo} />
|
||||
<ShortcutsPanel shortcutsInfo={pageShortcutsInfo} />
|
||||
<ShortcutsPanel shortcutsInfo={edgelessShortcutsInfo} />
|
||||
<ShortcutsPanel shortcutsInfo={markdownShortcutsInfo} />
|
||||
</div>
|
||||
</MuiClickAwayListener>
|
||||
</div>
|
||||
</MuiSlide>
|
||||
);
|
||||
};
|
||||
@@ -1,89 +0,0 @@
|
||||
import { globalStyle, style } from '@vanilla-extract/css';
|
||||
|
||||
export const shortcutsModal = style({
|
||||
width: '288px',
|
||||
height: '74vh',
|
||||
paddingBottom: '28px',
|
||||
backgroundColor: 'var(--affine-white)',
|
||||
boxShadow: 'var(--affine-popover-shadow)',
|
||||
borderRadius: `var(--affine-popover-radius)`,
|
||||
overflow: 'auto',
|
||||
position: 'fixed',
|
||||
right: '12px',
|
||||
top: '0',
|
||||
bottom: '0',
|
||||
margin: 'auto',
|
||||
zIndex: 'var(--affine-z-index-modal)',
|
||||
});
|
||||
// export const shortcutsModal = style({
|
||||
// color: 'var(--affine-text-primary-color)',
|
||||
// fontWeight: '500',
|
||||
// fontSize: 'var(--affine-font-sm)',
|
||||
// height: '44px',
|
||||
// display: 'flex',
|
||||
// justifyContent: 'center',
|
||||
// alignItems: 'center',
|
||||
// svg: {
|
||||
// width: '20px',
|
||||
// marginRight: '14px',
|
||||
// color: 'var(--affine-primary-color)',
|
||||
// },
|
||||
// });
|
||||
export const title = style({
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
fontWeight: '500',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
height: '44px',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
globalStyle(`${title} svg`, {
|
||||
width: '20px',
|
||||
marginRight: '14px',
|
||||
color: 'var(--affine-primary-color)',
|
||||
});
|
||||
|
||||
export const subtitle = style({
|
||||
fontWeight: '500',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
height: '34px',
|
||||
lineHeight: '36px',
|
||||
marginTop: '28px',
|
||||
padding: '0 16px',
|
||||
});
|
||||
export const modalHeader = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingTop: '8px 4px 0 4px',
|
||||
width: '100%',
|
||||
padding: '8px 16px 0 16px',
|
||||
position: 'sticky',
|
||||
left: '0',
|
||||
top: '0',
|
||||
background: 'var(--affine-white)',
|
||||
transition: 'background-color 0.5s',
|
||||
});
|
||||
|
||||
export const listItem = style({
|
||||
height: '34px',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
padding: '0 16px',
|
||||
});
|
||||
export const keyContainer = style({
|
||||
display: 'flex',
|
||||
});
|
||||
|
||||
export const key = style({
|
||||
selectors: {
|
||||
'&:not(:last-child)::after': {
|
||||
content: '+',
|
||||
margin: '0 4px',
|
||||
},
|
||||
},
|
||||
});
|
||||
+5
-4
@@ -18,8 +18,9 @@ import type { DragEndEvent } from '@dnd-kit/core';
|
||||
import { useDroppable } from '@dnd-kit/core';
|
||||
import * as Collapsible from '@radix-ui/react-collapsible';
|
||||
import { IconButton } from '@toeverything/components/button';
|
||||
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { collectionsCRUDAtom } from '../../../../atoms/collections';
|
||||
@@ -80,9 +81,9 @@ const CollectionRenderer = ({
|
||||
() => new Set(collection.allowList),
|
||||
[collection.allowList]
|
||||
);
|
||||
const removeFromAllowList = useCallback(
|
||||
(id: string) => {
|
||||
return setting.updateCollection({
|
||||
const removeFromAllowList = useAsyncCallback(
|
||||
async (id: string) => {
|
||||
await setting.updateCollection({
|
||||
...collection,
|
||||
allowList: collection.allowList?.filter(v => v != id),
|
||||
});
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
import { PlusIcon } from '@blocksuite/icons';
|
||||
import type { Workspace } from '@blocksuite/store';
|
||||
import { IconButton } from '@toeverything/components/button';
|
||||
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||
import { usePageMetaHelper } from '@toeverything/hooks/use-block-suite-page-meta';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { usePageHelper } from '../../../blocksuite/block-suite-page-list/utils';
|
||||
|
||||
@@ -13,7 +13,7 @@ type AddFavouriteButtonProps = {
|
||||
export const AddFavouriteButton = ({ workspace }: AddFavouriteButtonProps) => {
|
||||
const { createPage } = usePageHelper(workspace);
|
||||
const { setPageMeta } = usePageMetaHelper(workspace);
|
||||
const handleAddFavorite = useCallback(async () => {
|
||||
const handleAddFavorite = useAsyncCallback(async () => {
|
||||
const page = createPage();
|
||||
await page.waitForLoaded();
|
||||
setPageMeta(page.id, { favorite: true });
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
import { alpha, displayFlex, styled, textEllipsis } from '@affine/component';
|
||||
|
||||
export const StyledListItem = styled('div')<{
|
||||
active?: boolean;
|
||||
disabled?: boolean;
|
||||
}>(({ active, disabled }) => {
|
||||
return {
|
||||
height: '32px',
|
||||
color: active
|
||||
? 'var(--affine-primary-color)'
|
||||
: 'var(--affine-text-primary-color)',
|
||||
paddingLeft: '2px',
|
||||
paddingRight: '2px',
|
||||
borderRadius: '8px',
|
||||
cursor: 'pointer',
|
||||
marginBottom: '4px',
|
||||
position: 'relative',
|
||||
flexShrink: 0,
|
||||
userSelect: 'none',
|
||||
...displayFlex('flex-start', 'stretch'),
|
||||
...(disabled
|
||||
? {
|
||||
cursor: 'not-allowed',
|
||||
color: 'var(--affine-border-color)',
|
||||
}
|
||||
: {}),
|
||||
|
||||
'a > svg, div > svg': {
|
||||
fontSize: '20px',
|
||||
marginLeft: '14px',
|
||||
marginRight: '12px',
|
||||
color: active
|
||||
? 'var(--affine-primary-color)'
|
||||
: 'var(--affine-icon-color)',
|
||||
},
|
||||
':hover:not([disabled])': {
|
||||
backgroundColor: 'var(--affine-hover-color)',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledCollapseButton = styled('button')<{
|
||||
collapse: boolean;
|
||||
show?: boolean;
|
||||
}>(({ collapse, show = true }) => {
|
||||
return {
|
||||
width: '16px',
|
||||
height: '100%',
|
||||
...displayFlex('center', 'center'),
|
||||
fontSize: '16px',
|
||||
position: 'absolute',
|
||||
left: '0',
|
||||
top: '0',
|
||||
bottom: '0',
|
||||
margin: 'auto',
|
||||
color: 'var(--affine-icon-color)',
|
||||
opacity: '.6',
|
||||
transition: 'opacity .15s ease-in-out',
|
||||
display: show ? 'flex' : 'none',
|
||||
svg: {
|
||||
transform: `rotate(${collapse ? '0' : '-90'}deg)`,
|
||||
},
|
||||
':hover': {
|
||||
opacity: '1',
|
||||
},
|
||||
':focus-visible': {
|
||||
outline: '-webkit-focus-ring-color auto 1px',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledCollapseItem = styled('div')<{
|
||||
disable?: boolean;
|
||||
active?: boolean;
|
||||
isOver?: boolean;
|
||||
textWrap?: boolean;
|
||||
}>(({ disable = false, active = false, isOver, textWrap = false }) => {
|
||||
return {
|
||||
width: '100%',
|
||||
lineHeight: '1.5',
|
||||
minHeight: '32px',
|
||||
borderRadius: '8px',
|
||||
...displayFlex('flex-start', 'center'),
|
||||
paddingRight: '2px',
|
||||
position: 'relative',
|
||||
color: disable
|
||||
? 'var(--affine-text-disable-color)'
|
||||
: active
|
||||
? 'var(--affine-primary-color)'
|
||||
: 'var(--affine-text-primary-color)',
|
||||
cursor: disable ? 'not-allowed' : 'pointer',
|
||||
background: isOver ? alpha('var(--affine-primary-color)', 0.06) : '',
|
||||
userSelect: 'none',
|
||||
...(textWrap
|
||||
? {
|
||||
wordBreak: 'break-word',
|
||||
whiteSpace: 'pre-wrap',
|
||||
}
|
||||
: {}),
|
||||
span: {
|
||||
flexGrow: '1',
|
||||
textAlign: 'left',
|
||||
...textEllipsis(1),
|
||||
},
|
||||
'> svg': {
|
||||
fontSize: '20px',
|
||||
marginRight: '8px',
|
||||
flexShrink: '0',
|
||||
color: active
|
||||
? 'var(--affine-primary-color)'
|
||||
: 'var(--affine-icon-color)',
|
||||
},
|
||||
|
||||
':hover': disable
|
||||
? {}
|
||||
: {
|
||||
backgroundColor: 'var(--affine-hover-color)',
|
||||
'.operation-button': {
|
||||
visibility: 'visible',
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledRouteNavigationWrapper = styled('div')({
|
||||
height: '32px',
|
||||
width: '80px',
|
||||
marginRight: '16px',
|
||||
...displayFlex('space-between', 'center'),
|
||||
});
|
||||
@@ -1,85 +0,0 @@
|
||||
import { displayFlex, styled, textEllipsis } from '@affine/component';
|
||||
import { Link } from '@mui/material';
|
||||
import { baseTheme } from '@toeverything/theme';
|
||||
|
||||
export const StyledSliderBarInnerWrapper = styled('div')(() => {
|
||||
return {
|
||||
flexGrow: 1,
|
||||
margin: '0 2px',
|
||||
position: 'relative',
|
||||
height: 'calc(100% - 52px * 2)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledLink = styled(Link)(() => {
|
||||
return {
|
||||
flexGrow: 1,
|
||||
textAlign: 'left',
|
||||
color: 'inherit',
|
||||
...displayFlex('flex-start', 'center'),
|
||||
':visited': {
|
||||
color: 'inherit',
|
||||
},
|
||||
overflow: 'hidden',
|
||||
div: {
|
||||
wordBreak: 'break-all',
|
||||
wordWrap: 'break-word',
|
||||
whiteSpace: 'nowrap',
|
||||
...textEllipsis(1),
|
||||
},
|
||||
userDrag: 'none',
|
||||
userSelect: 'none',
|
||||
appRegion: 'no-drag',
|
||||
WebkitUserSelect: 'none',
|
||||
WebkitUserDrag: 'none',
|
||||
WebkitAppRegion: 'no-drag',
|
||||
};
|
||||
});
|
||||
export const StyledNewPageButton = styled('button')(() => {
|
||||
return {
|
||||
width: '100%',
|
||||
height: '52px',
|
||||
...displayFlex('flex-start', 'center'),
|
||||
padding: '0 16px',
|
||||
svg: {
|
||||
fontSize: '20px',
|
||||
color: 'var(--affine-icon-color)',
|
||||
marginRight: '12px',
|
||||
},
|
||||
':hover': {
|
||||
color: 'var(--affine-primary-color)',
|
||||
svg: {
|
||||
color: 'var(--affine-primary-color)',
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
export const StyledSliderModalBackground = styled('div')<{ active: boolean }>(({
|
||||
active,
|
||||
}) => {
|
||||
return {
|
||||
transition: 'opacity .15s',
|
||||
pointerEvents: active ? 'auto' : 'none',
|
||||
opacity: active ? 1 : 0,
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: active ? 0 : '100%',
|
||||
bottom: 0,
|
||||
zIndex: parseInt(baseTheme.zIndexModal) - 1,
|
||||
background: 'var(--affine-background-modal-color)',
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledScrollWrapper = styled('div')<{
|
||||
showTopBorder: boolean;
|
||||
}>(({ showTopBorder }) => {
|
||||
return {
|
||||
maxHeight: '50%',
|
||||
overflowY: 'auto',
|
||||
borderTop: '1px solid',
|
||||
borderColor: showTopBorder ? 'var(--affine-border-color)' : 'transparent',
|
||||
};
|
||||
});
|
||||
+2
-2
@@ -25,7 +25,7 @@ const SignInItem = () => {
|
||||
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const onClickSignIn = useCallback(async () => {
|
||||
const onClickSignIn = useCallback(() => {
|
||||
if (!runtimeConfig.enableCloud) {
|
||||
setDisableCloudOpen(true);
|
||||
} else {
|
||||
@@ -76,7 +76,7 @@ export const UserWithWorkspaceList = ({
|
||||
onEventEnd?.();
|
||||
}, [onEventEnd, setOpenCreateWorkspaceModal]);
|
||||
|
||||
const onAddWorkspace = useCallback(async () => {
|
||||
const onAddWorkspace = useCallback(() => {
|
||||
setOpenCreateWorkspaceModal('add');
|
||||
onEventEnd?.();
|
||||
}, [onEventEnd, setOpenCreateWorkspaceModal]);
|
||||
|
||||
+1
-1
@@ -193,7 +193,7 @@ export const AFFiNEWorkspaceList = ({
|
||||
onEventEnd?.();
|
||||
}, [onEventEnd, setOpenCreateWorkspaceModal]);
|
||||
|
||||
const onAddWorkspace = useCallback(async () => {
|
||||
const onAddWorkspace = useCallback(() => {
|
||||
setOpenCreateWorkspaceModal('add');
|
||||
onEventEnd?.();
|
||||
}, [onEventEnd, setOpenCreateWorkspaceModal]);
|
||||
|
||||
+44
-42
@@ -1,6 +1,11 @@
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import {
|
||||
type SyncEngineStatus,
|
||||
SyncEngineStep,
|
||||
} from '@affine/workspace/providers';
|
||||
import {
|
||||
CloudWorkspaceIcon,
|
||||
InformationFillDuotoneIcon,
|
||||
LocalWorkspaceIcon,
|
||||
NoNetworkIcon,
|
||||
UnsyncIcon,
|
||||
@@ -9,16 +14,17 @@ import { Avatar } from '@toeverything/components/avatar';
|
||||
import { Tooltip } from '@toeverything/components/tooltip';
|
||||
import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-block-suite-workspace-avatar-url';
|
||||
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
|
||||
import { atom, useSetAtom } from 'jotai';
|
||||
import { debounce } from 'lodash-es';
|
||||
import {
|
||||
forwardRef,
|
||||
type HTMLAttributes,
|
||||
type MouseEvent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import { useDatasourceSync } from '../../../../hooks/use-datasource-sync';
|
||||
import { useCurrentSyncEngine } from '../../../../hooks/current/use-current-sync-engine';
|
||||
import { useSystemOnline } from '../../../../hooks/use-system-online';
|
||||
import type { AllWorkspace } from '../../../../shared';
|
||||
import { Loading } from './loading-icon';
|
||||
@@ -29,8 +35,6 @@ import {
|
||||
StyledWorkspaceStatus,
|
||||
} from './styles';
|
||||
|
||||
const hoverAtom = atom(false);
|
||||
|
||||
// FIXME:
|
||||
// 1. Remove mui style
|
||||
// 2. Refactor the code to improve readability
|
||||
@@ -64,7 +68,11 @@ const UnSyncWorkspaceStatus = () => {
|
||||
const LocalWorkspaceStatus = () => {
|
||||
return (
|
||||
<>
|
||||
<LocalWorkspaceIcon />
|
||||
{!environment.isDesktop ? (
|
||||
<InformationFillDuotoneIcon data-warning-color="true" />
|
||||
) : (
|
||||
<LocalWorkspaceIcon />
|
||||
)}
|
||||
Local
|
||||
</>
|
||||
);
|
||||
@@ -86,63 +94,57 @@ const WorkspaceStatus = ({
|
||||
}) => {
|
||||
const isOnline = useSystemOnline();
|
||||
|
||||
// todo: finish display sync status
|
||||
const [forceSyncStatus, startForceSync] = useDatasourceSync(
|
||||
currentWorkspace.blockSuiteWorkspace
|
||||
);
|
||||
const [syncEngineStatus, setSyncEngineStatus] =
|
||||
useState<SyncEngineStatus | null>(null);
|
||||
|
||||
const setIsHovered = useSetAtom(hoverAtom);
|
||||
const syncEngine = useCurrentSyncEngine();
|
||||
|
||||
useEffect(() => {
|
||||
setSyncEngineStatus(syncEngine?.status ?? null);
|
||||
const disposable = syncEngine?.onStatusChange.on(
|
||||
debounce(status => {
|
||||
setSyncEngineStatus(status);
|
||||
}, 500)
|
||||
);
|
||||
return () => {
|
||||
disposable?.dispose();
|
||||
};
|
||||
}, [syncEngine]);
|
||||
|
||||
const content = useMemo(() => {
|
||||
// TODO: add i18n
|
||||
if (currentWorkspace.flavour === WorkspaceFlavour.LOCAL) {
|
||||
if (!environment.isDesktop) {
|
||||
return 'This is a local demo workspace.';
|
||||
}
|
||||
return 'Saved locally';
|
||||
}
|
||||
if (!isOnline) {
|
||||
return 'Disconnected, please check your network connection';
|
||||
}
|
||||
switch (forceSyncStatus.type) {
|
||||
case 'syncing':
|
||||
return 'Syncing with AFFiNE Cloud';
|
||||
case 'error':
|
||||
return 'Sync failed due to server issues, please try again later.';
|
||||
default:
|
||||
return 'Sync with AFFiNE Cloud';
|
||||
if (!syncEngineStatus || syncEngineStatus.step === SyncEngineStep.Syncing) {
|
||||
return 'Syncing with AFFiNE Cloud';
|
||||
}
|
||||
}, [currentWorkspace.flavour, forceSyncStatus.type, isOnline]);
|
||||
if (syncEngineStatus.retrying) {
|
||||
return 'Sync disconnected due to unexpected issues, reconnecting.';
|
||||
}
|
||||
return 'Synced with AFFiNE Cloud';
|
||||
}, [currentWorkspace.flavour, isOnline, syncEngineStatus]);
|
||||
|
||||
const CloudWorkspaceSyncStatus = useCallback(() => {
|
||||
if (forceSyncStatus.type === 'syncing') {
|
||||
if (!syncEngineStatus || syncEngineStatus.step === SyncEngineStep.Syncing) {
|
||||
return SyncingWorkspaceStatus();
|
||||
} else if (forceSyncStatus.type === 'error') {
|
||||
} else if (syncEngineStatus.retrying) {
|
||||
return UnSyncWorkspaceStatus();
|
||||
} else {
|
||||
return CloudWorkspaceStatus();
|
||||
}
|
||||
}, [forceSyncStatus.type]);
|
||||
}, [syncEngineStatus]);
|
||||
|
||||
const handleClick = useCallback(
|
||||
(e: MouseEvent<HTMLDivElement>) => {
|
||||
e.stopPropagation();
|
||||
if (
|
||||
currentWorkspace.flavour === WorkspaceFlavour.LOCAL ||
|
||||
forceSyncStatus.type === 'syncing'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
startForceSync();
|
||||
},
|
||||
[currentWorkspace.flavour, forceSyncStatus.type, startForceSync]
|
||||
);
|
||||
return (
|
||||
<div style={{ display: 'flex' }}>
|
||||
<Tooltip content={content}>
|
||||
<StyledWorkspaceStatus
|
||||
onMouseEnter={() => {
|
||||
setIsHovered(true);
|
||||
}}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<StyledWorkspaceStatus>
|
||||
{currentWorkspace.flavour === WorkspaceFlavour.AFFINE_CLOUD ? (
|
||||
!isOnline ? (
|
||||
<OfflineStatus />
|
||||
|
||||
+3
-5
@@ -44,11 +44,9 @@ export const StyledWorkspaceStatus = styled('div')(() => {
|
||||
svg: {
|
||||
color: 'var(--affine-icon-color)',
|
||||
fontSize: 'var(--affine-font-base)',
|
||||
},
|
||||
':hover': {
|
||||
cursor: 'pointer',
|
||||
borderRadius: '4px',
|
||||
background: 'var(--affine-hover-color)',
|
||||
'&[data-warning-color="true"]': {
|
||||
color: 'var(--affine-error-color)',
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { AnimatedDeleteIcon } from '@affine/component';
|
||||
import {
|
||||
AddPageButton,
|
||||
AppDownloadButton,
|
||||
AppSidebar,
|
||||
appSidebarOpenAtom,
|
||||
AppUpdaterButton,
|
||||
@@ -18,6 +19,7 @@ import { FolderIcon, SettingsIcon } from '@blocksuite/icons';
|
||||
import type { Page } from '@blocksuite/store';
|
||||
import { useDroppable } from '@dnd-kit/core';
|
||||
import { Menu } from '@toeverything/components/menu';
|
||||
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||
import { useAtom, useAtomValue } from 'jotai';
|
||||
import type { HTMLAttributes, ReactElement } from 'react';
|
||||
import { forwardRef, useCallback, useEffect, useMemo } from 'react';
|
||||
@@ -107,7 +109,7 @@ export const RootAppSidebar = ({
|
||||
);
|
||||
const generalShortcutsInfo = useGeneralShortcuts();
|
||||
|
||||
const onClickNewPage = useCallback(async () => {
|
||||
const onClickNewPage = useAsyncCallback(async () => {
|
||||
const page = createPage();
|
||||
await page.waitForLoaded();
|
||||
openPage(page.id);
|
||||
@@ -265,7 +267,7 @@ export const RootAppSidebar = ({
|
||||
)}
|
||||
</SidebarScrollableContainer>
|
||||
<SidebarContainer>
|
||||
{environment.isDesktop && <AppUpdaterButton />}
|
||||
{environment.isDesktop ? <AppUpdaterButton /> : <AppDownloadButton />}
|
||||
<div style={{ height: '4px' }} />
|
||||
<AddPageButton onClick={onClickNewPage} />
|
||||
</SidebarContainer>
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { Workspace } from '@blocksuite/store';
|
||||
import { useSetAtom } from 'jotai/react';
|
||||
|
||||
import type { PageMode } from '../atoms';
|
||||
import { appHeaderAtom, mainContainerAtom } from '../atoms/element';
|
||||
import { useWorkspace } from '../hooks/use-workspace';
|
||||
import { BlockSuiteHeaderTitle } from './blocksuite/block-suite-header-title';
|
||||
import ShareHeaderLeftItem from './cloud/share-header-left-item';
|
||||
import ShareHeaderRightItem from './cloud/share-header-right-item';
|
||||
import { Header } from './pure/header';
|
||||
|
||||
export function ShareHeader({
|
||||
workspace,
|
||||
pageId,
|
||||
publishMode,
|
||||
}: {
|
||||
workspace: Workspace;
|
||||
pageId: string;
|
||||
publishMode: PageMode;
|
||||
}) {
|
||||
const setAppHeader = useSetAtom(appHeaderAtom);
|
||||
|
||||
const currentWorkspace = useWorkspace(workspace.id);
|
||||
|
||||
return (
|
||||
<Header
|
||||
mainContainerAtom={mainContainerAtom}
|
||||
ref={setAppHeader}
|
||||
left={<ShareHeaderLeftItem />}
|
||||
center={
|
||||
<BlockSuiteHeaderTitle
|
||||
workspace={currentWorkspace}
|
||||
pageId={pageId}
|
||||
isPublic={true}
|
||||
publicMode={publishMode}
|
||||
/>
|
||||
}
|
||||
right={
|
||||
<ShareHeaderRightItem workspaceId={workspace.id} pageId={pageId} />
|
||||
}
|
||||
bottomBorder
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const iconWrapper = style({
|
||||
position: 'absolute',
|
||||
top: '16px',
|
||||
left: '16px',
|
||||
fontSize: '24px',
|
||||
cursor: 'pointer',
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
selectors: {
|
||||
'&:visited': {
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,122 @@
|
||||
import { BrowserWarning } from '@affine/component/affine-banner';
|
||||
import { LocalDemoTips } from '@affine/component/affine-banner';
|
||||
import {
|
||||
type AffineOfficialWorkspace,
|
||||
WorkspaceFlavour,
|
||||
} from '@affine/env/workspace';
|
||||
import { Trans } from '@affine/i18n';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { authAtom } from '../atoms';
|
||||
import { useCurrentLoginStatus } from '../hooks/affine/use-current-login-status';
|
||||
import { useOnTransformWorkspace } from '../hooks/root/use-on-transform-workspace';
|
||||
import { EnableAffineCloudModal } from './affine/enable-affine-cloud-modal';
|
||||
|
||||
const minimumChromeVersion = 106;
|
||||
|
||||
const shouldShowWarning = (() => {
|
||||
if (environment.isDesktop) {
|
||||
// even though desktop has compatibility issues,
|
||||
// we don't want to show the warning
|
||||
return false;
|
||||
}
|
||||
if (!environment.isBrowser) {
|
||||
// disable in SSR
|
||||
return false;
|
||||
}
|
||||
if (environment.isChrome) {
|
||||
return environment.chromeVersion < minimumChromeVersion;
|
||||
} else {
|
||||
return !environment.isMobile;
|
||||
}
|
||||
})();
|
||||
|
||||
const OSWarningMessage = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const notChrome = environment.isBrowser && !environment.isChrome;
|
||||
const notGoodVersion =
|
||||
environment.isBrowser &&
|
||||
environment.isChrome &&
|
||||
environment.chromeVersion < minimumChromeVersion;
|
||||
|
||||
if (notChrome) {
|
||||
return (
|
||||
<span>
|
||||
<Trans i18nKey="recommendBrowser">
|
||||
We recommend the <strong>Chrome</strong> browser for an optimal
|
||||
experience.
|
||||
</Trans>
|
||||
</span>
|
||||
);
|
||||
} else if (notGoodVersion) {
|
||||
return <span>{t['upgradeBrowser']()}</span>;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const TopTip = ({
|
||||
workspace,
|
||||
}: {
|
||||
workspace: AffineOfficialWorkspace;
|
||||
}) => {
|
||||
const loginStatus = useCurrentLoginStatus();
|
||||
const isLoggedIn = loginStatus === 'authenticated';
|
||||
|
||||
const [showWarning, setShowWarning] = useState(shouldShowWarning);
|
||||
const [showLocalDemoTips, setShowLocalDemoTips] = useState(true);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const setAuthModal = useSetAtom(authAtom);
|
||||
const onLogin = useCallback(() => {
|
||||
setAuthModal({ openModal: true, state: 'signIn' });
|
||||
}, [setAuthModal]);
|
||||
|
||||
const onTransformWorkspace = useOnTransformWorkspace();
|
||||
const handleConfirm = useCallback(() => {
|
||||
if (workspace.flavour !== WorkspaceFlavour.LOCAL) {
|
||||
return;
|
||||
}
|
||||
onTransformWorkspace(
|
||||
WorkspaceFlavour.LOCAL,
|
||||
WorkspaceFlavour.AFFINE_CLOUD,
|
||||
workspace
|
||||
);
|
||||
setOpen(false);
|
||||
}, [onTransformWorkspace, workspace]);
|
||||
|
||||
if (
|
||||
showLocalDemoTips &&
|
||||
!environment.isDesktop &&
|
||||
workspace.flavour === WorkspaceFlavour.LOCAL
|
||||
) {
|
||||
return (
|
||||
<>
|
||||
<LocalDemoTips
|
||||
isLoggedIn={isLoggedIn}
|
||||
onLogin={onLogin}
|
||||
onEnableCloud={() => setOpen(true)}
|
||||
onClose={() => {
|
||||
setShowLocalDemoTips(false);
|
||||
}}
|
||||
/>
|
||||
<EnableAffineCloudModal
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
onConfirm={handleConfirm}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<BrowserWarning
|
||||
show={showWarning}
|
||||
message={<OSWarningMessage />}
|
||||
onClose={() => {
|
||||
setShowWarning(false);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
useCollectionManager,
|
||||
} from '@affine/component/page-list';
|
||||
import { Unreachable } from '@affine/env/constant';
|
||||
import type { Collection } from '@affine/env/filter';
|
||||
import type { Collection, Filter } from '@affine/env/filter';
|
||||
import type {
|
||||
WorkspaceFlavour,
|
||||
WorkspaceHeaderProps,
|
||||
@@ -13,6 +13,7 @@ import type {
|
||||
import { WorkspaceSubPath } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { DeleteIcon } from '@blocksuite/icons';
|
||||
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||
import { useSetAtom } from 'jotai/react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
@@ -28,6 +29,7 @@ import { filterContainerStyle } from './filter-container.css';
|
||||
import { Header } from './pure/header';
|
||||
import { PluginHeader } from './pure/plugin-header';
|
||||
import { WorkspaceModeFilterTab } from './pure/workspace-mode-filter-tab';
|
||||
import { TopTip } from './top-tip';
|
||||
import * as styles from './workspace-header.css';
|
||||
|
||||
const FilterContainer = ({ workspaceId }: { workspaceId: string }) => {
|
||||
@@ -44,6 +46,17 @@ const FilterContainer = ({ workspaceId }: { workspaceId: string }) => {
|
||||
},
|
||||
[setting, navigateHelper, workspaceId]
|
||||
);
|
||||
|
||||
const onFilterChange = useAsyncCallback(
|
||||
async (filterList: Filter[]) => {
|
||||
await setting.updateCollection({
|
||||
...setting.currentCollection,
|
||||
filterList,
|
||||
});
|
||||
},
|
||||
[setting]
|
||||
);
|
||||
|
||||
if (!setting.isDefault || !setting.currentCollection.filterList.length) {
|
||||
return null;
|
||||
}
|
||||
@@ -54,12 +67,7 @@ const FilterContainer = ({ workspaceId }: { workspaceId: string }) => {
|
||||
<FilterList
|
||||
propertiesMeta={currentWorkspace.blockSuiteWorkspace.meta.properties}
|
||||
value={setting.currentCollection.filterList}
|
||||
onChange={filterList => {
|
||||
return setting.updateCollection({
|
||||
...setting.currentCollection,
|
||||
filterList,
|
||||
});
|
||||
}}
|
||||
onChange={onFilterChange}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -154,23 +162,26 @@ export function WorkspaceHeader({
|
||||
<SharePageModal workspace={currentWorkspace} page={currentPage} />
|
||||
) : null;
|
||||
return (
|
||||
<Header
|
||||
mainContainerAtom={mainContainerAtom}
|
||||
ref={setAppHeader}
|
||||
center={
|
||||
<BlockSuiteHeaderTitle
|
||||
workspace={currentWorkspace}
|
||||
pageId={currentEntry.pageId}
|
||||
/>
|
||||
}
|
||||
right={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
{sharePageModal}
|
||||
<PluginHeader />
|
||||
</div>
|
||||
}
|
||||
bottomBorder
|
||||
/>
|
||||
<>
|
||||
<Header
|
||||
mainContainerAtom={mainContainerAtom}
|
||||
ref={setAppHeader}
|
||||
center={
|
||||
<BlockSuiteHeaderTitle
|
||||
workspace={currentWorkspace}
|
||||
pageId={currentEntry.pageId}
|
||||
/>
|
||||
}
|
||||
right={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
{sharePageModal}
|
||||
<PluginHeader />
|
||||
</div>
|
||||
}
|
||||
bottomBorder
|
||||
/>
|
||||
<TopTip workspace={currentWorkspace} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,85 +1,122 @@
|
||||
import type {
|
||||
AffineSocketIOProvider,
|
||||
LocalIndexedDBBackgroundProvider,
|
||||
SQLiteProvider,
|
||||
} from '@affine/env/workspace';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import { forceUpgradePages } from '@toeverything/infra/blocksuite';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { syncDataSourceFromDoc, syncDocFromDataSource } from 'y-provider';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
|
||||
import { getOrCreateWorkspace } from '@affine/workspace/manager';
|
||||
import type { Workspace } from '@blocksuite/store';
|
||||
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||
import { getBlockSuiteWorkspaceAtom } from '@toeverything/infra/__internal__/workspace';
|
||||
import { getCurrentStore } from '@toeverything/infra/atom';
|
||||
import type { MigrationPoint } from '@toeverything/infra/blocksuite';
|
||||
import {
|
||||
migrateLocalBlobStorage,
|
||||
migrateWorkspace,
|
||||
} from '@toeverything/infra/blocksuite';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { useState } from 'react';
|
||||
import { applyUpdate, Doc as YDoc, encodeStateAsUpdate } from 'yjs';
|
||||
|
||||
import { WorkspaceAdapters } from '../../adapters/workspace';
|
||||
import { useCurrentSyncEngine } from '../../hooks/current/use-current-sync-engine';
|
||||
import { useCurrentWorkspace } from '../../hooks/current/use-current-workspace';
|
||||
|
||||
export type UpgradeState = 'pending' | 'upgrading' | 'done' | 'error';
|
||||
|
||||
export function useUpgradeWorkspace() {
|
||||
function applyDoc(target: YDoc, result: YDoc) {
|
||||
applyUpdate(target, encodeStateAsUpdate(result));
|
||||
for (const targetSubDoc of target.subdocs.values()) {
|
||||
const resultSubDocs = Array.from(result.subdocs.values());
|
||||
const resultSubDoc = resultSubDocs.find(
|
||||
item => item.guid === targetSubDoc.guid
|
||||
);
|
||||
if (resultSubDoc) {
|
||||
applyDoc(targetSubDoc, resultSubDoc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function useUpgradeWorkspace(migration: MigrationPoint) {
|
||||
const [state, setState] = useState<UpgradeState>('pending');
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
const [newWorkspaceId, setNewWorkspaceId] = useState<string | null>(null);
|
||||
|
||||
const [workspace] = useCurrentWorkspace();
|
||||
const providers = workspace.blockSuiteWorkspace.providers;
|
||||
const remoteProvider: AffineSocketIOProvider | undefined = useMemo(() => {
|
||||
return providers.find(
|
||||
(provider): provider is AffineSocketIOProvider =>
|
||||
provider.flavour === 'affine-socket-io'
|
||||
);
|
||||
}, [providers]);
|
||||
const localProvider = useMemo(() => {
|
||||
const sqliteProvider = providers.find(
|
||||
(provider): provider is SQLiteProvider => provider.flavour === 'sqlite'
|
||||
);
|
||||
const indexedDbProvider = providers.find(
|
||||
(provider): provider is LocalIndexedDBBackgroundProvider =>
|
||||
provider.flavour === 'local-indexeddb-background'
|
||||
);
|
||||
const provider = sqliteProvider || indexedDbProvider;
|
||||
assertExists(provider, 'no local provider');
|
||||
return provider;
|
||||
}, [providers]);
|
||||
const syncEngine = useCurrentSyncEngine();
|
||||
const rootStore = getCurrentStore();
|
||||
|
||||
const upgradeWorkspace = useCallback(() => {
|
||||
const upgradeWorkspace = useAsyncCallback(async () => {
|
||||
setState('upgrading');
|
||||
setError(null);
|
||||
try {
|
||||
// Migration need to wait for root doc and all subdocs loaded.
|
||||
await syncEngine?.waitForSynced();
|
||||
|
||||
(async () => {
|
||||
await syncDocFromDataSource(
|
||||
workspace.blockSuiteWorkspace.doc,
|
||||
localProvider.datasource
|
||||
);
|
||||
if (remoteProvider) {
|
||||
await syncDocFromDataSource(
|
||||
workspace.blockSuiteWorkspace.doc,
|
||||
remoteProvider.datasource
|
||||
);
|
||||
}
|
||||
|
||||
await forceUpgradePages({
|
||||
getCurrentRootDoc: async () => workspace.blockSuiteWorkspace.doc,
|
||||
getSchema: () => workspace.blockSuiteWorkspace.schema,
|
||||
// Clone a new doc to prevent change events.
|
||||
const clonedDoc = new YDoc({
|
||||
guid: workspace.blockSuiteWorkspace.doc.guid,
|
||||
});
|
||||
await syncDataSourceFromDoc(
|
||||
workspace.blockSuiteWorkspace.doc,
|
||||
localProvider.datasource
|
||||
);
|
||||
if (remoteProvider) {
|
||||
await syncDataSourceFromDoc(
|
||||
workspace.blockSuiteWorkspace.doc,
|
||||
remoteProvider.datasource
|
||||
applyDoc(clonedDoc, workspace.blockSuiteWorkspace.doc);
|
||||
const schema = workspace.blockSuiteWorkspace.schema;
|
||||
let newWorkspace: Workspace | null = null;
|
||||
|
||||
const resultDoc = await migrateWorkspace(migration, {
|
||||
doc: clonedDoc,
|
||||
schema,
|
||||
createWorkspace: () => {
|
||||
// Migrate to subdoc version need to create a new workspace.
|
||||
// It will only happened for old local workspace.
|
||||
newWorkspace = getOrCreateWorkspace(nanoid(), WorkspaceFlavour.LOCAL);
|
||||
return Promise.resolve(newWorkspace);
|
||||
},
|
||||
});
|
||||
|
||||
if (newWorkspace) {
|
||||
const localMetaString =
|
||||
localStorage.getItem('jotai-workspaces') ?? '[]';
|
||||
const localMetadataList = JSON.parse(
|
||||
localMetaString
|
||||
) as RootWorkspaceMetadata[];
|
||||
const currentLocalMetadata = localMetadataList.find(
|
||||
item => item.id === workspace.id
|
||||
);
|
||||
const flavour = currentLocalMetadata?.flavour ?? WorkspaceFlavour.LOCAL;
|
||||
|
||||
// Legacy logic moved from `setup.ts`.
|
||||
// It works well before, should be refactor or remove in the future.
|
||||
const adapter = WorkspaceAdapters[flavour];
|
||||
const newId = await adapter.CRUD.create(newWorkspace);
|
||||
const [workspaceAtom] = getBlockSuiteWorkspaceAtom(newId);
|
||||
await rootStore.get(workspaceAtom); // Trigger provider sync to persist data.
|
||||
|
||||
await adapter.CRUD.delete(workspace.blockSuiteWorkspace);
|
||||
await migrateLocalBlobStorage(workspace.id, newId);
|
||||
setNewWorkspaceId(newId);
|
||||
|
||||
const index = localMetadataList.findIndex(
|
||||
meta => meta.id === workspace.id
|
||||
);
|
||||
localMetadataList[index] = {
|
||||
...currentLocalMetadata,
|
||||
id: newId,
|
||||
flavour,
|
||||
};
|
||||
localStorage.setItem(
|
||||
'jotai-workspaces',
|
||||
JSON.stringify(localMetadataList)
|
||||
);
|
||||
localStorage.setItem('last_workspace_id', newId);
|
||||
localStorage.removeItem('last_page_id');
|
||||
} else {
|
||||
applyDoc(workspace.blockSuiteWorkspace.doc, resultDoc);
|
||||
}
|
||||
|
||||
await syncEngine?.waitForSynced();
|
||||
|
||||
setState('done');
|
||||
})().catch((e: any) => {
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
setError(e);
|
||||
setState('error');
|
||||
});
|
||||
}, [
|
||||
localProvider.datasource,
|
||||
remoteProvider,
|
||||
workspace.blockSuiteWorkspace.doc,
|
||||
workspace.blockSuiteWorkspace.schema,
|
||||
]);
|
||||
}
|
||||
}, [rootStore, workspace, syncEngine, migration]);
|
||||
|
||||
return [state, error, upgradeWorkspace] as const;
|
||||
return [state, error, upgradeWorkspace, newWorkspaceId] as const;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { AffineShapeIcon } from '@affine/component/page-list'; // TODO: import from page-list temporarily, need to defined common svg icon/images management.
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { Button } from '@toeverything/components/button';
|
||||
import type { MigrationPoint } from '@toeverything/infra/blocksuite';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { pathGenerator } from '../../shared';
|
||||
import * as styles from './upgrade.css';
|
||||
import { type UpgradeState, useUpgradeWorkspace } from './upgrade-hooks';
|
||||
import { ArrowCircleIcon, HeartBreakIcon } from './upgrade-icon';
|
||||
@@ -32,11 +34,18 @@ function UpgradeIcon({ upgradeState }: { upgradeState: UpgradeState }) {
|
||||
);
|
||||
}
|
||||
|
||||
interface WorkspaceUpgradeProps {
|
||||
migration: MigrationPoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: Help info is not implemented yet.
|
||||
*/
|
||||
export const WorkspaceUpgrade = function MigrationFallback() {
|
||||
const [upgradeState, , upgradeWorkspace] = useUpgradeWorkspace();
|
||||
export const WorkspaceUpgrade = function WorkspaceUpgrade(
|
||||
props: WorkspaceUpgradeProps
|
||||
) {
|
||||
const [upgradeState, , upgradeWorkspace, newWorkspaceId] =
|
||||
useUpgradeWorkspace(props.migration);
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const refreshPage = useCallback(() => {
|
||||
@@ -45,6 +54,12 @@ export const WorkspaceUpgrade = function MigrationFallback() {
|
||||
|
||||
const onButtonClick = useMemo(() => {
|
||||
if (upgradeState === 'done') {
|
||||
if (newWorkspaceId) {
|
||||
return () => {
|
||||
window.location.replace(pathGenerator.all(newWorkspaceId));
|
||||
};
|
||||
}
|
||||
|
||||
return refreshPage;
|
||||
}
|
||||
|
||||
@@ -53,7 +68,7 @@ export const WorkspaceUpgrade = function MigrationFallback() {
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}, [upgradeState, upgradeWorkspace, refreshPage]);
|
||||
}, [upgradeState, upgradeWorkspace, refreshPage, newWorkspaceId]);
|
||||
|
||||
return (
|
||||
<div className={styles.layout}>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { type User } from '@affine/component/auth-components';
|
||||
import type { DefaultSession, Session } from 'next-auth';
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
import { getSession, useSession } from 'next-auth/react';
|
||||
import { useEffect, useReducer } from 'react';
|
||||
import { useEffect, useMemo, useReducer } from 'react';
|
||||
|
||||
import { SessionFetchErrorRightAfterLoginOrSignUp } from '../../unexpected-application-state/errors';
|
||||
|
||||
@@ -60,7 +60,7 @@ export function useCurrentUser(): CheckedUser {
|
||||
// login succeed but the session request failed then.
|
||||
// also need a error boundary to handle this error.
|
||||
throw new SessionFetchErrorRightAfterLoginOrSignUp(
|
||||
'First session should not be null',
|
||||
'Fetching session failed',
|
||||
() => {
|
||||
getSession()
|
||||
.then(session => {
|
||||
@@ -97,12 +97,14 @@ export function useCurrentUser(): CheckedUser {
|
||||
|
||||
const user = session.user;
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
image: user.image,
|
||||
hasPassword: user?.hasPassword ?? false,
|
||||
update,
|
||||
};
|
||||
return useMemo(() => {
|
||||
return {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
image: user.image,
|
||||
hasPassword: user?.hasPassword ?? false,
|
||||
update,
|
||||
};
|
||||
}, [user, update]);
|
||||
}
|
||||
|
||||
@@ -1,57 +1,193 @@
|
||||
import { pushNotificationAtom } from '@affine/component/notification-center';
|
||||
import {
|
||||
getWorkspaceSharedPagesQuery,
|
||||
revokePageMutation,
|
||||
sharePageMutation,
|
||||
getWorkspacePublicPagesQuery,
|
||||
PublicPageMode,
|
||||
publishPageMutation,
|
||||
revokePublicPageMutation,
|
||||
} from '@affine/graphql';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useMutation, useQuery } from '@affine/workspace/affine/gql';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import type { PageMode } from '../../atoms';
|
||||
|
||||
type NoParametersKeys<T> = {
|
||||
[K in keyof T]: T[K] extends () => any ? K : never;
|
||||
}[keyof T];
|
||||
|
||||
type i18nKey = NoParametersKeys<ReturnType<typeof useAFFiNEI18N>>;
|
||||
|
||||
type NotificationKey =
|
||||
| 'enableSuccessTitle'
|
||||
| 'enableSuccessMessage'
|
||||
| 'enableErrorTitle'
|
||||
| 'enableErrorMessage'
|
||||
| 'changeSuccessTitle'
|
||||
| 'changeErrorTitle'
|
||||
| 'changeErrorMessage'
|
||||
| 'disableSuccessTitle'
|
||||
| 'disableSuccessMessage'
|
||||
| 'disableErrorTitle'
|
||||
| 'disableErrorMessage';
|
||||
|
||||
const notificationToI18nKey: Record<NotificationKey, i18nKey> = {
|
||||
enableSuccessTitle:
|
||||
'com.affine.share-menu.create-public-link.notification.success.title',
|
||||
enableSuccessMessage:
|
||||
'com.affine.share-menu.create-public-link.notification.success.message',
|
||||
enableErrorTitle:
|
||||
'com.affine.share-menu.create-public-link.notification.fail.title',
|
||||
enableErrorMessage:
|
||||
'com.affine.share-menu.create-public-link.notification.fail.message',
|
||||
changeSuccessTitle:
|
||||
'com.affine.share-menu.confirm-modify-mode.notification.success.title',
|
||||
changeErrorTitle:
|
||||
'com.affine.share-menu.confirm-modify-mode.notification.fail.title',
|
||||
changeErrorMessage:
|
||||
'com.affine.share-menu.confirm-modify-mode.notification.fail.message',
|
||||
disableSuccessTitle:
|
||||
'com.affine.share-menu.disable-publish-link.notification.success.title',
|
||||
disableSuccessMessage:
|
||||
'com.affine.share-menu.disable-publish-link.notification.success.message',
|
||||
disableErrorTitle:
|
||||
'com.affine.share-menu.disable-publish-link.notification.fail.title',
|
||||
disableErrorMessage:
|
||||
'com.affine.share-menu.disable-publish-link.notification.fail.message',
|
||||
};
|
||||
|
||||
export function useIsSharedPage(
|
||||
workspaceId: string,
|
||||
pageId: string
|
||||
): [isSharedPage: boolean, setSharedPage: (enable: boolean) => void] {
|
||||
): {
|
||||
isSharedPage: boolean;
|
||||
changeShare: (mode: PageMode) => void;
|
||||
disableShare: () => void;
|
||||
currentShareMode: PageMode;
|
||||
enableShare: (mode: PageMode) => void;
|
||||
} {
|
||||
const t = useAFFiNEI18N();
|
||||
const pushNotification = useSetAtom(pushNotificationAtom);
|
||||
const { data, mutate } = useQuery({
|
||||
query: getWorkspaceSharedPagesQuery,
|
||||
query: getWorkspacePublicPagesQuery,
|
||||
variables: {
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
const { trigger: enableSharePage } = useMutation({
|
||||
mutation: sharePageMutation,
|
||||
mutation: publishPageMutation,
|
||||
});
|
||||
const { trigger: disableSharePage } = useMutation({
|
||||
mutation: revokePageMutation,
|
||||
mutation: revokePublicPageMutation,
|
||||
});
|
||||
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]
|
||||
),
|
||||
];
|
||||
|
||||
const [isSharedPage, currentShareMode] = useMemo(() => {
|
||||
const publicPage = data?.workspace.publicPages.find(
|
||||
publicPage => publicPage.id === pageId
|
||||
);
|
||||
const isPageShared = !!publicPage;
|
||||
|
||||
const currentShareMode: PageMode =
|
||||
publicPage?.mode === PublicPageMode.Edgeless ? 'edgeless' : 'page';
|
||||
|
||||
return [isPageShared, currentShareMode];
|
||||
}, [data?.workspace.publicPages, pageId]);
|
||||
|
||||
const enableShare = useCallback(
|
||||
(mode: PageMode) => {
|
||||
const publishMode =
|
||||
mode === 'edgeless' ? PublicPageMode.Edgeless : PublicPageMode.Page;
|
||||
|
||||
enableSharePage({ workspaceId, pageId, mode: publishMode })
|
||||
.then(() => {
|
||||
pushNotification({
|
||||
title: t[notificationToI18nKey['enableSuccessTitle']](),
|
||||
message: t[notificationToI18nKey['enableSuccessMessage']](),
|
||||
type: 'success',
|
||||
theme: 'default',
|
||||
});
|
||||
return mutate();
|
||||
})
|
||||
.catch(e => {
|
||||
pushNotification({
|
||||
title: t[notificationToI18nKey['enableErrorTitle']](),
|
||||
message: t[notificationToI18nKey['enableErrorMessage']](),
|
||||
type: 'error',
|
||||
});
|
||||
console.error(e);
|
||||
});
|
||||
},
|
||||
[enableSharePage, mutate, pageId, pushNotification, t, workspaceId]
|
||||
);
|
||||
|
||||
const changeShare = useCallback(
|
||||
(mode: PageMode) => {
|
||||
const publishMode =
|
||||
mode === 'edgeless' ? PublicPageMode.Edgeless : PublicPageMode.Page;
|
||||
|
||||
enableSharePage({ workspaceId, pageId, mode: publishMode })
|
||||
.then(() => {
|
||||
pushNotification({
|
||||
title: t[notificationToI18nKey['changeSuccessTitle']](),
|
||||
message: t[
|
||||
'com.affine.share-menu.confirm-modify-mode.notification.success.message'
|
||||
]({
|
||||
preMode:
|
||||
publishMode === PublicPageMode.Edgeless
|
||||
? t['Page']()
|
||||
: t['Edgeless'](),
|
||||
currentMode:
|
||||
publishMode === PublicPageMode.Edgeless
|
||||
? t['Edgeless']()
|
||||
: t['Page'](),
|
||||
}),
|
||||
type: 'success',
|
||||
theme: 'default',
|
||||
});
|
||||
return mutate();
|
||||
})
|
||||
.catch(e => {
|
||||
pushNotification({
|
||||
title: t[notificationToI18nKey['changeErrorTitle']](),
|
||||
message: t[notificationToI18nKey['changeErrorMessage']](),
|
||||
type: 'error',
|
||||
});
|
||||
console.error(e);
|
||||
});
|
||||
},
|
||||
[enableSharePage, mutate, pageId, pushNotification, t, workspaceId]
|
||||
);
|
||||
|
||||
const disableShare = useCallback(() => {
|
||||
disableSharePage({ workspaceId, pageId })
|
||||
.then(() => {
|
||||
pushNotification({
|
||||
title: t[notificationToI18nKey['disableSuccessTitle']](),
|
||||
message: t[notificationToI18nKey['disableSuccessMessage']](),
|
||||
type: 'success',
|
||||
theme: 'default',
|
||||
});
|
||||
return mutate();
|
||||
})
|
||||
.catch(e => {
|
||||
pushNotification({
|
||||
title: t[notificationToI18nKey['disableErrorTitle']](),
|
||||
message: t[notificationToI18nKey['disableErrorMessage']](),
|
||||
type: 'error',
|
||||
});
|
||||
console.error(e);
|
||||
});
|
||||
}, [disableSharePage, mutate, pageId, pushNotification, t, workspaceId]);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
isSharedPage,
|
||||
currentShareMode,
|
||||
enableShare,
|
||||
disableShare,
|
||||
changeShare,
|
||||
}),
|
||||
[isSharedPage, currentShareMode, enableShare, disableShare, changeShare]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { LOCALES, useI18N } from '@affine/i18n';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export function useLanguageHelper() {
|
||||
const i18n = useI18N();
|
||||
@@ -16,9 +17,9 @@ export function useLanguageHelper() {
|
||||
})),
|
||||
[]
|
||||
);
|
||||
const onLanguageChange = useCallback(
|
||||
(event: string) => {
|
||||
i18n.changeLanguage(event);
|
||||
const onLanguageChange = useAsyncCallback(
|
||||
async (event: string) => {
|
||||
await i18n.changeLanguage(event);
|
||||
},
|
||||
[i18n]
|
||||
);
|
||||
|
||||
@@ -117,8 +117,8 @@ export function useRegisterBlocksuiteEditorCommands(
|
||||
category: `editor:${mode}`,
|
||||
icon: mode === 'page' ? <PageIcon /> : <EdgelessIcon />,
|
||||
label: t['Export to PDF'](),
|
||||
run() {
|
||||
exportHandler('pdf');
|
||||
async run() {
|
||||
await exportHandler('pdf');
|
||||
},
|
||||
})
|
||||
);
|
||||
@@ -130,8 +130,8 @@ export function useRegisterBlocksuiteEditorCommands(
|
||||
category: `editor:${mode}`,
|
||||
icon: mode === 'page' ? <PageIcon /> : <EdgelessIcon />,
|
||||
label: t['Export to HTML'](),
|
||||
run() {
|
||||
exportHandler('html');
|
||||
async run() {
|
||||
await exportHandler('html');
|
||||
},
|
||||
})
|
||||
);
|
||||
@@ -143,8 +143,8 @@ export function useRegisterBlocksuiteEditorCommands(
|
||||
category: `editor:${mode}`,
|
||||
icon: mode === 'page' ? <PageIcon /> : <EdgelessIcon />,
|
||||
label: t['Export to PNG'](),
|
||||
run() {
|
||||
exportHandler('png');
|
||||
async run() {
|
||||
await exportHandler('png');
|
||||
},
|
||||
})
|
||||
);
|
||||
@@ -156,8 +156,8 @@ export function useRegisterBlocksuiteEditorCommands(
|
||||
category: `editor:${mode}`,
|
||||
icon: mode === 'page' ? <PageIcon /> : <EdgelessIcon />,
|
||||
label: t['Export to Markdown'](),
|
||||
run() {
|
||||
exportHandler('markdown');
|
||||
async run() {
|
||||
await exportHandler('markdown');
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { SyncEngine, SyncEngineStatus } from '@affine/workspace/providers';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useCurrentWorkspace } from './use-current-workspace';
|
||||
|
||||
export function useCurrentSyncEngine(): SyncEngine | undefined {
|
||||
const [workspace] = useCurrentWorkspace();
|
||||
// FIXME: This is a hack to get the sync engine, we need refactor this in the future.
|
||||
const syncEngine = (
|
||||
workspace.blockSuiteWorkspace.providers[0] as { engine?: SyncEngine }
|
||||
)?.engine;
|
||||
|
||||
return syncEngine;
|
||||
}
|
||||
|
||||
export function useCurrentSyncEngineStatus(): SyncEngineStatus | undefined {
|
||||
const syncEngine = useCurrentSyncEngine();
|
||||
const [status, setStatus] = useState<SyncEngineStatus>();
|
||||
|
||||
useEffect(() => {
|
||||
if (syncEngine) {
|
||||
setStatus(syncEngine.status);
|
||||
return syncEngine.onStatusChange.on(status => {
|
||||
setStatus(status);
|
||||
}).dispose;
|
||||
} else {
|
||||
setStatus(undefined);
|
||||
}
|
||||
return;
|
||||
}, [syncEngine]);
|
||||
|
||||
return status;
|
||||
}
|
||||
@@ -7,10 +7,10 @@ import {
|
||||
rootWorkspacesMetadataAtom,
|
||||
workspaceAdaptersAtom,
|
||||
} from '@affine/workspace/atom';
|
||||
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||
import { currentPageIdAtom } from '@toeverything/infra/atom';
|
||||
import { WorkspaceVersion } from '@toeverything/infra/blocksuite';
|
||||
import { useAtomValue, useSetAtom } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { openSettingModalAtom } from '../../atoms';
|
||||
import { useNavigateHelper } from '../use-navigate-helper';
|
||||
@@ -24,7 +24,7 @@ export function useOnTransformWorkspace() {
|
||||
const currentPageId = useAtomValue(currentPageIdAtom);
|
||||
const pushNotification = useSetAtom(pushNotificationAtom);
|
||||
|
||||
return useCallback(
|
||||
return useAsyncCallback(
|
||||
async <From extends WorkspaceFlavour, To extends WorkspaceFlavour>(
|
||||
from: From,
|
||||
to: To,
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
import { pushNotificationAtom } from '@affine/component/notification-center';
|
||||
import type {
|
||||
AffineSocketIOProvider,
|
||||
LocalIndexedDBBackgroundProvider,
|
||||
SQLiteProvider,
|
||||
} from '@affine/env/workspace';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import type { Workspace } from '@blocksuite/store';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { startTransition, useCallback, useMemo, useState } from 'react';
|
||||
import { type Status, syncDataSource } from 'y-provider';
|
||||
|
||||
export function useDatasourceSync(workspace: Workspace) {
|
||||
const [status, setStatus] = useState<Status>({
|
||||
type: 'idle',
|
||||
});
|
||||
const pushNotification = useSetAtom(pushNotificationAtom);
|
||||
const providers = workspace.providers;
|
||||
const remoteProvider: AffineSocketIOProvider | undefined = useMemo(() => {
|
||||
return providers.find(
|
||||
(provider): provider is AffineSocketIOProvider =>
|
||||
provider.flavour === 'affine-socket-io'
|
||||
);
|
||||
}, [providers]);
|
||||
const localProvider = useMemo(() => {
|
||||
const sqliteProvider = providers.find(
|
||||
(provider): provider is SQLiteProvider => provider.flavour === 'sqlite'
|
||||
);
|
||||
const indexedDbProvider = providers.find(
|
||||
(provider): provider is LocalIndexedDBBackgroundProvider =>
|
||||
provider.flavour === 'local-indexeddb-background'
|
||||
);
|
||||
const provider = sqliteProvider || indexedDbProvider;
|
||||
assertExists(provider, 'no local provider');
|
||||
return provider;
|
||||
}, [providers]);
|
||||
return [
|
||||
status,
|
||||
useCallback(() => {
|
||||
if (!remoteProvider) {
|
||||
return;
|
||||
}
|
||||
startTransition(() => {
|
||||
setStatus({
|
||||
type: 'syncing',
|
||||
});
|
||||
});
|
||||
syncDataSource(
|
||||
() => [
|
||||
workspace.doc.guid,
|
||||
...[...workspace.doc.subdocs].map(doc => doc.guid),
|
||||
],
|
||||
remoteProvider.datasource,
|
||||
localProvider.datasource
|
||||
)
|
||||
.then(async () => {
|
||||
// by default, the syncing status will show for 2.4s
|
||||
setTimeout(() => {
|
||||
startTransition(() => {
|
||||
setStatus({
|
||||
type: 'synced',
|
||||
});
|
||||
pushNotification({
|
||||
title: 'Synced successfully',
|
||||
type: 'success',
|
||||
});
|
||||
});
|
||||
}, 2400);
|
||||
})
|
||||
.catch(error => {
|
||||
startTransition(() => {
|
||||
setStatus({
|
||||
type: 'error',
|
||||
error,
|
||||
});
|
||||
pushNotification({
|
||||
title: 'Unable to Sync',
|
||||
message: 'Server error, please try again later.',
|
||||
type: 'error',
|
||||
});
|
||||
});
|
||||
});
|
||||
}, [
|
||||
remoteProvider,
|
||||
localProvider.datasource,
|
||||
workspace.doc.guid,
|
||||
workspace.doc.subdocs,
|
||||
pushNotification,
|
||||
]),
|
||||
] as const;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { type SubscriptionQuery, subscriptionQuery } from '@affine/graphql';
|
||||
import { useQuery } from '@affine/workspace/affine/gql';
|
||||
import { useCallback } from 'react';
|
||||
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||
|
||||
export type Subscription = NonNullable<
|
||||
NonNullable<SubscriptionQuery['currentUser']>['subscription']
|
||||
@@ -16,9 +16,9 @@ export const useUserSubscription = () => {
|
||||
query: subscriptionQuery,
|
||||
});
|
||||
|
||||
const set: SubscriptionMutator = useCallback(
|
||||
(update?: Partial<Subscription>) => {
|
||||
mutate(prev => {
|
||||
const set: SubscriptionMutator = useAsyncCallback(
|
||||
async (update?: Partial<Subscription>) => {
|
||||
await mutate(prev => {
|
||||
if (!update || !prev?.currentUser?.subscription) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2,10 +2,7 @@ import { DebugLogger } from '@affine/debug';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
||||
import { saveWorkspaceToLocalStorage } from '@affine/workspace/local/crud';
|
||||
import {
|
||||
getOrCreateWorkspace,
|
||||
globalBlockSuiteSchema,
|
||||
} from '@affine/workspace/manager';
|
||||
import { getOrCreateWorkspace } from '@affine/workspace/manager';
|
||||
import { getWorkspace } from '@toeverything/infra/__internal__/workspace';
|
||||
import { getCurrentStore } from '@toeverything/infra/atom';
|
||||
import {
|
||||
@@ -76,7 +73,6 @@ export function useAppHelper() {
|
||||
WorkspaceFlavour.LOCAL
|
||||
);
|
||||
await buildShowcaseWorkspace(blockSuiteWorkspace, {
|
||||
schema: globalBlockSuiteSchema,
|
||||
store: getCurrentStore(),
|
||||
atoms: {
|
||||
pageMode: setPageModeAtom,
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import './polyfill/ses-lockdown';
|
||||
import './polyfill/intl-segmenter';
|
||||
|
||||
import { WorkspaceFallback } from '@affine/component/workspace';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import { getCurrentStore } from '@toeverything/infra/atom';
|
||||
@@ -36,4 +39,6 @@ async function main() {
|
||||
);
|
||||
}
|
||||
|
||||
await main();
|
||||
main().catch(err => {
|
||||
console.error('Failed to bootstrap app', err);
|
||||
});
|
||||
|
||||
@@ -14,8 +14,8 @@ import {
|
||||
} from '@affine/component/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
||||
import { getBlobEngine } from '@affine/workspace/manager';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import type { Page } from '@blocksuite/store';
|
||||
import type { DragEndEvent } from '@dnd-kit/core';
|
||||
import {
|
||||
DndContext,
|
||||
@@ -27,8 +27,8 @@ import {
|
||||
useSensors,
|
||||
} from '@dnd-kit/core';
|
||||
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
|
||||
import { loadPage } from '@toeverything/hooks/use-block-suite-workspace-page';
|
||||
import { currentWorkspaceIdAtom } from '@toeverything/infra/atom';
|
||||
import type { MigrationPoint } from '@toeverything/infra/blocksuite';
|
||||
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
|
||||
import type { PropsWithChildren, ReactNode } from 'react';
|
||||
import { lazy, Suspense, useCallback, useEffect, useState } from 'react';
|
||||
@@ -114,36 +114,47 @@ export const CurrentWorkspaceContext = ({
|
||||
};
|
||||
|
||||
type WorkspaceLayoutProps = {
|
||||
incompatible?: boolean;
|
||||
migration?: MigrationPoint;
|
||||
};
|
||||
|
||||
// fix https://github.com/toeverything/AFFiNE/issues/4825
|
||||
function useLoadWorkspacePages() {
|
||||
const useSyncWorkspaceBlob = () => {
|
||||
// temporary solution for sync blob
|
||||
|
||||
const [currentWorkspace] = useCurrentWorkspace();
|
||||
const pageMetas = useBlockSuitePageMeta(currentWorkspace.blockSuiteWorkspace);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentWorkspace) {
|
||||
const timer = setTimeout(() => {
|
||||
const pageIds = pageMetas.map(meta => meta.id);
|
||||
const pages = pageIds
|
||||
.map(id => currentWorkspace.blockSuiteWorkspace.getPage(id))
|
||||
.filter((p): p is Page => !!p);
|
||||
pages.forEach(page => {
|
||||
loadPage(page, -10);
|
||||
const blobEngine = getBlobEngine(currentWorkspace.blockSuiteWorkspace);
|
||||
let stopped = false;
|
||||
function sync() {
|
||||
if (stopped) {
|
||||
return;
|
||||
}
|
||||
|
||||
blobEngine
|
||||
?.sync()
|
||||
.catch(error => {
|
||||
console.error('sync blob error', error);
|
||||
})
|
||||
.finally(() => {
|
||||
// sync every 1 minute
|
||||
setTimeout(sync, 60000);
|
||||
});
|
||||
}, 10 * 1000); // load pages after 10s
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}
|
||||
return;
|
||||
}, [currentWorkspace, pageMetas]);
|
||||
}
|
||||
|
||||
// after currentWorkspace changed, wait 1 second to start sync
|
||||
setTimeout(sync, 1000);
|
||||
|
||||
return () => {
|
||||
stopped = true;
|
||||
};
|
||||
}, [currentWorkspace]);
|
||||
};
|
||||
|
||||
export const WorkspaceLayout = function WorkspacesSuspense({
|
||||
children,
|
||||
incompatible = false,
|
||||
migration,
|
||||
}: PropsWithChildren<WorkspaceLayoutProps>) {
|
||||
useSyncWorkspaceBlob();
|
||||
return (
|
||||
<AdapterProviderWrapper>
|
||||
<CurrentWorkspaceContext>
|
||||
@@ -153,7 +164,7 @@ export const WorkspaceLayout = function WorkspacesSuspense({
|
||||
<CurrentWorkspaceModals />
|
||||
</Suspense>
|
||||
<Suspense fallback={<WorkspaceFallback />}>
|
||||
<WorkspaceLayoutInner incompatible={incompatible}>
|
||||
<WorkspaceLayoutInner migration={migration}>
|
||||
{children}
|
||||
</WorkspaceLayoutInner>
|
||||
</Suspense>
|
||||
@@ -164,7 +175,7 @@ export const WorkspaceLayout = function WorkspacesSuspense({
|
||||
|
||||
export const WorkspaceLayoutInner = ({
|
||||
children,
|
||||
incompatible = false,
|
||||
migration,
|
||||
}: PropsWithChildren<WorkspaceLayoutProps>) => {
|
||||
const [currentWorkspace] = useCurrentWorkspace();
|
||||
const { openPage } = useNavigateHelper();
|
||||
@@ -254,8 +265,6 @@ export const WorkspaceLayoutInner = ({
|
||||
const inTrashPage = pageMeta?.trash ?? false;
|
||||
const setMainContainer = useSetAtom(mainContainerAtom);
|
||||
|
||||
useLoadWorkspacePages();
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* This DndContext is used for drag page from all-pages list into a folder in sidebar */}
|
||||
@@ -289,7 +298,11 @@ export const WorkspaceLayoutInner = ({
|
||||
padding={appSettings.clientBorder}
|
||||
inTrashPage={inTrashPage}
|
||||
>
|
||||
{incompatible ? <WorkspaceUpgrade /> : children}
|
||||
{migration ? (
|
||||
<WorkspaceUpgrade migration={migration} />
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
<ToolContainer inTrashPage={inTrashPage}>
|
||||
<RootBlockHub />
|
||||
<HelpIsland showList={pageId ? undefined : showList} />
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NotFoundPage } from '@affine/component/not-found-page';
|
||||
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
import { useSession } from 'next-auth/react';
|
||||
import type { ReactElement } from 'react';
|
||||
@@ -22,7 +23,7 @@ export const Component = (): ReactElement => {
|
||||
setOpen(true);
|
||||
}, [setOpen]);
|
||||
|
||||
const onConfirmSignOut = useCallback(async () => {
|
||||
const onConfirmSignOut = useAsyncCallback(async () => {
|
||||
setOpen(false);
|
||||
await signOutCloud({
|
||||
callbackUrl: '/signIn',
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { MainContainer } from '@affine/component/workspace';
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import type { CloudDoc } from '@affine/workspace/affine/download';
|
||||
import { downloadBinaryFromCloud } from '@affine/workspace/affine/download';
|
||||
import { getOrCreateWorkspace } from '@affine/workspace/manager';
|
||||
import { downloadBinaryFromCloud } from '@affine/workspace/providers';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import type { Page } from '@blocksuite/store';
|
||||
import { noop } from 'foxact/noop';
|
||||
@@ -18,12 +19,25 @@ import {
|
||||
import { applyUpdate } from 'yjs';
|
||||
|
||||
import { PageDetailEditor } from '../../adapters/shared';
|
||||
import type { PageMode } from '../../atoms';
|
||||
import { AppContainer } from '../../components/affine/app-container';
|
||||
import { ShareHeader } from '../../components/share-header';
|
||||
import { SharePageNotFoundError } from '../../components/share-page-not-found-error';
|
||||
|
||||
function assertArrayBuffer(value: unknown): asserts value is ArrayBuffer {
|
||||
if (!(value instanceof ArrayBuffer)) {
|
||||
throw new Error('value is not ArrayBuffer');
|
||||
type LoaderData = {
|
||||
page: Page;
|
||||
publishMode: PageMode;
|
||||
};
|
||||
|
||||
function assertDownloadResponse(
|
||||
value: CloudDoc | null
|
||||
): asserts value is CloudDoc {
|
||||
if (
|
||||
!value ||
|
||||
!((value as CloudDoc).arrayBuffer instanceof ArrayBuffer) ||
|
||||
typeof (value as CloudDoc).publishMode !== 'string'
|
||||
) {
|
||||
throw new Error('value is not a valid download response');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,33 +55,42 @@ export const loader: LoaderFunction = async ({ params }) => {
|
||||
);
|
||||
// download root workspace
|
||||
{
|
||||
const buffer = await downloadBinaryFromCloud(workspaceId, workspaceId);
|
||||
assertArrayBuffer(buffer);
|
||||
applyUpdate(workspace.doc, new Uint8Array(buffer));
|
||||
const response = await downloadBinaryFromCloud(workspaceId, workspaceId);
|
||||
assertDownloadResponse(response);
|
||||
const { arrayBuffer } = response;
|
||||
applyUpdate(workspace.doc, new Uint8Array(arrayBuffer));
|
||||
}
|
||||
const page = workspace.getPage(pageId);
|
||||
assertExists(page, 'cannot find page');
|
||||
// download page
|
||||
{
|
||||
const buffer = await downloadBinaryFromCloud(
|
||||
workspaceId,
|
||||
page.spaceDoc.guid
|
||||
);
|
||||
assertArrayBuffer(buffer);
|
||||
applyUpdate(page.spaceDoc, new Uint8Array(buffer));
|
||||
}
|
||||
|
||||
const response = await downloadBinaryFromCloud(
|
||||
workspaceId,
|
||||
page.spaceDoc.guid
|
||||
);
|
||||
assertDownloadResponse(response);
|
||||
const { arrayBuffer, publishMode } = response;
|
||||
|
||||
applyUpdate(page.spaceDoc, new Uint8Array(arrayBuffer));
|
||||
|
||||
logger.info('workspace', workspace);
|
||||
workspace.awarenessStore.setReadonly(page, true);
|
||||
return page;
|
||||
return { page, publishMode };
|
||||
};
|
||||
|
||||
export const Component = (): ReactElement => {
|
||||
const page = useLoaderData() as Page;
|
||||
const { page, publishMode } = useLoaderData() as LoaderData;
|
||||
return (
|
||||
<AppContainer>
|
||||
<MainContainer>
|
||||
<ShareHeader
|
||||
workspace={page.workspace}
|
||||
pageId={page.id}
|
||||
publishMode={publishMode}
|
||||
/>
|
||||
<PageDetailEditor
|
||||
isPublic
|
||||
publishMode={publishMode}
|
||||
workspace={page.workspace}
|
||||
pageId={page.id}
|
||||
onLoad={useCallback(() => noop, [])}
|
||||
|
||||
@@ -65,4 +65,5 @@ export const newPageButtonLabel = style({
|
||||
|
||||
export const headerCreateNewButtonHidden = style({
|
||||
opacity: 0,
|
||||
pointerEvents: 'none',
|
||||
});
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
PageIcon,
|
||||
ViewLayersIcon,
|
||||
} from '@blocksuite/icons';
|
||||
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||
import { getCurrentStore } from '@toeverything/infra/atom';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useSetAtom } from 'jotai';
|
||||
@@ -92,11 +93,13 @@ export const Component = function CollectionPage() {
|
||||
const Placeholder = ({ collection }: { collection: Collection }) => {
|
||||
const { updateCollection } = useCollectionManager(collectionsCRUDAtom);
|
||||
const { node, open } = useEditCollection(useAllPageListConfig());
|
||||
const openPageEdit = useCallback(() => {
|
||||
open({ ...collection }, 'page').then(updateCollection);
|
||||
const openPageEdit = useAsyncCallback(async () => {
|
||||
const ret = await open({ ...collection }, 'page');
|
||||
await updateCollection(ret);
|
||||
}, [open, collection, updateCollection]);
|
||||
const openRuleEdit = useCallback(() => {
|
||||
open({ ...collection }, 'rule').then(updateCollection);
|
||||
const openRuleEdit = useAsyncCallback(async () => {
|
||||
const ret = await open({ ...collection }, 'rule');
|
||||
await updateCollection(ret);
|
||||
}, [collection, open, updateCollection]);
|
||||
const [showTips, setShowTips] = useState(false);
|
||||
useEffect(() => {
|
||||
|
||||
@@ -5,20 +5,18 @@ import {
|
||||
} from '@affine/component/page-list';
|
||||
import { WorkspaceSubPath } from '@affine/env/workspace';
|
||||
import { globalBlockSuiteSchema } from '@affine/workspace/manager';
|
||||
import { SyncEngineStep } from '@affine/workspace/providers';
|
||||
import type { EditorContainer } from '@blocksuite/editor';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import type { Page } from '@blocksuite/store';
|
||||
import {
|
||||
contentLayoutAtom,
|
||||
currentPageIdAtom,
|
||||
currentWorkspaceAtom,
|
||||
currentWorkspaceIdAtom,
|
||||
getCurrentStore,
|
||||
} from '@toeverything/infra/atom';
|
||||
import { useAtomValue, useSetAtom } from 'jotai';
|
||||
import { type ReactElement, useCallback } from 'react';
|
||||
import type { LoaderFunction } from 'react-router-dom';
|
||||
import { redirect } from 'react-router-dom';
|
||||
import { type ReactElement, useCallback, useEffect, useState } from 'react';
|
||||
import { type LoaderFunction, useParams } from 'react-router-dom';
|
||||
import type { Map as YMap } from 'yjs';
|
||||
|
||||
import { getUIAdapter } from '../../adapters/workspace';
|
||||
@@ -28,6 +26,7 @@ import { currentModeAtom } from '../../atoms/mode';
|
||||
import { AffineErrorBoundary } from '../../components/affine/affine-error-boundary';
|
||||
import { WorkspaceHeader } from '../../components/workspace-header';
|
||||
import { useRegisterBlocksuiteEditorCommands } from '../../hooks/affine/use-register-blocksuite-editor-commands';
|
||||
import { useCurrentSyncEngineStatus } from '../../hooks/current/use-current-sync-engine';
|
||||
import { useCurrentWorkspace } from '../../hooks/current/use-current-workspace';
|
||||
import { useNavigateHelper } from '../../hooks/use-navigate-helper';
|
||||
import { performanceRenderLogger } from '../../shared';
|
||||
@@ -108,47 +107,114 @@ const DetailPageImpl = (): ReactElement => {
|
||||
|
||||
export const DetailPage = (): ReactElement => {
|
||||
const [currentWorkspace] = useCurrentWorkspace();
|
||||
const currentSyncEngineStatus = useCurrentSyncEngineStatus();
|
||||
const currentPageId = useAtomValue(currentPageIdAtom);
|
||||
const page = currentPageId
|
||||
? currentWorkspace.blockSuiteWorkspace.getPage(currentPageId)
|
||||
: null;
|
||||
const [page, setPage] = useState<Page | null>(null);
|
||||
const [pageLoaded, setPageLoaded] = useState<boolean>(false);
|
||||
|
||||
if (!currentPageId || !page) {
|
||||
// load page by current page id
|
||||
useEffect(() => {
|
||||
if (!currentPageId) {
|
||||
setPage(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const exists = currentWorkspace.blockSuiteWorkspace.getPage(currentPageId);
|
||||
|
||||
if (exists) {
|
||||
setPage(exists);
|
||||
return;
|
||||
}
|
||||
|
||||
const dispose = currentWorkspace.blockSuiteWorkspace.slots.pagesUpdated.on(
|
||||
() => {
|
||||
const exists =
|
||||
currentWorkspace.blockSuiteWorkspace.getPage(currentPageId);
|
||||
|
||||
if (exists) {
|
||||
setPage(exists);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return dispose.dispose;
|
||||
}, [currentPageId, currentWorkspace]);
|
||||
|
||||
const navigate = useNavigateHelper();
|
||||
|
||||
// if sync engine has been synced and the page is null, wait 1s and jump to 404 page.
|
||||
useEffect(() => {
|
||||
if (currentSyncEngineStatus?.step === SyncEngineStep.Synced && !page) {
|
||||
const timeout = setTimeout(() => {
|
||||
navigate.jumpTo404();
|
||||
}, 1000);
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}
|
||||
return;
|
||||
}, [currentSyncEngineStatus, navigate, page]);
|
||||
|
||||
// wait for page to be loaded
|
||||
useEffect(() => {
|
||||
if (page) {
|
||||
if (!page.isEmpty) {
|
||||
setPageLoaded(true);
|
||||
} else {
|
||||
setPageLoaded(false);
|
||||
// call waitForLoaded to trigger load
|
||||
page
|
||||
.load(() => {})
|
||||
.catch(() => {
|
||||
// do nothing
|
||||
});
|
||||
return page.slots.ready.on(() => {
|
||||
setPageLoaded(true);
|
||||
}).dispose;
|
||||
}
|
||||
} else {
|
||||
setPageLoaded(false);
|
||||
}
|
||||
return;
|
||||
}, [page]);
|
||||
|
||||
if (!currentPageId || !page || !pageLoaded) {
|
||||
return <PageDetailSkeleton key="current-page-is-null" />;
|
||||
}
|
||||
|
||||
if (page.meta.jumpOnce) {
|
||||
currentWorkspace.blockSuiteWorkspace.setPageMeta(page.id, {
|
||||
jumpOnce: false,
|
||||
});
|
||||
}
|
||||
|
||||
return <DetailPageImpl />;
|
||||
};
|
||||
|
||||
export const loader: LoaderFunction = async args => {
|
||||
const rootStore = getCurrentStore();
|
||||
rootStore.set(contentLayoutAtom, 'editor');
|
||||
if (args.params.workspaceId) {
|
||||
localStorage.setItem('last_workspace_id', args.params.workspaceId);
|
||||
rootStore.set(currentWorkspaceIdAtom, args.params.workspaceId);
|
||||
}
|
||||
const currentWorkspace = await rootStore.get(currentWorkspaceAtom);
|
||||
if (args.params.pageId) {
|
||||
const pageId = args.params.pageId;
|
||||
localStorage.setItem('last_page_id', pageId);
|
||||
const page = currentWorkspace.getPage(pageId);
|
||||
if (!page) {
|
||||
return redirect('/404');
|
||||
}
|
||||
if (page.meta.jumpOnce) {
|
||||
currentWorkspace.setPageMeta(page.id, {
|
||||
jumpOnce: false,
|
||||
});
|
||||
}
|
||||
rootStore.set(currentPageIdAtom, pageId);
|
||||
} else {
|
||||
return redirect('/404');
|
||||
}
|
||||
export const loader: LoaderFunction = async () => {
|
||||
return null;
|
||||
};
|
||||
|
||||
export const Component = () => {
|
||||
performanceRenderLogger.info('DetailPage');
|
||||
|
||||
const setContentLayout = useSetAtom(contentLayoutAtom);
|
||||
const setCurrentWorkspaceId = useSetAtom(currentWorkspaceIdAtom);
|
||||
const setCurrentPageId = useSetAtom(currentPageIdAtom);
|
||||
const params = useParams();
|
||||
|
||||
useEffect(() => {
|
||||
setContentLayout('editor');
|
||||
if (params.workspaceId) {
|
||||
localStorage.setItem('last_workspace_id', params.workspaceId);
|
||||
setCurrentWorkspaceId(params.workspaceId);
|
||||
}
|
||||
if (params.pageId) {
|
||||
localStorage.setItem('last_page_id', params.pageId);
|
||||
setCurrentPageId(params.pageId);
|
||||
}
|
||||
}, [params, setContentLayout, setCurrentPageId, setCurrentWorkspaceId]);
|
||||
|
||||
return (
|
||||
<AffineErrorBoundary>
|
||||
<DetailPage />
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
||||
import { getBlockSuiteWorkspaceAtom } from '@toeverything/infra/__internal__/workspace';
|
||||
import {
|
||||
@@ -6,15 +5,22 @@ import {
|
||||
currentWorkspaceIdAtom,
|
||||
getCurrentStore,
|
||||
} from '@toeverything/infra/atom';
|
||||
import { guidCompatibilityFix } from '@toeverything/infra/blocksuite';
|
||||
import type { ReactElement } from 'react';
|
||||
import type { MigrationPoint } from '@toeverything/infra/blocksuite';
|
||||
import {
|
||||
checkWorkspaceCompatibility,
|
||||
guidCompatibilityFix,
|
||||
} from '@toeverything/infra/blocksuite';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { type ReactElement, useEffect } from 'react';
|
||||
import {
|
||||
type LoaderFunction,
|
||||
Outlet,
|
||||
redirect,
|
||||
useLoaderData,
|
||||
useParams,
|
||||
} from 'react-router-dom';
|
||||
|
||||
import { AffineErrorBoundary } from '../../components/affine/affine-error-boundary';
|
||||
import { WorkspaceLayout } from '../../layouts/workspace-layout';
|
||||
import { performanceLogger, performanceRenderLogger } from '../../shared';
|
||||
|
||||
@@ -24,6 +30,12 @@ export const loader: LoaderFunction = async args => {
|
||||
workspaceLoaderLogger.info('start');
|
||||
|
||||
const rootStore = getCurrentStore();
|
||||
|
||||
if (args.params.workspaceId) {
|
||||
localStorage.setItem('last_workspace_id', args.params.workspaceId);
|
||||
rootStore.set(currentWorkspaceIdAtom, args.params.workspaceId);
|
||||
}
|
||||
|
||||
const meta = await rootStore.get(rootWorkspacesMetadataAtom);
|
||||
workspaceLoaderLogger.info('meta loaded');
|
||||
|
||||
@@ -31,44 +43,40 @@ export const loader: LoaderFunction = async args => {
|
||||
if (!currentMetadata) {
|
||||
return redirect('/404');
|
||||
}
|
||||
if (args.params.workspaceId) {
|
||||
localStorage.setItem('last_workspace_id', args.params.workspaceId);
|
||||
rootStore.set(currentWorkspaceIdAtom, args.params.workspaceId);
|
||||
}
|
||||
|
||||
if (!args.params.pageId) {
|
||||
rootStore.set(currentPageIdAtom, null);
|
||||
}
|
||||
if (currentMetadata.flavour === WorkspaceFlavour.AFFINE_CLOUD) {
|
||||
const [workspaceAtom] = getBlockSuiteWorkspaceAtom(currentMetadata.id);
|
||||
workspaceLoaderLogger.info('get cloud workspace atom');
|
||||
const [workspaceAtom] = getBlockSuiteWorkspaceAtom(currentMetadata.id);
|
||||
workspaceLoaderLogger.info('get cloud workspace atom');
|
||||
|
||||
const workspace = await rootStore.get(workspaceAtom);
|
||||
return (() => {
|
||||
guidCompatibilityFix(workspace.doc);
|
||||
const blockVersions = workspace.meta.blockVersions;
|
||||
if (!blockVersions) {
|
||||
return true;
|
||||
}
|
||||
for (const [flavour, schema] of workspace.schema.flavourSchemaMap) {
|
||||
if (blockVersions[flavour] !== schema.version) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
})();
|
||||
}
|
||||
const workspace = await rootStore.get(workspaceAtom);
|
||||
workspaceLoaderLogger.info('workspace loaded');
|
||||
|
||||
workspaceLoaderLogger.info('done');
|
||||
return null;
|
||||
guidCompatibilityFix(workspace.doc);
|
||||
return checkWorkspaceCompatibility(workspace);
|
||||
};
|
||||
|
||||
export const Component = (): ReactElement => {
|
||||
performanceRenderLogger.info('WorkspaceLayout');
|
||||
|
||||
const incompatible = useLoaderData();
|
||||
const setCurrentWorkspaceId = useSetAtom(currentWorkspaceIdAtom);
|
||||
|
||||
const params = useParams();
|
||||
|
||||
useEffect(() => {
|
||||
if (params.workspaceId) {
|
||||
localStorage.setItem('last_workspace_id', params.workspaceId);
|
||||
setCurrentWorkspaceId(params.workspaceId);
|
||||
}
|
||||
}, [params, setCurrentWorkspaceId]);
|
||||
|
||||
const migration = useLoaderData() as MigrationPoint | undefined;
|
||||
return (
|
||||
<WorkspaceLayout incompatible={!!incompatible}>
|
||||
<Outlet />
|
||||
</WorkspaceLayout>
|
||||
<AffineErrorBoundary height="100vh">
|
||||
<WorkspaceLayout migration={migration}>
|
||||
<Outlet />
|
||||
</WorkspaceLayout>
|
||||
</AffineErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { WorkspaceSubPath } from '@affine/env/workspace';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||
import { useAtom } from 'jotai';
|
||||
import type { ReactElement } from 'react';
|
||||
import { lazy, Suspense, useCallback } from 'react';
|
||||
@@ -154,13 +155,10 @@ export const SignOutConfirmModal = () => {
|
||||
const { jumpToIndex } = useNavigateHelper();
|
||||
const [open, setOpen] = useAtom(openSignOutModalAtom);
|
||||
|
||||
const onConfirm = useCallback(async () => {
|
||||
const onConfirm = useAsyncCallback(async () => {
|
||||
setOpen(false);
|
||||
signOutCloud()
|
||||
.then(() => {
|
||||
jumpToIndex();
|
||||
})
|
||||
.catch(console.error);
|
||||
await signOutCloud();
|
||||
jumpToIndex();
|
||||
}, [jumpToIndex, setOpen]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -3,6 +3,7 @@ import '@toeverything/hooks/use-affine-ipc-renderer';
|
||||
import { pushNotificationAtom } from '@affine/component/notification-center';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { refreshRootMetadataAtom } from '@affine/workspace/atom';
|
||||
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||
import { useAtom, useSetAtom } from 'jotai';
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
import { SessionProvider, useSession } from 'next-auth/react';
|
||||
@@ -24,6 +25,12 @@ const SessionDefence = (props: PropsWithChildren) => {
|
||||
const refreshMetadata = useSetAtom(refreshRootMetadataAtom);
|
||||
const onceSignedInEvents = useOnceSignedInEvents();
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const refreshAfterSignedInEvents = useAsyncCallback(async () => {
|
||||
await onceSignedInEvents();
|
||||
refreshMetadata();
|
||||
}, [onceSignedInEvents, refreshMetadata]);
|
||||
|
||||
useEffect(() => {
|
||||
if (sessionInAtom !== session && session.status === 'authenticated') {
|
||||
setSession(session);
|
||||
@@ -35,11 +42,7 @@ const SessionDefence = (props: PropsWithChildren) => {
|
||||
prevSession.current?.status === 'unauthenticated' &&
|
||||
session.status === 'authenticated'
|
||||
) {
|
||||
startTransition(() => {
|
||||
onceSignedInEvents().then(() => {
|
||||
refreshMetadata();
|
||||
});
|
||||
});
|
||||
startTransition(() => refreshAfterSignedInEvents());
|
||||
pushNotification({
|
||||
title: t['com.affine.auth.has.signed'](),
|
||||
message: t['com.affine.auth.has.signed.message'](),
|
||||
@@ -57,9 +60,8 @@ const SessionDefence = (props: PropsWithChildren) => {
|
||||
sessionInAtom,
|
||||
prevSession,
|
||||
setSession,
|
||||
onceSignedInEvents,
|
||||
pushNotification,
|
||||
refreshMetadata,
|
||||
refreshAfterSignedInEvents,
|
||||
t,
|
||||
]);
|
||||
return props.children;
|
||||
|
||||
Reference in New Issue
Block a user