mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-17 22:37:04 +08:00
refactor: workspace manager (#5060)
This commit is contained in:
10
packages/common/env/src/global.ts
vendored
10
packages/common/env/src/global.ts
vendored
@@ -1,5 +1,6 @@
|
|||||||
/// <reference types="@blocksuite/global" />
|
/// <reference types="@blocksuite/global" />
|
||||||
import { assertEquals } from '@blocksuite/global/utils';
|
import { assertEquals } from '@blocksuite/global/utils';
|
||||||
|
import type { Workspace } from '@blocksuite/store';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { isDesktop, isServer } from './constant.js';
|
import { isDesktop, isServer } from './constant.js';
|
||||||
@@ -149,3 +150,12 @@ export function setupGlobal() {
|
|||||||
|
|
||||||
globalThis.$AFFINE_SETUP = true;
|
globalThis.$AFFINE_SETUP = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setupEditorFlags(workspace: Workspace) {
|
||||||
|
Object.entries(runtimeConfig.editorFlags).forEach(([key, value]) => {
|
||||||
|
workspace.awarenessStore.setFlag(
|
||||||
|
key as keyof BlockSuiteFeatureFlags,
|
||||||
|
value
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
134
packages/common/env/src/workspace.ts
vendored
134
packages/common/env/src/workspace.ts
vendored
@@ -1,10 +1,4 @@
|
|||||||
import type {
|
|
||||||
ActiveDocProvider,
|
|
||||||
PassiveDocProvider,
|
|
||||||
Workspace as BlockSuiteWorkspace,
|
|
||||||
} from '@blocksuite/store';
|
|
||||||
import type { PropsWithChildren, ReactNode } from 'react';
|
import type { PropsWithChildren, ReactNode } from 'react';
|
||||||
import type { DataSourceAdapter } from 'y-provider';
|
|
||||||
|
|
||||||
export enum WorkspaceSubPath {
|
export enum WorkspaceSubPath {
|
||||||
ALL = 'all',
|
ALL = 'all',
|
||||||
@@ -14,73 +8,6 @@ export enum WorkspaceSubPath {
|
|||||||
SHARED = 'shared',
|
SHARED = 'shared',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AffineDownloadProvider extends PassiveDocProvider {
|
|
||||||
flavour: 'affine-download';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Download the first binary from local IndexedDB
|
|
||||||
*/
|
|
||||||
export interface BroadCastChannelProvider extends PassiveDocProvider {
|
|
||||||
flavour: 'broadcast-channel';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Long polling provider with local IndexedDB
|
|
||||||
*/
|
|
||||||
export interface LocalIndexedDBBackgroundProvider
|
|
||||||
extends DataSourceAdapter,
|
|
||||||
PassiveDocProvider {
|
|
||||||
flavour: 'local-indexeddb-background';
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LocalIndexedDBDownloadProvider extends ActiveDocProvider {
|
|
||||||
flavour: 'local-indexeddb';
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SQLiteProvider extends PassiveDocProvider, DataSourceAdapter {
|
|
||||||
flavour: 'sqlite';
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SQLiteDBDownloadProvider extends ActiveDocProvider {
|
|
||||||
flavour: 'sqlite-download';
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AffineSocketIOProvider
|
|
||||||
extends PassiveDocProvider,
|
|
||||||
DataSourceAdapter {
|
|
||||||
flavour: 'affine-socket-io';
|
|
||||||
}
|
|
||||||
|
|
||||||
type BaseWorkspace = {
|
|
||||||
flavour: string;
|
|
||||||
id: string;
|
|
||||||
blockSuiteWorkspace: BlockSuiteWorkspace;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface AffineCloudWorkspace extends BaseWorkspace {
|
|
||||||
flavour: WorkspaceFlavour.AFFINE_CLOUD;
|
|
||||||
id: string;
|
|
||||||
blockSuiteWorkspace: BlockSuiteWorkspace;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LocalWorkspace extends BaseWorkspace {
|
|
||||||
flavour: WorkspaceFlavour.LOCAL;
|
|
||||||
id: string;
|
|
||||||
blockSuiteWorkspace: BlockSuiteWorkspace;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AffinePublicWorkspace extends BaseWorkspace {
|
|
||||||
flavour: WorkspaceFlavour.AFFINE_PUBLIC;
|
|
||||||
id: string;
|
|
||||||
blockSuiteWorkspace: BlockSuiteWorkspace;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type AffineOfficialWorkspace =
|
|
||||||
| AffineCloudWorkspace
|
|
||||||
| LocalWorkspace
|
|
||||||
| AffinePublicWorkspace;
|
|
||||||
|
|
||||||
export enum ReleaseType {
|
export enum ReleaseType {
|
||||||
// if workspace is not released yet, we will not show it in the workspace list
|
// if workspace is not released yet, we will not show it in the workspace list
|
||||||
UNRELEASED = 'unreleased',
|
UNRELEASED = 'unreleased',
|
||||||
@@ -99,7 +26,6 @@ export enum WorkspaceFlavour {
|
|||||||
*/
|
*/
|
||||||
AFFINE_CLOUD = 'affine-cloud',
|
AFFINE_CLOUD = 'affine-cloud',
|
||||||
LOCAL = 'local',
|
LOCAL = 'local',
|
||||||
AFFINE_PUBLIC = 'affine-public',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const settingPanel = {
|
export const settingPanel = {
|
||||||
@@ -112,68 +38,30 @@ export const settingPanel = {
|
|||||||
export const settingPanelValues = Object.values(settingPanel);
|
export const settingPanelValues = Object.values(settingPanel);
|
||||||
export type SettingPanel = (typeof settingPanel)[keyof typeof settingPanel];
|
export type SettingPanel = (typeof settingPanel)[keyof typeof settingPanel];
|
||||||
|
|
||||||
// built-in workspaces
|
export type WorkspaceHeaderProps = {
|
||||||
export interface WorkspaceRegistry {
|
rightSlot?: ReactNode;
|
||||||
[WorkspaceFlavour.LOCAL]: LocalWorkspace;
|
currentEntry:
|
||||||
[WorkspaceFlavour.AFFINE_PUBLIC]: AffinePublicWorkspace;
|
| {
|
||||||
[WorkspaceFlavour.AFFINE_CLOUD]: AffineCloudWorkspace;
|
subPath: WorkspaceSubPath;
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
export interface WorkspaceCRUD<Flavour extends keyof WorkspaceRegistry> {
|
pageId: string;
|
||||||
create: (blockSuiteWorkspace: BlockSuiteWorkspace) => Promise<string>;
|
};
|
||||||
delete: (blockSuiteWorkspace: BlockSuiteWorkspace) => Promise<void>;
|
|
||||||
get: (workspaceId: string) => Promise<WorkspaceRegistry[Flavour] | null>;
|
|
||||||
// not supported yet
|
|
||||||
// update: (workspace: FlavourToWorkspace[Flavour]) => Promise<void>;
|
|
||||||
list: () => Promise<WorkspaceRegistry[Flavour][]>;
|
|
||||||
}
|
|
||||||
|
|
||||||
type UIBaseProps<_Flavour extends keyof WorkspaceRegistry> = {
|
|
||||||
currentWorkspaceId: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type NewSettingProps<Flavour extends keyof WorkspaceRegistry> =
|
|
||||||
UIBaseProps<Flavour> & {
|
|
||||||
onDeleteLocalWorkspace: () => void;
|
|
||||||
onDeleteCloudWorkspace: () => void;
|
|
||||||
onLeaveWorkspace: () => void;
|
|
||||||
onTransformWorkspace: <
|
|
||||||
From extends keyof WorkspaceRegistry,
|
|
||||||
To extends keyof WorkspaceRegistry,
|
|
||||||
>(
|
|
||||||
from: From,
|
|
||||||
to: To,
|
|
||||||
workspace: WorkspaceRegistry[From]
|
|
||||||
) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface FC<P> {
|
interface FC<P> {
|
||||||
(props: P): ReactNode;
|
(props: P): ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkspaceUISchema<Flavour extends keyof WorkspaceRegistry> {
|
export interface WorkspaceUISchema {
|
||||||
NewSettingsDetail: FC<NewSettingProps<Flavour>>;
|
|
||||||
Provider: FC<PropsWithChildren>;
|
Provider: FC<PropsWithChildren>;
|
||||||
LoginCard?: FC<object>;
|
LoginCard?: FC<object>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppEvents {
|
|
||||||
// event there is no workspace
|
|
||||||
// usually used to initialize workspace adapter
|
|
||||||
'app:init': () => string[];
|
|
||||||
// event if you have access to workspace adapter
|
|
||||||
'app:access': () => Promise<boolean>;
|
|
||||||
'service:start': () => void;
|
|
||||||
'service:stop': () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WorkspaceAdapter<Flavour extends WorkspaceFlavour> {
|
export interface WorkspaceAdapter<Flavour extends WorkspaceFlavour> {
|
||||||
releaseType: ReleaseType;
|
releaseType: ReleaseType;
|
||||||
flavour: Flavour;
|
flavour: Flavour;
|
||||||
// The Adapter will be loaded according to the priority
|
// The Adapter will be loaded according to the priority
|
||||||
loadPriority: LoadPriority;
|
loadPriority: LoadPriority;
|
||||||
Events: Partial<AppEvents>;
|
UI: WorkspaceUISchema;
|
||||||
// Fetch necessary data for the first render
|
|
||||||
CRUD: WorkspaceCRUD<Flavour>;
|
|
||||||
UI: WorkspaceUISchema<Flavour>;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,79 +0,0 @@
|
|||||||
import type { ActiveDocProvider, Workspace } from '@blocksuite/store';
|
|
||||||
import type { PassiveDocProvider } from '@blocksuite/store';
|
|
||||||
import type { Atom } from 'jotai/vanilla';
|
|
||||||
import { atom } from 'jotai/vanilla';
|
|
||||||
import { atomEffect } from 'jotai-effect';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Map: guid -> Workspace
|
|
||||||
*/
|
|
||||||
export const INTERNAL_BLOCKSUITE_HASH_MAP = new Map<string, Workspace>([]);
|
|
||||||
|
|
||||||
const workspaceActiveAtomWeakMap = new WeakMap<
|
|
||||||
Workspace,
|
|
||||||
Atom<Promise<Workspace>>
|
|
||||||
>();
|
|
||||||
|
|
||||||
const workspaceActiveWeakMap = new WeakMap<Workspace, boolean>();
|
|
||||||
const workspaceEffectAtomWeakMap = new WeakMap<Workspace, Atom<void>>();
|
|
||||||
|
|
||||||
export async function waitForWorkspace(workspace: Workspace) {
|
|
||||||
if (workspaceActiveWeakMap.get(workspace) !== true) {
|
|
||||||
const providers = workspace.providers.filter(
|
|
||||||
(provider): provider is ActiveDocProvider =>
|
|
||||||
'active' in provider && provider.active === true
|
|
||||||
);
|
|
||||||
for (const provider of providers) {
|
|
||||||
provider.sync();
|
|
||||||
// we will wait for the necessary providers to be ready
|
|
||||||
await provider.whenReady;
|
|
||||||
}
|
|
||||||
// timeout is INFINITE
|
|
||||||
workspaceActiveWeakMap.set(workspace, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getWorkspace(id: string) {
|
|
||||||
if (!INTERNAL_BLOCKSUITE_HASH_MAP.has(id)) {
|
|
||||||
throw new Error('Workspace not found');
|
|
||||||
}
|
|
||||||
return INTERNAL_BLOCKSUITE_HASH_MAP.get(id) as Workspace;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getBlockSuiteWorkspaceAtom(
|
|
||||||
id: string
|
|
||||||
): [workspaceAtom: Atom<Promise<Workspace>>, workspaceEffectAtom: Atom<void>] {
|
|
||||||
if (!INTERNAL_BLOCKSUITE_HASH_MAP.has(id)) {
|
|
||||||
throw new Error('Workspace not found');
|
|
||||||
}
|
|
||||||
const workspace = INTERNAL_BLOCKSUITE_HASH_MAP.get(id) as Workspace;
|
|
||||||
if (!workspaceActiveAtomWeakMap.has(workspace)) {
|
|
||||||
const baseAtom = atom(async () => {
|
|
||||||
await waitForWorkspace(workspace);
|
|
||||||
return workspace;
|
|
||||||
});
|
|
||||||
workspaceActiveAtomWeakMap.set(workspace, baseAtom);
|
|
||||||
}
|
|
||||||
if (!workspaceEffectAtomWeakMap.has(workspace)) {
|
|
||||||
const effectAtom = atomEffect(() => {
|
|
||||||
const providers = workspace.providers.filter(
|
|
||||||
(provider): provider is PassiveDocProvider =>
|
|
||||||
'passive' in provider && provider.passive === true
|
|
||||||
);
|
|
||||||
providers.forEach(provider => {
|
|
||||||
provider.connect();
|
|
||||||
});
|
|
||||||
return () => {
|
|
||||||
providers.forEach(provider => {
|
|
||||||
provider.disconnect();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
});
|
|
||||||
workspaceEffectAtomWeakMap.set(workspace, effectAtom);
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
workspaceActiveAtomWeakMap.get(workspace) as Atom<Promise<Workspace>>,
|
|
||||||
workspaceEffectAtomWeakMap.get(workspace) as Atom<void>,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
/**
|
|
||||||
* @vitest-environment happy-dom
|
|
||||||
*/
|
|
||||||
import { Schema, Workspace } from '@blocksuite/store';
|
|
||||||
import { waitFor } from '@testing-library/react';
|
|
||||||
import { getDefaultStore } from 'jotai/vanilla';
|
|
||||||
import { expect, test, vi } from 'vitest';
|
|
||||||
|
|
||||||
import {
|
|
||||||
getBlockSuiteWorkspaceAtom,
|
|
||||||
INTERNAL_BLOCKSUITE_HASH_MAP,
|
|
||||||
} from '../__internal__/workspace.js';
|
|
||||||
|
|
||||||
test('blocksuite atom', async () => {
|
|
||||||
const sync = vi.fn();
|
|
||||||
let connected = false;
|
|
||||||
const connect = vi.fn(() => (connected = true));
|
|
||||||
const workspace = new Workspace({
|
|
||||||
schema: new Schema(),
|
|
||||||
id: '1',
|
|
||||||
providerCreators: [
|
|
||||||
() => ({
|
|
||||||
flavour: 'fake',
|
|
||||||
active: true,
|
|
||||||
sync,
|
|
||||||
get whenReady(): Promise<void> {
|
|
||||||
return Promise.resolve();
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
() => ({
|
|
||||||
flavour: 'fake-2',
|
|
||||||
passive: true,
|
|
||||||
get connected() {
|
|
||||||
return connected;
|
|
||||||
},
|
|
||||||
connect,
|
|
||||||
disconnect: vi.fn(),
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
INTERNAL_BLOCKSUITE_HASH_MAP.set('1', workspace);
|
|
||||||
|
|
||||||
{
|
|
||||||
const [atom, effectAtom] = getBlockSuiteWorkspaceAtom('1');
|
|
||||||
const store = getDefaultStore();
|
|
||||||
const result = await store.get(atom);
|
|
||||||
expect(result).toBe(workspace);
|
|
||||||
expect(sync).toBeCalledTimes(1);
|
|
||||||
expect(connect).not.toHaveBeenCalled();
|
|
||||||
|
|
||||||
store.sub(effectAtom, vi.fn());
|
|
||||||
await waitFor(() => expect(connect).toBeCalledTimes(1));
|
|
||||||
expect(connected).toBe(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -1,14 +1,3 @@
|
|||||||
import { assertExists } from '@blocksuite/global/utils';
|
|
||||||
import type { Workspace } from '@blocksuite/store';
|
|
||||||
import { atom } from 'jotai';
|
import { atom } from 'jotai';
|
||||||
|
|
||||||
import { getBlockSuiteWorkspaceAtom } from '../__internal__/workspace';
|
|
||||||
|
|
||||||
export const currentWorkspaceIdAtom = atom<string | null>(null);
|
|
||||||
export const currentPageIdAtom = atom<string | null>(null);
|
export const currentPageIdAtom = atom<string | null>(null);
|
||||||
export const currentWorkspaceAtom = atom<Promise<Workspace>>(async get => {
|
|
||||||
const workspaceId = get(currentWorkspaceIdAtom);
|
|
||||||
assertExists(workspaceId);
|
|
||||||
const [currentWorkspaceAtom] = getBlockSuiteWorkspaceAtom(workspaceId);
|
|
||||||
return get(currentWorkspaceAtom);
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
export * from './initialization';
|
export * from './initialization';
|
||||||
export * from './migration/blob';
|
export {
|
||||||
export { migratePages as forceUpgradePages } from './migration/blocksuite'; // campatible with electron
|
migratePages as forceUpgradePages,
|
||||||
|
migrateGuidCompatibility,
|
||||||
|
} from './migration/blocksuite'; // campatible with electron
|
||||||
export * from './migration/fixing';
|
export * from './migration/fixing';
|
||||||
export { migrateToSubdoc } from './migration/subdoc';
|
export { migrateToSubdoc, upgradeV1ToV2 } from './migration/subdoc';
|
||||||
export * from './migration/workspace';
|
export * from './migration/workspace';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,12 +2,9 @@ import { assertExists } from '@blocksuite/global/utils';
|
|||||||
import type { Page, PageMeta, Workspace } from '@blocksuite/store';
|
import type { Page, PageMeta, Workspace } from '@blocksuite/store';
|
||||||
import type { createStore, WritableAtom } from 'jotai/vanilla';
|
import type { createStore, WritableAtom } from 'jotai/vanilla';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
|
import { Map as YMap } from 'yjs';
|
||||||
|
|
||||||
import { migratePages } from '../migration/blocksuite';
|
import { getLatestVersions } from '../migration/blocksuite';
|
||||||
import {
|
|
||||||
checkWorkspaceCompatibility,
|
|
||||||
MigrationPoint,
|
|
||||||
} from '../migration/workspace';
|
|
||||||
|
|
||||||
export async function initEmptyPage(page: Page, title?: string) {
|
export async function initEmptyPage(page: Page, title?: string) {
|
||||||
await page.load(() => {
|
await page.load(() => {
|
||||||
@@ -261,10 +258,11 @@ export async function buildShowcaseWorkspace(
|
|||||||
|
|
||||||
// The showcase building will create multiple pages once, and may skip the version writing.
|
// The showcase building will create multiple pages once, and may skip the version writing.
|
||||||
// https://github.com/toeverything/blocksuite/blob/master/packages/store/src/workspace/page.ts#L662
|
// https://github.com/toeverything/blocksuite/blob/master/packages/store/src/workspace/page.ts#L662
|
||||||
const compatibilityResult = checkWorkspaceCompatibility(workspace);
|
workspace.doc.getMap('meta').set('pageVersion', 2);
|
||||||
if (compatibilityResult === MigrationPoint.BlockVersion) {
|
const newVersions = getLatestVersions(workspace.schema);
|
||||||
await migratePages(workspace.doc, workspace.schema);
|
workspace.doc
|
||||||
}
|
.getMap('meta')
|
||||||
|
.set('blockVersions', new YMap(Object.entries(newVersions)));
|
||||||
|
|
||||||
Object.entries(pageMetas).forEach(([oldId, meta]) => {
|
Object.entries(pageMetas).forEach(([oldId, meta]) => {
|
||||||
const newId = idMap[oldId];
|
const newId = idMap[oldId];
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
import { createIndexeddbStorage } from '@blocksuite/store';
|
|
||||||
|
|
||||||
export async function migrateLocalBlobStorage(from: string, to: string) {
|
|
||||||
const fromStorage = createIndexeddbStorage(from);
|
|
||||||
const toStorage = createIndexeddbStorage(to);
|
|
||||||
const keys = await fromStorage.crud.list();
|
|
||||||
for (const key of keys) {
|
|
||||||
const value = await fromStorage.crud.get(key);
|
|
||||||
if (!value) {
|
|
||||||
console.warn('cannot find blob:', key);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
await toStorage.crud.set(key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,14 @@
|
|||||||
import type { Schema } from '@blocksuite/store';
|
import type { Schema } from '@blocksuite/store';
|
||||||
import type { Doc as YDoc } from 'yjs';
|
import type { Array as YArray } from 'yjs';
|
||||||
import { Map as YMap } from 'yjs';
|
import {
|
||||||
|
applyUpdate,
|
||||||
|
Doc as YDoc,
|
||||||
|
encodeStateAsUpdate,
|
||||||
|
Map as YMap,
|
||||||
|
transact,
|
||||||
|
} from 'yjs';
|
||||||
|
|
||||||
const getLatestVersions = (schema: Schema): Record<string, number> => {
|
export const getLatestVersions = (schema: Schema): Record<string, number> => {
|
||||||
return [...schema.flavourSchemaMap.entries()].reduce(
|
return [...schema.flavourSchemaMap.entries()].reduce(
|
||||||
(record, [flavour, schema]) => {
|
(record, [flavour, schema]) => {
|
||||||
record[flavour] = schema.version;
|
record[flavour] = schema.version;
|
||||||
@@ -28,14 +34,62 @@ export async function migratePages(
|
|||||||
|
|
||||||
// Hard code to upgrade page version to 2.
|
// Hard code to upgrade page version to 2.
|
||||||
// Let e2e to ensure the data version is correct.
|
// Let e2e to ensure the data version is correct.
|
||||||
const pageVersion = meta.get('pageVersion');
|
return transact(
|
||||||
if (typeof pageVersion !== 'number' || pageVersion < 2) {
|
rootDoc,
|
||||||
meta.set('pageVersion', 2);
|
() => {
|
||||||
}
|
const pageVersion = meta.get('pageVersion');
|
||||||
|
if (typeof pageVersion !== 'number' || pageVersion < 2) {
|
||||||
|
meta.set('pageVersion', 2);
|
||||||
|
}
|
||||||
|
|
||||||
const newVersions = getLatestVersions(schema);
|
const newVersions = getLatestVersions(schema);
|
||||||
meta.set('blockVersions', new YMap(Object.entries(newVersions)));
|
meta.set('blockVersions', new YMap(Object.entries(newVersions)));
|
||||||
return Object.entries(oldVersions).some(
|
return Object.entries(oldVersions).some(
|
||||||
([flavour, version]) => newVersions[flavour] !== version
|
([flavour, version]) => newVersions[flavour] !== version
|
||||||
|
);
|
||||||
|
},
|
||||||
|
'migratePages',
|
||||||
|
/**
|
||||||
|
* transact as remote update, because blocksuite will skip local changes.
|
||||||
|
* https://github.com/toeverything/blocksuite/blob/9c2df3f7aa5617c050e0dccdd73e99bb67e0c0f7/packages/store/src/reactive/utils.ts#L143
|
||||||
|
*/
|
||||||
|
false
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// patch root doc's space guid compatibility issue
|
||||||
|
//
|
||||||
|
// in version 0.10, page id in spaces no longer has prefix "space:"
|
||||||
|
// The data flow for fetching a doc's updates is:
|
||||||
|
// - page id in `meta.pages` -> find `${page-id}` in `doc.spaces` -> `doc` -> `doc.guid`
|
||||||
|
// if `doc` is not found in `doc.spaces`, a new doc will be created and its `doc.guid` is the same with its pageId
|
||||||
|
// - because of guid logic change, the doc that previously prefixed with "space:" will not be found in `doc.spaces`
|
||||||
|
// - when fetching the rows of this doc using the doc id === page id,
|
||||||
|
// it will return empty since there is no updates associated with the page id
|
||||||
|
export function migrateGuidCompatibility(rootDoc: YDoc) {
|
||||||
|
const meta = rootDoc.getMap('meta') as YMap<unknown>;
|
||||||
|
const pages = meta.get('pages') as YArray<YMap<unknown>>;
|
||||||
|
pages?.forEach(page => {
|
||||||
|
const pageId = page.get('id') as string | undefined;
|
||||||
|
if (pageId?.includes(':')) {
|
||||||
|
// remove the prefix "space:" from page id
|
||||||
|
page.set('id', pageId.split(':').at(-1));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const spaces = rootDoc.getMap('spaces') as YMap<YDoc>;
|
||||||
|
spaces?.forEach((doc: YDoc, pageId: string) => {
|
||||||
|
if (pageId.includes(':')) {
|
||||||
|
const newPageId = pageId.split(':').at(-1) ?? pageId;
|
||||||
|
const newDoc = new YDoc();
|
||||||
|
// clone the original doc. yjs is not happy to use the same doc instance
|
||||||
|
applyUpdate(newDoc, encodeStateAsUpdate(doc));
|
||||||
|
newDoc.guid = doc.guid;
|
||||||
|
spaces.set(newPageId, newDoc);
|
||||||
|
// should remove the old doc, otherwise we will do it again in the next run
|
||||||
|
spaces.delete(pageId);
|
||||||
|
console.debug(
|
||||||
|
`fixed space id ${pageId} -> ${newPageId}, doc id: ${doc.guid}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,48 +1,5 @@
|
|||||||
import type { Array as YArray, Map as YMap } from 'yjs';
|
import type { Doc as YDoc, Map as YMap } from 'yjs';
|
||||||
import { Doc as YDoc, transact } from 'yjs';
|
import { transact } from 'yjs';
|
||||||
import { applyUpdate, encodeStateAsUpdate } from 'yjs';
|
|
||||||
|
|
||||||
// patch root doc's space guid compatibility issue
|
|
||||||
//
|
|
||||||
// in version 0.10, page id in spaces no longer has prefix "space:"
|
|
||||||
// The data flow for fetching a doc's updates is:
|
|
||||||
// - page id in `meta.pages` -> find `${page-id}` in `doc.spaces` -> `doc` -> `doc.guid`
|
|
||||||
// if `doc` is not found in `doc.spaces`, a new doc will be created and its `doc.guid` is the same with its pageId
|
|
||||||
// - because of guid logic change, the doc that previously prefixed with "space:" will not be found in `doc.spaces`
|
|
||||||
// - when fetching the rows of this doc using the doc id === page id,
|
|
||||||
// it will return empty since there is no updates associated with the page id
|
|
||||||
export function guidCompatibilityFix(rootDoc: YDoc) {
|
|
||||||
let changed = false;
|
|
||||||
transact(rootDoc, () => {
|
|
||||||
const meta = rootDoc.getMap('meta') as YMap<unknown>;
|
|
||||||
const pages = meta.get('pages') as YArray<YMap<unknown>>;
|
|
||||||
pages?.forEach(page => {
|
|
||||||
const pageId = page.get('id') as string | undefined;
|
|
||||||
if (pageId?.includes(':')) {
|
|
||||||
// remove the prefix "space:" from page id
|
|
||||||
page.set('id', pageId.split(':').at(-1));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const spaces = rootDoc.getMap('spaces') as YMap<YDoc>;
|
|
||||||
spaces?.forEach((doc: YDoc, pageId: string) => {
|
|
||||||
if (pageId.includes(':')) {
|
|
||||||
const newPageId = pageId.split(':').at(-1) ?? pageId;
|
|
||||||
const newDoc = new YDoc();
|
|
||||||
// clone the original doc. yjs is not happy to use the same doc instance
|
|
||||||
applyUpdate(newDoc, encodeStateAsUpdate(doc));
|
|
||||||
newDoc.guid = doc.guid;
|
|
||||||
spaces.set(newPageId, newDoc);
|
|
||||||
// should remove the old doc, otherwise we will do it again in the next run
|
|
||||||
spaces.delete(pageId);
|
|
||||||
changed = true;
|
|
||||||
console.debug(
|
|
||||||
`fixed space id ${pageId} -> ${newPageId}, doc id: ${doc.guid}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return changed;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hard code to fix workspace version to be compatible with legacy data.
|
* Hard code to fix workspace version to be compatible with legacy data.
|
||||||
@@ -55,13 +12,35 @@ export function fixWorkspaceVersion(rootDoc: YDoc) {
|
|||||||
* It doesn't matter to upgrade workspace version from 1 or undefined to 2.
|
* It doesn't matter to upgrade workspace version from 1 or undefined to 2.
|
||||||
* Blocksuite just set the value, do nothing else.
|
* Blocksuite just set the value, do nothing else.
|
||||||
*/
|
*/
|
||||||
const workspaceVersion = meta.get('workspaceVersion');
|
function doFix() {
|
||||||
if (typeof workspaceVersion !== 'number' || workspaceVersion < 2) {
|
const workspaceVersion = meta.get('workspaceVersion');
|
||||||
meta.set('workspaceVersion', 2);
|
if (typeof workspaceVersion !== 'number' || workspaceVersion < 2) {
|
||||||
|
transact(
|
||||||
|
rootDoc,
|
||||||
|
() => {
|
||||||
|
meta.set('workspaceVersion', 2);
|
||||||
|
},
|
||||||
|
'fixWorkspaceVersion',
|
||||||
|
// transact as remote update, because blocksuite will skip local changes.
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
const pageVersion = meta.get('pageVersion');
|
const pageVersion = meta.get('pageVersion');
|
||||||
if (typeof pageVersion !== 'number') {
|
if (typeof pageVersion !== 'number' || pageVersion < 2) {
|
||||||
meta.set('pageVersion', 1);
|
transact(
|
||||||
|
rootDoc,
|
||||||
|
() => {
|
||||||
|
meta.set('pageVersion', 2);
|
||||||
|
},
|
||||||
|
'fixPageVersion',
|
||||||
|
// transact as remote update, because blocksuite will skip local changes.
|
||||||
|
false
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
doFix();
|
||||||
|
|
||||||
|
// do fix every time when meta changed
|
||||||
|
meta.observe(() => doFix());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import type { Workspace } from '@blocksuite/store';
|
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import { Array as YArray, Doc as YDoc, Map as YMap } from 'yjs';
|
import { Array as YArray, Doc as YDoc, Map as YMap } from 'yjs';
|
||||||
import { applyUpdate, encodeStateAsUpdate } from 'yjs';
|
import { applyUpdate, encodeStateAsUpdate } from 'yjs';
|
||||||
@@ -264,19 +263,17 @@ export function migrateToSubdoc(oldDoc: YDoc): YDoc {
|
|||||||
return newDoc;
|
return newDoc;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const upgradeV1ToV2 = async (
|
/**
|
||||||
oldDoc: YDoc,
|
* upgrade oldDoc to v2, write to targetDoc
|
||||||
createWorkspace: () => Promise<Workspace>
|
*/
|
||||||
) => {
|
export const upgradeV1ToV2 = async (oldDoc: YDoc, targetDoc: YDoc) => {
|
||||||
const newDoc = migrateToSubdoc(oldDoc);
|
const newDoc = migrateToSubdoc(oldDoc);
|
||||||
const newWorkspace = await createWorkspace();
|
applyUpdate(targetDoc, encodeStateAsUpdate(newDoc), migrationOrigin);
|
||||||
applyUpdate(newWorkspace.doc, encodeStateAsUpdate(newDoc), migrationOrigin);
|
|
||||||
newDoc.getSubdocs().forEach(subdoc => {
|
newDoc.getSubdocs().forEach(subdoc => {
|
||||||
newWorkspace.doc.getSubdocs().forEach(newDoc => {
|
targetDoc.getSubdocs().forEach(newDoc => {
|
||||||
if (subdoc.guid === newDoc.guid) {
|
if (subdoc.guid === newDoc.guid) {
|
||||||
applyUpdate(newDoc, encodeStateAsUpdate(subdoc), migrationOrigin);
|
applyUpdate(newDoc, encodeStateAsUpdate(subdoc), migrationOrigin);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return newWorkspace;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,63 +1,50 @@
|
|||||||
import type { Workspace } from '@blocksuite/store';
|
import type { Workspace } from '@blocksuite/store';
|
||||||
import type { Schema } from '@blocksuite/store';
|
import type { Array as YArray, Doc as YDoc, Map as YMap } from 'yjs';
|
||||||
import type { Doc as YDoc } from 'yjs';
|
|
||||||
|
|
||||||
import { migratePages } from './blocksuite';
|
|
||||||
import { upgradeV1ToV2 } from './subdoc';
|
|
||||||
|
|
||||||
interface MigrationOptions {
|
|
||||||
doc: YDoc;
|
|
||||||
schema: Schema;
|
|
||||||
createWorkspace: () => Promise<Workspace>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createMigrationQueue(options: MigrationOptions) {
|
|
||||||
return [
|
|
||||||
async (doc: YDoc) => {
|
|
||||||
const newWorkspace = await upgradeV1ToV2(doc, options.createWorkspace);
|
|
||||||
return newWorkspace.doc;
|
|
||||||
},
|
|
||||||
async (doc: YDoc) => {
|
|
||||||
await migratePages(doc, options.schema);
|
|
||||||
return doc;
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For split migrate function from MigrationQueue.
|
* For split migrate function from MigrationQueue.
|
||||||
*/
|
*/
|
||||||
export enum MigrationPoint {
|
export enum MigrationPoint {
|
||||||
SubDoc = 1,
|
SubDoc = 1,
|
||||||
BlockVersion = 2,
|
GuidFix = 2,
|
||||||
}
|
BlockVersion = 3,
|
||||||
|
|
||||||
export async function migrateWorkspace(
|
|
||||||
point: MigrationPoint,
|
|
||||||
options: MigrationOptions
|
|
||||||
) {
|
|
||||||
const migrationQueue = createMigrationQueue(options);
|
|
||||||
const migrationFns = migrationQueue.slice(point - 1);
|
|
||||||
|
|
||||||
let doc = options.doc;
|
|
||||||
for (const migrate of migrationFns) {
|
|
||||||
doc = await migrate(doc);
|
|
||||||
}
|
|
||||||
return doc;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function checkWorkspaceCompatibility(
|
export function checkWorkspaceCompatibility(
|
||||||
workspace: Workspace
|
workspace: Workspace
|
||||||
): MigrationPoint | null {
|
): MigrationPoint | null {
|
||||||
const workspaceDocJSON = workspace.doc.toJSON();
|
// check if there is any key starts with 'space:' on root doc
|
||||||
const spaceMetaObj = workspaceDocJSON['space:meta'];
|
const spaceMetaObj = workspace.doc.share.get('space:meta') as
|
||||||
const docKeys = Object.keys(workspaceDocJSON);
|
| YMap<any>
|
||||||
const haveSpaceMeta = !!spaceMetaObj && Object.keys(spaceMetaObj).length > 0;
|
| undefined;
|
||||||
|
const docKeys = Array.from(workspace.doc.share.keys());
|
||||||
|
const haveSpaceMeta = !!spaceMetaObj && spaceMetaObj.size > 0;
|
||||||
const haveLegacySpace = docKeys.some(key => key.startsWith('space:'));
|
const haveLegacySpace = docKeys.some(key => key.startsWith('space:'));
|
||||||
if (haveSpaceMeta || haveLegacySpace) {
|
if (haveSpaceMeta || haveLegacySpace) {
|
||||||
return MigrationPoint.SubDoc;
|
return MigrationPoint.SubDoc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// exit if no pages
|
||||||
|
if (!workspace.meta.pages?.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check guid compatibility
|
||||||
|
const meta = workspace.doc.getMap('meta') as YMap<unknown>;
|
||||||
|
const pages = meta.get('pages') as YArray<YMap<unknown>>;
|
||||||
|
for (const page of pages) {
|
||||||
|
const pageId = page.get('id') as string | undefined;
|
||||||
|
if (pageId?.includes(':')) {
|
||||||
|
return MigrationPoint.GuidFix;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const spaces = workspace.doc.getMap('spaces') as YMap<YDoc>;
|
||||||
|
for (const [pageId, _] of spaces) {
|
||||||
|
if (pageId.includes(':')) {
|
||||||
|
return MigrationPoint.GuidFix;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const hasVersion = workspace.meta.hasVersion;
|
const hasVersion = workspace.meta.hasVersion;
|
||||||
if (!hasVersion) {
|
if (!hasVersion) {
|
||||||
return MigrationPoint.BlockVersion;
|
return MigrationPoint.BlockVersion;
|
||||||
|
|||||||
@@ -18,10 +18,6 @@ export default defineConfig({
|
|||||||
type: resolve(root, 'src/type.ts'),
|
type: resolve(root, 'src/type.ts'),
|
||||||
'core/event-emitter': resolve(root, 'src/core/event-emitter.ts'),
|
'core/event-emitter': resolve(root, 'src/core/event-emitter.ts'),
|
||||||
'preload/electron': resolve(root, 'src/preload/electron.ts'),
|
'preload/electron': resolve(root, 'src/preload/electron.ts'),
|
||||||
'__internal__/workspace': resolve(
|
|
||||||
root,
|
|
||||||
'src/__internal__/workspace.ts'
|
|
||||||
),
|
|
||||||
'__internal__/plugin': resolve(root, 'src/__internal__/plugin.ts'),
|
'__internal__/plugin': resolve(root, 'src/__internal__/plugin.ts'),
|
||||||
},
|
},
|
||||||
formats: ['es', 'cjs'],
|
formats: ['es', 'cjs'],
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
|
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
|
import type { WorkspaceMetadata } from '@affine/workspace';
|
||||||
import { CollaborationIcon, SettingsIcon } from '@blocksuite/icons';
|
import { CollaborationIcon, SettingsIcon } from '@blocksuite/icons';
|
||||||
import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-block-suite-workspace-avatar-url';
|
import { useWorkspaceBlobObjectUrl } from '@toeverything/hooks/use-workspace-blob';
|
||||||
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
|
import { useWorkspaceInfo } from '@toeverything/hooks/use-workspace-info';
|
||||||
import { getBlockSuiteWorkspaceAtom } from '@toeverything/infra/__internal__/workspace';
|
|
||||||
import { useAtomValue } from 'jotai/react';
|
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import { Avatar } from '../../../ui/avatar';
|
import { Avatar } from '../../../ui/avatar';
|
||||||
@@ -68,10 +67,10 @@ const WorkspaceType = ({ flavour, isOwner }: WorkspaceTypeProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export interface WorkspaceCardProps {
|
export interface WorkspaceCardProps {
|
||||||
currentWorkspaceId: string | null;
|
currentWorkspaceId?: string | null;
|
||||||
meta: RootWorkspaceMetadata;
|
meta: WorkspaceMetadata;
|
||||||
onClick: (workspaceId: string) => void;
|
onClick: (metadata: WorkspaceMetadata) => void;
|
||||||
onSettingClick: (workspaceId: string) => void;
|
onSettingClick: (metadata: WorkspaceMetadata) => void;
|
||||||
isOwner?: boolean;
|
isOwner?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,29 +97,31 @@ export const WorkspaceCard = ({
|
|||||||
meta,
|
meta,
|
||||||
isOwner = true,
|
isOwner = true,
|
||||||
}: WorkspaceCardProps) => {
|
}: WorkspaceCardProps) => {
|
||||||
const [workspaceAtom] = getBlockSuiteWorkspaceAtom(meta.id);
|
const information = useWorkspaceInfo(meta);
|
||||||
const workspace = useAtomValue(workspaceAtom);
|
const avatarUrl = useWorkspaceBlobObjectUrl(meta, information?.avatar);
|
||||||
const [name] = useBlockSuiteWorkspaceName(workspace);
|
|
||||||
const [workspaceAvatar] = useBlockSuiteWorkspaceAvatarUrl(workspace);
|
const name = information?.name ?? UNTITLED_WORKSPACE_NAME;
|
||||||
return (
|
return (
|
||||||
<StyledCard
|
<StyledCard
|
||||||
data-testid="workspace-card"
|
data-testid="workspace-card"
|
||||||
onClick={useCallback(() => {
|
onClick={useCallback(() => {
|
||||||
onClick(meta.id);
|
onClick(meta);
|
||||||
}, [onClick, meta.id])}
|
}, [onClick, meta])}
|
||||||
active={workspace.id === currentWorkspaceId}
|
active={meta.id === currentWorkspaceId}
|
||||||
>
|
>
|
||||||
<Avatar size={28} url={workspaceAvatar} name={name} colorfulFallback />
|
<Avatar size={28} url={avatarUrl} name={name} colorfulFallback />
|
||||||
<StyledWorkspaceInfo>
|
<StyledWorkspaceInfo>
|
||||||
<StyledWorkspaceTitleArea style={{ display: 'flex' }}>
|
<StyledWorkspaceTitleArea style={{ display: 'flex' }}>
|
||||||
<StyledWorkspaceTitle>{name}</StyledWorkspaceTitle>
|
<StyledWorkspaceTitle>
|
||||||
|
{information?.name ?? UNTITLED_WORKSPACE_NAME}
|
||||||
|
</StyledWorkspaceTitle>
|
||||||
|
|
||||||
<StyledSettingLink
|
<StyledSettingLink
|
||||||
size="small"
|
size="small"
|
||||||
className="setting-entry"
|
className="setting-entry"
|
||||||
onClick={e => {
|
onClick={e => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onSettingClick(meta.id);
|
onSettingClick(meta);
|
||||||
}}
|
}}
|
||||||
withoutHoverStyle={true}
|
withoutHoverStyle={true}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { expect, test } from 'vitest';
|
|||||||
|
|
||||||
import { createDefaultFilter, vars } from '../filter/vars';
|
import { createDefaultFilter, vars } from '../filter/vars';
|
||||||
import {
|
import {
|
||||||
type CollectionsCRUDAtom,
|
type CollectionsCRUD,
|
||||||
useCollectionManager,
|
useCollectionManager,
|
||||||
} from '../use-collection-manager';
|
} from '../use-collection-manager';
|
||||||
|
|
||||||
@@ -27,18 +27,18 @@ const baseAtom = atomWithObservable<Collection[]>(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const mockAtom: CollectionsCRUDAtom = atom(get => {
|
const mockAtom = atom(get => {
|
||||||
return {
|
return {
|
||||||
collections: get(baseAtom),
|
collections: get(baseAtom),
|
||||||
addCollection: async (...collections) => {
|
addCollection: (...collections) => {
|
||||||
const prev = collectionsSubject.value;
|
const prev = collectionsSubject.value;
|
||||||
collectionsSubject.next([...collections, ...prev]);
|
collectionsSubject.next([...collections, ...prev]);
|
||||||
},
|
},
|
||||||
deleteCollection: async (...ids) => {
|
deleteCollection: (...ids) => {
|
||||||
const prev = collectionsSubject.value;
|
const prev = collectionsSubject.value;
|
||||||
collectionsSubject.next(prev.filter(v => !ids.includes(v.id)));
|
collectionsSubject.next(prev.filter(v => !ids.includes(v.id)));
|
||||||
},
|
},
|
||||||
updateCollection: async (id, updater) => {
|
updateCollection: (id, updater) => {
|
||||||
const prev = collectionsSubject.value;
|
const prev = collectionsSubject.value;
|
||||||
collectionsSubject.next(
|
collectionsSubject.next(
|
||||||
prev.map(v => {
|
prev.map(v => {
|
||||||
@@ -49,14 +49,14 @@ const mockAtom: CollectionsCRUDAtom = atom(get => {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
};
|
} satisfies CollectionsCRUD;
|
||||||
});
|
});
|
||||||
|
|
||||||
test('useAllPageSetting', async () => {
|
test('useAllPageSetting', async () => {
|
||||||
const settingHook = renderHook(() => useCollectionManager(mockAtom));
|
const settingHook = renderHook(() => useCollectionManager(mockAtom));
|
||||||
const prevCollection = settingHook.result.current.currentCollection;
|
const prevCollection = settingHook.result.current.currentCollection;
|
||||||
expect(settingHook.result.current.savedCollections).toEqual([]);
|
expect(settingHook.result.current.savedCollections).toEqual([]);
|
||||||
await settingHook.result.current.updateCollection({
|
settingHook.result.current.updateCollection({
|
||||||
...settingHook.result.current.currentCollection,
|
...settingHook.result.current.currentCollection,
|
||||||
filterList: [createDefaultFilter(vars[0], defaultMeta)],
|
filterList: [createDefaultFilter(vars[0], defaultMeta)],
|
||||||
});
|
});
|
||||||
@@ -66,7 +66,7 @@ test('useAllPageSetting', async () => {
|
|||||||
expect(nextCollection.filterList).toEqual([
|
expect(nextCollection.filterList).toEqual([
|
||||||
createDefaultFilter(vars[0], defaultMeta),
|
createDefaultFilter(vars[0], defaultMeta),
|
||||||
]);
|
]);
|
||||||
await settingHook.result.current.createCollection({
|
settingHook.result.current.createCollection({
|
||||||
...settingHook.result.current.currentCollection,
|
...settingHook.result.current.currentCollection,
|
||||||
id: '1',
|
id: '1',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -33,22 +33,21 @@ export const currentCollectionAtom = atomWithReset<string>(NIL);
|
|||||||
export type Updater<T> = (value: T) => T;
|
export type Updater<T> = (value: T) => T;
|
||||||
export type CollectionUpdater = Updater<Collection>;
|
export type CollectionUpdater = Updater<Collection>;
|
||||||
export type CollectionsCRUD = {
|
export type CollectionsCRUD = {
|
||||||
addCollection: (...collections: Collection[]) => Promise<void>;
|
addCollection: (...collections: Collection[]) => void;
|
||||||
collections: Collection[];
|
collections: Collection[];
|
||||||
updateCollection: (id: string, updater: CollectionUpdater) => Promise<void>;
|
updateCollection: (id: string, updater: CollectionUpdater) => void;
|
||||||
deleteCollection: (
|
deleteCollection: (info: DeleteCollectionInfo, ...ids: string[]) => void;
|
||||||
info: DeleteCollectionInfo,
|
|
||||||
...ids: string[]
|
|
||||||
) => Promise<void>;
|
|
||||||
};
|
};
|
||||||
export type CollectionsCRUDAtom = Atom<CollectionsCRUD>;
|
export type CollectionsCRUDAtom = Atom<
|
||||||
|
Promise<CollectionsCRUD> | CollectionsCRUD
|
||||||
|
>;
|
||||||
|
|
||||||
export const useSavedCollections = (collectionAtom: CollectionsCRUDAtom) => {
|
export const useSavedCollections = (collectionAtom: CollectionsCRUDAtom) => {
|
||||||
const [{ collections, addCollection, deleteCollection, updateCollection }] =
|
const [{ collections, addCollection, deleteCollection, updateCollection }] =
|
||||||
useAtom(collectionAtom);
|
useAtom(collectionAtom);
|
||||||
const addPage = useCallback(
|
const addPage = useCallback(
|
||||||
async (collectionId: string, pageId: string) => {
|
(collectionId: string, pageId: string) => {
|
||||||
await updateCollection(collectionId, old => {
|
updateCollection(collectionId, old => {
|
||||||
return {
|
return {
|
||||||
...old,
|
...old,
|
||||||
allowList: [pageId, ...(old.allowList ?? [])],
|
allowList: [pageId, ...(old.allowList ?? [])],
|
||||||
@@ -79,11 +78,11 @@ export const useCollectionManager = (collectionsAtom: CollectionsCRUDAtom) => {
|
|||||||
defaultCollectionAtom
|
defaultCollectionAtom
|
||||||
);
|
);
|
||||||
const update = useCallback(
|
const update = useCallback(
|
||||||
async (collection: Collection) => {
|
(collection: Collection) => {
|
||||||
if (collection.id === NIL) {
|
if (collection.id === NIL) {
|
||||||
updateDefaultCollection(collection);
|
updateDefaultCollection(collection);
|
||||||
} else {
|
} else {
|
||||||
await updateCollection(collection.id, () => collection);
|
updateCollection(collection.id, () => collection);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[updateDefaultCollection, updateCollection]
|
[updateDefaultCollection, updateCollection]
|
||||||
|
|||||||
@@ -35,14 +35,10 @@ export const CollectionList = ({
|
|||||||
const [collection, setCollection] = useState<Collection>();
|
const [collection, setCollection] = useState<Collection>();
|
||||||
const onChange = useCallback(
|
const onChange = useCallback(
|
||||||
(filterList: Filter[]) => {
|
(filterList: Filter[]) => {
|
||||||
setting
|
setting.updateCollection({
|
||||||
.updateCollection({
|
...setting.currentCollection,
|
||||||
...setting.currentCollection,
|
filterList,
|
||||||
filterList,
|
});
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.error(err);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
[setting]
|
[setting]
|
||||||
);
|
);
|
||||||
@@ -53,8 +49,8 @@ export const CollectionList = ({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onConfirm = useCallback(
|
const onConfirm = useCallback(
|
||||||
async (view: Collection) => {
|
(view: Collection) => {
|
||||||
await setting.updateCollection(view);
|
setting.updateCollection(view);
|
||||||
closeUpdateCollectionModal(false);
|
closeUpdateCollectionModal(false);
|
||||||
},
|
},
|
||||||
[closeUpdateCollectionModal, setting]
|
[closeUpdateCollectionModal, setting]
|
||||||
|
|||||||
@@ -107,9 +107,7 @@ export const CollectionOperations = ({
|
|||||||
),
|
),
|
||||||
name: t['Delete'](),
|
name: t['Delete'](),
|
||||||
click: () => {
|
click: () => {
|
||||||
setting.deleteCollection(info, collection.id).catch(err => {
|
setting.deleteCollection(info, collection.id);
|
||||||
console.error(err);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
type: 'danger',
|
type: 'danger',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export interface CreateCollectionModalProps {
|
|||||||
title?: string;
|
title?: string;
|
||||||
onConfirmText?: string;
|
onConfirmText?: string;
|
||||||
init: string;
|
init: string;
|
||||||
onConfirm: (title: string) => Promise<void>;
|
onConfirm: (title: string) => void;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
showTips?: boolean;
|
showTips?: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
@@ -27,13 +27,8 @@ export const CreateCollectionModal = ({
|
|||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const onConfirmTitle = useCallback(
|
const onConfirmTitle = useCallback(
|
||||||
(title: string) => {
|
(title: string) => {
|
||||||
onConfirm(title)
|
onConfirm(title);
|
||||||
.then(() => {
|
onOpenChange(false);
|
||||||
onOpenChange(false);
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.error(err);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
[onConfirm, onOpenChange]
|
[onConfirm, onOpenChange]
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export interface EditCollectionModalProps {
|
|||||||
open: boolean;
|
open: boolean;
|
||||||
mode?: EditCollectionMode;
|
mode?: EditCollectionMode;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
onConfirm: (view: Collection) => Promise<void>;
|
onConfirm: (view: Collection) => void;
|
||||||
allPageListConfig: AllPageListConfig;
|
allPageListConfig: AllPageListConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,13 +45,7 @@ export const EditCollectionModal = ({
|
|||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const onConfirmOnCollection = useCallback(
|
const onConfirmOnCollection = useCallback(
|
||||||
(view: Collection) => {
|
(view: Collection) => {
|
||||||
onConfirm(view)
|
onConfirm(view);
|
||||||
.then(() => {
|
|
||||||
onOpenChange(false);
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.error(err);
|
|
||||||
});
|
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
},
|
},
|
||||||
[onConfirm, onOpenChange]
|
[onConfirm, onOpenChange]
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { createEmptyCollection } from '../use-collection-manager';
|
|||||||
import { useEditCollectionName } from './use-edit-collection';
|
import { useEditCollectionName } from './use-edit-collection';
|
||||||
|
|
||||||
interface SaveAsCollectionButtonProps {
|
interface SaveAsCollectionButtonProps {
|
||||||
onConfirm: (collection: Collection) => Promise<void>;
|
onConfirm: (collection: Collection) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SaveAsCollectionButton = ({
|
export const SaveAsCollectionButton = ({
|
||||||
|
|||||||
@@ -40,9 +40,7 @@ export const useActions = ({
|
|||||||
name: 'delete',
|
name: 'delete',
|
||||||
tooltip: t['com.affine.collection-bar.action.tooltip.delete'](),
|
tooltip: t['com.affine.collection-bar.action.tooltip.delete'](),
|
||||||
click: () => {
|
click: () => {
|
||||||
setting.deleteCollection(info, collection.id).catch(err => {
|
setting.deleteCollection(info, collection.id);
|
||||||
console.error(err);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export const useEditCollection = (config: AllPageListConfig) => {
|
|||||||
const [data, setData] = useState<{
|
const [data, setData] = useState<{
|
||||||
collection: Collection;
|
collection: Collection;
|
||||||
mode?: 'page' | 'rule';
|
mode?: 'page' | 'rule';
|
||||||
onConfirm: (collection: Collection) => Promise<void>;
|
onConfirm: (collection: Collection) => void;
|
||||||
}>();
|
}>();
|
||||||
const close = useCallback(() => setData(undefined), []);
|
const close = useCallback(() => setData(undefined), []);
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ export const useEditCollection = (config: AllPageListConfig) => {
|
|||||||
setData({
|
setData({
|
||||||
collection,
|
collection,
|
||||||
mode,
|
mode,
|
||||||
onConfirm: async collection => {
|
onConfirm: collection => {
|
||||||
res(collection);
|
res(collection);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -52,7 +52,7 @@ export const useEditCollectionName = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [data, setData] = useState<{
|
const [data, setData] = useState<{
|
||||||
name: string;
|
name: string;
|
||||||
onConfirm: (name: string) => Promise<void>;
|
onConfirm: (name: string) => void;
|
||||||
}>();
|
}>();
|
||||||
const close = useCallback(() => setData(undefined), []);
|
const close = useCallback(() => setData(undefined), []);
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ export const useEditCollectionName = ({
|
|||||||
new Promise<string>(res => {
|
new Promise<string>(res => {
|
||||||
setData({
|
setData({
|
||||||
name,
|
name,
|
||||||
onConfirm: async collection => {
|
onConfirm: collection => {
|
||||||
res(collection);
|
res(collection);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
import type {
|
import type { WorkspaceMetadata } from '@affine/workspace';
|
||||||
AffineCloudWorkspace,
|
|
||||||
LocalWorkspace,
|
|
||||||
} from '@affine/env/workspace';
|
|
||||||
import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
|
|
||||||
import type { DragEndEvent } from '@dnd-kit/core';
|
import type { DragEndEvent } from '@dnd-kit/core';
|
||||||
import {
|
import {
|
||||||
DndContext,
|
DndContext,
|
||||||
@@ -26,17 +22,17 @@ import { workspaceItemStyle } from './index.css';
|
|||||||
|
|
||||||
export interface WorkspaceListProps {
|
export interface WorkspaceListProps {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
currentWorkspaceId: string | null;
|
currentWorkspaceId?: string | null;
|
||||||
items: (AffineCloudWorkspace | LocalWorkspace)[];
|
items: WorkspaceMetadata[];
|
||||||
onClick: (workspaceId: string) => void;
|
onClick: (workspaceMetadata: WorkspaceMetadata) => void;
|
||||||
onSettingClick: (workspaceId: string) => void;
|
onSettingClick: (workspaceMetadata: WorkspaceMetadata) => void;
|
||||||
onDragEnd: (event: DragEndEvent) => void;
|
onDragEnd: (event: DragEndEvent) => void;
|
||||||
useIsWorkspaceOwner?: (workspaceId: string) => boolean;
|
useIsWorkspaceOwner?: (workspaceMetadata: WorkspaceMetadata) => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SortableWorkspaceItemProps extends Omit<WorkspaceListProps, 'items'> {
|
interface SortableWorkspaceItemProps extends Omit<WorkspaceListProps, 'items'> {
|
||||||
item: RootWorkspaceMetadata;
|
item: WorkspaceMetadata;
|
||||||
useIsWorkspaceOwner?: (workspaceId: string) => boolean;
|
useIsWorkspaceOwner?: (workspaceMetadata: WorkspaceMetadata) => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SortableWorkspaceItem = ({
|
const SortableWorkspaceItem = ({
|
||||||
@@ -62,7 +58,7 @@ const SortableWorkspaceItem = ({
|
|||||||
}),
|
}),
|
||||||
[disabled, transform, transition]
|
[disabled, transform, transition]
|
||||||
);
|
);
|
||||||
const isOwner = useIsWorkspaceOwner?.(item.id);
|
const isOwner = useIsWorkspaceOwner?.(item);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={workspaceItemStyle}
|
className={workspaceItemStyle}
|
||||||
|
|||||||
@@ -59,6 +59,7 @@
|
|||||||
"foxact": "^0.2.20",
|
"foxact": "^0.2.20",
|
||||||
"graphql": "^16.8.1",
|
"graphql": "^16.8.1",
|
||||||
"idb": "^8.0.0",
|
"idb": "^8.0.0",
|
||||||
|
"image-blob-reduce": "^4.1.0",
|
||||||
"intl-segmenter-polyfill-rs": "^0.1.6",
|
"intl-segmenter-polyfill-rs": "^0.1.6",
|
||||||
"jotai": "^2.5.1",
|
"jotai": "^2.5.1",
|
||||||
"jotai-devtools": "^0.7.0",
|
"jotai-devtools": "^0.7.0",
|
||||||
@@ -93,6 +94,7 @@
|
|||||||
"@swc/core": "^1.3.93",
|
"@swc/core": "^1.3.93",
|
||||||
"@testing-library/react": "^14.0.0",
|
"@testing-library/react": "^14.0.0",
|
||||||
"@types/bytes": "^3.1.3",
|
"@types/bytes": "^3.1.3",
|
||||||
|
"@types/image-blob-reduce": "^4.1.3",
|
||||||
"@types/lodash-es": "^4.17.9",
|
"@types/lodash-es": "^4.17.9",
|
||||||
"@types/uuid": "^9.0.5",
|
"@types/uuid": "^9.0.5",
|
||||||
"@types/webpack-env": "^1.18.2",
|
"@types/webpack-env": "^1.18.2",
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { bootstrapPluginSystem } from '../bootstrap/register-plugins';
|
|||||||
async function main() {
|
async function main() {
|
||||||
const { setup } = await import('../bootstrap/setup');
|
const { setup } = await import('../bootstrap/setup');
|
||||||
const rootStore = getCurrentStore();
|
const rootStore = getCurrentStore();
|
||||||
await setup(rootStore);
|
setup();
|
||||||
const { _pluginNestedImportsMap } = createSetup(rootStore);
|
const { _pluginNestedImportsMap } = createSetup(rootStore);
|
||||||
const pluginRegisterPromise = bootstrapPluginSystem(rootStore);
|
const pluginRegisterPromise = bootstrapPluginSystem(rootStore);
|
||||||
const root = document.getElementById('app');
|
const root = document.getElementById('app');
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
import type {
|
import type { WorkspaceUISchema } from '@affine/env/workspace';
|
||||||
WorkspaceFlavour,
|
|
||||||
WorkspaceUISchema,
|
|
||||||
} from '@affine/env/workspace';
|
|
||||||
import { lazy } from 'react';
|
import { lazy } from 'react';
|
||||||
|
|
||||||
import { useIsWorkspaceOwner } from '../../hooks/affine/use-is-workspace-owner';
|
import { Provider } from '../shared';
|
||||||
import { NewWorkspaceSettingDetail, Provider } from '../shared';
|
|
||||||
|
|
||||||
const LoginCard = lazy(() =>
|
const LoginCard = lazy(() =>
|
||||||
import('../../components/cloud/login-card').then(({ LoginCard }) => ({
|
import('../../components/cloud/login-card').then(({ LoginCard }) => ({
|
||||||
@@ -16,23 +12,4 @@ const LoginCard = lazy(() =>
|
|||||||
export const UI = {
|
export const UI = {
|
||||||
Provider,
|
Provider,
|
||||||
LoginCard,
|
LoginCard,
|
||||||
NewSettingsDetail: ({
|
} satisfies WorkspaceUISchema;
|
||||||
currentWorkspaceId,
|
|
||||||
onTransformWorkspace,
|
|
||||||
onDeleteLocalWorkspace,
|
|
||||||
onDeleteCloudWorkspace,
|
|
||||||
onLeaveWorkspace,
|
|
||||||
}) => {
|
|
||||||
const isOwner = useIsWorkspaceOwner(currentWorkspaceId);
|
|
||||||
return (
|
|
||||||
<NewWorkspaceSettingDetail
|
|
||||||
onDeleteLocalWorkspace={onDeleteLocalWorkspace}
|
|
||||||
onDeleteCloudWorkspace={onDeleteCloudWorkspace}
|
|
||||||
onLeaveWorkspace={onLeaveWorkspace}
|
|
||||||
workspaceId={currentWorkspaceId}
|
|
||||||
onTransferWorkspace={onTransformWorkspace}
|
|
||||||
isOwner={isOwner}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
} satisfies WorkspaceUISchema<WorkspaceFlavour.AFFINE_CLOUD>;
|
|
||||||
|
|||||||
@@ -1,81 +1,17 @@
|
|||||||
import { DebugLogger } from '@affine/debug';
|
|
||||||
import { DEFAULT_WORKSPACE_NAME } from '@affine/env/constant';
|
|
||||||
import type { WorkspaceAdapter } from '@affine/env/workspace';
|
import type { WorkspaceAdapter } from '@affine/env/workspace';
|
||||||
import {
|
import {
|
||||||
LoadPriority,
|
LoadPriority,
|
||||||
ReleaseType,
|
ReleaseType,
|
||||||
WorkspaceFlavour,
|
WorkspaceFlavour,
|
||||||
} from '@affine/env/workspace';
|
} from '@affine/env/workspace';
|
||||||
import {
|
|
||||||
CRUD,
|
|
||||||
saveWorkspaceToLocalStorage,
|
|
||||||
} from '@affine/workspace/local/crud';
|
|
||||||
import { getOrCreateWorkspace } from '@affine/workspace/manager';
|
|
||||||
import { getCurrentStore } from '@toeverything/infra/atom';
|
|
||||||
import { initEmptyPage } from '@toeverything/infra/blocksuite';
|
|
||||||
import { buildShowcaseWorkspace } from '@toeverything/infra/blocksuite';
|
|
||||||
import { nanoid } from 'nanoid';
|
|
||||||
|
|
||||||
import { setPageModeAtom } from '../../atoms';
|
import { Provider } from '../shared';
|
||||||
import { NewWorkspaceSettingDetail, Provider } from '../shared';
|
|
||||||
|
|
||||||
const logger = new DebugLogger('use-create-first-workspace');
|
|
||||||
|
|
||||||
export const LocalAdapter: WorkspaceAdapter<WorkspaceFlavour.LOCAL> = {
|
export const LocalAdapter: WorkspaceAdapter<WorkspaceFlavour.LOCAL> = {
|
||||||
releaseType: ReleaseType.STABLE,
|
releaseType: ReleaseType.STABLE,
|
||||||
flavour: WorkspaceFlavour.LOCAL,
|
flavour: WorkspaceFlavour.LOCAL,
|
||||||
loadPriority: LoadPriority.LOW,
|
loadPriority: LoadPriority.LOW,
|
||||||
Events: {
|
|
||||||
'app:access': async () => true,
|
|
||||||
'app:init': () => {
|
|
||||||
const blockSuiteWorkspace = getOrCreateWorkspace(
|
|
||||||
nanoid(),
|
|
||||||
WorkspaceFlavour.LOCAL
|
|
||||||
);
|
|
||||||
blockSuiteWorkspace.meta.setName(DEFAULT_WORKSPACE_NAME);
|
|
||||||
if (runtimeConfig.enablePreloading) {
|
|
||||||
buildShowcaseWorkspace(blockSuiteWorkspace, {
|
|
||||||
store: getCurrentStore(),
|
|
||||||
atoms: {
|
|
||||||
pageMode: setPageModeAtom,
|
|
||||||
},
|
|
||||||
}).catch(err => {
|
|
||||||
logger.error('init page with preloading failed', err);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const page = blockSuiteWorkspace.createPage();
|
|
||||||
blockSuiteWorkspace.setPageMeta(page.id, {
|
|
||||||
jumpOnce: true,
|
|
||||||
});
|
|
||||||
initEmptyPage(page).catch(error => {
|
|
||||||
logger.error('init page with empty failed', error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
saveWorkspaceToLocalStorage(blockSuiteWorkspace.id);
|
|
||||||
logger.debug('create first workspace');
|
|
||||||
return [blockSuiteWorkspace.id];
|
|
||||||
},
|
|
||||||
},
|
|
||||||
CRUD,
|
|
||||||
UI: {
|
UI: {
|
||||||
Provider,
|
Provider,
|
||||||
NewSettingsDetail: ({
|
|
||||||
currentWorkspaceId,
|
|
||||||
onTransformWorkspace,
|
|
||||||
onDeleteLocalWorkspace,
|
|
||||||
onDeleteCloudWorkspace,
|
|
||||||
onLeaveWorkspace,
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<NewWorkspaceSettingDetail
|
|
||||||
onDeleteLocalWorkspace={onDeleteLocalWorkspace}
|
|
||||||
onDeleteCloudWorkspace={onDeleteCloudWorkspace}
|
|
||||||
onLeaveWorkspace={onLeaveWorkspace}
|
|
||||||
workspaceId={currentWorkspaceId}
|
|
||||||
onTransferWorkspace={onTransformWorkspace}
|
|
||||||
isOwner={true}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
import type { WorkspaceFlavour } from '@affine/env/workspace';
|
|
||||||
import { type WorkspaceUISchema } from '@affine/env/workspace';
|
|
||||||
|
|
||||||
import { Provider } from '../shared';
|
|
||||||
|
|
||||||
export const UI = {
|
|
||||||
Provider,
|
|
||||||
NewSettingsDetail: () => {
|
|
||||||
throw new Error('Not implemented');
|
|
||||||
},
|
|
||||||
} satisfies WorkspaceUISchema<WorkspaceFlavour.AFFINE_PUBLIC>;
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Unreachable } from '@affine/env/constant';
|
import { Unreachable } from '@affine/env/constant';
|
||||||
import type {
|
import type {
|
||||||
AppEvents,
|
|
||||||
WorkspaceAdapter,
|
WorkspaceAdapter,
|
||||||
WorkspaceUISchema,
|
WorkspaceUISchema,
|
||||||
} from '@affine/env/workspace';
|
} from '@affine/env/workspace';
|
||||||
@@ -9,19 +8,9 @@ import {
|
|||||||
ReleaseType,
|
ReleaseType,
|
||||||
WorkspaceFlavour,
|
WorkspaceFlavour,
|
||||||
} from '@affine/env/workspace';
|
} from '@affine/env/workspace';
|
||||||
import { CRUD as CloudCRUD } from '@affine/workspace/affine/crud';
|
|
||||||
|
|
||||||
import { UI as CloudUI } from './cloud/ui';
|
import { UI as CloudUI } from './cloud/ui';
|
||||||
import { LocalAdapter } from './local';
|
import { LocalAdapter } from './local';
|
||||||
import { UI as PublicCloudUI } from './public-cloud/ui';
|
|
||||||
|
|
||||||
const unimplemented = () => {
|
|
||||||
throw new Error('Not implemented');
|
|
||||||
};
|
|
||||||
|
|
||||||
const bypassList = async () => {
|
|
||||||
return [];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const WorkspaceAdapters = {
|
export const WorkspaceAdapters = {
|
||||||
[WorkspaceFlavour.LOCAL]: LocalAdapter,
|
[WorkspaceFlavour.LOCAL]: LocalAdapter,
|
||||||
@@ -29,43 +18,16 @@ export const WorkspaceAdapters = {
|
|||||||
releaseType: ReleaseType.UNRELEASED,
|
releaseType: ReleaseType.UNRELEASED,
|
||||||
flavour: WorkspaceFlavour.AFFINE_CLOUD,
|
flavour: WorkspaceFlavour.AFFINE_CLOUD,
|
||||||
loadPriority: LoadPriority.HIGH,
|
loadPriority: LoadPriority.HIGH,
|
||||||
Events: {
|
|
||||||
'app:access': async () => {
|
|
||||||
try {
|
|
||||||
const { getSession } = await import('next-auth/react');
|
|
||||||
const session = await getSession();
|
|
||||||
return !!session;
|
|
||||||
} catch (e) {
|
|
||||||
console.error('failed to get session', e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
} as Partial<AppEvents>,
|
|
||||||
CRUD: CloudCRUD,
|
|
||||||
UI: CloudUI,
|
UI: CloudUI,
|
||||||
},
|
},
|
||||||
[WorkspaceFlavour.AFFINE_PUBLIC]: {
|
|
||||||
releaseType: ReleaseType.UNRELEASED,
|
|
||||||
flavour: WorkspaceFlavour.AFFINE_PUBLIC,
|
|
||||||
loadPriority: LoadPriority.LOW,
|
|
||||||
Events: {} as Partial<AppEvents>,
|
|
||||||
// todo: implement this
|
|
||||||
CRUD: {
|
|
||||||
get: unimplemented,
|
|
||||||
list: bypassList,
|
|
||||||
delete: unimplemented,
|
|
||||||
create: unimplemented,
|
|
||||||
},
|
|
||||||
UI: PublicCloudUI,
|
|
||||||
},
|
|
||||||
} satisfies {
|
} satisfies {
|
||||||
[Key in WorkspaceFlavour]: WorkspaceAdapter<Key>;
|
[Key in WorkspaceFlavour]: WorkspaceAdapter<Key>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getUIAdapter<Flavour extends WorkspaceFlavour>(
|
export function getUIAdapter<Flavour extends WorkspaceFlavour>(
|
||||||
flavour: Flavour
|
flavour: Flavour
|
||||||
): WorkspaceUISchema<Flavour> {
|
): WorkspaceUISchema {
|
||||||
const ui = WorkspaceAdapters[flavour].UI as WorkspaceUISchema<Flavour>;
|
const ui = WorkspaceAdapters[flavour].UI as WorkspaceUISchema;
|
||||||
if (!ui) {
|
if (!ui) {
|
||||||
throw new Unreachable();
|
throw new Unreachable();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import { AffineContext } from '@affine/component/context';
|
|||||||
import { GlobalLoading } from '@affine/component/global-loading';
|
import { GlobalLoading } from '@affine/component/global-loading';
|
||||||
import { NotificationCenter } from '@affine/component/notification-center';
|
import { NotificationCenter } from '@affine/component/notification-center';
|
||||||
import { WorkspaceFallback } from '@affine/component/workspace';
|
import { WorkspaceFallback } from '@affine/component/workspace';
|
||||||
|
import { createI18n, setUpLanguage } from '@affine/i18n';
|
||||||
import { CacheProvider } from '@emotion/react';
|
import { CacheProvider } from '@emotion/react';
|
||||||
import { getCurrentStore } from '@toeverything/infra/atom';
|
import { getCurrentStore } from '@toeverything/infra/atom';
|
||||||
import { use } from 'foxact/use';
|
|
||||||
import type { PropsWithChildren, ReactElement } from 'react';
|
import type { PropsWithChildren, ReactElement } from 'react';
|
||||||
import { lazy, memo, Suspense } from 'react';
|
import { lazy, memo, Suspense } from 'react';
|
||||||
import { RouterProvider } from 'react-router-dom';
|
import { RouterProvider } from 'react-router-dom';
|
||||||
@@ -41,7 +41,6 @@ async function loadLanguage() {
|
|||||||
if (environment.isBrowser) {
|
if (environment.isBrowser) {
|
||||||
performanceI18nLogger.info('start');
|
performanceI18nLogger.info('start');
|
||||||
|
|
||||||
const { createI18n, setUpLanguage } = await import('@affine/i18n');
|
|
||||||
const i18n = createI18n();
|
const i18n = createI18n();
|
||||||
document.documentElement.lang = i18n.language;
|
document.documentElement.lang = i18n.language;
|
||||||
|
|
||||||
@@ -51,12 +50,15 @@ async function loadLanguage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const languageLoadingPromise = loadLanguage().catch(console.error);
|
let languageLoadingPromise: Promise<void> | null = null;
|
||||||
|
|
||||||
export const App = memo(function App() {
|
export const App = memo(function App() {
|
||||||
performanceRenderLogger.info('App');
|
performanceRenderLogger.info('App');
|
||||||
|
|
||||||
use(languageLoadingPromise);
|
if (!languageLoadingPromise) {
|
||||||
|
languageLoadingPromise = loadLanguage().catch(console.error);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CacheProvider value={cache}>
|
<CacheProvider value={cache}>
|
||||||
<AffineContext store={getCurrentStore()}>
|
<AffineContext store={getCurrentStore()}>
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
import type { CollectionsCRUDAtom } from '@affine/component/page-list';
|
import type {
|
||||||
|
CollectionsCRUD,
|
||||||
|
CollectionsCRUDAtom,
|
||||||
|
} from '@affine/component/page-list';
|
||||||
import type { Collection, DeprecatedCollection } from '@affine/env/filter';
|
import type { Collection, DeprecatedCollection } from '@affine/env/filter';
|
||||||
|
import {
|
||||||
|
currentWorkspaceAtom,
|
||||||
|
waitForCurrentWorkspaceAtom,
|
||||||
|
} from '@affine/workspace/atom';
|
||||||
import { DisposableGroup } from '@blocksuite/global/utils';
|
import { DisposableGroup } from '@blocksuite/global/utils';
|
||||||
import type { Workspace } from '@blocksuite/store';
|
import type { Workspace } from '@blocksuite/store';
|
||||||
import { currentWorkspaceAtom } from '@toeverything/infra/atom';
|
|
||||||
import { type DBSchema, openDB } from 'idb';
|
import { type DBSchema, openDB } from 'idb';
|
||||||
import { atom } from 'jotai';
|
import { atom } from 'jotai';
|
||||||
import { atomWithObservable } from 'jotai/utils';
|
import { atomWithObservable } from 'jotai/utils';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable, of } from 'rxjs';
|
||||||
|
|
||||||
import { getUserSetting } from '../utils/user-setting';
|
import { getUserSetting } from '../utils/user-setting';
|
||||||
import { getWorkspaceSetting } from '../utils/workspace-setting';
|
import { getWorkspaceSetting } from '../utils/workspace-setting';
|
||||||
@@ -95,7 +101,11 @@ type BaseCollectionsDataType = {
|
|||||||
export const pageCollectionBaseAtom =
|
export const pageCollectionBaseAtom =
|
||||||
atomWithObservable<BaseCollectionsDataType>(
|
atomWithObservable<BaseCollectionsDataType>(
|
||||||
get => {
|
get => {
|
||||||
const currentWorkspacePromise = get(currentWorkspaceAtom);
|
const currentWorkspace = get(currentWorkspaceAtom);
|
||||||
|
if (!currentWorkspace) {
|
||||||
|
return of({ loading: true, collections: [] });
|
||||||
|
}
|
||||||
|
|
||||||
const session = get(sessionAtom);
|
const session = get(sessionAtom);
|
||||||
const userId = session?.data?.user.id ?? null;
|
const userId = session?.data?.user.id ?? null;
|
||||||
const migrateCollectionsFromIdbData = async (
|
const migrateCollectionsFromIdbData = async (
|
||||||
@@ -149,48 +159,44 @@ export const pageCollectionBaseAtom =
|
|||||||
|
|
||||||
return new Observable<BaseCollectionsDataType>(subscriber => {
|
return new Observable<BaseCollectionsDataType>(subscriber => {
|
||||||
const group = new DisposableGroup();
|
const group = new DisposableGroup();
|
||||||
currentWorkspacePromise
|
const workspaceSetting = getWorkspaceSetting(
|
||||||
.then(async currentWorkspace => {
|
currentWorkspace.blockSuiteWorkspace
|
||||||
const workspaceSetting = getWorkspaceSetting(currentWorkspace);
|
);
|
||||||
migrateCollectionsFromIdbData(currentWorkspace)
|
migrateCollectionsFromIdbData(currentWorkspace.blockSuiteWorkspace)
|
||||||
.then(collections => {
|
.then(collections => {
|
||||||
if (collections.length) {
|
if (collections.length) {
|
||||||
workspaceSetting.addCollection(...collections);
|
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,
|
|
||||||
});
|
|
||||||
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 => {
|
.catch(error => {
|
||||||
subscriber.error(error);
|
console.error(error);
|
||||||
});
|
});
|
||||||
|
migrateCollectionsFromUserData(currentWorkspace.blockSuiteWorkspace)
|
||||||
|
.then(collections => {
|
||||||
|
if (collections.length) {
|
||||||
|
workspaceSetting.addCollection(...collections);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
|
subscriber.next({
|
||||||
|
loading: false,
|
||||||
|
collections: workspaceSetting.collections,
|
||||||
|
});
|
||||||
|
if (group.disposed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const fn = () => {
|
||||||
|
subscriber.next({
|
||||||
|
loading: false,
|
||||||
|
collections: workspaceSetting.collections,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
workspaceSetting.collectionsYArray.observe(fn);
|
||||||
|
group.add(() => {
|
||||||
|
workspaceSetting.collectionsYArray.unobserve(fn);
|
||||||
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
group.dispose();
|
group.dispose();
|
||||||
@@ -199,21 +205,27 @@ export const pageCollectionBaseAtom =
|
|||||||
},
|
},
|
||||||
{ initialValue: { loading: true, collections: [] } }
|
{ initialValue: { loading: true, collections: [] } }
|
||||||
);
|
);
|
||||||
export const collectionsCRUDAtom: CollectionsCRUDAtom = atom(get => {
|
|
||||||
const workspacePromise = get(currentWorkspaceAtom);
|
export const collectionsCRUDAtom: CollectionsCRUDAtom = atom(async get => {
|
||||||
|
const workspace = await get(waitForCurrentWorkspaceAtom);
|
||||||
return {
|
return {
|
||||||
addCollection: async (...collections) => {
|
addCollection: (...collections) => {
|
||||||
const workspace = await workspacePromise;
|
getWorkspaceSetting(workspace.blockSuiteWorkspace).addCollection(
|
||||||
getWorkspaceSetting(workspace).addCollection(...collections);
|
...collections
|
||||||
|
);
|
||||||
},
|
},
|
||||||
collections: get(pageCollectionBaseAtom).collections,
|
collections: get(pageCollectionBaseAtom).collections,
|
||||||
updateCollection: async (id, updater) => {
|
updateCollection: (id, updater) => {
|
||||||
const workspace = await workspacePromise;
|
getWorkspaceSetting(workspace.blockSuiteWorkspace).updateCollection(
|
||||||
getWorkspaceSetting(workspace).updateCollection(id, updater);
|
id,
|
||||||
|
updater
|
||||||
|
);
|
||||||
},
|
},
|
||||||
deleteCollection: async (info, ...ids) => {
|
deleteCollection: (info, ...ids) => {
|
||||||
const workspace = await workspacePromise;
|
getWorkspaceSetting(workspace.blockSuiteWorkspace).deleteCollection(
|
||||||
getWorkspaceSetting(workspace).deleteCollection(info, ...ids);
|
info,
|
||||||
|
...ids
|
||||||
|
);
|
||||||
},
|
},
|
||||||
};
|
} satisfies CollectionsCRUD;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,13 +14,15 @@ export const openOnboardingModalAtom = atom(false);
|
|||||||
export const openSignOutModalAtom = atom(false);
|
export const openSignOutModalAtom = atom(false);
|
||||||
export const openPaymentDisableAtom = atom(false);
|
export const openPaymentDisableAtom = atom(false);
|
||||||
|
|
||||||
export type SettingAtom = Pick<SettingProps, 'activeTab' | 'workspaceId'> & {
|
export type SettingAtom = Pick<
|
||||||
|
SettingProps,
|
||||||
|
'activeTab' | 'workspaceMetadata'
|
||||||
|
> & {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const openSettingModalAtom = atom<SettingAtom>({
|
export const openSettingModalAtom = atom<SettingAtom>({
|
||||||
activeTab: 'appearance',
|
activeTab: 'appearance',
|
||||||
workspaceId: null,
|
|
||||||
open: false,
|
open: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
43
packages/frontend/core/src/bootstrap/first-app-data.ts
Normal file
43
packages/frontend/core/src/bootstrap/first-app-data.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { DebugLogger } from '@affine/debug';
|
||||||
|
import { DEFAULT_WORKSPACE_NAME } from '@affine/env/constant';
|
||||||
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
|
import { workspaceManager } from '@affine/workspace';
|
||||||
|
import { getCurrentStore } from '@toeverything/infra/atom';
|
||||||
|
import {
|
||||||
|
buildShowcaseWorkspace,
|
||||||
|
initEmptyPage,
|
||||||
|
} from '@toeverything/infra/blocksuite';
|
||||||
|
|
||||||
|
import { setPageModeAtom } from '../atoms';
|
||||||
|
|
||||||
|
const logger = new DebugLogger('affine:first-app-data');
|
||||||
|
|
||||||
|
export async function createFirstAppData() {
|
||||||
|
if (localStorage.getItem('is-first-open') !== null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
localStorage.setItem('is-first-open', 'false');
|
||||||
|
const workspaceId = await workspaceManager.createWorkspace(
|
||||||
|
WorkspaceFlavour.LOCAL,
|
||||||
|
async workspace => {
|
||||||
|
workspace.meta.setName(DEFAULT_WORKSPACE_NAME);
|
||||||
|
if (runtimeConfig.enablePreloading) {
|
||||||
|
await buildShowcaseWorkspace(workspace, {
|
||||||
|
store: getCurrentStore(),
|
||||||
|
atoms: {
|
||||||
|
pageMode: setPageModeAtom,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const page = workspace.createPage();
|
||||||
|
workspace.setPageMeta(page.id, {
|
||||||
|
jumpOnce: true,
|
||||||
|
});
|
||||||
|
await initEmptyPage(page);
|
||||||
|
}
|
||||||
|
logger.debug('create first workspace');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
console.info('create first workspace', workspaceId);
|
||||||
|
return workspaceId;
|
||||||
|
}
|
||||||
@@ -14,11 +14,7 @@ import {
|
|||||||
pluginSettingAtom,
|
pluginSettingAtom,
|
||||||
pluginWindowAtom,
|
pluginWindowAtom,
|
||||||
} from '@toeverything/infra/__internal__/plugin';
|
} from '@toeverything/infra/__internal__/plugin';
|
||||||
import {
|
import { contentLayoutAtom, currentPageIdAtom } from '@toeverything/infra/atom';
|
||||||
contentLayoutAtom,
|
|
||||||
currentPageIdAtom,
|
|
||||||
currentWorkspaceAtom,
|
|
||||||
} from '@toeverything/infra/atom';
|
|
||||||
import { atom } from 'jotai';
|
import { atom } from 'jotai';
|
||||||
import { Provider } from 'jotai/react';
|
import { Provider } from 'jotai/react';
|
||||||
import type { createStore } from 'jotai/vanilla';
|
import type { createStore } from 'jotai/vanilla';
|
||||||
@@ -149,7 +145,6 @@ function createSetupImpl(rootStore: ReturnType<typeof createStore>) {
|
|||||||
'@blocksuite/inline': import('@blocksuite/inline'),
|
'@blocksuite/inline': import('@blocksuite/inline'),
|
||||||
'@affine/sdk/entry': {
|
'@affine/sdk/entry': {
|
||||||
rootStore,
|
rootStore,
|
||||||
currentWorkspaceAtom: currentWorkspaceAtom,
|
|
||||||
currentPageIdAtom: currentPageIdAtom,
|
currentPageIdAtom: currentPageIdAtom,
|
||||||
pushLayoutAtom: pushLayoutAtom,
|
pushLayoutAtom: pushLayoutAtom,
|
||||||
deleteLayoutAtom: deleteLayoutAtom,
|
deleteLayoutAtom: deleteLayoutAtom,
|
||||||
|
|||||||
@@ -1,15 +1,7 @@
|
|||||||
import './register-blocksuite-components';
|
import './register-blocksuite-components';
|
||||||
|
|
||||||
import { setupGlobal } from '@affine/env/global';
|
import { setupGlobal } from '@affine/env/global';
|
||||||
import type { WorkspaceAdapter } from '@affine/env/workspace';
|
|
||||||
import type { WorkspaceFlavour } from '@affine/env/workspace';
|
|
||||||
import {
|
|
||||||
type RootWorkspaceMetadataV2,
|
|
||||||
rootWorkspacesMetadataAtom,
|
|
||||||
workspaceAdaptersAtom,
|
|
||||||
} from '@affine/workspace/atom';
|
|
||||||
import * as Sentry from '@sentry/react';
|
import * as Sentry from '@sentry/react';
|
||||||
import type { createStore } from 'jotai/vanilla';
|
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
createRoutesFromChildren,
|
createRoutesFromChildren,
|
||||||
@@ -18,45 +10,12 @@ import {
|
|||||||
useNavigationType,
|
useNavigationType,
|
||||||
} from 'react-router-dom';
|
} from 'react-router-dom';
|
||||||
|
|
||||||
import { WorkspaceAdapters } from '../adapters/workspace';
|
|
||||||
import { performanceLogger } from '../shared';
|
import { performanceLogger } from '../shared';
|
||||||
|
|
||||||
const performanceSetupLogger = performanceLogger.namespace('setup');
|
const performanceSetupLogger = performanceLogger.namespace('setup');
|
||||||
|
|
||||||
export function createFirstAppData(store: ReturnType<typeof createStore>) {
|
export function setup() {
|
||||||
const createFirst = (): RootWorkspaceMetadataV2[] => {
|
|
||||||
const Plugins = Object.values(WorkspaceAdapters).sort(
|
|
||||||
(a, b) => a.loadPriority - b.loadPriority
|
|
||||||
);
|
|
||||||
|
|
||||||
return Plugins.flatMap(Plugin => {
|
|
||||||
return Plugin.Events['app:init']?.().map(
|
|
||||||
id =>
|
|
||||||
<RootWorkspaceMetadataV2>{
|
|
||||||
id,
|
|
||||||
flavour: Plugin.flavour,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}).filter((ids): ids is RootWorkspaceMetadataV2 => !!ids);
|
|
||||||
};
|
|
||||||
if (localStorage.getItem('is-first-open') !== null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const result = createFirst();
|
|
||||||
console.info('create first workspace', result);
|
|
||||||
localStorage.setItem('is-first-open', 'false');
|
|
||||||
store.set(rootWorkspacesMetadataAtom, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function setup(store: ReturnType<typeof createStore>) {
|
|
||||||
performanceSetupLogger.info('start');
|
performanceSetupLogger.info('start');
|
||||||
store.set(
|
|
||||||
workspaceAdaptersAtom,
|
|
||||||
WorkspaceAdapters as Record<
|
|
||||||
WorkspaceFlavour,
|
|
||||||
WorkspaceAdapter<WorkspaceFlavour>
|
|
||||||
>
|
|
||||||
);
|
|
||||||
|
|
||||||
performanceSetupLogger.info('setup global');
|
performanceSetupLogger.info('setup global');
|
||||||
setupGlobal();
|
setupGlobal();
|
||||||
@@ -88,9 +47,5 @@ export async function setup(store: ReturnType<typeof createStore>) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
performanceSetupLogger.info('get root workspace meta');
|
|
||||||
// do not read `rootWorkspacesMetadataAtom` before migration
|
|
||||||
await store.get(rootWorkspacesMetadataAtom);
|
|
||||||
|
|
||||||
performanceSetupLogger.info('done');
|
performanceSetupLogger.info('done');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export function registerAffineHelpCommands({
|
|||||||
store.set(openSettingModalAtom, {
|
store.set(openSettingModalAtom, {
|
||||||
open: true,
|
open: true,
|
||||||
activeTab: 'about',
|
activeTab: 'about',
|
||||||
workspaceId: null,
|
workspaceMetadata: null,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -94,7 +94,6 @@ export function registerAffineNavigationCommands({
|
|||||||
run() {
|
run() {
|
||||||
store.set(openSettingModalAtom, {
|
store.set(openSettingModalAtom, {
|
||||||
activeTab: 'appearance',
|
activeTab: 'appearance',
|
||||||
workspaceId: null,
|
|
||||||
open: true,
|
open: true,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
|
import { waitForCurrentWorkspaceAtom } from '@affine/workspace/atom';
|
||||||
import { assertExists } from '@blocksuite/global/utils';
|
import { assertExists } from '@blocksuite/global/utils';
|
||||||
|
import { useAtomValue } from 'jotai';
|
||||||
import type { FC, PropsWithChildren } from 'react';
|
import type { FC, PropsWithChildren } from 'react';
|
||||||
|
|
||||||
import { WorkspaceAdapters } from '../adapters/workspace';
|
import { WorkspaceAdapters } from '../adapters/workspace';
|
||||||
import { useCurrentWorkspace } from '../hooks/current/use-current-workspace';
|
|
||||||
|
|
||||||
export const AdapterProviderWrapper: FC<PropsWithChildren> = ({ children }) => {
|
export const AdapterProviderWrapper: FC<PropsWithChildren> = ({ children }) => {
|
||||||
const [currentWorkspace] = useCurrentWorkspace();
|
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
|
||||||
|
|
||||||
const Provider = WorkspaceAdapters[currentWorkspace.flavour].UI.Provider;
|
const Provider = WorkspaceAdapters[currentWorkspace.flavour].UI.Provider;
|
||||||
assertExists(Provider);
|
assertExists(Provider);
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
|
||||||
import {
|
import {
|
||||||
currentPageIdAtom,
|
currentWorkspaceAtom,
|
||||||
currentWorkspaceIdAtom,
|
workspaceListAtom,
|
||||||
} from '@toeverything/infra/atom';
|
} from '@affine/workspace/atom';
|
||||||
|
import { currentPageIdAtom } from '@toeverything/infra/atom';
|
||||||
import { useAtomValue } from 'jotai/react';
|
import { useAtomValue } from 'jotai/react';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useLocation, useParams } from 'react-router-dom';
|
import { useLocation, useParams } from 'react-router-dom';
|
||||||
@@ -13,8 +13,8 @@ export interface DumpInfoProps {
|
|||||||
|
|
||||||
export const DumpInfo = (_props: DumpInfoProps) => {
|
export const DumpInfo = (_props: DumpInfoProps) => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const metadata = useAtomValue(rootWorkspacesMetadataAtom);
|
const workspaceList = useAtomValue(workspaceListAtom);
|
||||||
const currentWorkspaceId = useAtomValue(currentWorkspaceIdAtom);
|
const currentWorkspace = useAtomValue(currentWorkspaceAtom);
|
||||||
const currentPageId = useAtomValue(currentPageIdAtom);
|
const currentPageId = useAtomValue(currentPageIdAtom);
|
||||||
const path = location.pathname;
|
const path = location.pathname;
|
||||||
const query = useParams();
|
const query = useParams();
|
||||||
@@ -22,10 +22,10 @@ export const DumpInfo = (_props: DumpInfoProps) => {
|
|||||||
console.info('DumpInfo', {
|
console.info('DumpInfo', {
|
||||||
path,
|
path,
|
||||||
query,
|
query,
|
||||||
currentWorkspaceId,
|
currentWorkspaceId: currentWorkspace?.id,
|
||||||
currentPageId,
|
currentPageId,
|
||||||
metadata,
|
workspaceList,
|
||||||
});
|
});
|
||||||
}, [path, query, currentWorkspaceId, currentPageId, metadata]);
|
}, [path, query, currentWorkspace, currentPageId, workspaceList]);
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ const UserPlanButtonWithData = () => {
|
|||||||
setSettingModalAtom({
|
setSettingModalAtom({
|
||||||
open: true,
|
open: true,
|
||||||
activeTab: 'plans',
|
activeTab: 'plans',
|
||||||
workspaceId: null,
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[setSettingModalAtom]
|
[setSettingModalAtom]
|
||||||
|
|||||||
@@ -1,26 +1,33 @@
|
|||||||
|
import { waitForCurrentWorkspaceAtom } from '@affine/workspace/atom';
|
||||||
|
import { useAtomValue } from 'jotai';
|
||||||
import { Suspense, useEffect } from 'react';
|
import { Suspense, useEffect } from 'react';
|
||||||
|
|
||||||
import { useCurrentLoginStatus } from '../../../hooks/affine/use-current-login-status';
|
import { useCurrentLoginStatus } from '../../../hooks/affine/use-current-login-status';
|
||||||
import { useCurrentUser } from '../../../hooks/affine/use-current-user';
|
import { useCurrentUser } from '../../../hooks/affine/use-current-user';
|
||||||
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
|
|
||||||
|
|
||||||
const SyncAwarenessInnerLoggedIn = () => {
|
const SyncAwarenessInnerLoggedIn = () => {
|
||||||
const currentUser = useCurrentUser();
|
const currentUser = useCurrentUser();
|
||||||
const [{ blockSuiteWorkspace: workspace }] = useCurrentWorkspace();
|
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentUser && workspace) {
|
if (currentUser && currentWorkspace) {
|
||||||
workspace.awarenessStore.awareness.setLocalStateField('user', {
|
currentWorkspace.blockSuiteWorkspace.awarenessStore.awareness.setLocalStateField(
|
||||||
name: currentUser.name,
|
'user',
|
||||||
// todo: add avatar?
|
{
|
||||||
});
|
name: currentUser.name,
|
||||||
|
// todo: add avatar?
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
workspace.awarenessStore.awareness.setLocalStateField('user', null);
|
currentWorkspace.blockSuiteWorkspace.awarenessStore.awareness.setLocalStateField(
|
||||||
|
'user',
|
||||||
|
null
|
||||||
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}, [currentUser, workspace]);
|
}, [currentUser, currentWorkspace]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,27 +1,26 @@
|
|||||||
import { Input, toast } from '@affine/component';
|
import { Input, toast } from '@affine/component';
|
||||||
import { Button } from '@affine/component/ui/button';
|
|
||||||
import {
|
import {
|
||||||
ConfirmModal,
|
ConfirmModal,
|
||||||
type ConfirmModalProps,
|
type ConfirmModalProps,
|
||||||
Modal,
|
Modal,
|
||||||
} from '@affine/component/ui/modal';
|
} from '@affine/component/ui/modal';
|
||||||
import { Tooltip } from '@affine/component/ui/tooltip';
|
|
||||||
import { DebugLogger } from '@affine/debug';
|
import { DebugLogger } from '@affine/debug';
|
||||||
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { HelpIcon } from '@blocksuite/icons';
|
import { workspaceManagerAtom } from '@affine/workspace/atom';
|
||||||
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||||
import type {
|
import { getCurrentStore } from '@toeverything/infra/atom';
|
||||||
LoadDBFileResult,
|
import {
|
||||||
SelectDBFileLocationResult,
|
buildShowcaseWorkspace,
|
||||||
} from '@toeverything/infra/type';
|
initEmptyPage,
|
||||||
import { useSetAtom } from 'jotai';
|
} from '@toeverything/infra/blocksuite';
|
||||||
|
import type { LoadDBFileResult } from '@toeverything/infra/type';
|
||||||
|
import { useAtomValue } from 'jotai';
|
||||||
import type { KeyboardEvent } from 'react';
|
import type { KeyboardEvent } from 'react';
|
||||||
import { useEffect } from 'react';
|
|
||||||
import { useLayoutEffect } from 'react';
|
import { useLayoutEffect } from 'react';
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
import { openDisableCloudAlertModalAtom } from '../../../atoms';
|
import { setPageModeAtom } from '../../../atoms';
|
||||||
import { useAppHelper } from '../../../hooks/use-workspaces';
|
|
||||||
import * as style from './index.css';
|
import * as style from './index.css';
|
||||||
|
|
||||||
type CreateWorkspaceStep =
|
type CreateWorkspaceStep =
|
||||||
@@ -94,159 +93,14 @@ const NameWorkspaceContent = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
interface SetDBLocationContentProps {
|
|
||||||
onConfirmLocation: (dir?: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const useDefaultDBLocation = () => {
|
|
||||||
const [defaultDBLocation, setDefaultDBLocation] = useState('');
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
window.apis?.db
|
|
||||||
.getDefaultStorageLocation()
|
|
||||||
.then(dir => {
|
|
||||||
setDefaultDBLocation(dir);
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.error(err);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return defaultDBLocation;
|
|
||||||
};
|
|
||||||
|
|
||||||
const SetDBLocationContent = ({
|
|
||||||
onConfirmLocation,
|
|
||||||
}: SetDBLocationContentProps) => {
|
|
||||||
const t = useAFFiNEI18N();
|
|
||||||
const defaultDBLocation = useDefaultDBLocation();
|
|
||||||
const [opening, setOpening] = useState(false);
|
|
||||||
|
|
||||||
const handleSelectDBFileLocation = useCallback(() => {
|
|
||||||
if (opening) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setOpening(true);
|
|
||||||
(async function () {
|
|
||||||
const result: SelectDBFileLocationResult =
|
|
||||||
await window.apis?.dialog.selectDBFileLocation();
|
|
||||||
setOpening(false);
|
|
||||||
if (result?.filePath) {
|
|
||||||
onConfirmLocation(result.filePath);
|
|
||||||
} else if (result?.error) {
|
|
||||||
toast(t[result.error]());
|
|
||||||
}
|
|
||||||
})().catch(err => {
|
|
||||||
logger.error(err);
|
|
||||||
});
|
|
||||||
}, [onConfirmLocation, opening, t]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={style.content}>
|
|
||||||
<div className={style.contentTitle}>
|
|
||||||
{t['com.affine.setDBLocation.title']()}
|
|
||||||
</div>
|
|
||||||
<p>{t['com.affine.setDBLocation.description']()}</p>
|
|
||||||
<div className={style.buttonGroup}>
|
|
||||||
<Button
|
|
||||||
disabled={opening}
|
|
||||||
data-testid="create-workspace-customize-button"
|
|
||||||
type="primary"
|
|
||||||
onClick={handleSelectDBFileLocation}
|
|
||||||
>
|
|
||||||
{t['com.affine.setDBLocation.button.customize']()}
|
|
||||||
</Button>
|
|
||||||
<Tooltip
|
|
||||||
content={t['com.affine.setDBLocation.tooltip.defaultLocation']({
|
|
||||||
location: defaultDBLocation,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
data-testid="create-workspace-default-location-button"
|
|
||||||
type="primary"
|
|
||||||
onClick={() => {
|
|
||||||
onConfirmLocation();
|
|
||||||
}}
|
|
||||||
icon={<HelpIcon />}
|
|
||||||
iconPosition="end"
|
|
||||||
>
|
|
||||||
{t['com.affine.setDBLocation.button.defaultLocation']()}
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface SetSyncingModeContentProps {
|
|
||||||
mode: CreateWorkspaceMode;
|
|
||||||
onConfirmMode: (enableCloudSyncing: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SetSyncingModeContent = ({
|
|
||||||
mode,
|
|
||||||
onConfirmMode,
|
|
||||||
}: SetSyncingModeContentProps) => {
|
|
||||||
const t = useAFFiNEI18N();
|
|
||||||
const [enableCloudSyncing, setEnableCloudSyncing] = useState(false);
|
|
||||||
return (
|
|
||||||
<div className={style.content}>
|
|
||||||
<div className={style.contentTitle}>
|
|
||||||
{mode === 'new'
|
|
||||||
? t['com.affine.setSyncingMode.title.created']()
|
|
||||||
: t['com.affine.setSyncingMode.title.added']()}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={style.radioGroup}>
|
|
||||||
<label onClick={() => setEnableCloudSyncing(false)}>
|
|
||||||
<input
|
|
||||||
className={style.radio}
|
|
||||||
type="radio"
|
|
||||||
readOnly
|
|
||||||
checked={!enableCloudSyncing}
|
|
||||||
/>
|
|
||||||
{t['com.affine.setSyncingMode.deviceOnly']()}
|
|
||||||
</label>
|
|
||||||
<label onClick={() => setEnableCloudSyncing(true)}>
|
|
||||||
<input
|
|
||||||
className={style.radio}
|
|
||||||
type="radio"
|
|
||||||
readOnly
|
|
||||||
checked={enableCloudSyncing}
|
|
||||||
/>
|
|
||||||
{t['com.affine.setSyncingMode.cloud']()}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={style.buttonGroup}>
|
|
||||||
<Button
|
|
||||||
data-testid="create-workspace-continue-button"
|
|
||||||
type="primary"
|
|
||||||
onClick={() => {
|
|
||||||
onConfirmMode(enableCloudSyncing);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t['com.affine.setSyncingMode.button.continue']()}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CreateWorkspaceModal = ({
|
export const CreateWorkspaceModal = ({
|
||||||
mode,
|
mode,
|
||||||
onClose,
|
onClose,
|
||||||
onCreate,
|
onCreate,
|
||||||
}: ModalProps) => {
|
}: ModalProps) => {
|
||||||
const { createLocalWorkspace, addLocalWorkspace } = useAppHelper();
|
|
||||||
const [step, setStep] = useState<CreateWorkspaceStep>();
|
const [step, setStep] = useState<CreateWorkspaceStep>();
|
||||||
const [addedId, setAddedId] = useState<string>();
|
|
||||||
const [workspaceName, setWorkspaceName] = useState<string>();
|
|
||||||
const [dbFileLocation, setDBFileLocation] = useState<string>();
|
|
||||||
const setOpenDisableCloudAlertModal = useSetAtom(
|
|
||||||
openDisableCloudAlertModalAtom
|
|
||||||
);
|
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
|
const workspaceManager = useAtomValue(workspaceManagerAtom);
|
||||||
|
|
||||||
// todo: maybe refactor using xstate?
|
// todo: maybe refactor using xstate?
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
@@ -265,9 +119,8 @@ export const CreateWorkspaceModal = ({
|
|||||||
setStep(undefined);
|
setStep(undefined);
|
||||||
const result: LoadDBFileResult = await window.apis.dialog.loadDBFile();
|
const result: LoadDBFileResult = await window.apis.dialog.loadDBFile();
|
||||||
if (result.workspaceId && !canceled) {
|
if (result.workspaceId && !canceled) {
|
||||||
setAddedId(result.workspaceId);
|
workspaceManager._addLocalWorkspace(result.workspaceId);
|
||||||
const newWorkspaceId = await addLocalWorkspace(result.workspaceId);
|
onCreate(result.workspaceId);
|
||||||
onCreate(newWorkspaceId);
|
|
||||||
} else if (result.error || result.canceled) {
|
} else if (result.error || result.canceled) {
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
toast(t[result.error]());
|
toast(t[result.error]());
|
||||||
@@ -285,77 +138,38 @@ export const CreateWorkspaceModal = ({
|
|||||||
return () => {
|
return () => {
|
||||||
canceled = true;
|
canceled = true;
|
||||||
};
|
};
|
||||||
}, [addLocalWorkspace, mode, onClose, onCreate, t]);
|
}, [mode, onClose, onCreate, t, workspaceManager]);
|
||||||
|
|
||||||
const onConfirmEnableCloudSyncing = useCallback(
|
|
||||||
(enableCloudSyncing: boolean) => {
|
|
||||||
(async function () {
|
|
||||||
if (!runtimeConfig.enableCloud && enableCloudSyncing) {
|
|
||||||
setOpenDisableCloudAlertModal(true);
|
|
||||||
} else {
|
|
||||||
let id = addedId;
|
|
||||||
// syncing mode is also the last step
|
|
||||||
if (addedId && mode === 'add') {
|
|
||||||
await addLocalWorkspace(addedId);
|
|
||||||
} else if (mode === 'new' && workspaceName) {
|
|
||||||
id = await createLocalWorkspace(workspaceName);
|
|
||||||
// if dbFileLocation is set, move db file to that location
|
|
||||||
if (dbFileLocation) {
|
|
||||||
await window.apis?.dialog.moveDBFile(id, dbFileLocation);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logger.error('invalid state');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (id) {
|
|
||||||
onCreate(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})().catch(e => {
|
|
||||||
logger.error(e);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[
|
|
||||||
addLocalWorkspace,
|
|
||||||
addedId,
|
|
||||||
createLocalWorkspace,
|
|
||||||
dbFileLocation,
|
|
||||||
mode,
|
|
||||||
onCreate,
|
|
||||||
setOpenDisableCloudAlertModal,
|
|
||||||
workspaceName,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onConfirmName = useAsyncCallback(
|
const onConfirmName = useAsyncCallback(
|
||||||
async (name: string) => {
|
async (name: string) => {
|
||||||
setWorkspaceName(name);
|
|
||||||
// this will be the last step for web for now
|
// this will be the last step for web for now
|
||||||
// fix me later
|
// fix me later
|
||||||
const id = await createLocalWorkspace(name);
|
const id = await workspaceManager.createWorkspace(
|
||||||
|
WorkspaceFlavour.LOCAL,
|
||||||
|
async workspace => {
|
||||||
|
workspace.meta.setName(name);
|
||||||
|
if (runtimeConfig.enablePreloading) {
|
||||||
|
await buildShowcaseWorkspace(workspace, {
|
||||||
|
store: getCurrentStore(),
|
||||||
|
atoms: {
|
||||||
|
pageMode: setPageModeAtom,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const page = workspace.createPage();
|
||||||
|
workspace.setPageMeta(page.id, {
|
||||||
|
jumpOnce: true,
|
||||||
|
});
|
||||||
|
await initEmptyPage(page);
|
||||||
|
}
|
||||||
|
logger.debug('create first workspace');
|
||||||
|
}
|
||||||
|
);
|
||||||
onCreate(id);
|
onCreate(id);
|
||||||
},
|
},
|
||||||
[createLocalWorkspace, onCreate]
|
[onCreate, workspaceManager]
|
||||||
);
|
);
|
||||||
|
|
||||||
const setDBLocationNode =
|
|
||||||
step === 'set-db-location' ? (
|
|
||||||
<SetDBLocationContent
|
|
||||||
onConfirmLocation={dir => {
|
|
||||||
setDBFileLocation(dir);
|
|
||||||
setStep('name-workspace');
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : null;
|
|
||||||
|
|
||||||
const setSyncingModeNode =
|
|
||||||
step === 'set-syncing-mode' ? (
|
|
||||||
<SetSyncingModeContent
|
|
||||||
mode={mode}
|
|
||||||
onConfirmMode={onConfirmEnableCloudSyncing}
|
|
||||||
/>
|
|
||||||
) : null;
|
|
||||||
|
|
||||||
const onOpenChange = useCallback(
|
const onOpenChange = useCallback(
|
||||||
(open: boolean) => {
|
(open: boolean) => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
@@ -384,8 +198,6 @@ export const CreateWorkspaceModal = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className={style.header}></div>
|
<div className={style.header}></div>
|
||||||
{setDBLocationNode}
|
|
||||||
{setSyncingModeNode}
|
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,28 +3,28 @@ import {
|
|||||||
ConfirmModal,
|
ConfirmModal,
|
||||||
type ConfirmModalProps,
|
type ConfirmModalProps,
|
||||||
} from '@affine/component/ui/modal';
|
} from '@affine/component/ui/modal';
|
||||||
import type { AffineOfficialWorkspace } from '@affine/env/workspace';
|
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
import { Trans } from '@affine/i18n';
|
import { Trans } from '@affine/i18n';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
|
import type { WorkspaceMetadata } from '@affine/workspace/metadata';
|
||||||
|
import { useWorkspaceInfo } from '@toeverything/hooks/use-workspace-info';
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
import * as styles from './style.css';
|
import * as styles from './style.css';
|
||||||
|
|
||||||
interface WorkspaceDeleteProps extends ConfirmModalProps {
|
interface WorkspaceDeleteProps extends ConfirmModalProps {
|
||||||
workspace: AffineOfficialWorkspace;
|
workspaceMetadata: WorkspaceMetadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WorkspaceDeleteModal = ({
|
export const WorkspaceDeleteModal = ({
|
||||||
workspace,
|
workspaceMetadata,
|
||||||
...props
|
...props
|
||||||
}: WorkspaceDeleteProps) => {
|
}: WorkspaceDeleteProps) => {
|
||||||
const { onConfirm } = props;
|
const { onConfirm } = props;
|
||||||
const [workspaceName] = useBlockSuiteWorkspaceName(
|
|
||||||
workspace.blockSuiteWorkspace
|
|
||||||
);
|
|
||||||
const [deleteStr, setDeleteStr] = useState<string>('');
|
const [deleteStr, setDeleteStr] = useState<string>('');
|
||||||
|
const info = useWorkspaceInfo(workspaceMetadata);
|
||||||
|
const workspaceName = info?.name ?? UNTITLED_WORKSPACE_NAME;
|
||||||
const allowDelete = deleteStr === workspaceName;
|
const allowDelete = deleteStr === workspaceName;
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ export const WorkspaceDeleteModal = ({
|
|||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{workspace.flavour === WorkspaceFlavour.LOCAL ? (
|
{workspaceMetadata.flavour === WorkspaceFlavour.LOCAL ? (
|
||||||
<Trans i18nKey="com.affine.workspaceDelete.description">
|
<Trans i18nKey="com.affine.workspaceDelete.description">
|
||||||
Deleting (
|
Deleting (
|
||||||
<span className={styles.workspaceName}>
|
<span className={styles.workspaceName}>
|
||||||
|
|||||||
@@ -1,29 +1,44 @@
|
|||||||
|
import { pushNotificationAtom } from '@affine/component/notification-center';
|
||||||
import { SettingRow } from '@affine/component/setting-components';
|
import { SettingRow } from '@affine/component/setting-components';
|
||||||
import { ConfirmModal } from '@affine/component/ui/modal';
|
import { ConfirmModal } from '@affine/component/ui/modal';
|
||||||
import type { AffineOfficialWorkspace } from '@affine/env/workspace';
|
import { WorkspaceSubPath } from '@affine/env/workspace';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
|
import {
|
||||||
|
currentWorkspaceAtom,
|
||||||
|
workspaceListAtom,
|
||||||
|
workspaceManagerAtom,
|
||||||
|
} from '@affine/workspace/atom';
|
||||||
import { ArrowRightSmallIcon } from '@blocksuite/icons';
|
import { ArrowRightSmallIcon } from '@blocksuite/icons';
|
||||||
|
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||||
|
import { useAtomValue, useSetAtom } from 'jotai';
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
|
import { openSettingModalAtom } from '../../../../atoms';
|
||||||
|
import {
|
||||||
|
RouteLogic,
|
||||||
|
useNavigateHelper,
|
||||||
|
} from '../../../../hooks/use-navigate-helper';
|
||||||
import type { WorkspaceSettingDetailProps } from '../types';
|
import type { WorkspaceSettingDetailProps } from '../types';
|
||||||
import { WorkspaceDeleteModal } from './delete';
|
import { WorkspaceDeleteModal } from './delete';
|
||||||
|
|
||||||
export interface DeleteLeaveWorkspaceProps extends WorkspaceSettingDetailProps {
|
export interface DeleteLeaveWorkspaceProps
|
||||||
workspace: AffineOfficialWorkspace;
|
extends WorkspaceSettingDetailProps {}
|
||||||
}
|
|
||||||
|
|
||||||
export const DeleteLeaveWorkspace = ({
|
export const DeleteLeaveWorkspace = ({
|
||||||
workspace,
|
workspaceMetadata,
|
||||||
onDeleteCloudWorkspace,
|
|
||||||
onDeleteLocalWorkspace,
|
|
||||||
onLeaveWorkspace,
|
|
||||||
isOwner,
|
isOwner,
|
||||||
}: DeleteLeaveWorkspaceProps) => {
|
}: DeleteLeaveWorkspaceProps) => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
|
const { jumpToSubPath, jumpToIndex } = useNavigateHelper();
|
||||||
// fixme: cloud regression
|
// fixme: cloud regression
|
||||||
const [showDelete, setShowDelete] = useState(false);
|
const [showDelete, setShowDelete] = useState(false);
|
||||||
const [showLeave, setShowLeave] = useState(false);
|
const [showLeave, setShowLeave] = useState(false);
|
||||||
|
const setSettingModal = useSetAtom(openSettingModalAtom);
|
||||||
|
|
||||||
|
const workspaceManager = useAtomValue(workspaceManagerAtom);
|
||||||
|
const workspaceList = useAtomValue(workspaceListAtom);
|
||||||
|
const currentWorkspace = useAtomValue(currentWorkspaceAtom);
|
||||||
|
const pushNotification = useSetAtom(pushNotificationAtom);
|
||||||
|
|
||||||
const onLeaveOrDelete = useCallback(() => {
|
const onLeaveOrDelete = useCallback(() => {
|
||||||
if (isOwner) {
|
if (isOwner) {
|
||||||
@@ -33,18 +48,41 @@ export const DeleteLeaveWorkspace = ({
|
|||||||
}
|
}
|
||||||
}, [isOwner]);
|
}, [isOwner]);
|
||||||
|
|
||||||
const onLeaveConfirm = useCallback(() => {
|
const onDeleteConfirm = useAsyncCallback(async () => {
|
||||||
return onLeaveWorkspace();
|
setSettingModal(prev => ({ ...prev, open: false, workspaceId: null }));
|
||||||
}, [onLeaveWorkspace]);
|
|
||||||
|
|
||||||
const onDeleteConfirm = useCallback(() => {
|
if (currentWorkspace?.id === workspaceMetadata.id) {
|
||||||
if (workspace.flavour === WorkspaceFlavour.LOCAL) {
|
const backWorkspace = workspaceList.find(
|
||||||
return onDeleteLocalWorkspace();
|
ws => ws.id !== workspaceMetadata.id
|
||||||
|
);
|
||||||
|
// TODO: if there is no workspace, jump to a new page(wait for design)
|
||||||
|
if (backWorkspace) {
|
||||||
|
jumpToSubPath(
|
||||||
|
backWorkspace?.id || '',
|
||||||
|
WorkspaceSubPath.ALL,
|
||||||
|
RouteLogic.REPLACE
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
jumpToIndex(RouteLogic.REPLACE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD) {
|
|
||||||
return onDeleteCloudWorkspace();
|
await workspaceManager.deleteWorkspace(workspaceMetadata);
|
||||||
}
|
pushNotification({
|
||||||
}, [onDeleteCloudWorkspace, onDeleteLocalWorkspace, workspace.flavour]);
|
title: t['Successfully deleted'](),
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
|
}, [
|
||||||
|
currentWorkspace?.id,
|
||||||
|
jumpToIndex,
|
||||||
|
jumpToSubPath,
|
||||||
|
pushNotification,
|
||||||
|
setSettingModal,
|
||||||
|
t,
|
||||||
|
workspaceList,
|
||||||
|
workspaceManager,
|
||||||
|
workspaceMetadata,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -68,13 +106,13 @@ export const DeleteLeaveWorkspace = ({
|
|||||||
onConfirm={onDeleteConfirm}
|
onConfirm={onDeleteConfirm}
|
||||||
open={showDelete}
|
open={showDelete}
|
||||||
onOpenChange={setShowDelete}
|
onOpenChange={setShowDelete}
|
||||||
workspace={workspace}
|
workspaceMetadata={workspaceMetadata}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
open={showLeave}
|
open={showLeave}
|
||||||
cancelText={t['com.affine.confirmModal.button.cancel']()}
|
cancelText={t['com.affine.confirmModal.button.cancel']()}
|
||||||
onConfirm={onLeaveConfirm}
|
onConfirm={onDeleteConfirm}
|
||||||
onOpenChange={setShowLeave}
|
onOpenChange={setShowLeave}
|
||||||
title={`${t['com.affine.deleteLeaveWorkspace.leave']()}?`}
|
title={`${t['com.affine.deleteLeaveWorkspace.leave']()}?`}
|
||||||
description={t['com.affine.deleteLeaveWorkspace.leaveDescription']()}
|
description={t['com.affine.deleteLeaveWorkspace.leaveDescription']()}
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import { SettingRow } from '@affine/component/setting-components';
|
||||||
|
import { Button } from '@affine/component/ui/button';
|
||||||
|
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
|
||||||
|
import { WorkspaceFlavour, WorkspaceSubPath } from '@affine/env/workspace';
|
||||||
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
|
import type { Workspace } from '@affine/workspace';
|
||||||
|
import { workspaceManagerAtom } from '@affine/workspace/atom';
|
||||||
|
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||||
|
import { useWorkspaceInfo } from '@toeverything/hooks/use-workspace-info';
|
||||||
|
import { useAtomValue, useSetAtom } from 'jotai';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { openSettingModalAtom } from '../../../atoms';
|
||||||
|
import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
|
||||||
|
import { EnableAffineCloudModal } from '../enable-affine-cloud-modal';
|
||||||
|
import { TmpDisableAffineCloudModal } from '../tmp-disable-affine-cloud-modal';
|
||||||
|
import type { WorkspaceSettingDetailProps } from './types';
|
||||||
|
|
||||||
|
export interface PublishPanelProps extends WorkspaceSettingDetailProps {
|
||||||
|
workspace: Workspace | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EnableCloudPanel = ({
|
||||||
|
workspaceMetadata,
|
||||||
|
workspace,
|
||||||
|
}: PublishPanelProps) => {
|
||||||
|
const t = useAFFiNEI18N();
|
||||||
|
|
||||||
|
const { openPage } = useNavigateHelper();
|
||||||
|
|
||||||
|
const workspaceManager = useAtomValue(workspaceManagerAtom);
|
||||||
|
const workspaceInfo = useWorkspaceInfo(workspaceMetadata);
|
||||||
|
const setSettingModal = useSetAtom(openSettingModalAtom);
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const handleEnableCloud = useAsyncCallback(async () => {
|
||||||
|
if (!workspace) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { id: newId } =
|
||||||
|
await workspaceManager.transformLocalToCloud(workspace);
|
||||||
|
openPage(newId, WorkspaceSubPath.ALL);
|
||||||
|
setOpen(false);
|
||||||
|
setSettingModal(settings => ({
|
||||||
|
...settings,
|
||||||
|
open: false,
|
||||||
|
}));
|
||||||
|
}, [openPage, setSettingModal, workspace, workspaceManager]);
|
||||||
|
|
||||||
|
if (workspaceMetadata.flavour !== WorkspaceFlavour.LOCAL) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SettingRow
|
||||||
|
name={t['Workspace saved locally']({
|
||||||
|
name: workspaceInfo?.name ?? UNTITLED_WORKSPACE_NAME,
|
||||||
|
})}
|
||||||
|
desc={t['Enable cloud hint']()}
|
||||||
|
spreadCol={false}
|
||||||
|
style={{
|
||||||
|
padding: '10px',
|
||||||
|
background: 'var(--affine-background-secondary-color)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
data-testid="publish-enable-affine-cloud-button"
|
||||||
|
type="primary"
|
||||||
|
onClick={() => {
|
||||||
|
setOpen(true);
|
||||||
|
}}
|
||||||
|
style={{ marginTop: '12px' }}
|
||||||
|
>
|
||||||
|
{t['Enable AFFiNE Cloud']()}
|
||||||
|
</Button>
|
||||||
|
</SettingRow>
|
||||||
|
{runtimeConfig.enableCloud ? (
|
||||||
|
<EnableAffineCloudModal
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
onConfirm={handleEnableCloud}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<TmpDisableAffineCloudModal open={open} onOpenChange={setOpen} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,69 +1,35 @@
|
|||||||
import { pushNotificationAtom } from '@affine/component/notification-center';
|
import { pushNotificationAtom } from '@affine/component/notification-center';
|
||||||
import { SettingRow } from '@affine/component/setting-components';
|
import { SettingRow } from '@affine/component/setting-components';
|
||||||
import { Button } from '@affine/component/ui/button';
|
import { Button } from '@affine/component/ui/button';
|
||||||
import type { AffineOfficialWorkspace } from '@affine/env/workspace';
|
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
|
import type { Workspace, WorkspaceMetadata } from '@affine/workspace';
|
||||||
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||||
import type { SaveDBFileResult } from '@toeverything/infra/type';
|
import type { SaveDBFileResult } from '@toeverything/infra/type';
|
||||||
import { useSetAtom } from 'jotai';
|
import { useSetAtom } from 'jotai';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import type { Doc } from 'yjs';
|
|
||||||
import { encodeStateAsUpdate } from 'yjs';
|
|
||||||
|
|
||||||
async function syncBlobsToSqliteDb(workspace: AffineOfficialWorkspace) {
|
|
||||||
if (window.apis && environment.isDesktop) {
|
|
||||||
const bs = workspace.blockSuiteWorkspace.blob;
|
|
||||||
const blobsInDb = await window.apis.db.getBlobKeys(workspace.id);
|
|
||||||
const blobsInStorage = await bs.list();
|
|
||||||
const blobsToSync = blobsInStorage.filter(
|
|
||||||
blob => !blobsInDb.includes(blob)
|
|
||||||
);
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
blobsToSync.map(async blobKey => {
|
|
||||||
const blob = await bs.get(blobKey);
|
|
||||||
if (blob) {
|
|
||||||
const bin = new Uint8Array(await blob.arrayBuffer());
|
|
||||||
await window.apis.db.addBlob(workspace.id, blobKey, bin);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function syncDocsToSqliteDb(workspace: AffineOfficialWorkspace) {
|
|
||||||
if (window.apis && environment.isDesktop) {
|
|
||||||
const workspaceId = workspace.blockSuiteWorkspace.doc.guid;
|
|
||||||
const syncDoc = async (doc: Doc) => {
|
|
||||||
await window.apis.db.applyDocUpdate(
|
|
||||||
workspace.id,
|
|
||||||
encodeStateAsUpdate(doc),
|
|
||||||
doc.guid === workspaceId ? undefined : doc.guid
|
|
||||||
);
|
|
||||||
await Promise.all([...doc.subdocs].map(subdoc => syncDoc(subdoc)));
|
|
||||||
};
|
|
||||||
|
|
||||||
return syncDoc(workspace.blockSuiteWorkspace.doc);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ExportPanelProps {
|
interface ExportPanelProps {
|
||||||
workspace: AffineOfficialWorkspace;
|
workspaceMetadata: WorkspaceMetadata;
|
||||||
|
workspace: Workspace | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ExportPanel = ({ workspace }: ExportPanelProps) => {
|
export const ExportPanel = ({
|
||||||
const workspaceId = workspace.id;
|
workspaceMetadata,
|
||||||
|
workspace,
|
||||||
|
}: ExportPanelProps) => {
|
||||||
|
const workspaceId = workspaceMetadata.id;
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const [syncing, setSyncing] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
||||||
const pushNotification = useSetAtom(pushNotificationAtom);
|
const pushNotification = useSetAtom(pushNotificationAtom);
|
||||||
const onExport = useAsyncCallback(async () => {
|
const onExport = useAsyncCallback(async () => {
|
||||||
if (syncing) {
|
if (saving || !workspace) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setSyncing(true);
|
setSaving(true);
|
||||||
try {
|
try {
|
||||||
await syncBlobsToSqliteDb(workspace);
|
await workspace.engine.sync.waitForSynced();
|
||||||
await syncDocsToSqliteDb(workspace);
|
await workspace.engine.blob.sync();
|
||||||
const result: SaveDBFileResult =
|
const result: SaveDBFileResult =
|
||||||
await window.apis?.dialog.saveDBFileAs(workspaceId);
|
await window.apis?.dialog.saveDBFileAs(workspaceId);
|
||||||
if (result?.error) {
|
if (result?.error) {
|
||||||
@@ -81,16 +47,16 @@ export const ExportPanel = ({ workspace }: ExportPanelProps) => {
|
|||||||
message: e.message,
|
message: e.message,
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setSyncing(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
}, [pushNotification, syncing, t, workspace, workspaceId]);
|
}, [pushNotification, saving, t, workspace, workspaceId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingRow name={t['Export']()} desc={t['Export Description']()}>
|
<SettingRow name={t['Export']()} desc={t['Export Description']()}>
|
||||||
<Button
|
<Button
|
||||||
data-testid="export-affine-backup"
|
data-testid="export-affine-backup"
|
||||||
onClick={onExport}
|
onClick={onExport}
|
||||||
disabled={syncing}
|
disabled={saving}
|
||||||
>
|
>
|
||||||
{t['Export']()}
|
{t['Export']()}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -3,47 +3,38 @@ import {
|
|||||||
SettingRow,
|
SettingRow,
|
||||||
SettingWrapper,
|
SettingWrapper,
|
||||||
} from '@affine/component/setting-components';
|
} from '@affine/component/setting-components';
|
||||||
|
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
|
import { useWorkspace } from '@toeverything/hooks/use-workspace';
|
||||||
import { useMemo } from 'react';
|
import { useWorkspaceInfo } from '@toeverything/hooks/use-workspace-info';
|
||||||
|
|
||||||
import { useSelfHosted } from '../../../hooks/affine/use-server-flavor';
|
import { useSelfHosted } from '../../../hooks/affine/use-server-flavor';
|
||||||
import { useWorkspace } from '../../../hooks/use-workspace';
|
|
||||||
import { DeleteLeaveWorkspace } from './delete-leave-workspace';
|
import { DeleteLeaveWorkspace } from './delete-leave-workspace';
|
||||||
|
import { EnableCloudPanel } from './enable-cloud';
|
||||||
import { ExportPanel } from './export';
|
import { ExportPanel } from './export';
|
||||||
import { LabelsPanel } from './labels';
|
import { LabelsPanel } from './labels';
|
||||||
import { MembersPanel } from './members';
|
import { MembersPanel } from './members';
|
||||||
import { ProfilePanel } from './profile';
|
import { ProfilePanel } from './profile';
|
||||||
import { PublishPanel } from './publish';
|
|
||||||
import { StoragePanel } from './storage';
|
import { StoragePanel } from './storage';
|
||||||
import type { WorkspaceSettingDetailProps } from './types';
|
import type { WorkspaceSettingDetailProps } from './types';
|
||||||
|
|
||||||
export const WorkspaceSettingDetail = (props: WorkspaceSettingDetailProps) => {
|
export const WorkspaceSettingDetail = (props: WorkspaceSettingDetailProps) => {
|
||||||
const { workspaceId } = props;
|
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const isSelfHosted = useSelfHosted();
|
const isSelfHosted = useSelfHosted();
|
||||||
const workspace = useWorkspace(workspaceId);
|
const workspaceMetadata = props.workspaceMetadata;
|
||||||
const [name] = useBlockSuiteWorkspaceName(workspace.blockSuiteWorkspace);
|
|
||||||
|
|
||||||
const storageAndExportSetting = useMemo(() => {
|
// useWorkspace hook is a vary heavy operation here, but we need syncing name and avatar changes here,
|
||||||
if (environment.isDesktop) {
|
// we don't have a better way to do this now
|
||||||
return (
|
const workspace = useWorkspace(workspaceMetadata);
|
||||||
<SettingWrapper title={t['Storage and Export']()}>
|
|
||||||
{runtimeConfig.enableMoveDatabase ? (
|
const workspaceInfo = useWorkspaceInfo(workspaceMetadata);
|
||||||
<StoragePanel workspace={workspace} />
|
|
||||||
) : null}
|
|
||||||
<ExportPanel workspace={workspace} />
|
|
||||||
</SettingWrapper>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}, [t, workspace]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SettingHeader
|
<SettingHeader
|
||||||
title={t[`Workspace Settings with name`]({ name })}
|
title={t[`Workspace Settings with name`]({
|
||||||
|
name: workspaceInfo?.name ?? UNTITLED_WORKSPACE_NAME,
|
||||||
|
})}
|
||||||
subtitle={t['com.affine.settings.workspace.description']()}
|
subtitle={t['com.affine.settings.workspace.description']()}
|
||||||
/>
|
/>
|
||||||
<SettingWrapper title={t['Info']()}>
|
<SettingWrapper title={t['Info']()}>
|
||||||
@@ -53,20 +44,26 @@ export const WorkspaceSettingDetail = (props: WorkspaceSettingDetailProps) => {
|
|||||||
spreadCol={false}
|
spreadCol={false}
|
||||||
>
|
>
|
||||||
<ProfilePanel workspace={workspace} {...props} />
|
<ProfilePanel workspace={workspace} {...props} />
|
||||||
<LabelsPanel workspace={workspace} {...props} />
|
<LabelsPanel {...props} />
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
</SettingWrapper>
|
</SettingWrapper>
|
||||||
<SettingWrapper title={t['com.affine.brand.affineCloud']()}>
|
<SettingWrapper title={t['com.affine.brand.affineCloud']()}>
|
||||||
<PublishPanel workspace={workspace} {...props} />
|
<EnableCloudPanel workspace={workspace} {...props} />
|
||||||
<MembersPanel
|
<MembersPanel upgradable={!isSelfHosted} {...props} />
|
||||||
workspace={workspace}
|
|
||||||
upgradable={!isSelfHosted}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</SettingWrapper>
|
</SettingWrapper>
|
||||||
{storageAndExportSetting}
|
{environment.isDesktop && (
|
||||||
|
<SettingWrapper title={t['Storage and Export']()}>
|
||||||
|
{runtimeConfig.enableMoveDatabase ? (
|
||||||
|
<StoragePanel workspaceMetadata={workspaceMetadata} />
|
||||||
|
) : null}
|
||||||
|
<ExportPanel
|
||||||
|
workspace={workspace}
|
||||||
|
workspaceMetadata={workspaceMetadata}
|
||||||
|
/>
|
||||||
|
</SettingWrapper>
|
||||||
|
)}
|
||||||
<SettingWrapper>
|
<SettingWrapper>
|
||||||
<DeleteLeaveWorkspace workspace={workspace} {...props} />
|
<DeleteLeaveWorkspace {...props} />
|
||||||
</SettingWrapper>
|
</SettingWrapper>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
import type { AffineOfficialWorkspace } from '@affine/env/workspace';
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import * as style from './style.css';
|
import * as style from './style.css';
|
||||||
import type { WorkspaceSettingDetailProps } from './types';
|
import type { WorkspaceSettingDetailProps } from './types';
|
||||||
|
|
||||||
export interface LabelsPanelProps extends WorkspaceSettingDetailProps {
|
export interface LabelsPanelProps extends WorkspaceSettingDetailProps {}
|
||||||
workspace: AffineOfficialWorkspace;
|
|
||||||
}
|
|
||||||
|
|
||||||
type WorkspaceStatus =
|
type WorkspaceStatus =
|
||||||
| 'local'
|
| 'local'
|
||||||
@@ -38,7 +35,10 @@ const Label = ({ value, background }: LabelProps) => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
export const LabelsPanel = ({ workspace, isOwner }: LabelsPanelProps) => {
|
export const LabelsPanel = ({
|
||||||
|
workspaceMetadata,
|
||||||
|
isOwner,
|
||||||
|
}: LabelsPanelProps) => {
|
||||||
const labelMap: LabelMap = useMemo(
|
const labelMap: LabelMap = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
local: {
|
local: {
|
||||||
@@ -74,11 +74,10 @@ export const LabelsPanel = ({ workspace, isOwner }: LabelsPanelProps) => {
|
|||||||
);
|
);
|
||||||
const labelConditions: labelConditionsProps[] = [
|
const labelConditions: labelConditionsProps[] = [
|
||||||
{ condition: !isOwner, label: 'joinedWorkspace' },
|
{ condition: !isOwner, label: 'joinedWorkspace' },
|
||||||
{ condition: workspace.flavour === 'local', label: 'local' },
|
{ condition: workspaceMetadata.flavour === 'local', label: 'local' },
|
||||||
{ condition: workspace.flavour === 'affine-cloud', label: 'syncCloud' },
|
|
||||||
{
|
{
|
||||||
condition: workspace.flavour === 'affine-public',
|
condition: workspaceMetadata.flavour === 'affine-cloud',
|
||||||
label: 'publishedToWeb',
|
label: 'syncCloud',
|
||||||
},
|
},
|
||||||
//TODO: add these labels
|
//TODO: add these labels
|
||||||
// { status==="synced", label: 'availableOffline' }
|
// { status==="synced", label: 'availableOffline' }
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import { Button, IconButton } from '@affine/component/ui/button';
|
|||||||
import { Loading } from '@affine/component/ui/loading';
|
import { Loading } from '@affine/component/ui/loading';
|
||||||
import { Menu, MenuItem } from '@affine/component/ui/menu';
|
import { Menu, MenuItem } from '@affine/component/ui/menu';
|
||||||
import { Tooltip } from '@affine/component/ui/tooltip';
|
import { Tooltip } from '@affine/component/ui/tooltip';
|
||||||
import type { AffineOfficialWorkspace } from '@affine/env/workspace';
|
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
import { Permission } from '@affine/graphql';
|
import { Permission } from '@affine/graphql';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
@@ -45,7 +44,6 @@ import type { WorkspaceSettingDetailProps } from './types';
|
|||||||
const COUNT_PER_PAGE = 8;
|
const COUNT_PER_PAGE = 8;
|
||||||
export interface MembersPanelProps extends WorkspaceSettingDetailProps {
|
export interface MembersPanelProps extends WorkspaceSettingDetailProps {
|
||||||
upgradable: boolean;
|
upgradable: boolean;
|
||||||
workspace: AffineOfficialWorkspace;
|
|
||||||
}
|
}
|
||||||
type OnRevoke = (memberId: string) => void;
|
type OnRevoke = (memberId: string) => void;
|
||||||
const MembersPanelLocal = () => {
|
const MembersPanelLocal = () => {
|
||||||
@@ -62,11 +60,11 @@ const MembersPanelLocal = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const CloudWorkspaceMembersPanel = ({
|
export const CloudWorkspaceMembersPanel = ({
|
||||||
workspace,
|
|
||||||
isOwner,
|
isOwner,
|
||||||
upgradable,
|
upgradable,
|
||||||
|
workspaceMetadata,
|
||||||
}: MembersPanelProps) => {
|
}: MembersPanelProps) => {
|
||||||
const workspaceId = workspace.id;
|
const workspaceId = workspaceMetadata.id;
|
||||||
const memberCount = useMemberCount(workspaceId);
|
const memberCount = useMemberCount(workspaceId);
|
||||||
|
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
@@ -138,7 +136,6 @@ export const CloudWorkspaceMembersPanel = ({
|
|||||||
setSettingModalAtom({
|
setSettingModalAtom({
|
||||||
open: true,
|
open: true,
|
||||||
activeTab: 'plans',
|
activeTab: 'plans',
|
||||||
workspaceId: null,
|
|
||||||
});
|
});
|
||||||
}, [setSettingModalAtom]);
|
}, [setSettingModalAtom]);
|
||||||
|
|
||||||
@@ -345,7 +342,7 @@ const MemberItem = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const MembersPanel = (props: MembersPanelProps): ReactElement | null => {
|
export const MembersPanel = (props: MembersPanelProps): ReactElement | null => {
|
||||||
if (props.workspace.flavour === WorkspaceFlavour.LOCAL) {
|
if (props.workspaceMetadata.flavour === WorkspaceFlavour.LOCAL) {
|
||||||
return <MembersPanelLocal />;
|
return <MembersPanelLocal />;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -2,51 +2,120 @@ import { FlexWrapper, Input, Wrapper } from '@affine/component';
|
|||||||
import { pushNotificationAtom } from '@affine/component/notification-center';
|
import { pushNotificationAtom } from '@affine/component/notification-center';
|
||||||
import { Avatar } from '@affine/component/ui/avatar';
|
import { Avatar } from '@affine/component/ui/avatar';
|
||||||
import { Button } from '@affine/component/ui/button';
|
import { Button } from '@affine/component/ui/button';
|
||||||
import type { AffineOfficialWorkspace } from '@affine/env/workspace';
|
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
|
import type { Workspace } from '@affine/workspace';
|
||||||
|
import { SyncPeerStep } from '@affine/workspace';
|
||||||
import { CameraIcon } from '@blocksuite/icons';
|
import { CameraIcon } from '@blocksuite/icons';
|
||||||
import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-block-suite-workspace-avatar-url';
|
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||||
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
|
import { useWorkspaceBlobObjectUrl } from '@toeverything/hooks/use-workspace-blob';
|
||||||
|
import { useWorkspaceStatus } from '@toeverything/hooks/use-workspace-status';
|
||||||
import { useSetAtom } from 'jotai';
|
import { useSetAtom } from 'jotai';
|
||||||
import {
|
import {
|
||||||
type KeyboardEvent,
|
type KeyboardEvent,
|
||||||
type MouseEvent,
|
type MouseEvent,
|
||||||
startTransition,
|
startTransition,
|
||||||
useCallback,
|
useCallback,
|
||||||
|
useEffect,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
|
import { validateAndReduceImage } from '../../../utils/reduce-image';
|
||||||
import { Upload } from '../../pure/file-upload';
|
import { Upload } from '../../pure/file-upload';
|
||||||
import * as style from './style.css';
|
import * as style from './style.css';
|
||||||
import type { WorkspaceSettingDetailProps } from './types';
|
import type { WorkspaceSettingDetailProps } from './types';
|
||||||
|
|
||||||
export interface ProfilePanelProps extends WorkspaceSettingDetailProps {
|
export interface ProfilePanelProps extends WorkspaceSettingDetailProps {
|
||||||
workspace: AffineOfficialWorkspace;
|
workspace: Workspace | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProfilePanel = ({ workspace, isOwner }: ProfilePanelProps) => {
|
export const ProfilePanel = ({ isOwner, workspace }: ProfilePanelProps) => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const pushNotification = useSetAtom(pushNotificationAtom);
|
const pushNotification = useSetAtom(pushNotificationAtom);
|
||||||
|
|
||||||
const [workspaceAvatar, update] = useBlockSuiteWorkspaceAvatarUrl(
|
const workspaceIsLoading =
|
||||||
workspace.blockSuiteWorkspace
|
useWorkspaceStatus(
|
||||||
|
workspace,
|
||||||
|
status =>
|
||||||
|
!status.engine.sync.local ||
|
||||||
|
status.engine.sync.local?.step <= SyncPeerStep.LoadingRootDoc
|
||||||
|
) ?? true;
|
||||||
|
|
||||||
|
const [avatarBlob, setAvatarBlob] = useState<string | null>(null);
|
||||||
|
const [name, setName] = useState('');
|
||||||
|
|
||||||
|
const avatarUrl = useWorkspaceBlobObjectUrl(workspace?.meta, avatarBlob);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (workspace?.blockSuiteWorkspace) {
|
||||||
|
setAvatarBlob(workspace.blockSuiteWorkspace.meta.avatar ?? null);
|
||||||
|
setName(
|
||||||
|
workspace.blockSuiteWorkspace.meta.name ?? UNTITLED_WORKSPACE_NAME
|
||||||
|
);
|
||||||
|
const dispose = workspace.blockSuiteWorkspace.meta.commonFieldsUpdated.on(
|
||||||
|
() => {
|
||||||
|
setAvatarBlob(workspace.blockSuiteWorkspace.meta.avatar ?? null);
|
||||||
|
setName(
|
||||||
|
workspace.blockSuiteWorkspace.meta.name ?? UNTITLED_WORKSPACE_NAME
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return () => {
|
||||||
|
dispose.dispose();
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
setAvatarBlob(null);
|
||||||
|
setName(UNTITLED_WORKSPACE_NAME);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}, [workspace]);
|
||||||
|
|
||||||
|
const setWorkspaceAvatar = useCallback(
|
||||||
|
async (file: File | null) => {
|
||||||
|
if (!workspace) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!file) {
|
||||||
|
workspace.blockSuiteWorkspace.meta.setAvatar('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const reducedFile = await validateAndReduceImage(file);
|
||||||
|
const blobs = workspace.blockSuiteWorkspace.blob;
|
||||||
|
const blobId = await blobs.set(reducedFile);
|
||||||
|
workspace.blockSuiteWorkspace.meta.setAvatar(blobId);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[workspace]
|
||||||
);
|
);
|
||||||
|
|
||||||
const [name, setName] = useBlockSuiteWorkspaceName(
|
const setWorkspaceName = useCallback(
|
||||||
workspace.blockSuiteWorkspace
|
(name: string) => {
|
||||||
|
if (!workspace) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
workspace.blockSuiteWorkspace.meta.setName(name);
|
||||||
|
},
|
||||||
|
[workspace]
|
||||||
);
|
);
|
||||||
|
|
||||||
const [input, setInput] = useState<string>(name);
|
const [input, setInput] = useState<string>('');
|
||||||
|
useEffect(() => {
|
||||||
|
setInput(name);
|
||||||
|
}, [name]);
|
||||||
|
|
||||||
const handleUpdateWorkspaceName = useCallback(
|
const handleUpdateWorkspaceName = useCallback(
|
||||||
(name: string) => {
|
(name: string) => {
|
||||||
setName(name);
|
setWorkspaceName(name);
|
||||||
pushNotification({
|
pushNotification({
|
||||||
title: t['Update workspace name success'](),
|
title: t['Update workspace name success'](),
|
||||||
type: 'success',
|
type: 'success',
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[pushNotification, setName, t]
|
[pushNotification, setWorkspaceName, t]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSetInput = useCallback((value: string) => {
|
const handleSetInput = useCallback((value: string) => {
|
||||||
@@ -68,17 +137,17 @@ export const ProfilePanel = ({ workspace, isOwner }: ProfilePanelProps) => {
|
|||||||
handleUpdateWorkspaceName(input);
|
handleUpdateWorkspaceName(input);
|
||||||
}, [handleUpdateWorkspaceName, input]);
|
}, [handleUpdateWorkspaceName, input]);
|
||||||
|
|
||||||
const handleRemoveUserAvatar = useCallback(
|
const handleRemoveUserAvatar = useAsyncCallback(
|
||||||
async (e: MouseEvent<HTMLButtonElement>) => {
|
async (e: MouseEvent<HTMLButtonElement>) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
await update(null);
|
await setWorkspaceAvatar(null);
|
||||||
},
|
},
|
||||||
[update]
|
[setWorkspaceAvatar]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleUploadAvatar = useCallback(
|
const handleUploadAvatar = useCallback(
|
||||||
(file: File) => {
|
(file: File) => {
|
||||||
update(file)
|
setWorkspaceAvatar(file)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
pushNotification({
|
pushNotification({
|
||||||
title: 'Update workspace avatar success',
|
title: 'Update workspace avatar success',
|
||||||
@@ -93,10 +162,10 @@ export const ProfilePanel = ({ workspace, isOwner }: ProfilePanelProps) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[pushNotification, update]
|
[pushNotification, setWorkspaceAvatar]
|
||||||
);
|
);
|
||||||
|
|
||||||
const canAdjustAvatar = workspaceAvatar && isOwner;
|
const canAdjustAvatar = !workspaceIsLoading && avatarUrl && isOwner;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.profileWrapper}>
|
<div className={style.profileWrapper}>
|
||||||
@@ -108,7 +177,7 @@ export const ProfilePanel = ({ workspace, isOwner }: ProfilePanelProps) => {
|
|||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
size={56}
|
size={56}
|
||||||
url={workspaceAvatar}
|
url={avatarUrl}
|
||||||
name={name}
|
name={name}
|
||||||
colorfulFallback
|
colorfulFallback
|
||||||
hoverIcon={isOwner ? <CameraIcon /> : undefined}
|
hoverIcon={isOwner ? <CameraIcon /> : undefined}
|
||||||
@@ -132,10 +201,10 @@ export const ProfilePanel = ({ workspace, isOwner }: ProfilePanelProps) => {
|
|||||||
<div className={style.label}>{t['Workspace Name']()}</div>
|
<div className={style.label}>{t['Workspace Name']()}</div>
|
||||||
<FlexWrapper alignItems="center" flexGrow="1">
|
<FlexWrapper alignItems="center" flexGrow="1">
|
||||||
<Input
|
<Input
|
||||||
disabled={!isOwner}
|
disabled={workspaceIsLoading || !isOwner}
|
||||||
width={280}
|
width={280}
|
||||||
height={32}
|
height={32}
|
||||||
defaultValue={input}
|
value={input}
|
||||||
data-testid="workspace-name-input"
|
data-testid="workspace-name-input"
|
||||||
placeholder={t['Workspace Name']()}
|
placeholder={t['Workspace Name']()}
|
||||||
maxLength={64}
|
maxLength={64}
|
||||||
@@ -143,7 +212,7 @@ export const ProfilePanel = ({ workspace, isOwner }: ProfilePanelProps) => {
|
|||||||
onChange={handleSetInput}
|
onChange={handleSetInput}
|
||||||
onKeyUp={handleKeyUp}
|
onKeyUp={handleKeyUp}
|
||||||
/>
|
/>
|
||||||
{input === workspace.blockSuiteWorkspace.meta.name ? null : (
|
{input === name ? null : (
|
||||||
<Button
|
<Button
|
||||||
data-testid="save-workspace-name"
|
data-testid="save-workspace-name"
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
|
|||||||
@@ -1,169 +0,0 @@
|
|||||||
import { FlexWrapper, Input, Switch } from '@affine/component';
|
|
||||||
import { SettingRow } from '@affine/component/setting-components';
|
|
||||||
import { Button } from '@affine/component/ui/button';
|
|
||||||
import { Tooltip } from '@affine/component/ui/tooltip';
|
|
||||||
import { Unreachable } from '@affine/env/constant';
|
|
||||||
import type {
|
|
||||||
AffineCloudWorkspace,
|
|
||||||
AffinePublicWorkspace,
|
|
||||||
LocalWorkspace,
|
|
||||||
} from '@affine/env/workspace';
|
|
||||||
import type { AffineOfficialWorkspace } from '@affine/env/workspace';
|
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
|
||||||
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
|
||||||
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
|
|
||||||
import { noop } from 'foxact/noop';
|
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
|
||||||
|
|
||||||
import { toast } from '../../../utils';
|
|
||||||
import { EnableAffineCloudModal } from '../enable-affine-cloud-modal';
|
|
||||||
import { TmpDisableAffineCloudModal } from '../tmp-disable-affine-cloud-modal';
|
|
||||||
import * as style from './style.css';
|
|
||||||
import type { WorkspaceSettingDetailProps } from './types';
|
|
||||||
|
|
||||||
export interface PublishPanelProps
|
|
||||||
extends Omit<WorkspaceSettingDetailProps, 'workspaceId'> {
|
|
||||||
workspace: AffineOfficialWorkspace;
|
|
||||||
}
|
|
||||||
export interface PublishPanelLocalProps
|
|
||||||
extends Omit<WorkspaceSettingDetailProps, 'workspaceId'> {
|
|
||||||
workspace: LocalWorkspace;
|
|
||||||
}
|
|
||||||
export interface PublishPanelAffineProps
|
|
||||||
extends Omit<WorkspaceSettingDetailProps, 'workspaceId'> {
|
|
||||||
workspace: AffineCloudWorkspace | AffinePublicWorkspace;
|
|
||||||
}
|
|
||||||
|
|
||||||
const PublishPanelAffine = (props: PublishPanelAffineProps) => {
|
|
||||||
const { workspace } = props;
|
|
||||||
const t = useAFFiNEI18N();
|
|
||||||
// const toggleWorkspacePublish = useToggleWorkspacePublish(workspace);
|
|
||||||
const isPublic = useMemo(() => {
|
|
||||||
return workspace.flavour === WorkspaceFlavour.AFFINE_PUBLIC;
|
|
||||||
}, [workspace]);
|
|
||||||
const [origin, setOrigin] = useState('');
|
|
||||||
const shareUrl = origin + '/public-workspace/' + workspace.id;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setOrigin(
|
|
||||||
typeof window !== 'undefined' && window.location.origin
|
|
||||||
? window.location.origin
|
|
||||||
: ''
|
|
||||||
);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const copyUrl = useAsyncCallback(async () => {
|
|
||||||
await navigator.clipboard.writeText(shareUrl);
|
|
||||||
toast(t['Copied link to clipboard']());
|
|
||||||
}, [shareUrl, t]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ display: 'none' }}>
|
|
||||||
<SettingRow
|
|
||||||
name={t['Publish']()}
|
|
||||||
desc={isPublic ? t['Unpublished hint']() : t['Published hint']()}
|
|
||||||
style={{
|
|
||||||
marginBottom: isPublic ? '12px' : '25px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Switch checked={isPublic} />
|
|
||||||
</SettingRow>
|
|
||||||
{isPublic ? (
|
|
||||||
<FlexWrapper justifyContent="space-between" marginBottom={25}>
|
|
||||||
<Input value={shareUrl} disabled />
|
|
||||||
<Button
|
|
||||||
onClick={copyUrl}
|
|
||||||
style={{
|
|
||||||
marginLeft: '20px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t['Copy']()}
|
|
||||||
</Button>
|
|
||||||
</FlexWrapper>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface FakePublishPanelAffineProps {
|
|
||||||
workspace: AffineOfficialWorkspace;
|
|
||||||
}
|
|
||||||
|
|
||||||
const FakePublishPanelAffine = (_props: FakePublishPanelAffineProps) => {
|
|
||||||
const t = useAFFiNEI18N();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tooltip content={t['com.affine.settings.workspace.publish-tooltip']()}>
|
|
||||||
<div className={style.fakeWrapper}>
|
|
||||||
<SettingRow name={t['Publish']()} desc={t['Unpublished hint']()}>
|
|
||||||
<Switch checked={false} onChange={noop} />
|
|
||||||
</SettingRow>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const PublishPanelLocal = ({
|
|
||||||
workspace,
|
|
||||||
onTransferWorkspace,
|
|
||||||
}: PublishPanelLocalProps) => {
|
|
||||||
const t = useAFFiNEI18N();
|
|
||||||
const [name] = useBlockSuiteWorkspaceName(workspace.blockSuiteWorkspace);
|
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<SettingRow
|
|
||||||
name={t['Workspace saved locally']({ name })}
|
|
||||||
desc={t['Enable cloud hint']()}
|
|
||||||
spreadCol={false}
|
|
||||||
style={{
|
|
||||||
padding: '10px',
|
|
||||||
background: 'var(--affine-background-secondary-color)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
data-testid="publish-enable-affine-cloud-button"
|
|
||||||
type="primary"
|
|
||||||
onClick={() => {
|
|
||||||
setOpen(true);
|
|
||||||
}}
|
|
||||||
style={{ marginTop: '12px' }}
|
|
||||||
>
|
|
||||||
{t['Enable AFFiNE Cloud']()}
|
|
||||||
</Button>
|
|
||||||
</SettingRow>
|
|
||||||
<FakePublishPanelAffine workspace={workspace} />
|
|
||||||
{runtimeConfig.enableCloud ? (
|
|
||||||
<EnableAffineCloudModal
|
|
||||||
open={open}
|
|
||||||
onOpenChange={setOpen}
|
|
||||||
onConfirm={() => {
|
|
||||||
onTransferWorkspace(
|
|
||||||
WorkspaceFlavour.LOCAL,
|
|
||||||
WorkspaceFlavour.AFFINE_CLOUD,
|
|
||||||
workspace
|
|
||||||
);
|
|
||||||
setOpen(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<TmpDisableAffineCloudModal open={open} onOpenChange={setOpen} />
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const PublishPanel = (props: PublishPanelProps) => {
|
|
||||||
if (
|
|
||||||
props.workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD ||
|
|
||||||
props.workspace.flavour === WorkspaceFlavour.AFFINE_PUBLIC
|
|
||||||
) {
|
|
||||||
return <PublishPanelAffine {...props} workspace={props.workspace} />;
|
|
||||||
} else if (props.workspace.flavour === WorkspaceFlavour.LOCAL) {
|
|
||||||
return <PublishPanelLocal {...props} workspace={props.workspace} />;
|
|
||||||
}
|
|
||||||
throw new Unreachable();
|
|
||||||
};
|
|
||||||
@@ -2,8 +2,8 @@ import { FlexWrapper, toast } from '@affine/component';
|
|||||||
import { SettingRow } from '@affine/component/setting-components';
|
import { SettingRow } from '@affine/component/setting-components';
|
||||||
import { Button } from '@affine/component/ui/button';
|
import { Button } from '@affine/component/ui/button';
|
||||||
import { Tooltip } from '@affine/component/ui/tooltip';
|
import { Tooltip } from '@affine/component/ui/tooltip';
|
||||||
import type { AffineOfficialWorkspace } from '@affine/env/workspace';
|
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
|
import type { WorkspaceMetadata } from '@affine/workspace/metadata';
|
||||||
import type { MoveDBFileResult } from '@toeverything/infra/type';
|
import type { MoveDBFileResult } from '@toeverything/infra/type';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
@@ -33,11 +33,11 @@ const useDBFileSecondaryPath = (workspaceId: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface StoragePanelProps {
|
interface StoragePanelProps {
|
||||||
workspace: AffineOfficialWorkspace;
|
workspaceMetadata: WorkspaceMetadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StoragePanel = ({ workspace }: StoragePanelProps) => {
|
export const StoragePanel = ({ workspaceMetadata }: StoragePanelProps) => {
|
||||||
const workspaceId = workspace.id;
|
const workspaceId = workspaceMetadata.id;
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const secondaryPath = useDBFileSecondaryPath(workspaceId);
|
const secondaryPath = useDBFileSecondaryPath(workspaceId);
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,6 @@
|
|||||||
import type {
|
import type { WorkspaceMetadata } from '@affine/workspace/metadata';
|
||||||
WorkspaceFlavour,
|
|
||||||
WorkspaceRegistry,
|
|
||||||
} from '@affine/env/workspace';
|
|
||||||
|
|
||||||
export interface WorkspaceSettingDetailProps {
|
export interface WorkspaceSettingDetailProps {
|
||||||
workspaceId: string;
|
|
||||||
isOwner: boolean;
|
isOwner: boolean;
|
||||||
onDeleteLocalWorkspace: () => void;
|
workspaceMetadata: WorkspaceMetadata;
|
||||||
onDeleteCloudWorkspace: () => void;
|
|
||||||
onLeaveWorkspace: () => void;
|
|
||||||
onTransferWorkspace: <
|
|
||||||
From extends WorkspaceFlavour,
|
|
||||||
To extends WorkspaceFlavour,
|
|
||||||
>(
|
|
||||||
from: From,
|
|
||||||
to: To,
|
|
||||||
workspace: WorkspaceRegistry[From]
|
|
||||||
) => void;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,13 +5,15 @@ import {
|
|||||||
listHistoryQuery,
|
listHistoryQuery,
|
||||||
recoverDocMutation,
|
recoverDocMutation,
|
||||||
} from '@affine/graphql';
|
} from '@affine/graphql';
|
||||||
|
import {
|
||||||
|
createAffineCloudBlobStorage,
|
||||||
|
globalBlockSuiteSchema,
|
||||||
|
} from '@affine/workspace';
|
||||||
import {
|
import {
|
||||||
useMutateQueryResource,
|
useMutateQueryResource,
|
||||||
useMutation,
|
useMutation,
|
||||||
useQueryInfinite,
|
useQueryInfinite,
|
||||||
} from '@affine/workspace/affine/gql';
|
} from '@affine/workspace/affine/gql';
|
||||||
import { createAffineCloudBlobEngine } from '@affine/workspace/blob';
|
|
||||||
import { globalBlockSuiteSchema } from '@affine/workspace/manager';
|
|
||||||
import { assertEquals } from '@blocksuite/global/utils';
|
import { assertEquals } from '@blocksuite/global/utils';
|
||||||
import { Workspace } from '@blocksuite/store';
|
import { Workspace } from '@blocksuite/store';
|
||||||
import { usePageMetaHelper } from '@toeverything/hooks/use-block-suite-page-meta';
|
import { usePageMetaHelper } from '@toeverything/hooks/use-block-suite-page-meta';
|
||||||
@@ -107,27 +109,13 @@ const workspaceMap = new Map<string, Workspace>();
|
|||||||
const getOrCreateWorkspace = (workspaceId: string) => {
|
const getOrCreateWorkspace = (workspaceId: string) => {
|
||||||
let workspace = workspaceMap.get(workspaceId);
|
let workspace = workspaceMap.get(workspaceId);
|
||||||
if (!workspace) {
|
if (!workspace) {
|
||||||
const blobEngine = createAffineCloudBlobEngine(workspaceId);
|
const blobStorage = createAffineCloudBlobStorage(workspaceId);
|
||||||
workspace = new Workspace({
|
workspace = new Workspace({
|
||||||
id: workspaceId,
|
id: workspaceId,
|
||||||
providerCreators: [],
|
providerCreators: [],
|
||||||
blobStorages: [
|
blobStorages: [
|
||||||
() => ({
|
() => ({
|
||||||
crud: {
|
crud: blobStorage,
|
||||||
async get(key) {
|
|
||||||
return (await blobEngine.get(key)) ?? null;
|
|
||||||
},
|
|
||||||
async set(key, value) {
|
|
||||||
await blobEngine.set(key, value);
|
|
||||||
return key;
|
|
||||||
},
|
|
||||||
async delete(key) {
|
|
||||||
return blobEngine.delete(key);
|
|
||||||
},
|
|
||||||
async list() {
|
|
||||||
return blobEngine.list();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
schema: globalBlockSuiteSchema,
|
schema: globalBlockSuiteSchema,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { Button } from '@affine/component/ui/button';
|
|||||||
import { ConfirmModal, Modal } from '@affine/component/ui/modal';
|
import { ConfirmModal, Modal } from '@affine/component/ui/modal';
|
||||||
import type { PageMode } from '@affine/core/atoms';
|
import type { PageMode } from '@affine/core/atoms';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
|
import { waitForCurrentWorkspaceAtom } from '@affine/workspace/atom';
|
||||||
import type { Workspace } from '@blocksuite/store';
|
import type { Workspace } from '@blocksuite/store';
|
||||||
import type { DialogContentProps } from '@radix-ui/react-dialog';
|
import type { DialogContentProps } from '@radix-ui/react-dialog';
|
||||||
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||||
@@ -22,7 +23,6 @@ import {
|
|||||||
|
|
||||||
import { currentModeAtom } from '../../../atoms/mode';
|
import { currentModeAtom } from '../../../atoms/mode';
|
||||||
import { pageHistoryModalAtom } from '../../../atoms/page-history';
|
import { pageHistoryModalAtom } from '../../../atoms/page-history';
|
||||||
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
|
|
||||||
import { StyledEditorModeSwitch } from '../../blocksuite/block-suite-mode-switch/style';
|
import { StyledEditorModeSwitch } from '../../blocksuite/block-suite-mode-switch/style';
|
||||||
import {
|
import {
|
||||||
EdgelessSwitchItem,
|
EdgelessSwitchItem,
|
||||||
@@ -423,7 +423,7 @@ export const PageHistoryModal = ({
|
|||||||
|
|
||||||
export const GlobalPageHistoryModal = () => {
|
export const GlobalPageHistoryModal = () => {
|
||||||
const [{ open, pageId }, setState] = useAtom(pageHistoryModalAtom);
|
const [{ open, pageId }, setState] = useAtom(pageHistoryModalAtom);
|
||||||
const [workspace] = useCurrentWorkspace();
|
const workspace = useAtomValue(waitForCurrentWorkspaceAtom);
|
||||||
|
|
||||||
const handleOpenChange = useCallback(
|
const handleOpenChange = useCallback(
|
||||||
(open: boolean) => {
|
(open: boolean) => {
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
|||||||
import { useMutation, useQuery } from '@affine/workspace/affine/gql';
|
import { useMutation, useQuery } from '@affine/workspace/affine/gql';
|
||||||
import { ArrowRightSmallIcon, CameraIcon } from '@blocksuite/icons';
|
import { ArrowRightSmallIcon, CameraIcon } from '@blocksuite/icons';
|
||||||
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||||
import { validateAndReduceImage } from '@toeverything/hooks/use-block-suite-workspace-avatar-url';
|
|
||||||
import bytes from 'bytes';
|
import bytes from 'bytes';
|
||||||
import { useSetAtom } from 'jotai';
|
import { useSetAtom } from 'jotai';
|
||||||
import {
|
import {
|
||||||
@@ -37,6 +36,7 @@ import {
|
|||||||
import { useCurrentUser } from '../../../../hooks/affine/use-current-user';
|
import { useCurrentUser } from '../../../../hooks/affine/use-current-user';
|
||||||
import { useSelfHosted } from '../../../../hooks/affine/use-server-flavor';
|
import { useSelfHosted } from '../../../../hooks/affine/use-server-flavor';
|
||||||
import { useUserSubscription } from '../../../../hooks/use-subscription';
|
import { useUserSubscription } from '../../../../hooks/use-subscription';
|
||||||
|
import { validateAndReduceImage } from '../../../../utils/reduce-image';
|
||||||
import { Upload } from '../../../pure/file-upload';
|
import { Upload } from '../../../pure/file-upload';
|
||||||
import * as style from './style.css';
|
import * as style from './style.css';
|
||||||
|
|
||||||
@@ -187,7 +187,6 @@ const StoragePanel = () => {
|
|||||||
setSettingModalAtom({
|
setSettingModalAtom({
|
||||||
open: true,
|
open: true,
|
||||||
activeTab: 'plans',
|
activeTab: 'plans',
|
||||||
workspaceId: null,
|
|
||||||
});
|
});
|
||||||
}, [setSettingModalAtom]);
|
}, [setSettingModalAtom]);
|
||||||
|
|
||||||
|
|||||||
@@ -119,7 +119,6 @@ const SubscriptionSettings = () => {
|
|||||||
setOpenSettingModalAtom({
|
setOpenSettingModalAtom({
|
||||||
open: true,
|
open: true,
|
||||||
activeTab: 'plans',
|
activeTab: 'plans',
|
||||||
workspaceId: null,
|
|
||||||
});
|
});
|
||||||
}, [setOpenSettingModalAtom]);
|
}, [setOpenSettingModalAtom]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { WorkspaceDetailSkeleton } from '@affine/component/setting-components';
|
import { WorkspaceDetailSkeleton } from '@affine/component/setting-components';
|
||||||
import { Modal, type ModalProps } from '@affine/component/ui/modal';
|
import { Modal, type ModalProps } from '@affine/component/ui/modal';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
|
import type { WorkspaceMetadata } from '@affine/workspace/metadata';
|
||||||
import { ContactWithUsIcon } from '@blocksuite/icons';
|
import { ContactWithUsIcon } from '@blocksuite/icons';
|
||||||
import { debounce } from 'lodash-es';
|
import { debounce } from 'lodash-es';
|
||||||
import { Suspense, useCallback, useLayoutEffect, useRef } from 'react';
|
import { Suspense, useCallback, useLayoutEffect, useRef } from 'react';
|
||||||
@@ -20,16 +21,16 @@ type ActiveTab = GeneralSettingKeys | 'workspace' | 'account';
|
|||||||
|
|
||||||
export interface SettingProps extends ModalProps {
|
export interface SettingProps extends ModalProps {
|
||||||
activeTab: ActiveTab;
|
activeTab: ActiveTab;
|
||||||
workspaceId: string | null;
|
workspaceMetadata?: WorkspaceMetadata | null;
|
||||||
onSettingClick: (params: {
|
onSettingClick: (params: {
|
||||||
activeTab: ActiveTab;
|
activeTab: ActiveTab;
|
||||||
workspaceId: string | null;
|
workspaceMetadata: WorkspaceMetadata | null;
|
||||||
}) => void;
|
}) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SettingModal = ({
|
export const SettingModal = ({
|
||||||
activeTab = 'appearance',
|
activeTab = 'appearance',
|
||||||
workspaceId = null,
|
workspaceMetadata = null,
|
||||||
onSettingClick,
|
onSettingClick,
|
||||||
...modalProps
|
...modalProps
|
||||||
}: SettingProps) => {
|
}: SettingProps) => {
|
||||||
@@ -75,22 +76,22 @@ export const SettingModal = ({
|
|||||||
(key: GeneralSettingKeys) => {
|
(key: GeneralSettingKeys) => {
|
||||||
onSettingClick({
|
onSettingClick({
|
||||||
activeTab: key,
|
activeTab: key,
|
||||||
workspaceId: null,
|
workspaceMetadata: null,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[onSettingClick]
|
[onSettingClick]
|
||||||
);
|
);
|
||||||
const onWorkspaceSettingClick = useCallback(
|
const onWorkspaceSettingClick = useCallback(
|
||||||
(workspaceId: string) => {
|
(workspaceMetadata: WorkspaceMetadata) => {
|
||||||
onSettingClick({
|
onSettingClick({
|
||||||
activeTab: 'workspace',
|
activeTab: 'workspace',
|
||||||
workspaceId,
|
workspaceMetadata,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[onSettingClick]
|
[onSettingClick]
|
||||||
);
|
);
|
||||||
const onAccountSettingClick = useCallback(() => {
|
const onAccountSettingClick = useCallback(() => {
|
||||||
onSettingClick({ activeTab: 'account', workspaceId: null });
|
onSettingClick({ activeTab: 'account', workspaceMetadata: null });
|
||||||
}, [onSettingClick]);
|
}, [onSettingClick]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -114,7 +115,7 @@ export const SettingModal = ({
|
|||||||
onGeneralSettingClick={onGeneralSettingClick}
|
onGeneralSettingClick={onGeneralSettingClick}
|
||||||
onWorkspaceSettingClick={onWorkspaceSettingClick}
|
onWorkspaceSettingClick={onWorkspaceSettingClick}
|
||||||
selectedGeneralKey={activeTab}
|
selectedGeneralKey={activeTab}
|
||||||
selectedWorkspaceId={workspaceId}
|
selectedWorkspaceId={workspaceMetadata?.id ?? null}
|
||||||
onAccountSettingClick={onAccountSettingClick}
|
onAccountSettingClick={onAccountSettingClick}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -125,9 +126,12 @@ export const SettingModal = ({
|
|||||||
>
|
>
|
||||||
<div ref={modalContentRef} className={style.centerContainer}>
|
<div ref={modalContentRef} className={style.centerContainer}>
|
||||||
<div className={style.content}>
|
<div className={style.content}>
|
||||||
{activeTab === 'workspace' && workspaceId ? (
|
{activeTab === 'workspace' && workspaceMetadata ? (
|
||||||
<Suspense fallback={<WorkspaceDetailSkeleton />}>
|
<Suspense fallback={<WorkspaceDetailSkeleton />}>
|
||||||
<WorkspaceSetting key={workspaceId} workspaceId={workspaceId} />
|
<WorkspaceSetting
|
||||||
|
key={workspaceMetadata.id}
|
||||||
|
workspaceMetadata={workspaceMetadata}
|
||||||
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
) : null}
|
) : null}
|
||||||
{generalSettingList.some(v => v.key === activeTab) ? (
|
{generalSettingList.some(v => v.key === activeTab) ? (
|
||||||
|
|||||||
@@ -4,22 +4,23 @@ import {
|
|||||||
} from '@affine/component/setting-components';
|
} from '@affine/component/setting-components';
|
||||||
import { Avatar } from '@affine/component/ui/avatar';
|
import { Avatar } from '@affine/component/ui/avatar';
|
||||||
import { Tooltip } from '@affine/component/ui/tooltip';
|
import { Tooltip } from '@affine/component/ui/tooltip';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
|
import type { WorkspaceMetadata } from '@affine/workspace';
|
||||||
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
import {
|
||||||
|
waitForCurrentWorkspaceAtom,
|
||||||
|
workspaceListAtom,
|
||||||
|
} from '@affine/workspace/atom';
|
||||||
import { Logo1Icon } from '@blocksuite/icons';
|
import { Logo1Icon } from '@blocksuite/icons';
|
||||||
import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-block-suite-workspace-avatar-url';
|
import { useWorkspaceBlobObjectUrl } from '@toeverything/hooks/use-workspace-blob';
|
||||||
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
|
import { useWorkspaceInfo } from '@toeverything/hooks/use-workspace-info';
|
||||||
import { getBlockSuiteWorkspaceAtom } from '@toeverything/infra/__internal__/workspace';
|
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useAtom, useAtomValue } from 'jotai/react';
|
import { useAtom, useAtomValue } from 'jotai/react';
|
||||||
import { type ReactElement, Suspense, useCallback, useMemo } from 'react';
|
import { type ReactElement, Suspense, useCallback } from 'react';
|
||||||
|
|
||||||
import { authAtom } from '../../../../atoms';
|
import { authAtom } from '../../../../atoms';
|
||||||
import { useCurrentLoginStatus } from '../../../../hooks/affine/use-current-login-status';
|
import { useCurrentLoginStatus } from '../../../../hooks/affine/use-current-login-status';
|
||||||
import { useCurrentUser } from '../../../../hooks/affine/use-current-user';
|
import { useCurrentUser } from '../../../../hooks/affine/use-current-user';
|
||||||
import { useCurrentWorkspace } from '../../../../hooks/current/use-current-workspace';
|
|
||||||
import { UserPlanButton } from '../../auth/user-plan-button';
|
import { UserPlanButton } from '../../auth/user-plan-button';
|
||||||
import type {
|
import type {
|
||||||
GeneralSettingKeys,
|
GeneralSettingKeys,
|
||||||
@@ -109,7 +110,7 @@ export const SettingSidebar = ({
|
|||||||
}: {
|
}: {
|
||||||
generalSettingList: GeneralSettingList;
|
generalSettingList: GeneralSettingList;
|
||||||
onGeneralSettingClick: (key: GeneralSettingKeys) => void;
|
onGeneralSettingClick: (key: GeneralSettingKeys) => void;
|
||||||
onWorkspaceSettingClick: (workspaceId: string) => void;
|
onWorkspaceSettingClick: (workspaceMetadata: WorkspaceMetadata) => void;
|
||||||
selectedWorkspaceId: string | null;
|
selectedWorkspaceId: string | null;
|
||||||
selectedGeneralKey: string | null;
|
selectedGeneralKey: string | null;
|
||||||
onAccountSettingClick: () => void;
|
onAccountSettingClick: () => void;
|
||||||
@@ -182,25 +183,20 @@ export const WorkspaceList = ({
|
|||||||
onWorkspaceSettingClick,
|
onWorkspaceSettingClick,
|
||||||
selectedWorkspaceId,
|
selectedWorkspaceId,
|
||||||
}: {
|
}: {
|
||||||
onWorkspaceSettingClick: (workspaceId: string) => void;
|
onWorkspaceSettingClick: (workspaceMetadata: WorkspaceMetadata) => void;
|
||||||
selectedWorkspaceId: string | null;
|
selectedWorkspaceId: string | null;
|
||||||
}) => {
|
}) => {
|
||||||
const workspaces = useAtomValue(rootWorkspacesMetadataAtom);
|
const workspaces = useAtomValue(workspaceListAtom);
|
||||||
const [currentWorkspace] = useCurrentWorkspace();
|
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
|
||||||
const workspaceList = useMemo(() => {
|
|
||||||
return workspaces.filter(
|
|
||||||
({ flavour }) => flavour !== WorkspaceFlavour.AFFINE_PUBLIC
|
|
||||||
);
|
|
||||||
}, [workspaces]);
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{workspaceList.map(workspace => {
|
{workspaces.map(workspace => {
|
||||||
return (
|
return (
|
||||||
<Suspense key={workspace.id} fallback={<WorkspaceListItemSkeleton />}>
|
<Suspense key={workspace.id} fallback={<WorkspaceListItemSkeleton />}>
|
||||||
<WorkspaceListItem
|
<WorkspaceListItem
|
||||||
meta={workspace}
|
meta={workspace}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onWorkspaceSettingClick(workspace.id);
|
onWorkspaceSettingClick(workspace);
|
||||||
}}
|
}}
|
||||||
isCurrent={workspace.id === currentWorkspace.id}
|
isCurrent={workspace.id === currentWorkspace.id}
|
||||||
isActive={workspace.id === selectedWorkspaceId}
|
isActive={workspace.id === selectedWorkspaceId}
|
||||||
@@ -218,33 +214,34 @@ const WorkspaceListItem = ({
|
|||||||
isCurrent,
|
isCurrent,
|
||||||
isActive,
|
isActive,
|
||||||
}: {
|
}: {
|
||||||
meta: RootWorkspaceMetadata;
|
meta: WorkspaceMetadata;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
isCurrent: boolean;
|
isCurrent: boolean;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const [workspaceAtom] = getBlockSuiteWorkspaceAtom(meta.id);
|
const information = useWorkspaceInfo(meta);
|
||||||
const workspace = useAtomValue(workspaceAtom);
|
|
||||||
const [workspaceAvatar] = useBlockSuiteWorkspaceAvatarUrl(workspace);
|
const avatarUrl = useWorkspaceBlobObjectUrl(meta, information?.avatar);
|
||||||
const [workspaceName] = useBlockSuiteWorkspaceName(workspace);
|
|
||||||
|
const name = information?.name ?? UNTITLED_WORKSPACE_NAME;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(sidebarSelectItem, { active: isActive })}
|
className={clsx(sidebarSelectItem, { active: isActive })}
|
||||||
title={workspaceName}
|
title={name}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
data-testid="workspace-list-item"
|
data-testid="workspace-list-item"
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
size={14}
|
size={14}
|
||||||
url={workspaceAvatar}
|
url={avatarUrl}
|
||||||
name={workspaceName}
|
name={name}
|
||||||
colorfulFallback
|
colorfulFallback
|
||||||
style={{
|
style={{
|
||||||
marginRight: '10px',
|
marginRight: '10px',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span className="setting-name">{workspaceName}</span>
|
<span className="setting-name">{name}</span>
|
||||||
{isCurrent ? (
|
{isCurrent ? (
|
||||||
<Tooltip content="Current" side="top">
|
<Tooltip content="Current" side="top">
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,113 +1,18 @@
|
|||||||
import { pushNotificationAtom } from '@affine/component/notification-center';
|
import type { WorkspaceMetadata } from '@affine/workspace/metadata';
|
||||||
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';
|
|
||||||
import { useCallback } from 'react';
|
|
||||||
|
|
||||||
import { getUIAdapter } from '../../../../adapters/workspace';
|
import { NewWorkspaceSettingDetail } from '../../../../adapters/shared';
|
||||||
import { openSettingModalAtom } from '../../../../atoms';
|
import { useIsWorkspaceOwner } from '../../../../hooks/affine/use-is-workspace-owner';
|
||||||
import { useLeaveWorkspace } from '../../../../hooks/affine/use-leave-workspace';
|
|
||||||
import { useCurrentWorkspace } from '../../../../hooks/current/use-current-workspace';
|
|
||||||
import { useOnTransformWorkspace } from '../../../../hooks/root/use-on-transform-workspace';
|
|
||||||
import {
|
|
||||||
RouteLogic,
|
|
||||||
useNavigateHelper,
|
|
||||||
} from '../../../../hooks/use-navigate-helper';
|
|
||||||
import { useWorkspace } from '../../../../hooks/use-workspace';
|
|
||||||
import { useAppHelper } from '../../../../hooks/use-workspaces';
|
|
||||||
|
|
||||||
export const WorkspaceSetting = ({ workspaceId }: { workspaceId: string }) => {
|
|
||||||
const t = useAFFiNEI18N();
|
|
||||||
|
|
||||||
const { jumpToSubPath, jumpToIndex } = useNavigateHelper();
|
|
||||||
const [currentWorkspace] = useCurrentWorkspace();
|
|
||||||
|
|
||||||
const workspace = useWorkspace(workspaceId);
|
|
||||||
const [workspaceName] = useBlockSuiteWorkspaceName(
|
|
||||||
workspace.blockSuiteWorkspace
|
|
||||||
);
|
|
||||||
const workspaces = useAtomValue(rootWorkspacesMetadataAtom);
|
|
||||||
const pushNotification = useSetAtom(pushNotificationAtom);
|
|
||||||
|
|
||||||
const leaveWorkspace = useLeaveWorkspace();
|
|
||||||
const setSettingModal = useSetAtom(openSettingModalAtom);
|
|
||||||
const { deleteWorkspace } = useAppHelper();
|
|
||||||
|
|
||||||
const { NewSettingsDetail } = getUIAdapter(workspace.flavour);
|
|
||||||
|
|
||||||
const closeAndJumpOut = useCallback(() => {
|
|
||||||
setSettingModal(prev => ({ ...prev, open: false, workspaceId: null }));
|
|
||||||
|
|
||||||
if (currentWorkspace.id === workspaceId) {
|
|
||||||
const backWorkspace = workspaces.find(ws => ws.id !== workspaceId);
|
|
||||||
// TODO: if there is no workspace, jump to a new page(wait for design)
|
|
||||||
if (backWorkspace) {
|
|
||||||
jumpToSubPath(
|
|
||||||
backWorkspace?.id || '',
|
|
||||||
WorkspaceSubPath.ALL,
|
|
||||||
RouteLogic.REPLACE
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
setTimeout(() => {
|
|
||||||
jumpToIndex(RouteLogic.REPLACE);
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
currentWorkspace.id,
|
|
||||||
jumpToIndex,
|
|
||||||
jumpToSubPath,
|
|
||||||
setSettingModal,
|
|
||||||
workspaceId,
|
|
||||||
workspaces,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const handleDeleteWorkspace = useAsyncCallback(async () => {
|
|
||||||
closeAndJumpOut();
|
|
||||||
await deleteWorkspace(workspaceId);
|
|
||||||
|
|
||||||
pushNotification({
|
|
||||||
title: t['Successfully deleted'](),
|
|
||||||
type: 'success',
|
|
||||||
});
|
|
||||||
}, [closeAndJumpOut, deleteWorkspace, pushNotification, t, workspaceId]);
|
|
||||||
|
|
||||||
const handleLeaveWorkspace = useAsyncCallback(async () => {
|
|
||||||
closeAndJumpOut();
|
|
||||||
await leaveWorkspace(workspaceId, workspaceName);
|
|
||||||
|
|
||||||
pushNotification({
|
|
||||||
title: 'Successfully leave',
|
|
||||||
type: 'success',
|
|
||||||
});
|
|
||||||
}, [
|
|
||||||
closeAndJumpOut,
|
|
||||||
leaveWorkspace,
|
|
||||||
pushNotification,
|
|
||||||
workspaceId,
|
|
||||||
workspaceName,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const onTransformWorkspace = useOnTransformWorkspace();
|
|
||||||
// const handleDelete = useCallback(async () => {
|
|
||||||
// await onDeleteWorkspace();
|
|
||||||
// toast(t['Successfully deleted'](), {
|
|
||||||
// portal: document.body,
|
|
||||||
// });
|
|
||||||
// onClose();
|
|
||||||
// }, [onClose, onDeleteWorkspace, t, workspace.id]);
|
|
||||||
|
|
||||||
|
export const WorkspaceSetting = ({
|
||||||
|
workspaceMetadata,
|
||||||
|
}: {
|
||||||
|
workspaceMetadata: WorkspaceMetadata;
|
||||||
|
}) => {
|
||||||
|
const isOwner = useIsWorkspaceOwner(workspaceMetadata);
|
||||||
return (
|
return (
|
||||||
<NewSettingsDetail
|
<NewWorkspaceSettingDetail
|
||||||
onDeleteCloudWorkspace={handleDeleteWorkspace}
|
workspaceMetadata={workspaceMetadata}
|
||||||
onDeleteLocalWorkspace={handleDeleteWorkspace}
|
isOwner={isOwner}
|
||||||
onLeaveWorkspace={handleLeaveWorkspace}
|
|
||||||
onTransformWorkspace={onTransformWorkspace}
|
|
||||||
currentWorkspaceId={workspaceId}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,39 +1,41 @@
|
|||||||
import {
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
type AffineOfficialWorkspace,
|
import type { Workspace } from '@affine/workspace';
|
||||||
WorkspaceFlavour,
|
import { workspaceManagerAtom } from '@affine/workspace/atom';
|
||||||
} from '@affine/env/workspace';
|
|
||||||
import type { Page } from '@blocksuite/store';
|
import type { Page } from '@blocksuite/store';
|
||||||
import { useCallback, useState } from 'react';
|
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||||
|
import { useAtomValue } from 'jotai';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { useOnTransformWorkspace } from '../../../hooks/root/use-on-transform-workspace';
|
import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
|
||||||
import { EnableAffineCloudModal } from '../enable-affine-cloud-modal';
|
import { EnableAffineCloudModal } from '../enable-affine-cloud-modal';
|
||||||
import { ShareMenu } from './share-menu';
|
import { ShareMenu } from './share-menu';
|
||||||
|
|
||||||
type SharePageModalProps = {
|
type SharePageModalProps = {
|
||||||
workspace: AffineOfficialWorkspace;
|
workspace: Workspace;
|
||||||
page: Page;
|
page: Page;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SharePageButton = ({ workspace, page }: SharePageModalProps) => {
|
export const SharePageButton = ({ workspace, page }: SharePageModalProps) => {
|
||||||
const onTransformWorkspace = useOnTransformWorkspace();
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const handleConfirm = useCallback(() => {
|
const { openPage } = useNavigateHelper();
|
||||||
|
|
||||||
|
const workspaceManager = useAtomValue(workspaceManagerAtom);
|
||||||
|
|
||||||
|
const handleConfirm = useAsyncCallback(async () => {
|
||||||
if (workspace.flavour !== WorkspaceFlavour.LOCAL) {
|
if (workspace.flavour !== WorkspaceFlavour.LOCAL) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
onTransformWorkspace(
|
const { id: newId } =
|
||||||
WorkspaceFlavour.LOCAL,
|
await workspaceManager.transformLocalToCloud(workspace);
|
||||||
WorkspaceFlavour.AFFINE_CLOUD,
|
openPage(newId, page.id);
|
||||||
workspace
|
|
||||||
);
|
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
}, [onTransformWorkspace, workspace]);
|
}, [openPage, page.id, workspace, workspaceManager]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ShareMenu
|
<ShareMenu
|
||||||
workspace={workspace}
|
workspaceMetadata={workspace.meta}
|
||||||
currentPage={page}
|
currentPage={page}
|
||||||
onEnableAffineCloud={() => setOpen(true)}
|
onEnableAffineCloud={() => setOpen(true)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -10,7 +10,10 @@ import * as styles from './index.css';
|
|||||||
import type { ShareMenuProps } from './share-menu';
|
import type { ShareMenuProps } from './share-menu';
|
||||||
import { useSharingUrl } from './use-share-url';
|
import { useSharingUrl } from './use-share-url';
|
||||||
|
|
||||||
export const ShareExport = ({ workspace, currentPage }: ShareMenuProps) => {
|
export const ShareExport = ({
|
||||||
|
workspaceMetadata: workspace,
|
||||||
|
currentPage,
|
||||||
|
}: ShareMenuProps) => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const workspaceId = workspace.id;
|
const workspaceId = workspace.id;
|
||||||
const pageId = currentPage.id;
|
const pageId = currentPage.id;
|
||||||
|
|||||||
@@ -1,14 +1,9 @@
|
|||||||
import { Button } from '@affine/component/ui/button';
|
import { Button } from '@affine/component/ui/button';
|
||||||
import { Divider } from '@affine/component/ui/divider';
|
import { Divider } from '@affine/component/ui/divider';
|
||||||
import { Menu } from '@affine/component/ui/menu';
|
import { Menu } from '@affine/component/ui/menu';
|
||||||
import {
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
type AffineCloudWorkspace,
|
|
||||||
type AffineOfficialWorkspace,
|
|
||||||
type AffinePublicWorkspace,
|
|
||||||
type LocalWorkspace,
|
|
||||||
WorkspaceFlavour,
|
|
||||||
} from '@affine/env/workspace';
|
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
|
import type { WorkspaceMetadata } from '@affine/workspace';
|
||||||
import { WebIcon } from '@blocksuite/icons';
|
import { WebIcon } from '@blocksuite/icons';
|
||||||
import type { Page } from '@blocksuite/store';
|
import type { Page } from '@blocksuite/store';
|
||||||
|
|
||||||
@@ -17,13 +12,8 @@ import * as styles from './index.css';
|
|||||||
import { ShareExport } from './share-export';
|
import { ShareExport } from './share-export';
|
||||||
import { SharePage } from './share-page';
|
import { SharePage } from './share-page';
|
||||||
|
|
||||||
export interface ShareMenuProps<
|
export interface ShareMenuProps {
|
||||||
Workspace extends AffineOfficialWorkspace =
|
workspaceMetadata: WorkspaceMetadata;
|
||||||
| AffineCloudWorkspace
|
|
||||||
| LocalWorkspace
|
|
||||||
| AffinePublicWorkspace,
|
|
||||||
> {
|
|
||||||
workspace: Workspace;
|
|
||||||
currentPage: Page;
|
currentPage: Page;
|
||||||
onEnableAffineCloud: () => void;
|
onEnableAffineCloud: () => void;
|
||||||
}
|
}
|
||||||
@@ -70,7 +60,7 @@ const LocalShareMenu = (props: ShareMenuProps) => {
|
|||||||
const CloudShareMenu = (props: ShareMenuProps) => {
|
const CloudShareMenu = (props: ShareMenuProps) => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const {
|
const {
|
||||||
workspace: { id: workspaceId },
|
workspaceMetadata: { id: workspaceId },
|
||||||
currentPage,
|
currentPage,
|
||||||
} = props;
|
} = props;
|
||||||
const { isSharedPage } = useIsSharedPage(workspaceId, currentPage.id);
|
const { isSharedPage } = useIsSharedPage(workspaceId, currentPage.id);
|
||||||
@@ -96,9 +86,9 @@ const CloudShareMenu = (props: ShareMenuProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const ShareMenu = (props: ShareMenuProps) => {
|
export const ShareMenu = (props: ShareMenuProps) => {
|
||||||
const { workspace } = props;
|
const { workspaceMetadata } = props;
|
||||||
|
|
||||||
if (workspace.flavour === WorkspaceFlavour.LOCAL) {
|
if (workspaceMetadata.flavour === WorkspaceFlavour.LOCAL) {
|
||||||
return <LocalShareMenu {...props} />;
|
return <LocalShareMenu {...props} />;
|
||||||
}
|
}
|
||||||
return <CloudShareMenu {...props} />;
|
return <CloudShareMenu {...props} />;
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export const LocalSharePage = (props: ShareMenuProps) => {
|
|||||||
|
|
||||||
export const AffineSharePage = (props: ShareMenuProps) => {
|
export const AffineSharePage = (props: ShareMenuProps) => {
|
||||||
const {
|
const {
|
||||||
workspace: { id: workspaceId },
|
workspaceMetadata: { id: workspaceId },
|
||||||
currentPage,
|
currentPage,
|
||||||
} = props;
|
} = props;
|
||||||
const pageId = currentPage.id;
|
const pageId = currentPage.id;
|
||||||
@@ -239,9 +239,11 @@ export const AffineSharePage = (props: ShareMenuProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const SharePage = (props: ShareMenuProps) => {
|
export const SharePage = (props: ShareMenuProps) => {
|
||||||
if (props.workspace.flavour === WorkspaceFlavour.LOCAL) {
|
if (props.workspaceMetadata.flavour === WorkspaceFlavour.LOCAL) {
|
||||||
return <LocalSharePage {...props} />;
|
return <LocalSharePage {...props} />;
|
||||||
} else if (props.workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD) {
|
} else if (
|
||||||
|
props.workspaceMetadata.flavour === WorkspaceFlavour.AFFINE_CLOUD
|
||||||
|
) {
|
||||||
return <AffineSharePage {...props} />;
|
return <AffineSharePage {...props} />;
|
||||||
}
|
}
|
||||||
throw new Error('Unreachable');
|
throw new Error('Unreachable');
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { AffineOfficialWorkspace } from '@affine/env/workspace';
|
import type { Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
|
||||||
import {
|
import {
|
||||||
useBlockSuitePageMeta,
|
useBlockSuitePageMeta,
|
||||||
usePageMetaHelper,
|
usePageMetaHelper,
|
||||||
@@ -18,7 +18,7 @@ import { PageHeaderMenuButton } from './operation-menu';
|
|||||||
import * as styles from './styles.css';
|
import * as styles from './styles.css';
|
||||||
|
|
||||||
export interface BlockSuiteHeaderTitleProps {
|
export interface BlockSuiteHeaderTitleProps {
|
||||||
workspace: AffineOfficialWorkspace;
|
blockSuiteWorkspace: BlockSuiteWorkspace;
|
||||||
pageId: string;
|
pageId: string;
|
||||||
isPublic?: boolean;
|
isPublic?: boolean;
|
||||||
publicMode?: PageMode;
|
publicMode?: PageMode;
|
||||||
@@ -53,7 +53,7 @@ const EditableTitle = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const StableTitle = ({
|
const StableTitle = ({
|
||||||
workspace,
|
blockSuiteWorkspace: workspace,
|
||||||
pageId,
|
pageId,
|
||||||
onRename,
|
onRename,
|
||||||
isPublic,
|
isPublic,
|
||||||
@@ -61,8 +61,8 @@ const StableTitle = ({
|
|||||||
}: BlockSuiteHeaderTitleProps & {
|
}: BlockSuiteHeaderTitleProps & {
|
||||||
onRename?: () => void;
|
onRename?: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const currentPage = workspace.blockSuiteWorkspace.getPage(pageId);
|
const currentPage = workspace.getPage(pageId);
|
||||||
const pageMeta = useBlockSuitePageMeta(workspace.blockSuiteWorkspace).find(
|
const pageMeta = useBlockSuitePageMeta(workspace).find(
|
||||||
meta => meta.id === currentPage?.id
|
meta => meta.id === currentPage?.id
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -77,7 +77,7 @@ const StableTitle = ({
|
|||||||
return (
|
return (
|
||||||
<div className={styles.headerTitleContainer}>
|
<div className={styles.headerTitleContainer}>
|
||||||
<EditorModeSwitch
|
<EditorModeSwitch
|
||||||
blockSuiteWorkspace={workspace.blockSuiteWorkspace}
|
blockSuiteWorkspace={workspace}
|
||||||
pageId={pageId}
|
pageId={pageId}
|
||||||
isPublic={isPublic}
|
isPublic={isPublic}
|
||||||
publicMode={publicMode}
|
publicMode={publicMode}
|
||||||
@@ -97,12 +97,12 @@ const StableTitle = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const BlockSuiteTitleWithRename = (props: BlockSuiteHeaderTitleProps) => {
|
const BlockSuiteTitleWithRename = (props: BlockSuiteHeaderTitleProps) => {
|
||||||
const { workspace, pageId } = props;
|
const { blockSuiteWorkspace: workspace, pageId } = props;
|
||||||
const currentPage = workspace.blockSuiteWorkspace.getPage(pageId);
|
const currentPage = workspace.getPage(pageId);
|
||||||
const pageMeta = useBlockSuitePageMeta(workspace.blockSuiteWorkspace).find(
|
const pageMeta = useBlockSuitePageMeta(workspace).find(
|
||||||
meta => meta.id === currentPage?.id
|
meta => meta.id === currentPage?.id
|
||||||
);
|
);
|
||||||
const pageTitleMeta = usePageMetaHelper(workspace.blockSuiteWorkspace);
|
const pageTitleMeta = usePageMetaHelper(workspace);
|
||||||
|
|
||||||
const [isEditable, setIsEditable] = useState(false);
|
const [isEditable, setIsEditable] = useState(false);
|
||||||
const [title, setPageTitle] = useState(pageMeta?.title || 'Untitled');
|
const [title, setPageTitle] = useState(pageMeta?.title || 'Untitled');
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
} from '@affine/component/ui/menu';
|
} from '@affine/component/ui/menu';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
|
import { waitForCurrentWorkspaceAtom } from '@affine/workspace/atom';
|
||||||
import { assertExists } from '@blocksuite/global/utils';
|
import { assertExists } from '@blocksuite/global/utils';
|
||||||
import {
|
import {
|
||||||
DuplicateIcon,
|
DuplicateIcon,
|
||||||
@@ -18,7 +19,6 @@ import {
|
|||||||
ImportIcon,
|
ImportIcon,
|
||||||
PageIcon,
|
PageIcon,
|
||||||
} from '@blocksuite/icons';
|
} from '@blocksuite/icons';
|
||||||
import type { PageMeta } from '@blocksuite/store';
|
|
||||||
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
|
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
@@ -27,7 +27,6 @@ import { currentModeAtom } from '../../../atoms/mode';
|
|||||||
import { useBlockSuiteMetaHelper } from '../../../hooks/affine/use-block-suite-meta-helper';
|
import { useBlockSuiteMetaHelper } from '../../../hooks/affine/use-block-suite-meta-helper';
|
||||||
import { useExportPage } from '../../../hooks/affine/use-export-page';
|
import { useExportPage } from '../../../hooks/affine/use-export-page';
|
||||||
import { useTrashModalHelper } from '../../../hooks/affine/use-trash-modal-helper';
|
import { useTrashModalHelper } from '../../../hooks/affine/use-trash-modal-helper';
|
||||||
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
|
|
||||||
import { toast } from '../../../utils';
|
import { toast } from '../../../utils';
|
||||||
import { PageHistoryModal } from '../../affine/page-history-modal/history-modal';
|
import { PageHistoryModal } from '../../affine/page-history-modal/history-modal';
|
||||||
import { HeaderDropDownButton } from '../../pure/header-drop-down-button';
|
import { HeaderDropDownButton } from '../../pure/header-drop-down-button';
|
||||||
@@ -42,16 +41,16 @@ export const PageHeaderMenuButton = ({ rename, pageId }: PageMenuProps) => {
|
|||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
|
|
||||||
// fixme(himself65): remove these hooks ASAP
|
// fixme(himself65): remove these hooks ASAP
|
||||||
const [workspace] = useCurrentWorkspace();
|
const workspace = useAtomValue(waitForCurrentWorkspaceAtom);
|
||||||
const blockSuiteWorkspace = workspace.blockSuiteWorkspace;
|
const blockSuiteWorkspace = workspace.blockSuiteWorkspace;
|
||||||
const currentPage = blockSuiteWorkspace.getPage(pageId);
|
const currentPage = blockSuiteWorkspace.getPage(pageId);
|
||||||
assertExists(currentPage);
|
assertExists(currentPage);
|
||||||
|
|
||||||
const pageMeta = useBlockSuitePageMeta(blockSuiteWorkspace).find(
|
const pageMeta = useBlockSuitePageMeta(blockSuiteWorkspace).find(
|
||||||
meta => meta.id === pageId
|
meta => meta.id === pageId
|
||||||
) as PageMeta;
|
);
|
||||||
const currentMode = useAtomValue(currentModeAtom);
|
const currentMode = useAtomValue(currentModeAtom);
|
||||||
const favorite = pageMeta.favorite ?? false;
|
const favorite = pageMeta?.favorite ?? false;
|
||||||
|
|
||||||
const { togglePageMode, toggleFavorite, duplicate } =
|
const { togglePageMode, toggleFavorite, duplicate } =
|
||||||
useBlockSuiteMetaHelper(blockSuiteWorkspace);
|
useBlockSuiteMetaHelper(blockSuiteWorkspace);
|
||||||
@@ -65,12 +64,15 @@ export const PageHeaderMenuButton = ({ rename, pageId }: PageMenuProps) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleOpenTrashModal = useCallback(() => {
|
const handleOpenTrashModal = useCallback(() => {
|
||||||
|
if (!pageMeta) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
setTrashModal({
|
setTrashModal({
|
||||||
open: true,
|
open: true,
|
||||||
pageIds: [pageId],
|
pageIds: [pageId],
|
||||||
pageTitles: [pageMeta.title],
|
pageTitles: [pageMeta.title],
|
||||||
});
|
});
|
||||||
}, [pageId, pageMeta.title, setTrashModal]);
|
}, [pageId, pageMeta, setTrashModal]);
|
||||||
|
|
||||||
const handleFavorite = useCallback(() => {
|
const handleFavorite = useCallback(() => {
|
||||||
toggleFavorite(pageId);
|
toggleFavorite(pageId);
|
||||||
@@ -205,7 +207,7 @@ export const PageHeaderMenuButton = ({ rename, pageId }: PageMenuProps) => {
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
if (pageMeta.trash) {
|
if (pageMeta?.trash) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Tooltip } from '@affine/component/ui/tooltip';
|
import { Tooltip } from '@affine/component/ui/tooltip';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { assertExists } from '@blocksuite/global/utils';
|
|
||||||
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
|
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
import type { CSSProperties } from 'react';
|
import type { CSSProperties } from 'react';
|
||||||
@@ -44,8 +43,7 @@ export const EditorModeSwitch = ({
|
|||||||
const pageMeta = useBlockSuitePageMeta(blockSuiteWorkspace).find(
|
const pageMeta = useBlockSuitePageMeta(blockSuiteWorkspace).find(
|
||||||
meta => meta.id === pageId
|
meta => meta.id === pageId
|
||||||
);
|
);
|
||||||
assertExists(pageMeta);
|
const trash = pageMeta?.trash ?? false;
|
||||||
const { trash } = pageMeta;
|
|
||||||
|
|
||||||
const { togglePageMode, switchToEdgelessMode, switchToPageMode } =
|
const { togglePageMode, switchToEdgelessMode, switchToPageMode } =
|
||||||
useBlockSuiteMetaHelper(blockSuiteWorkspace);
|
useBlockSuiteMetaHelper(blockSuiteWorkspace);
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import './page-detail-editor.css';
|
import './page-detail-editor.css';
|
||||||
|
|
||||||
import { PageNotFoundError } from '@affine/env/constant';
|
|
||||||
import { assertExists, DisposableGroup } from '@blocksuite/global/utils';
|
import { assertExists, DisposableGroup } from '@blocksuite/global/utils';
|
||||||
import type { AffineEditorContainer } from '@blocksuite/presets';
|
import type { AffineEditorContainer } from '@blocksuite/presets';
|
||||||
import type { Page, Workspace } from '@blocksuite/store';
|
import type { Page, Workspace } from '@blocksuite/store';
|
||||||
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
|
|
||||||
import { useBlockSuiteWorkspacePage } from '@toeverything/hooks/use-block-suite-workspace-page';
|
import { useBlockSuiteWorkspacePage } from '@toeverything/hooks/use-block-suite-workspace-page';
|
||||||
import { pluginEditorAtom } from '@toeverything/infra/__internal__/plugin';
|
import { pluginEditorAtom } from '@toeverything/infra/__internal__/plugin';
|
||||||
import { getCurrentStore } from '@toeverything/infra/atom';
|
import { getCurrentStore } from '@toeverything/infra/atom';
|
||||||
@@ -51,10 +49,6 @@ const PageDetailEditorMain = memo(function PageDetailEditorMain({
|
|||||||
isPublic,
|
isPublic,
|
||||||
publishMode,
|
publishMode,
|
||||||
}: PageDetailEditorProps & { page: Page }) {
|
}: PageDetailEditorProps & { page: Page }) {
|
||||||
const meta = useBlockSuitePageMeta(workspace).find(
|
|
||||||
meta => meta.id === pageId
|
|
||||||
);
|
|
||||||
|
|
||||||
const { switchToEdgelessMode, switchToPageMode } =
|
const { switchToEdgelessMode, switchToPageMode } =
|
||||||
useBlockSuiteMetaHelper(workspace);
|
useBlockSuiteMetaHelper(workspace);
|
||||||
|
|
||||||
@@ -73,7 +67,6 @@ const PageDetailEditorMain = memo(function PageDetailEditorMain({
|
|||||||
|
|
||||||
const { appSettings } = useAppSettingHelper();
|
const { appSettings } = useAppSettingHelper();
|
||||||
|
|
||||||
assertExists(meta);
|
|
||||||
const value = useMemo(() => {
|
const value = useMemo(() => {
|
||||||
const fontStyle = fontStyleOptions.find(
|
const fontStyle = fontStyleOptions.find(
|
||||||
option => option.key === appSettings.fontStyle
|
option => option.key === appSettings.fontStyle
|
||||||
@@ -171,9 +164,8 @@ export const PageDetailEditor = (props: PageDetailEditorProps) => {
|
|||||||
const { workspace, pageId } = props;
|
const { workspace, pageId } = props;
|
||||||
const page = useBlockSuiteWorkspacePage(workspace, pageId);
|
const page = useBlockSuiteWorkspacePage(workspace, pageId);
|
||||||
if (!page) {
|
if (!page) {
|
||||||
throw new PageNotFoundError(workspace, pageId);
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<PageDetailEditorMain {...props} page={page} />
|
<PageDetailEditorMain {...props} page={page} />
|
||||||
|
|||||||
@@ -2,21 +2,17 @@ import { commandScore } from '@affine/cmdk';
|
|||||||
import { useCollectionManager } from '@affine/component/page-list';
|
import { useCollectionManager } from '@affine/component/page-list';
|
||||||
import type { Collection } from '@affine/env/filter';
|
import type { Collection } from '@affine/env/filter';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
|
import {
|
||||||
|
currentWorkspaceAtom,
|
||||||
|
waitForCurrentWorkspaceAtom,
|
||||||
|
} from '@affine/workspace/atom';
|
||||||
import { EdgelessIcon, PageIcon, ViewLayersIcon } from '@blocksuite/icons';
|
import { EdgelessIcon, PageIcon, ViewLayersIcon } from '@blocksuite/icons';
|
||||||
import type { Page, PageMeta } from '@blocksuite/store';
|
import type { Page, PageMeta } from '@blocksuite/store';
|
||||||
import {
|
import {
|
||||||
useBlockSuitePageMeta,
|
useBlockSuitePageMeta,
|
||||||
usePageMetaHelper,
|
usePageMetaHelper,
|
||||||
} from '@toeverything/hooks/use-block-suite-page-meta';
|
} from '@toeverything/hooks/use-block-suite-page-meta';
|
||||||
import {
|
import { currentPageIdAtom, getCurrentStore } from '@toeverything/infra/atom';
|
||||||
getWorkspace,
|
|
||||||
waitForWorkspace,
|
|
||||||
} from '@toeverything/infra/__internal__/workspace';
|
|
||||||
import {
|
|
||||||
currentPageIdAtom,
|
|
||||||
currentWorkspaceIdAtom,
|
|
||||||
getCurrentStore,
|
|
||||||
} from '@toeverything/infra/atom';
|
|
||||||
import {
|
import {
|
||||||
type AffineCommand,
|
type AffineCommand,
|
||||||
AffineCommandRegistry,
|
AffineCommandRegistry,
|
||||||
@@ -33,7 +29,6 @@ import {
|
|||||||
recentPageIdsBaseAtom,
|
recentPageIdsBaseAtom,
|
||||||
} from '../../../atoms';
|
} from '../../../atoms';
|
||||||
import { collectionsCRUDAtom } from '../../../atoms/collections';
|
import { collectionsCRUDAtom } from '../../../atoms/collections';
|
||||||
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
|
|
||||||
import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
|
import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
|
||||||
import { WorkspaceSubPath } from '../../../shared';
|
import { WorkspaceSubPath } from '../../../shared';
|
||||||
import { usePageHelper } from '../../blocksuite/block-suite-page-list/utils';
|
import { usePageHelper } from '../../blocksuite/block-suite-page-list/utils';
|
||||||
@@ -53,8 +48,8 @@ export const cmdkValueAtom = atom('');
|
|||||||
|
|
||||||
// like currentWorkspaceAtom, but not throw error
|
// like currentWorkspaceAtom, but not throw error
|
||||||
const safeCurrentPageAtom = atom<Promise<Page | undefined>>(async get => {
|
const safeCurrentPageAtom = atom<Promise<Page | undefined>>(async get => {
|
||||||
const currentWorkspaceId = get(currentWorkspaceIdAtom);
|
const currentWorkspace = get(currentWorkspaceAtom);
|
||||||
if (!currentWorkspaceId) {
|
if (!currentWorkspace) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,9 +59,7 @@ const safeCurrentPageAtom = atom<Promise<Page | undefined>>(async get => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const workspace = getWorkspace(currentWorkspaceId);
|
const page = currentWorkspace.blockSuiteWorkspace.getPage(currentPageId);
|
||||||
await waitForWorkspace(workspace);
|
|
||||||
const page = workspace.getPage(currentPageId);
|
|
||||||
|
|
||||||
if (!page) {
|
if (!page) {
|
||||||
return;
|
return;
|
||||||
@@ -132,7 +125,7 @@ export const filteredAffineCommands = atom(async get => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const useWorkspacePages = () => {
|
const useWorkspacePages = () => {
|
||||||
const [currentWorkspace] = useCurrentWorkspace();
|
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
|
||||||
const pages = useBlockSuitePageMeta(currentWorkspace.blockSuiteWorkspace);
|
const pages = useBlockSuitePageMeta(currentWorkspace.blockSuiteWorkspace);
|
||||||
return pages;
|
return pages;
|
||||||
};
|
};
|
||||||
@@ -166,7 +159,7 @@ export const pageToCommand = (
|
|||||||
blockId?: string
|
blockId?: string
|
||||||
): CMDKCommand => {
|
): CMDKCommand => {
|
||||||
const pageMode = store.get(pageSettingsAtom)?.[page.id]?.mode;
|
const pageMode = store.get(pageSettingsAtom)?.[page.id]?.mode;
|
||||||
const currentWorkspaceId = store.get(currentWorkspaceIdAtom);
|
const currentWorkspace = store.get(currentWorkspaceAtom);
|
||||||
|
|
||||||
const title = page.title || t['Untitled']();
|
const title = page.title || t['Untitled']();
|
||||||
const commandLabel = label || {
|
const commandLabel = label || {
|
||||||
@@ -191,18 +184,18 @@ export const pageToCommand = (
|
|||||||
originalValue: title,
|
originalValue: title,
|
||||||
category: category,
|
category: category,
|
||||||
run: () => {
|
run: () => {
|
||||||
if (!currentWorkspaceId) {
|
if (!currentWorkspace) {
|
||||||
console.error('current workspace not found');
|
console.error('current workspace not found');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (blockId) {
|
if (blockId) {
|
||||||
return navigationHelper.jumpToPageBlock(
|
return navigationHelper.jumpToPageBlock(
|
||||||
currentWorkspaceId,
|
currentWorkspace.id,
|
||||||
page.id,
|
page.id,
|
||||||
blockId
|
blockId
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return navigationHelper.jumpToPage(currentWorkspaceId, page.id);
|
return navigationHelper.jumpToPage(currentWorkspace.id, page.id);
|
||||||
},
|
},
|
||||||
icon: pageMode === 'edgeless' ? <EdgelessIcon /> : <PageIcon />,
|
icon: pageMode === 'edgeless' ? <EdgelessIcon /> : <PageIcon />,
|
||||||
timestamp: page.updatedDate,
|
timestamp: page.updatedDate,
|
||||||
@@ -217,7 +210,7 @@ export const usePageCommands = () => {
|
|||||||
const recentPages = useRecentPages();
|
const recentPages = useRecentPages();
|
||||||
const pages = useWorkspacePages();
|
const pages = useWorkspacePages();
|
||||||
const store = getCurrentStore();
|
const store = getCurrentStore();
|
||||||
const [workspace] = useCurrentWorkspace();
|
const workspace = useAtomValue(waitForCurrentWorkspaceAtom);
|
||||||
const pageHelper = usePageHelper(workspace.blockSuiteWorkspace);
|
const pageHelper = usePageHelper(workspace.blockSuiteWorkspace);
|
||||||
const pageMetaHelper = usePageMetaHelper(workspace.blockSuiteWorkspace);
|
const pageMetaHelper = usePageMetaHelper(workspace.blockSuiteWorkspace);
|
||||||
const query = useAtomValue(cmdkQueryAtom);
|
const query = useAtomValue(cmdkQueryAtom);
|
||||||
@@ -359,7 +352,7 @@ export const collectionToCommand = (
|
|||||||
selectCollection: (id: string) => void,
|
selectCollection: (id: string) => void,
|
||||||
t: ReturnType<typeof useAFFiNEI18N>
|
t: ReturnType<typeof useAFFiNEI18N>
|
||||||
): CMDKCommand => {
|
): CMDKCommand => {
|
||||||
const currentWorkspaceId = store.get(currentWorkspaceIdAtom);
|
const currentWorkspace = store.get(currentWorkspaceAtom);
|
||||||
const label = collection.name || t['Untitled']();
|
const label = collection.name || t['Untitled']();
|
||||||
const category = 'affine:collections';
|
const category = 'affine:collections';
|
||||||
return {
|
return {
|
||||||
@@ -377,11 +370,11 @@ export const collectionToCommand = (
|
|||||||
originalValue: label,
|
originalValue: label,
|
||||||
category: category,
|
category: category,
|
||||||
run: () => {
|
run: () => {
|
||||||
if (!currentWorkspaceId) {
|
if (!currentWorkspace) {
|
||||||
console.error('current workspace not found');
|
console.error('current workspace not found');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
navigationHelper.jumpToSubPath(currentWorkspaceId, WorkspaceSubPath.ALL);
|
navigationHelper.jumpToSubPath(currentWorkspace.id, WorkspaceSubPath.ALL);
|
||||||
selectCollection(collection.id);
|
selectCollection(collection.id);
|
||||||
},
|
},
|
||||||
icon: <ViewLayersIcon />,
|
icon: <ViewLayersIcon />,
|
||||||
@@ -395,7 +388,7 @@ export const useCollectionsCommands = () => {
|
|||||||
const query = useAtomValue(cmdkQueryAtom);
|
const query = useAtomValue(cmdkQueryAtom);
|
||||||
const navigationHelper = useNavigateHelper();
|
const navigationHelper = useNavigateHelper();
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const [workspace] = useCurrentWorkspace();
|
const workspace = useAtomValue(waitForCurrentWorkspaceAtom);
|
||||||
const selectCollection = useCallback(
|
const selectCollection = useCallback(
|
||||||
(id: string) => {
|
(id: string) => {
|
||||||
navigationHelper.jumpToCollection(workspace.id, id);
|
navigationHelper.jumpToCollection(workspace.id, id);
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ export const HelpIsland = () => {
|
|||||||
setOpenSettingModalAtom({
|
setOpenSettingModalAtom({
|
||||||
open: true,
|
open: true,
|
||||||
activeTab: tab,
|
activeTab: tab,
|
||||||
workspaceId: null,
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[setOpenSettingModalAtom]
|
[setOpenSettingModalAtom]
|
||||||
|
|||||||
@@ -3,21 +3,21 @@ import { ConfirmModal } from '@affine/component/ui/modal';
|
|||||||
import { Tooltip } from '@affine/component/ui/tooltip';
|
import { Tooltip } from '@affine/component/ui/tooltip';
|
||||||
import { WorkspaceSubPath } from '@affine/env/workspace';
|
import { WorkspaceSubPath } from '@affine/env/workspace';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
|
import { waitForCurrentWorkspaceAtom } from '@affine/workspace/atom';
|
||||||
import { assertExists } from '@blocksuite/global/utils';
|
import { assertExists } from '@blocksuite/global/utils';
|
||||||
import { DeleteIcon, ResetIcon } from '@blocksuite/icons';
|
import { DeleteIcon, ResetIcon } from '@blocksuite/icons';
|
||||||
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
|
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
|
||||||
|
import { useAtomValue } from 'jotai';
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
import { useAppSettingHelper } from '../../../hooks/affine/use-app-setting-helper';
|
import { useAppSettingHelper } from '../../../hooks/affine/use-app-setting-helper';
|
||||||
import { useBlockSuiteMetaHelper } from '../../../hooks/affine/use-block-suite-meta-helper';
|
import { useBlockSuiteMetaHelper } from '../../../hooks/affine/use-block-suite-meta-helper';
|
||||||
import { useCurrentWorkspace } from '../../../hooks/current/use-current-workspace';
|
|
||||||
import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
|
import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
|
||||||
import { toast } from '../../../utils';
|
import { toast } from '../../../utils';
|
||||||
import * as styles from './styles.css';
|
import * as styles from './styles.css';
|
||||||
|
|
||||||
export const TrashPageFooter = ({ pageId }: { pageId: string }) => {
|
export const TrashPageFooter = ({ pageId }: { pageId: string }) => {
|
||||||
// fixme(himself65): remove these hooks ASAP
|
const workspace = useAtomValue(waitForCurrentWorkspaceAtom);
|
||||||
const [workspace] = useCurrentWorkspace();
|
|
||||||
assertExists(workspace);
|
assertExists(workspace);
|
||||||
const blockSuiteWorkspace = workspace.blockSuiteWorkspace;
|
const blockSuiteWorkspace = workspace.blockSuiteWorkspace;
|
||||||
const pageMeta = useBlockSuitePageMeta(blockSuiteWorkspace).find(
|
const pageMeta = useBlockSuitePageMeta(blockSuiteWorkspace).find(
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import { MoreHorizontalIcon, ViewLayersIcon } from '@blocksuite/icons';
|
|||||||
import type { PageMeta, Workspace } from '@blocksuite/store';
|
import type { PageMeta, Workspace } from '@blocksuite/store';
|
||||||
import { useDroppable } from '@dnd-kit/core';
|
import { useDroppable } from '@dnd-kit/core';
|
||||||
import * as Collapsible from '@radix-ui/react-collapsible';
|
import * as Collapsible from '@radix-ui/react-collapsible';
|
||||||
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
|
||||||
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
|
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
@@ -44,9 +43,9 @@ const CollectionRenderer = ({
|
|||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const dragItemId = getDropItemId('collections', collection.id);
|
const dragItemId = getDropItemId('collections', collection.id);
|
||||||
|
|
||||||
const removeFromAllowList = useAsyncCallback(
|
const removeFromAllowList = useCallback(
|
||||||
async (id: string) => {
|
(id: string) => {
|
||||||
await setting.updateCollection({
|
setting.updateCollection({
|
||||||
...collection,
|
...collection,
|
||||||
allowList: collection.allowList?.filter(v => v !== id),
|
allowList: collection.allowList?.filter(v => v !== id),
|
||||||
});
|
});
|
||||||
@@ -66,9 +65,7 @@ const CollectionRenderer = ({
|
|||||||
} else {
|
} else {
|
||||||
toast(t['com.affine.collection.addPage.success']());
|
toast(t['com.affine.collection.addPage.success']());
|
||||||
}
|
}
|
||||||
setting.addPage(collection.id, id).catch(err => {
|
setting.addPage(collection.id, id);
|
||||||
console.error(err);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -90,9 +87,9 @@ const CollectionRenderer = ({
|
|||||||
const currentPath = location.pathname.split('?')[0];
|
const currentPath = location.pathname.split('?')[0];
|
||||||
const path = `/workspace/${workspace.id}/collection/${collection.id}`;
|
const path = `/workspace/${workspace.id}/collection/${collection.id}`;
|
||||||
|
|
||||||
const onRename = useAsyncCallback(
|
const onRename = useCallback(
|
||||||
async (name: string) => {
|
(name: string) => {
|
||||||
await setting.updateCollection({
|
setting.updateCollection({
|
||||||
...collection,
|
...collection,
|
||||||
name,
|
name,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
import { Divider } from '@affine/component/ui/divider';
|
import { Divider } from '@affine/component/ui/divider';
|
||||||
import { MenuItem } from '@affine/component/ui/menu';
|
import { MenuItem } from '@affine/component/ui/menu';
|
||||||
|
import { Unreachable } from '@affine/env/constant';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
import {
|
||||||
|
workspaceListAtom,
|
||||||
|
workspaceManagerAtom,
|
||||||
|
} from '@affine/workspace/atom';
|
||||||
import { Logo1Icon } from '@blocksuite/icons';
|
import { Logo1Icon } from '@blocksuite/icons';
|
||||||
import { useAtomValue, useSetAtom } from 'jotai';
|
import { useAtomValue, useSetAtom } from 'jotai';
|
||||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||||
import { useSession } from 'next-auth/react';
|
import { useSession } from 'next-auth/react';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useEffect, useMemo } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
authAtom,
|
authAtom,
|
||||||
@@ -81,9 +85,16 @@ export const UserWithWorkspaceList = ({
|
|||||||
onEventEnd?.();
|
onEventEnd?.();
|
||||||
}, [onEventEnd, setOpenCreateWorkspaceModal]);
|
}, [onEventEnd, setOpenCreateWorkspaceModal]);
|
||||||
|
|
||||||
const workspaces = useAtomValue(rootWorkspacesMetadataAtom, {
|
const workspaces = useAtomValue(workspaceListAtom);
|
||||||
delay: 0,
|
|
||||||
});
|
const workspaceManager = useAtomValue(workspaceManagerAtom);
|
||||||
|
|
||||||
|
// revalidate workspace list when mounted
|
||||||
|
useEffect(() => {
|
||||||
|
workspaceManager.list.revalidate().catch(err => {
|
||||||
|
throw new Unreachable('revlidate should never throw, ' + err);
|
||||||
|
});
|
||||||
|
}, [workspaceManager]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.workspaceListWrapper}>
|
<div className={styles.workspaceListWrapper}>
|
||||||
|
|||||||
@@ -1,39 +1,29 @@
|
|||||||
import { ScrollableContainer } from '@affine/component';
|
import { ScrollableContainer } from '@affine/component';
|
||||||
import { Divider } from '@affine/component/ui/divider';
|
import { Divider } from '@affine/component/ui/divider';
|
||||||
import { WorkspaceList } from '@affine/component/workspace-list';
|
import { WorkspaceList } from '@affine/component/workspace-list';
|
||||||
import type {
|
|
||||||
AffineCloudWorkspace,
|
|
||||||
LocalWorkspace,
|
|
||||||
} from '@affine/env/workspace';
|
|
||||||
import { WorkspaceFlavour, WorkspaceSubPath } from '@affine/env/workspace';
|
import { WorkspaceFlavour, WorkspaceSubPath } from '@affine/env/workspace';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
|
import type { WorkspaceMetadata } from '@affine/workspace';
|
||||||
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
import { currentWorkspaceAtom } from '@affine/workspace/atom';
|
||||||
import type { DragEndEvent } from '@dnd-kit/core';
|
import type { DragEndEvent } from '@dnd-kit/core';
|
||||||
import { arrayMove } from '@dnd-kit/sortable';
|
import { useAtomValue, useSetAtom } from 'jotai';
|
||||||
import {
|
|
||||||
currentPageIdAtom,
|
|
||||||
currentWorkspaceIdAtom,
|
|
||||||
} from '@toeverything/infra/atom';
|
|
||||||
import { useAtom, useSetAtom } from 'jotai';
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||||
import { useSession } from 'next-auth/react';
|
import { useSession } from 'next-auth/react';
|
||||||
import { startTransition, useCallback, useMemo, useTransition } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
openCreateWorkspaceModalAtom,
|
openCreateWorkspaceModalAtom,
|
||||||
openSettingModalAtom,
|
openSettingModalAtom,
|
||||||
} from '../../../../../atoms';
|
} from '../../../../../atoms';
|
||||||
import type { AllWorkspace } from '../../../../../shared';
|
|
||||||
import { useIsWorkspaceOwner } from '../.././../../../hooks/affine/use-is-workspace-owner';
|
import { useIsWorkspaceOwner } from '../.././../../../hooks/affine/use-is-workspace-owner';
|
||||||
import { useNavigateHelper } from '../.././../../../hooks/use-navigate-helper';
|
import { useNavigateHelper } from '../.././../../../hooks/use-navigate-helper';
|
||||||
import * as styles from './index.css';
|
import * as styles from './index.css';
|
||||||
interface WorkspaceModalProps {
|
interface WorkspaceModalProps {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
workspaces: (AffineCloudWorkspace | LocalWorkspace)[];
|
workspaces: WorkspaceMetadata[];
|
||||||
currentWorkspaceId: AllWorkspace['id'] | null;
|
currentWorkspaceId?: string | null;
|
||||||
onClickWorkspace: (workspace: RootWorkspaceMetadata['id']) => void;
|
onClickWorkspace: (workspaceMetadata: WorkspaceMetadata) => void;
|
||||||
onClickWorkspaceSetting: (workspace: RootWorkspaceMetadata['id']) => void;
|
onClickWorkspaceSetting: (workspaceMetadata: WorkspaceMetadata) => void;
|
||||||
onNewWorkspace: () => void;
|
onNewWorkspace: () => void;
|
||||||
onAddWorkspace: () => void;
|
onAddWorkspace: () => void;
|
||||||
onDragEnd: (event: DragEndEvent) => void;
|
onDragEnd: (event: DragEndEvent) => void;
|
||||||
@@ -102,22 +92,14 @@ export const AFFiNEWorkspaceList = ({
|
|||||||
workspaces,
|
workspaces,
|
||||||
onEventEnd,
|
onEventEnd,
|
||||||
}: {
|
}: {
|
||||||
workspaces: RootWorkspaceMetadata[];
|
workspaces: WorkspaceMetadata[];
|
||||||
onEventEnd?: () => void;
|
onEventEnd?: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const setOpenCreateWorkspaceModal = useSetAtom(openCreateWorkspaceModalAtom);
|
const setOpenCreateWorkspaceModal = useSetAtom(openCreateWorkspaceModalAtom);
|
||||||
|
|
||||||
const { jumpToSubPath } = useNavigateHelper();
|
const { jumpToSubPath } = useNavigateHelper();
|
||||||
|
|
||||||
const setWorkspaces = useSetAtom(rootWorkspacesMetadataAtom);
|
const currentWorkspace = useAtomValue(currentWorkspaceAtom);
|
||||||
|
|
||||||
const [currentWorkspaceId, setCurrentWorkspaceId] = useAtom(
|
|
||||||
currentWorkspaceIdAtom
|
|
||||||
);
|
|
||||||
|
|
||||||
const setCurrentPageId = useSetAtom(currentPageIdAtom);
|
|
||||||
|
|
||||||
const [, startCloseTransition] = useTransition();
|
|
||||||
|
|
||||||
const setOpenSettingModalAtom = useSetAtom(openSettingModalAtom);
|
const setOpenSettingModalAtom = useSetAtom(openSettingModalAtom);
|
||||||
|
|
||||||
@@ -130,7 +112,7 @@ export const AFFiNEWorkspaceList = ({
|
|||||||
() =>
|
() =>
|
||||||
workspaces.filter(
|
workspaces.filter(
|
||||||
({ flavour }) => flavour === WorkspaceFlavour.AFFINE_CLOUD
|
({ flavour }) => flavour === WorkspaceFlavour.AFFINE_CLOUD
|
||||||
) as (AffineCloudWorkspace | LocalWorkspace)[],
|
) as WorkspaceMetadata[],
|
||||||
[workspaces]
|
[workspaces]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -138,44 +120,37 @@ export const AFFiNEWorkspaceList = ({
|
|||||||
() =>
|
() =>
|
||||||
workspaces.filter(
|
workspaces.filter(
|
||||||
({ flavour }) => flavour === WorkspaceFlavour.LOCAL
|
({ flavour }) => flavour === WorkspaceFlavour.LOCAL
|
||||||
) as (AffineCloudWorkspace | LocalWorkspace)[],
|
) as WorkspaceMetadata[],
|
||||||
[workspaces]
|
[workspaces]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onClickWorkspaceSetting = useCallback(
|
const onClickWorkspaceSetting = useCallback(
|
||||||
(workspaceId: string) => {
|
(workspaceMetadata: WorkspaceMetadata) => {
|
||||||
setOpenSettingModalAtom({
|
setOpenSettingModalAtom({
|
||||||
open: true,
|
open: true,
|
||||||
activeTab: 'workspace',
|
activeTab: 'workspace',
|
||||||
workspaceId,
|
workspaceMetadata,
|
||||||
});
|
});
|
||||||
onEventEnd?.();
|
onEventEnd?.();
|
||||||
},
|
},
|
||||||
[onEventEnd, setOpenSettingModalAtom]
|
[onEventEnd, setOpenSettingModalAtom]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onMoveWorkspace = useCallback(
|
const onMoveWorkspace = useCallback((_activeId: string, _overId: string) => {
|
||||||
(activeId: string, overId: string) => {
|
// TODO: order
|
||||||
const oldIndex = workspaces.findIndex(w => w.id === activeId);
|
// const oldIndex = workspaces.findIndex(w => w.id === activeId);
|
||||||
|
// const newIndex = workspaces.findIndex(w => w.id === overId);
|
||||||
const newIndex = workspaces.findIndex(w => w.id === overId);
|
// startTransition(() => {
|
||||||
startTransition(() => {
|
// setWorkspaces(workspaces => arrayMove(workspaces, oldIndex, newIndex));
|
||||||
setWorkspaces(workspaces => arrayMove(workspaces, oldIndex, newIndex));
|
// });
|
||||||
});
|
}, []);
|
||||||
},
|
|
||||||
[setWorkspaces, workspaces]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onClickWorkspace = useCallback(
|
const onClickWorkspace = useCallback(
|
||||||
(workspaceId: string) => {
|
(workspaceMetadata: WorkspaceMetadata) => {
|
||||||
startCloseTransition(() => {
|
jumpToSubPath(workspaceMetadata.id, WorkspaceSubPath.ALL);
|
||||||
setCurrentWorkspaceId(workspaceId);
|
|
||||||
setCurrentPageId(null);
|
|
||||||
jumpToSubPath(workspaceId, WorkspaceSubPath.ALL);
|
|
||||||
});
|
|
||||||
onEventEnd?.();
|
onEventEnd?.();
|
||||||
},
|
},
|
||||||
[jumpToSubPath, onEventEnd, setCurrentPageId, setCurrentWorkspaceId]
|
[jumpToSubPath, onEventEnd]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onDragEnd = useCallback(
|
const onDragEnd = useCallback(
|
||||||
@@ -211,7 +186,7 @@ export const AFFiNEWorkspaceList = ({
|
|||||||
onClickWorkspaceSetting={onClickWorkspaceSetting}
|
onClickWorkspaceSetting={onClickWorkspaceSetting}
|
||||||
onNewWorkspace={onNewWorkspace}
|
onNewWorkspace={onNewWorkspace}
|
||||||
onAddWorkspace={onAddWorkspace}
|
onAddWorkspace={onAddWorkspace}
|
||||||
currentWorkspaceId={currentWorkspaceId}
|
currentWorkspaceId={currentWorkspace?.id}
|
||||||
onDragEnd={onDragEnd}
|
onDragEnd={onDragEnd}
|
||||||
/>
|
/>
|
||||||
{localWorkspaces.length > 0 && cloudWorkspaces.length > 0 ? (
|
{localWorkspaces.length > 0 && cloudWorkspaces.length > 0 ? (
|
||||||
@@ -225,7 +200,7 @@ export const AFFiNEWorkspaceList = ({
|
|||||||
onClickWorkspaceSetting={onClickWorkspaceSetting}
|
onClickWorkspaceSetting={onClickWorkspaceSetting}
|
||||||
onNewWorkspace={onNewWorkspace}
|
onNewWorkspace={onNewWorkspace}
|
||||||
onAddWorkspace={onAddWorkspace}
|
onAddWorkspace={onAddWorkspace}
|
||||||
currentWorkspaceId={currentWorkspaceId}
|
currentWorkspaceId={currentWorkspace?.id}
|
||||||
onDragEnd={onDragEnd}
|
onDragEnd={onDragEnd}
|
||||||
/>
|
/>
|
||||||
</ScrollableContainer>
|
</ScrollableContainer>
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import { Avatar } from '@affine/component/ui/avatar';
|
import { Avatar } from '@affine/component/ui/avatar';
|
||||||
import { Loading } from '@affine/component/ui/loading';
|
import { Loading } from '@affine/component/ui/loading';
|
||||||
import { Tooltip } from '@affine/component/ui/tooltip';
|
import { Tooltip } from '@affine/component/ui/tooltip';
|
||||||
import { useCurrentSyncEngine } from '@affine/core/hooks/current/use-current-sync-engine';
|
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
import {
|
import { type SyncEngineStatus, SyncEngineStep } from '@affine/workspace';
|
||||||
type SyncEngineStatus,
|
import { waitForCurrentWorkspaceAtom } from '@affine/workspace/atom';
|
||||||
SyncEngineStep,
|
|
||||||
} from '@affine/workspace/providers';
|
|
||||||
import {
|
import {
|
||||||
CloudWorkspaceIcon,
|
CloudWorkspaceIcon,
|
||||||
InformationFillDuotoneIcon,
|
InformationFillDuotoneIcon,
|
||||||
@@ -14,8 +12,9 @@ import {
|
|||||||
NoNetworkIcon,
|
NoNetworkIcon,
|
||||||
UnsyncIcon,
|
UnsyncIcon,
|
||||||
} from '@blocksuite/icons';
|
} from '@blocksuite/icons';
|
||||||
import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-block-suite-workspace-avatar-url';
|
import { useWorkspaceBlobObjectUrl } from '@toeverything/hooks/use-workspace-blob';
|
||||||
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
|
import { useWorkspaceInfo } from '@toeverything/hooks/use-workspace-info';
|
||||||
|
import { useAtomValue } from 'jotai';
|
||||||
import { debounce } from 'lodash-es';
|
import { debounce } from 'lodash-es';
|
||||||
import {
|
import {
|
||||||
forwardRef,
|
forwardRef,
|
||||||
@@ -27,7 +26,6 @@ import {
|
|||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
import { useSystemOnline } from '../../../../hooks/use-system-online';
|
import { useSystemOnline } from '../../../../hooks/use-system-online';
|
||||||
import type { AllWorkspace } from '../../../../shared';
|
|
||||||
import {
|
import {
|
||||||
StyledSelectorContainer,
|
StyledSelectorContainer,
|
||||||
StyledSelectorWrapper,
|
StyledSelectorWrapper,
|
||||||
@@ -87,21 +85,18 @@ const OfflineStatus = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const WorkspaceStatus = ({
|
const WorkspaceStatus = () => {
|
||||||
currentWorkspace,
|
|
||||||
}: {
|
|
||||||
currentWorkspace: AllWorkspace;
|
|
||||||
}) => {
|
|
||||||
const isOnline = useSystemOnline();
|
const isOnline = useSystemOnline();
|
||||||
|
|
||||||
const [syncEngineStatus, setSyncEngineStatus] =
|
const [syncEngineStatus, setSyncEngineStatus] =
|
||||||
useState<SyncEngineStatus | null>(null);
|
useState<SyncEngineStatus | null>(null);
|
||||||
|
|
||||||
const syncEngine = useCurrentSyncEngine();
|
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
|
||||||
|
|
||||||
|
// debounce sync engine status
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSyncEngineStatus(syncEngine?.status ?? null);
|
setSyncEngineStatus(currentWorkspace.engine.sync.status);
|
||||||
const disposable = syncEngine?.onStatusChange.on(
|
const disposable = currentWorkspace.engine.sync.onStatusChange.on(
|
||||||
debounce(status => {
|
debounce(status => {
|
||||||
setSyncEngineStatus(status);
|
setSyncEngineStatus(status);
|
||||||
}, 500)
|
}, 500)
|
||||||
@@ -109,7 +104,7 @@ const WorkspaceStatus = ({
|
|||||||
return () => {
|
return () => {
|
||||||
disposable?.dispose();
|
disposable?.dispose();
|
||||||
};
|
};
|
||||||
}, [syncEngine]);
|
}, [currentWorkspace]);
|
||||||
|
|
||||||
const content = useMemo(() => {
|
const content = useMemo(() => {
|
||||||
// TODO: add i18n
|
// TODO: add i18n
|
||||||
@@ -162,17 +157,18 @@ const WorkspaceStatus = ({
|
|||||||
|
|
||||||
export const WorkspaceCard = forwardRef<
|
export const WorkspaceCard = forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
{
|
HTMLAttributes<HTMLDivElement>
|
||||||
currentWorkspace: AllWorkspace;
|
>(({ ...props }, ref) => {
|
||||||
} & HTMLAttributes<HTMLDivElement>
|
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
|
||||||
>(({ currentWorkspace, ...props }, ref) => {
|
|
||||||
const [name] = useBlockSuiteWorkspaceName(
|
const information = useWorkspaceInfo(currentWorkspace.meta);
|
||||||
currentWorkspace.blockSuiteWorkspace
|
|
||||||
|
const avatarUrl = useWorkspaceBlobObjectUrl(
|
||||||
|
currentWorkspace.meta,
|
||||||
|
information?.avatar
|
||||||
);
|
);
|
||||||
|
|
||||||
const [workspaceAvatar] = useBlockSuiteWorkspaceAvatarUrl(
|
const name = information?.name ?? UNTITLED_WORKSPACE_NAME;
|
||||||
currentWorkspace.blockSuiteWorkspace
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledSelectorContainer
|
<StyledSelectorContainer
|
||||||
@@ -186,7 +182,7 @@ export const WorkspaceCard = forwardRef<
|
|||||||
<Avatar
|
<Avatar
|
||||||
data-testid="workspace-avatar"
|
data-testid="workspace-avatar"
|
||||||
size={40}
|
size={40}
|
||||||
url={workspaceAvatar}
|
url={avatarUrl}
|
||||||
name={name}
|
name={name}
|
||||||
colorfulFallback
|
colorfulFallback
|
||||||
/>
|
/>
|
||||||
@@ -194,7 +190,7 @@ export const WorkspaceCard = forwardRef<
|
|||||||
<StyledWorkspaceName data-testid="workspace-name">
|
<StyledWorkspaceName data-testid="workspace-name">
|
||||||
{name}
|
{name}
|
||||||
</StyledWorkspaceName>
|
</StyledWorkspaceName>
|
||||||
<WorkspaceStatus currentWorkspace={currentWorkspace} />
|
<WorkspaceStatus />
|
||||||
</StyledSelectorWrapper>
|
</StyledSelectorWrapper>
|
||||||
</StyledSelectorContainer>
|
</StyledSelectorContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { Menu } from '@affine/component/ui/menu';
|
|||||||
import { collectionsCRUDAtom } from '@affine/core/atoms/collections';
|
import { collectionsCRUDAtom } from '@affine/core/atoms/collections';
|
||||||
import { WorkspaceSubPath } from '@affine/env/workspace';
|
import { WorkspaceSubPath } from '@affine/env/workspace';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
|
import type { Workspace } from '@affine/workspace';
|
||||||
import { FolderIcon, SettingsIcon } from '@blocksuite/icons';
|
import { FolderIcon, SettingsIcon } from '@blocksuite/icons';
|
||||||
import { type Page } from '@blocksuite/store';
|
import { type Page } from '@blocksuite/store';
|
||||||
import { useDroppable } from '@dnd-kit/core';
|
import { useDroppable } from '@dnd-kit/core';
|
||||||
@@ -40,7 +41,6 @@ import { getDropItemId } from '../../hooks/affine/use-sidebar-drag';
|
|||||||
import { useTrashModalHelper } from '../../hooks/affine/use-trash-modal-helper';
|
import { useTrashModalHelper } from '../../hooks/affine/use-trash-modal-helper';
|
||||||
import { useRegisterBrowserHistoryCommands } from '../../hooks/use-browser-history-commands';
|
import { useRegisterBrowserHistoryCommands } from '../../hooks/use-browser-history-commands';
|
||||||
import { useNavigateHelper } from '../../hooks/use-navigate-helper';
|
import { useNavigateHelper } from '../../hooks/use-navigate-helper';
|
||||||
import type { AllWorkspace } from '../../shared';
|
|
||||||
import { CollectionsList } from '../pure/workspace-slider-bar/collections';
|
import { CollectionsList } from '../pure/workspace-slider-bar/collections';
|
||||||
import { AddCollectionButton } from '../pure/workspace-slider-bar/collections/add-collection-button';
|
import { AddCollectionButton } from '../pure/workspace-slider-bar/collections/add-collection-button';
|
||||||
import { AddFavouriteButton } from '../pure/workspace-slider-bar/favorite/add-favourite-button';
|
import { AddFavouriteButton } from '../pure/workspace-slider-bar/favorite/add-favourite-button';
|
||||||
@@ -53,7 +53,7 @@ export type RootAppSidebarProps = {
|
|||||||
isPublicWorkspace: boolean;
|
isPublicWorkspace: boolean;
|
||||||
onOpenQuickSearchModal: () => void;
|
onOpenQuickSearchModal: () => void;
|
||||||
onOpenSettingModal: () => void;
|
onOpenSettingModal: () => void;
|
||||||
currentWorkspace: AllWorkspace;
|
currentWorkspace: Workspace;
|
||||||
openPage: (pageId: string) => void;
|
openPage: (pageId: string) => void;
|
||||||
createPage: () => Page;
|
createPage: () => Page;
|
||||||
currentPath: string;
|
currentPath: string;
|
||||||
@@ -185,9 +185,9 @@ export const RootAppSidebar = ({
|
|||||||
});
|
});
|
||||||
const handleCreateCollection = useCallback(() => {
|
const handleCreateCollection = useCallback(() => {
|
||||||
open('')
|
open('')
|
||||||
.then(async name => {
|
.then(name => {
|
||||||
const id = nanoid();
|
const id = nanoid();
|
||||||
await setting.createCollection(createEmptyCollection(id, { name }));
|
setting.createCollection(createEmptyCollection(id, { name }));
|
||||||
navigateHelper.jumpToCollection(blockSuiteWorkspace.id, id);
|
navigateHelper.jumpToCollection(blockSuiteWorkspace.id, id);
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
@@ -230,7 +230,6 @@ export const RootAppSidebar = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<WorkspaceCard
|
<WorkspaceCard
|
||||||
currentWorkspace={currentWorkspace}
|
|
||||||
onClick={useCallback(() => {
|
onClick={useCallback(() => {
|
||||||
setOpenUserWorkspaceList(true);
|
setOpenUserWorkspaceList(true);
|
||||||
}, [setOpenUserWorkspaceList])}
|
}, [setOpenUserWorkspaceList])}
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
import { BrowserWarning } from '@affine/component/affine-banner';
|
import { BrowserWarning } from '@affine/component/affine-banner';
|
||||||
import { LocalDemoTips } from '@affine/component/affine-banner';
|
import { LocalDemoTips } from '@affine/component/affine-banner';
|
||||||
import {
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
type AffineOfficialWorkspace,
|
|
||||||
WorkspaceFlavour,
|
|
||||||
} from '@affine/env/workspace';
|
|
||||||
import { Trans } from '@affine/i18n';
|
import { Trans } from '@affine/i18n';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { useSetAtom } from 'jotai';
|
import type { Workspace } from '@affine/workspace';
|
||||||
|
import { workspaceManagerAtom } from '@affine/workspace/atom';
|
||||||
|
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||||
|
import { useAtomValue, useSetAtom } from 'jotai';
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
import { authAtom } from '../atoms';
|
import { authAtom } from '../atoms';
|
||||||
import { useCurrentLoginStatus } from '../hooks/affine/use-current-login-status';
|
import { useCurrentLoginStatus } from '../hooks/affine/use-current-login-status';
|
||||||
import { useOnTransformWorkspace } from '../hooks/root/use-on-transform-workspace';
|
import { useNavigateHelper } from '../hooks/use-navigate-helper';
|
||||||
|
import { WorkspaceSubPath } from '../shared';
|
||||||
import { EnableAffineCloudModal } from './affine/enable-affine-cloud-modal';
|
import { EnableAffineCloudModal } from './affine/enable-affine-cloud-modal';
|
||||||
|
|
||||||
const minimumChromeVersion = 106;
|
const minimumChromeVersion = 106;
|
||||||
@@ -57,9 +58,11 @@ const OSWarningMessage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const TopTip = ({
|
export const TopTip = ({
|
||||||
|
pageId,
|
||||||
workspace,
|
workspace,
|
||||||
}: {
|
}: {
|
||||||
workspace: AffineOfficialWorkspace;
|
pageId?: string;
|
||||||
|
workspace: Workspace;
|
||||||
}) => {
|
}) => {
|
||||||
const loginStatus = useCurrentLoginStatus();
|
const loginStatus = useCurrentLoginStatus();
|
||||||
const isLoggedIn = loginStatus === 'authenticated';
|
const isLoggedIn = loginStatus === 'authenticated';
|
||||||
@@ -73,18 +76,18 @@ export const TopTip = ({
|
|||||||
setAuthModal({ openModal: true, state: 'signIn' });
|
setAuthModal({ openModal: true, state: 'signIn' });
|
||||||
}, [setAuthModal]);
|
}, [setAuthModal]);
|
||||||
|
|
||||||
const onTransformWorkspace = useOnTransformWorkspace();
|
const { openPage } = useNavigateHelper();
|
||||||
const handleConfirm = useCallback(() => {
|
const workspaceManager = useAtomValue(workspaceManagerAtom);
|
||||||
|
const handleConfirm = useAsyncCallback(async () => {
|
||||||
if (workspace.flavour !== WorkspaceFlavour.LOCAL) {
|
if (workspace.flavour !== WorkspaceFlavour.LOCAL) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
onTransformWorkspace(
|
// TODO: we need to transform local to cloud
|
||||||
WorkspaceFlavour.LOCAL,
|
const { id: newId } =
|
||||||
WorkspaceFlavour.AFFINE_CLOUD,
|
await workspaceManager.transformLocalToCloud(workspace);
|
||||||
workspace
|
openPage(newId, pageId || WorkspaceSubPath.ALL);
|
||||||
);
|
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
}, [onTransformWorkspace, workspace]);
|
}, [openPage, pageId, workspace, workspaceManager]);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
showLocalDemoTips &&
|
showLocalDemoTips &&
|
||||||
|
|||||||
@@ -1,122 +0,0 @@
|
|||||||
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';
|
|
||||||
|
|
||||||
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 syncEngine = useCurrentSyncEngine();
|
|
||||||
const rootStore = getCurrentStore();
|
|
||||||
|
|
||||||
const upgradeWorkspace = useAsyncCallback(async () => {
|
|
||||||
setState('upgrading');
|
|
||||||
setError(null);
|
|
||||||
try {
|
|
||||||
// Migration need to wait for root doc and all subdocs loaded.
|
|
||||||
await syncEngine?.waitForSynced();
|
|
||||||
|
|
||||||
// Clone a new doc to prevent change events.
|
|
||||||
const clonedDoc = new YDoc({
|
|
||||||
guid: workspace.blockSuiteWorkspace.doc.guid,
|
|
||||||
});
|
|
||||||
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) {
|
|
||||||
console.error(e);
|
|
||||||
setError(e);
|
|
||||||
setState('error');
|
|
||||||
}
|
|
||||||
}, [rootStore, workspace, syncEngine, migration]);
|
|
||||||
|
|
||||||
return [state, error, upgradeWorkspace, newWorkspaceId] as const;
|
|
||||||
}
|
|
||||||
@@ -1,90 +1,84 @@
|
|||||||
import { AffineShapeIcon } from '@affine/component/page-list'; // TODO: import from page-list temporarily, need to defined common svg icon/images management.
|
import { AffineShapeIcon } from '@affine/component/page-list'; // TODO: import from page-list temporarily, need to defined common svg icon/images management.
|
||||||
import { Button } from '@affine/component/ui/button';
|
import { Button } from '@affine/component/ui/button';
|
||||||
|
import { WorkspaceSubPath } from '@affine/env/workspace';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import type { MigrationPoint } from '@toeverything/infra/blocksuite';
|
import {
|
||||||
import { useCallback, useMemo } from 'react';
|
waitForCurrentWorkspaceAtom,
|
||||||
|
workspaceManagerAtom,
|
||||||
|
} from '@affine/workspace/atom';
|
||||||
|
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
|
||||||
|
import { useWorkspaceStatus } from '@toeverything/hooks/use-workspace-status';
|
||||||
|
import { useAtomValue } from 'jotai';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { pathGenerator } from '../../shared';
|
import { useNavigateHelper } from '../../hooks/use-navigate-helper';
|
||||||
import * as styles from './upgrade.css';
|
import * as styles from './upgrade.css';
|
||||||
import { type UpgradeState, useUpgradeWorkspace } from './upgrade-hooks';
|
|
||||||
import { ArrowCircleIcon, HeartBreakIcon } from './upgrade-icon';
|
import { ArrowCircleIcon, HeartBreakIcon } from './upgrade-icon';
|
||||||
|
|
||||||
const UPGRADE_TIPS_KEYS = {
|
|
||||||
pending: 'com.affine.upgrade.tips.normal',
|
|
||||||
upgrading: 'com.affine.upgrade.tips.normal',
|
|
||||||
done: 'com.affine.upgrade.tips.done',
|
|
||||||
error: 'com.affine.upgrade.tips.error',
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
const BUTTON_TEXT_KEYS = {
|
|
||||||
pending: 'com.affine.upgrade.button-text.pending',
|
|
||||||
upgrading: 'com.affine.upgrade.button-text.upgrading',
|
|
||||||
done: 'com.affine.upgrade.button-text.done',
|
|
||||||
error: 'com.affine.upgrade.button-text.error',
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
function UpgradeIcon({ upgradeState }: { upgradeState: UpgradeState }) {
|
|
||||||
if (upgradeState === 'error') {
|
|
||||||
return <HeartBreakIcon />;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<ArrowCircleIcon
|
|
||||||
className={upgradeState === 'upgrading' ? styles.loadingIcon : undefined}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface WorkspaceUpgradeProps {
|
|
||||||
migration: MigrationPoint;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO: Help info is not implemented yet.
|
* TODO: Help info is not implemented yet.
|
||||||
*/
|
*/
|
||||||
export const WorkspaceUpgrade = function WorkspaceUpgrade(
|
export const WorkspaceUpgrade = function WorkspaceUpgrade() {
|
||||||
props: WorkspaceUpgradeProps
|
const [error, setError] = useState<string | null>(null);
|
||||||
) {
|
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
|
||||||
const [upgradeState, error, upgradeWorkspace, newWorkspaceId] =
|
const workspaceManager = useAtomValue(workspaceManagerAtom);
|
||||||
useUpgradeWorkspace(props.migration);
|
const upgradeStatus = useWorkspaceStatus(currentWorkspace, s => s.upgrade);
|
||||||
|
const { openPage } = useNavigateHelper();
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
|
|
||||||
const refreshPage = useCallback(() => {
|
const onButtonClick = useAsyncCallback(async () => {
|
||||||
window.location.reload();
|
if (upgradeStatus?.upgrading) {
|
||||||
}, []);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const onButtonClick = useMemo(() => {
|
try {
|
||||||
if (upgradeState === 'done') {
|
const newWorkspaceId =
|
||||||
|
await currentWorkspace.upgrade.upgrade(workspaceManager);
|
||||||
if (newWorkspaceId) {
|
if (newWorkspaceId) {
|
||||||
return () => {
|
openPage(newWorkspaceId, WorkspaceSubPath.ALL);
|
||||||
window.location.replace(pathGenerator.all(newWorkspaceId));
|
} else {
|
||||||
};
|
// blocksuite may enter an incorrect state, reload to reset it.
|
||||||
|
location.reload();
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
return refreshPage;
|
setError(error instanceof Error ? error.message : '' + error);
|
||||||
}
|
}
|
||||||
|
}, [
|
||||||
if (upgradeState === 'pending') {
|
upgradeStatus?.upgrading,
|
||||||
return upgradeWorkspace;
|
currentWorkspace.upgrade,
|
||||||
}
|
workspaceManager,
|
||||||
|
openPage,
|
||||||
return undefined;
|
]);
|
||||||
}, [upgradeState, upgradeWorkspace, refreshPage, newWorkspaceId]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.layout}>
|
<div className={styles.layout}>
|
||||||
<div className={styles.upgradeBox}>
|
<div className={styles.upgradeBox}>
|
||||||
<AffineShapeIcon width={180} height={180} />
|
<AffineShapeIcon width={180} height={180} />
|
||||||
<p className={styles.upgradeTips}>
|
<p className={styles.upgradeTips}>
|
||||||
{error ? error.message : t[UPGRADE_TIPS_KEYS[upgradeState]]()}
|
{error ? error : t['com.affine.upgrade.tips.normal']()}
|
||||||
</p>
|
</p>
|
||||||
<Button
|
<Button
|
||||||
data-testid="upgrade-workspace-button"
|
data-testid="upgrade-workspace-button"
|
||||||
onClick={onButtonClick}
|
onClick={onButtonClick}
|
||||||
size="extraLarge"
|
size="extraLarge"
|
||||||
icon={<UpgradeIcon upgradeState={upgradeState} />}
|
icon={
|
||||||
type={upgradeState === 'error' ? 'error' : 'default'}
|
error ? (
|
||||||
|
<HeartBreakIcon />
|
||||||
|
) : (
|
||||||
|
<ArrowCircleIcon
|
||||||
|
className={
|
||||||
|
upgradeStatus?.upgrading ? styles.loadingIcon : undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
type={error ? 'error' : 'default'}
|
||||||
>
|
>
|
||||||
{t[BUTTON_TEXT_KEYS[upgradeState]]()}
|
{error
|
||||||
|
? t['com.affine.upgrade.button-text.error']()
|
||||||
|
: upgradeStatus?.upgrading
|
||||||
|
? t['com.affine.upgrade.button-text.upgrading']()
|
||||||
|
: t['com.affine.upgrade.button-text.pending']()}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,16 +4,17 @@ import {
|
|||||||
FavoriteTag,
|
FavoriteTag,
|
||||||
} from '@affine/component/page-list';
|
} from '@affine/component/page-list';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
|
import { waitForCurrentWorkspaceAtom } from '@affine/workspace/atom';
|
||||||
import type { PageMeta } from '@blocksuite/store';
|
import type { PageMeta } from '@blocksuite/store';
|
||||||
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
|
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
|
||||||
|
import { useAtomValue } from 'jotai';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
import { usePageHelper } from '../../components/blocksuite/block-suite-page-list/utils';
|
import { usePageHelper } from '../../components/blocksuite/block-suite-page-list/utils';
|
||||||
import { useCurrentWorkspace } from '../current/use-current-workspace';
|
|
||||||
import { useBlockSuiteMetaHelper } from './use-block-suite-meta-helper';
|
import { useBlockSuiteMetaHelper } from './use-block-suite-meta-helper';
|
||||||
|
|
||||||
export const useAllPageListConfig = () => {
|
export const useAllPageListConfig = () => {
|
||||||
const [currentWorkspace] = useCurrentWorkspace();
|
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
|
||||||
const workspace = currentWorkspace.blockSuiteWorkspace;
|
const workspace = currentWorkspace.blockSuiteWorkspace;
|
||||||
const pageMetas = useBlockSuitePageMeta(workspace);
|
const pageMetas = useBlockSuitePageMeta(workspace);
|
||||||
const { isPreferredEdgeless } = usePageHelper(workspace);
|
const { isPreferredEdgeless } = usePageHelper(workspace);
|
||||||
|
|||||||
@@ -1,13 +1,23 @@
|
|||||||
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
import { getIsOwnerQuery } from '@affine/graphql';
|
import { getIsOwnerQuery } from '@affine/graphql';
|
||||||
import { useQueryImmutable } from '@affine/workspace/affine/gql';
|
import { useQueryImmutable } from '@affine/workspace/affine/gql';
|
||||||
|
import type { WorkspaceMetadata } from '@affine/workspace/metadata';
|
||||||
|
|
||||||
export function useIsWorkspaceOwner(workspaceId: string) {
|
export function useIsWorkspaceOwner(workspaceMetadata: WorkspaceMetadata) {
|
||||||
const { data } = useQueryImmutable({
|
const { data } = useQueryImmutable(
|
||||||
query: getIsOwnerQuery,
|
workspaceMetadata.flavour !== WorkspaceFlavour.LOCAL
|
||||||
variables: {
|
? {
|
||||||
workspaceId,
|
query: getIsOwnerQuery,
|
||||||
},
|
variables: {
|
||||||
});
|
workspaceId: workspaceMetadata.id,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
if (workspaceMetadata.flavour === WorkspaceFlavour.LOCAL) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return data.isOwner;
|
return data.isOwner;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
import { leaveWorkspaceMutation } from '@affine/graphql';
|
|
||||||
import { useMutation } from '@affine/workspace/affine/gql';
|
|
||||||
import { useCallback } from 'react';
|
|
||||||
|
|
||||||
import { useAppHelper } from '../use-workspaces';
|
|
||||||
|
|
||||||
export function useLeaveWorkspace() {
|
|
||||||
const { deleteWorkspaceMeta } = useAppHelper();
|
|
||||||
|
|
||||||
const { trigger: leaveWorkspace } = useMutation({
|
|
||||||
mutation: leaveWorkspaceMutation,
|
|
||||||
});
|
|
||||||
|
|
||||||
return useCallback(
|
|
||||||
async (workspaceId: string, workspaceName: string) => {
|
|
||||||
deleteWorkspaceMeta(workspaceId);
|
|
||||||
|
|
||||||
await leaveWorkspace({
|
|
||||||
workspaceId,
|
|
||||||
workspaceName,
|
|
||||||
sendLeaveMail: true,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[deleteWorkspaceMeta, leaveWorkspace]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { toast } from '@affine/component';
|
import { toast } from '@affine/component';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
|
import { waitForCurrentWorkspaceAtom } from '@affine/workspace/atom';
|
||||||
import { assertExists } from '@blocksuite/global/utils';
|
import { assertExists } from '@blocksuite/global/utils';
|
||||||
import { EdgelessIcon, HistoryIcon, PageIcon } from '@blocksuite/icons';
|
import { EdgelessIcon, HistoryIcon, PageIcon } from '@blocksuite/icons';
|
||||||
import { usePageMetaHelper } from '@toeverything/hooks/use-block-suite-page-meta';
|
import { usePageMetaHelper } from '@toeverything/hooks/use-block-suite-page-meta';
|
||||||
@@ -8,11 +9,10 @@ import {
|
|||||||
PreconditionStrategy,
|
PreconditionStrategy,
|
||||||
registerAffineCommand,
|
registerAffineCommand,
|
||||||
} from '@toeverything/infra/command';
|
} from '@toeverything/infra/command';
|
||||||
import { useSetAtom } from 'jotai';
|
import { useAtomValue, useSetAtom } from 'jotai';
|
||||||
import { useCallback, useEffect } from 'react';
|
import { useCallback, useEffect } from 'react';
|
||||||
|
|
||||||
import { pageHistoryModalAtom } from '../../atoms/page-history';
|
import { pageHistoryModalAtom } from '../../atoms/page-history';
|
||||||
import { useCurrentWorkspace } from '../current/use-current-workspace';
|
|
||||||
import { useBlockSuiteMetaHelper } from './use-block-suite-meta-helper';
|
import { useBlockSuiteMetaHelper } from './use-block-suite-meta-helper';
|
||||||
import { useExportPage } from './use-export-page';
|
import { useExportPage } from './use-export-page';
|
||||||
import { useTrashModalHelper } from './use-trash-modal-helper';
|
import { useTrashModalHelper } from './use-trash-modal-helper';
|
||||||
@@ -22,7 +22,7 @@ export function useRegisterBlocksuiteEditorCommands(
|
|||||||
mode: 'page' | 'edgeless'
|
mode: 'page' | 'edgeless'
|
||||||
) {
|
) {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const [workspace] = useCurrentWorkspace();
|
const workspace = useAtomValue(waitForCurrentWorkspaceAtom);
|
||||||
const blockSuiteWorkspace = workspace.blockSuiteWorkspace;
|
const blockSuiteWorkspace = workspace.blockSuiteWorkspace;
|
||||||
const { getPageMeta } = usePageMetaHelper(blockSuiteWorkspace);
|
const { getPageMeta } = usePageMetaHelper(blockSuiteWorkspace);
|
||||||
const currentPage = blockSuiteWorkspace.getPage(pageId);
|
const currentPage = blockSuiteWorkspace.getPage(pageId);
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { toast } from '@affine/component';
|
import { toast } from '@affine/component';
|
||||||
import type { DraggableTitleCellData } from '@affine/component/page-list';
|
import type { DraggableTitleCellData } from '@affine/component/page-list';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
|
import { waitForCurrentWorkspaceAtom } from '@affine/workspace/atom';
|
||||||
import type { DragEndEvent, UniqueIdentifier } from '@dnd-kit/core';
|
import type { DragEndEvent, UniqueIdentifier } from '@dnd-kit/core';
|
||||||
import { usePageMetaHelper } from '@toeverything/hooks/use-block-suite-page-meta';
|
import { usePageMetaHelper } from '@toeverything/hooks/use-block-suite-page-meta';
|
||||||
|
import { useAtomValue } from 'jotai';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import { useCurrentWorkspace } from '../current/use-current-workspace';
|
|
||||||
import { useBlockSuiteMetaHelper } from './use-block-suite-meta-helper';
|
import { useBlockSuiteMetaHelper } from './use-block-suite-meta-helper';
|
||||||
import { useTrashModalHelper } from './use-trash-modal-helper';
|
import { useTrashModalHelper } from './use-trash-modal-helper';
|
||||||
|
|
||||||
@@ -68,7 +69,7 @@ export function getDragItemId(
|
|||||||
|
|
||||||
export const useSidebarDrag = () => {
|
export const useSidebarDrag = () => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const [currentWorkspace] = useCurrentWorkspace();
|
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
|
||||||
const workspace = currentWorkspace.blockSuiteWorkspace;
|
const workspace = currentWorkspace.blockSuiteWorkspace;
|
||||||
const { setTrashModal } = useTrashModalHelper(workspace);
|
const { setTrashModal } = useTrashModalHelper(workspace);
|
||||||
const { addToFavorite, removeFromFavorite } =
|
const { addToFavorite, removeFromFavorite } =
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import {
|
import { waitForCurrentWorkspaceAtom } from '@affine/workspace/atom';
|
||||||
currentPageIdAtom,
|
import { useBlockSuiteWorkspacePage } from '@toeverything/hooks/use-block-suite-workspace-page';
|
||||||
currentWorkspaceAtom,
|
import { currentPageIdAtom } from '@toeverything/infra/atom';
|
||||||
} from '@toeverything/infra/atom';
|
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
|
|
||||||
export const useCurrentPage = () => {
|
export const useCurrentPage = () => {
|
||||||
const currentPageId = useAtomValue(currentPageIdAtom);
|
const currentPageId = useAtomValue(currentPageIdAtom);
|
||||||
const currentWorkspace = useAtomValue(currentWorkspaceAtom);
|
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
|
||||||
|
|
||||||
return currentPageId ? currentWorkspace.getPage(currentPageId) : null;
|
return useBlockSuiteWorkspacePage(
|
||||||
|
currentWorkspace?.blockSuiteWorkspace,
|
||||||
|
currentPageId
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
import { assertExists } from '@blocksuite/global/utils';
|
|
||||||
import {
|
|
||||||
currentPageIdAtom,
|
|
||||||
currentWorkspaceIdAtom,
|
|
||||||
} from '@toeverything/infra/atom';
|
|
||||||
import { useAtom, useSetAtom } from 'jotai';
|
|
||||||
import { useCallback, useEffect } from 'react';
|
|
||||||
|
|
||||||
import type { AllWorkspace } from '../../shared';
|
|
||||||
import { useWorkspace, useWorkspaceEffect } from '../use-workspace';
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
/**
|
|
||||||
* @internal debug only
|
|
||||||
*/
|
|
||||||
// eslint-disable-next-line no-var
|
|
||||||
var currentWorkspace: AllWorkspace | undefined;
|
|
||||||
interface WindowEventMap {
|
|
||||||
'affine:workspace:change': CustomEvent<{ id: string }>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useCurrentWorkspace(): [
|
|
||||||
AllWorkspace,
|
|
||||||
(id: string | null) => void,
|
|
||||||
] {
|
|
||||||
const [id, setId] = useAtom(currentWorkspaceIdAtom);
|
|
||||||
assertExists(id);
|
|
||||||
const currentWorkspace = useWorkspace(id);
|
|
||||||
// when you call current workspace, effect is always called
|
|
||||||
useWorkspaceEffect(currentWorkspace.id);
|
|
||||||
useEffect(() => {
|
|
||||||
globalThis.currentWorkspace = currentWorkspace;
|
|
||||||
globalThis.dispatchEvent(
|
|
||||||
new CustomEvent('affine:workspace:change', {
|
|
||||||
detail: { id: currentWorkspace.id },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}, [currentWorkspace]);
|
|
||||||
const setPageId = useSetAtom(currentPageIdAtom);
|
|
||||||
return [
|
|
||||||
currentWorkspace,
|
|
||||||
useCallback(
|
|
||||||
(id: string | null) => {
|
|
||||||
if (environment.isBrowser && id) {
|
|
||||||
localStorage.setItem('last_workspace_id', id);
|
|
||||||
}
|
|
||||||
setPageId(null);
|
|
||||||
setId(id);
|
|
||||||
},
|
|
||||||
[setId, setPageId]
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
import { pushNotificationAtom } from '@affine/component/notification-center';
|
|
||||||
import type { WorkspaceRegistry } from '@affine/env/workspace';
|
|
||||||
import type { WorkspaceFlavour } from '@affine/env/workspace';
|
|
||||||
import { WorkspaceSubPath } from '@affine/env/workspace';
|
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
|
||||||
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 { openSettingModalAtom } from '../../atoms';
|
|
||||||
import { useNavigateHelper } from '../use-navigate-helper';
|
|
||||||
|
|
||||||
export function useOnTransformWorkspace() {
|
|
||||||
const t = useAFFiNEI18N();
|
|
||||||
const setSettingModal = useSetAtom(openSettingModalAtom);
|
|
||||||
const WorkspaceAdapters = useAtomValue(workspaceAdaptersAtom);
|
|
||||||
const setMetadata = useSetAtom(rootWorkspacesMetadataAtom);
|
|
||||||
const { openPage } = useNavigateHelper();
|
|
||||||
const currentPageId = useAtomValue(currentPageIdAtom);
|
|
||||||
const pushNotification = useSetAtom(pushNotificationAtom);
|
|
||||||
|
|
||||||
return useAsyncCallback(
|
|
||||||
async <From extends WorkspaceFlavour, To extends WorkspaceFlavour>(
|
|
||||||
from: From,
|
|
||||||
to: To,
|
|
||||||
workspace: WorkspaceRegistry[From]
|
|
||||||
): Promise<void> => {
|
|
||||||
// create first, then delete, in case of failure
|
|
||||||
const newId = await WorkspaceAdapters[to].CRUD.create(
|
|
||||||
workspace.blockSuiteWorkspace
|
|
||||||
);
|
|
||||||
await WorkspaceAdapters[from].CRUD.delete(workspace.blockSuiteWorkspace);
|
|
||||||
setMetadata(workspaces => {
|
|
||||||
const idx = workspaces.findIndex(ws => ws.id === workspace.id);
|
|
||||||
workspaces.splice(idx, 1, {
|
|
||||||
id: newId,
|
|
||||||
flavour: to,
|
|
||||||
version: WorkspaceVersion.SubDoc,
|
|
||||||
});
|
|
||||||
return [...workspaces];
|
|
||||||
}, newId);
|
|
||||||
// fixme(himself65): setting modal could still open and open the non-exist workspace
|
|
||||||
setSettingModal(settings => ({
|
|
||||||
...settings,
|
|
||||||
open: false,
|
|
||||||
}));
|
|
||||||
window.dispatchEvent(
|
|
||||||
new CustomEvent('affine-workspace:transform', {
|
|
||||||
detail: {
|
|
||||||
from,
|
|
||||||
to,
|
|
||||||
oldId: workspace.id,
|
|
||||||
newId: newId,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
openPage(newId, currentPageId ?? WorkspaceSubPath.ALL);
|
|
||||||
pushNotification({
|
|
||||||
title: t['Successfully enabled AFFiNE Cloud'](),
|
|
||||||
type: 'success',
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[
|
|
||||||
WorkspaceAdapters,
|
|
||||||
setMetadata,
|
|
||||||
setSettingModal,
|
|
||||||
openPage,
|
|
||||||
currentPageId,
|
|
||||||
pushNotification,
|
|
||||||
t,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
// global Events
|
|
||||||
interface WindowEventMap {
|
|
||||||
'affine-workspace:transform': CustomEvent<{
|
|
||||||
from: WorkspaceFlavour;
|
|
||||||
to: WorkspaceFlavour;
|
|
||||||
oldId: string;
|
|
||||||
newId: string;
|
|
||||||
}>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { useAtom, useStore } from 'jotai';
|
import { waitForCurrentWorkspaceAtom } from '@affine/workspace/atom';
|
||||||
|
import { useAtom, useAtomValue, useStore } from 'jotai';
|
||||||
import { useTheme } from 'next-themes';
|
import { useTheme } from 'next-themes';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
@@ -14,14 +15,13 @@ import {
|
|||||||
} from '../commands';
|
} from '../commands';
|
||||||
import { usePageHelper } from '../components/blocksuite/block-suite-page-list/utils';
|
import { usePageHelper } from '../components/blocksuite/block-suite-page-list/utils';
|
||||||
import { useLanguageHelper } from './affine/use-language-helper';
|
import { useLanguageHelper } from './affine/use-language-helper';
|
||||||
import { useCurrentWorkspace } from './current/use-current-workspace';
|
|
||||||
import { useNavigateHelper } from './use-navigate-helper';
|
import { useNavigateHelper } from './use-navigate-helper';
|
||||||
|
|
||||||
export function useRegisterWorkspaceCommands() {
|
export function useRegisterWorkspaceCommands() {
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const [currentWorkspace] = useCurrentWorkspace();
|
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
|
||||||
const languageHelper = useLanguageHelper();
|
const languageHelper = useLanguageHelper();
|
||||||
const pageHelper = usePageHelper(currentWorkspace.blockSuiteWorkspace);
|
const pageHelper = usePageHelper(currentWorkspace.blockSuiteWorkspace);
|
||||||
const navigationHelper = useNavigateHelper();
|
const navigationHelper = useNavigateHelper();
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
import { DebugLogger } from '@affine/debug';
|
|
||||||
import type { BlobManager } from '@blocksuite/store';
|
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
||||||
|
|
||||||
import type { BlockSuiteWorkspace } from '../shared';
|
|
||||||
|
|
||||||
const logger = new DebugLogger('useWorkspaceBlob');
|
|
||||||
|
|
||||||
export function useWorkspaceBlob(
|
|
||||||
blockSuiteWorkspace: BlockSuiteWorkspace
|
|
||||||
): BlobManager {
|
|
||||||
return useMemo(() => blockSuiteWorkspace.blob, [blockSuiteWorkspace.blob]);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useWorkspaceBlobImage(
|
|
||||||
key: string | null,
|
|
||||||
blockSuiteWorkspace: BlockSuiteWorkspace
|
|
||||||
) {
|
|
||||||
const blobManager = useWorkspaceBlob(blockSuiteWorkspace);
|
|
||||||
const [blob, setBlob] = useState<Blob | null>(null);
|
|
||||||
useEffect(() => {
|
|
||||||
const controller = new AbortController();
|
|
||||||
if (key === null) {
|
|
||||||
setBlob(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
blobManager
|
|
||||||
?.get(key)
|
|
||||||
.then(blob => {
|
|
||||||
if (controller.signal.aborted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (blob) {
|
|
||||||
setBlob(blob);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
logger.error('Failed to get blob', err);
|
|
||||||
});
|
|
||||||
return () => {
|
|
||||||
controller.abort();
|
|
||||||
};
|
|
||||||
}, [blobManager, key]);
|
|
||||||
const [url, setUrl] = useState<string | null>(null);
|
|
||||||
const ref = useRef<string | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (ref.current) {
|
|
||||||
URL.revokeObjectURL(ref.current);
|
|
||||||
}
|
|
||||||
if (blob) {
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
setUrl(url);
|
|
||||||
ref.current = url;
|
|
||||||
}
|
|
||||||
}, [blob]);
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import {
|
|
||||||
type AffineOfficialWorkspace,
|
|
||||||
WorkspaceFlavour,
|
|
||||||
} from '@affine/env/workspace';
|
|
||||||
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
|
||||||
import type { Workspace } from '@blocksuite/store';
|
|
||||||
import { getBlockSuiteWorkspaceAtom } from '@toeverything/infra/__internal__/workspace';
|
|
||||||
import type { Atom } from 'jotai';
|
|
||||||
import { atom, useAtomValue } from 'jotai';
|
|
||||||
|
|
||||||
const workspaceWeakMap = new WeakMap<
|
|
||||||
Workspace,
|
|
||||||
Atom<Promise<AffineOfficialWorkspace>>
|
|
||||||
>();
|
|
||||||
|
|
||||||
// workspace effect is the side effect like connect to the server/indexeddb,
|
|
||||||
// this will save the workspace updates permanently.
|
|
||||||
export function useWorkspaceEffect(workspaceId: string): void {
|
|
||||||
const [, effectAtom] = getBlockSuiteWorkspaceAtom(workspaceId);
|
|
||||||
useAtomValue(effectAtom);
|
|
||||||
}
|
|
||||||
|
|
||||||
// todo(himself65): remove this hook
|
|
||||||
export function useWorkspace(workspaceId: string): AffineOfficialWorkspace {
|
|
||||||
const [workspaceAtom] = getBlockSuiteWorkspaceAtom(workspaceId);
|
|
||||||
const workspace = useAtomValue(workspaceAtom);
|
|
||||||
if (!workspaceWeakMap.has(workspace)) {
|
|
||||||
const baseAtom = atom(async get => {
|
|
||||||
const metadataList = await get(rootWorkspacesMetadataAtom);
|
|
||||||
const flavour = metadataList.find(({ id }) => id === workspaceId)
|
|
||||||
?.flavour;
|
|
||||||
|
|
||||||
if (!flavour) {
|
|
||||||
// when last workspace is removed, we may encounter this warning. it should be fine
|
|
||||||
console.warn(
|
|
||||||
'workspace not found in rootWorkspacesMetadataAtom, maybe it is removed',
|
|
||||||
workspaceId
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: workspaceId,
|
|
||||||
flavour: flavour ?? WorkspaceFlavour.LOCAL,
|
|
||||||
blockSuiteWorkspace: workspace,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
workspaceWeakMap.set(workspace, baseAtom);
|
|
||||||
}
|
|
||||||
|
|
||||||
return useAtomValue(
|
|
||||||
workspaceWeakMap.get(workspace) as Atom<Promise<AffineOfficialWorkspace>>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
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 } from '@affine/workspace/manager';
|
|
||||||
import { getWorkspace } from '@toeverything/infra/__internal__/workspace';
|
|
||||||
import { getCurrentStore } from '@toeverything/infra/atom';
|
|
||||||
import {
|
|
||||||
buildShowcaseWorkspace,
|
|
||||||
WorkspaceVersion,
|
|
||||||
} from '@toeverything/infra/blocksuite';
|
|
||||||
import { useAtomValue, useSetAtom } from 'jotai';
|
|
||||||
import { nanoid } from 'nanoid';
|
|
||||||
import { useCallback } from 'react';
|
|
||||||
|
|
||||||
import { LocalAdapter } from '../adapters/local';
|
|
||||||
import { WorkspaceAdapters } from '../adapters/workspace';
|
|
||||||
import { setPageModeAtom } from '../atoms';
|
|
||||||
|
|
||||||
const logger = new DebugLogger('use-workspaces');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This hook has the permission to all workspaces. Be careful when using it.
|
|
||||||
*/
|
|
||||||
export function useAppHelper() {
|
|
||||||
const jotaiWorkspaces = useAtomValue(rootWorkspacesMetadataAtom);
|
|
||||||
const set = useSetAtom(rootWorkspacesMetadataAtom);
|
|
||||||
return {
|
|
||||||
addLocalWorkspace: useCallback(
|
|
||||||
async (workspaceId: string): Promise<string> => {
|
|
||||||
getOrCreateWorkspace(workspaceId, WorkspaceFlavour.LOCAL);
|
|
||||||
saveWorkspaceToLocalStorage(workspaceId);
|
|
||||||
set(workspaces => [
|
|
||||||
...workspaces,
|
|
||||||
{
|
|
||||||
id: workspaceId,
|
|
||||||
flavour: WorkspaceFlavour.LOCAL,
|
|
||||||
version: WorkspaceVersion.DatabaseV3,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
logger.debug('imported local workspace', workspaceId);
|
|
||||||
return workspaceId;
|
|
||||||
},
|
|
||||||
[set]
|
|
||||||
),
|
|
||||||
addCloudWorkspace: useCallback(
|
|
||||||
(workspaceId: string) => {
|
|
||||||
getOrCreateWorkspace(workspaceId, WorkspaceFlavour.AFFINE_CLOUD);
|
|
||||||
set(workspaces => [
|
|
||||||
...workspaces,
|
|
||||||
{
|
|
||||||
id: workspaceId,
|
|
||||||
flavour: WorkspaceFlavour.AFFINE_CLOUD,
|
|
||||||
version: WorkspaceVersion.DatabaseV3,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
logger.debug('imported cloud workspace', workspaceId);
|
|
||||||
},
|
|
||||||
[set]
|
|
||||||
),
|
|
||||||
createLocalWorkspace: useCallback(
|
|
||||||
async (name: string): Promise<string> => {
|
|
||||||
const blockSuiteWorkspace = getOrCreateWorkspace(
|
|
||||||
nanoid(),
|
|
||||||
WorkspaceFlavour.LOCAL
|
|
||||||
);
|
|
||||||
blockSuiteWorkspace.meta.setName(name);
|
|
||||||
const id = await LocalAdapter.CRUD.create(blockSuiteWorkspace);
|
|
||||||
{
|
|
||||||
// this is hack, because CRUD doesn't return the workspace
|
|
||||||
const blockSuiteWorkspace = getOrCreateWorkspace(
|
|
||||||
id,
|
|
||||||
WorkspaceFlavour.LOCAL
|
|
||||||
);
|
|
||||||
await buildShowcaseWorkspace(blockSuiteWorkspace, {
|
|
||||||
store: getCurrentStore(),
|
|
||||||
atoms: {
|
|
||||||
pageMode: setPageModeAtom,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
set(workspaces => [
|
|
||||||
...workspaces,
|
|
||||||
{
|
|
||||||
id,
|
|
||||||
flavour: WorkspaceFlavour.LOCAL,
|
|
||||||
version: WorkspaceVersion.DatabaseV3,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
logger.debug('created local workspace', id);
|
|
||||||
return id;
|
|
||||||
},
|
|
||||||
[set]
|
|
||||||
),
|
|
||||||
deleteWorkspace: useCallback(
|
|
||||||
async (workspaceId: string) => {
|
|
||||||
const targetJotaiWorkspace = jotaiWorkspaces.find(
|
|
||||||
ws => ws.id === workspaceId
|
|
||||||
);
|
|
||||||
if (!targetJotaiWorkspace) {
|
|
||||||
throw new Error('page cannot be found');
|
|
||||||
}
|
|
||||||
|
|
||||||
const targetWorkspace = getWorkspace(targetJotaiWorkspace.id);
|
|
||||||
|
|
||||||
// delete workspace from plugin
|
|
||||||
await WorkspaceAdapters[targetJotaiWorkspace.flavour].CRUD.delete(
|
|
||||||
targetWorkspace
|
|
||||||
);
|
|
||||||
// delete workspace from jotai storage
|
|
||||||
set(workspaces => workspaces.filter(ws => ws.id !== workspaceId));
|
|
||||||
},
|
|
||||||
[jotaiWorkspaces, set]
|
|
||||||
),
|
|
||||||
deleteWorkspaceMeta: useCallback(
|
|
||||||
(workspaceId: string) => {
|
|
||||||
set(workspaces => workspaces.filter(ws => ws.id !== workspaceId));
|
|
||||||
},
|
|
||||||
[set]
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -2,23 +2,23 @@ import './polyfill/ses-lockdown';
|
|||||||
import './polyfill/intl-segmenter';
|
import './polyfill/intl-segmenter';
|
||||||
import './polyfill/request-idle-callback';
|
import './polyfill/request-idle-callback';
|
||||||
|
|
||||||
import { WorkspaceFallback } from '@affine/component/workspace';
|
|
||||||
import { assertExists } from '@blocksuite/global/utils';
|
import { assertExists } from '@blocksuite/global/utils';
|
||||||
import { getCurrentStore } from '@toeverything/infra/atom';
|
import { getCurrentStore } from '@toeverything/infra/atom';
|
||||||
import { StrictMode, Suspense } from 'react';
|
import { StrictMode } from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
|
|
||||||
|
import { App } from './app';
|
||||||
import { bootstrapPluginSystem } from './bootstrap/register-plugins';
|
import { bootstrapPluginSystem } from './bootstrap/register-plugins';
|
||||||
|
import { setup } from './bootstrap/setup';
|
||||||
import { performanceLogger } from './shared';
|
import { performanceLogger } from './shared';
|
||||||
|
|
||||||
const performanceMainLogger = performanceLogger.namespace('main');
|
const performanceMainLogger = performanceLogger.namespace('main');
|
||||||
async function main() {
|
function main() {
|
||||||
performanceMainLogger.info('start');
|
performanceMainLogger.info('start');
|
||||||
const { setup } = await import('./bootstrap/setup');
|
|
||||||
|
|
||||||
const rootStore = getCurrentStore();
|
const rootStore = getCurrentStore();
|
||||||
performanceMainLogger.info('setup start');
|
performanceMainLogger.info('setup start');
|
||||||
await setup(rootStore);
|
setup();
|
||||||
performanceMainLogger.info('setup done');
|
performanceMainLogger.info('setup done');
|
||||||
|
|
||||||
bootstrapPluginSystem(rootStore).catch(err => {
|
bootstrapPluginSystem(rootStore).catch(err => {
|
||||||
@@ -26,20 +26,19 @@ async function main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
performanceMainLogger.info('import app');
|
performanceMainLogger.info('import app');
|
||||||
const { App } = await import('./app');
|
|
||||||
const root = document.getElementById('app');
|
const root = document.getElementById('app');
|
||||||
assertExists(root);
|
assertExists(root);
|
||||||
|
|
||||||
performanceMainLogger.info('render app');
|
performanceMainLogger.info('render app');
|
||||||
createRoot(root).render(
|
createRoot(root).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<Suspense fallback={<WorkspaceFallback key="AppLoading" />}>
|
<App />
|
||||||
<App />
|
|
||||||
</Suspense>
|
|
||||||
</StrictMode>
|
</StrictMode>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch(err => {
|
try {
|
||||||
|
main();
|
||||||
|
} catch (err) {
|
||||||
console.error('Failed to bootstrap app', err);
|
console.error('Failed to bootstrap app', err);
|
||||||
});
|
}
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ import {
|
|||||||
PageListDragOverlay,
|
PageListDragOverlay,
|
||||||
} from '@affine/component/page-list';
|
} from '@affine/component/page-list';
|
||||||
import { MainContainer, WorkspaceFallback } from '@affine/component/workspace';
|
import { MainContainer, WorkspaceFallback } from '@affine/component/workspace';
|
||||||
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
import { waitForCurrentWorkspaceAtom } from '@affine/workspace/atom';
|
||||||
import { getBlobEngine } from '@affine/workspace/manager';
|
|
||||||
import { assertExists } from '@blocksuite/global/utils';
|
import { assertExists } from '@blocksuite/global/utils';
|
||||||
import {
|
import {
|
||||||
DndContext,
|
DndContext,
|
||||||
@@ -20,8 +19,7 @@ import {
|
|||||||
useSensors,
|
useSensors,
|
||||||
} from '@dnd-kit/core';
|
} from '@dnd-kit/core';
|
||||||
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
|
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
|
||||||
import { currentWorkspaceIdAtom } from '@toeverything/infra/atom';
|
import { useWorkspaceStatus } from '@toeverything/hooks/use-workspace-status';
|
||||||
import type { MigrationPoint } from '@toeverything/infra/blocksuite';
|
|
||||||
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
|
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
|
||||||
import type { PropsWithChildren, ReactNode } from 'react';
|
import type { PropsWithChildren, ReactNode } from 'react';
|
||||||
import { lazy, Suspense, useCallback, useEffect, useState } from 'react';
|
import { lazy, Suspense, useCallback, useEffect, useState } from 'react';
|
||||||
@@ -37,7 +35,6 @@ import { RootAppSidebar } from '../components/root-app-sidebar';
|
|||||||
import { WorkspaceUpgrade } from '../components/workspace-upgrade';
|
import { WorkspaceUpgrade } from '../components/workspace-upgrade';
|
||||||
import { useAppSettingHelper } from '../hooks/affine/use-app-setting-helper';
|
import { useAppSettingHelper } from '../hooks/affine/use-app-setting-helper';
|
||||||
import { useSidebarDrag } from '../hooks/affine/use-sidebar-drag';
|
import { useSidebarDrag } from '../hooks/affine/use-sidebar-drag';
|
||||||
import { useCurrentWorkspace } from '../hooks/current/use-current-workspace';
|
|
||||||
import { useNavigateHelper } from '../hooks/use-navigate-helper';
|
import { useNavigateHelper } from '../hooks/use-navigate-helper';
|
||||||
import { useRegisterWorkspaceCommands } from '../hooks/use-register-workspace-commands';
|
import { useRegisterWorkspaceCommands } from '../hooks/use-register-workspace-commands';
|
||||||
import {
|
import {
|
||||||
@@ -57,11 +54,11 @@ export const QuickSearch = () => {
|
|||||||
openQuickSearchModalAtom
|
openQuickSearchModalAtom
|
||||||
);
|
);
|
||||||
|
|
||||||
const [currentWorkspace] = useCurrentWorkspace();
|
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
|
||||||
const { pageId } = useParams();
|
const { pageId } = useParams();
|
||||||
const blockSuiteWorkspace = currentWorkspace?.blockSuiteWorkspace;
|
const blockSuiteWorkspace = currentWorkspace.blockSuiteWorkspace;
|
||||||
const pageMeta = useBlockSuitePageMeta(
|
const pageMeta = useBlockSuitePageMeta(
|
||||||
currentWorkspace?.blockSuiteWorkspace
|
currentWorkspace.blockSuiteWorkspace
|
||||||
).find(meta => meta.id === pageId);
|
).find(meta => meta.id === pageId);
|
||||||
|
|
||||||
if (!blockSuiteWorkspace) {
|
if (!blockSuiteWorkspace) {
|
||||||
@@ -77,89 +74,25 @@ export const QuickSearch = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CurrentWorkspaceContext = ({
|
export const WorkspaceLayout = function WorkspaceLayout({
|
||||||
children,
|
children,
|
||||||
}: PropsWithChildren): ReactNode => {
|
}: PropsWithChildren) {
|
||||||
const workspaceId = useAtomValue(currentWorkspaceIdAtom);
|
|
||||||
const metadata = useAtomValue(rootWorkspacesMetadataAtom);
|
|
||||||
const exist = metadata.find(m => m.id === workspaceId);
|
|
||||||
if (metadata.length === 0) {
|
|
||||||
return <WorkspaceFallback key="no-workspace" />;
|
|
||||||
}
|
|
||||||
if (!workspaceId) {
|
|
||||||
return <WorkspaceFallback key="finding-workspace-id" />;
|
|
||||||
}
|
|
||||||
if (!exist) {
|
|
||||||
return <WorkspaceFallback key="workspace-not-found" />;
|
|
||||||
}
|
|
||||||
return children;
|
|
||||||
};
|
|
||||||
|
|
||||||
type WorkspaceLayoutProps = {
|
|
||||||
migration?: MigrationPoint;
|
|
||||||
};
|
|
||||||
|
|
||||||
const useSyncWorkspaceBlob = () => {
|
|
||||||
// temporary solution for sync blob
|
|
||||||
|
|
||||||
const [currentWorkspace] = useCurrentWorkspace();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// after currentWorkspace changed, wait 1 second to start sync
|
|
||||||
setTimeout(sync, 1000);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
stopped = true;
|
|
||||||
};
|
|
||||||
}, [currentWorkspace]);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const WorkspaceLayout = function WorkspacesSuspense({
|
|
||||||
children,
|
|
||||||
migration,
|
|
||||||
}: PropsWithChildren<WorkspaceLayoutProps>) {
|
|
||||||
useSyncWorkspaceBlob();
|
|
||||||
return (
|
return (
|
||||||
<AdapterProviderWrapper>
|
<AdapterProviderWrapper>
|
||||||
<CurrentWorkspaceContext>
|
{/* load all workspaces is costly, do not block the whole UI */}
|
||||||
{/* load all workspaces is costly, do not block the whole UI */}
|
<Suspense>
|
||||||
<Suspense>
|
<AllWorkspaceModals />
|
||||||
<AllWorkspaceModals />
|
<CurrentWorkspaceModals />
|
||||||
<CurrentWorkspaceModals />
|
</Suspense>
|
||||||
</Suspense>
|
<Suspense fallback={<WorkspaceFallback />}>
|
||||||
<Suspense fallback={<WorkspaceFallback />}>
|
<WorkspaceLayoutInner>{children}</WorkspaceLayoutInner>
|
||||||
<WorkspaceLayoutInner migration={migration}>
|
</Suspense>
|
||||||
{children}
|
|
||||||
</WorkspaceLayoutInner>
|
|
||||||
</Suspense>
|
|
||||||
</CurrentWorkspaceContext>
|
|
||||||
</AdapterProviderWrapper>
|
</AdapterProviderWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WorkspaceLayoutInner = ({
|
export const WorkspaceLayoutInner = ({ children }: PropsWithChildren) => {
|
||||||
children,
|
const currentWorkspace = useAtomValue(waitForCurrentWorkspaceAtom);
|
||||||
migration,
|
|
||||||
}: PropsWithChildren<WorkspaceLayoutProps>) => {
|
|
||||||
const [currentWorkspace] = useCurrentWorkspace();
|
|
||||||
const { openPage } = useNavigateHelper();
|
const { openPage } = useNavigateHelper();
|
||||||
const pageHelper = usePageHelper(currentWorkspace.blockSuiteWorkspace);
|
const pageHelper = usePageHelper(currentWorkspace.blockSuiteWorkspace);
|
||||||
|
|
||||||
@@ -200,7 +133,6 @@ export const WorkspaceLayoutInner = ({
|
|||||||
const handleOpenSettingModal = useCallback(() => {
|
const handleOpenSettingModal = useCallback(() => {
|
||||||
setOpenSettingModalAtom({
|
setOpenSettingModalAtom({
|
||||||
activeTab: 'appearance',
|
activeTab: 'appearance',
|
||||||
workspaceId: null,
|
|
||||||
open: true,
|
open: true,
|
||||||
});
|
});
|
||||||
}, [setOpenSettingModalAtom]);
|
}, [setOpenSettingModalAtom]);
|
||||||
@@ -224,6 +156,8 @@ export const WorkspaceLayoutInner = ({
|
|||||||
// todo: refactor this that the root layout do not need to check route state
|
// todo: refactor this that the root layout do not need to check route state
|
||||||
const isInPageDetail = !!pageId;
|
const isInPageDetail = !!pageId;
|
||||||
|
|
||||||
|
const upgradeStatus = useWorkspaceStatus(currentWorkspace, s => s.upgrade);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* This DndContext is used for drag page from all-pages list into a folder in sidebar */}
|
{/* This DndContext is used for drag page from all-pages list into a folder in sidebar */}
|
||||||
@@ -256,8 +190,8 @@ export const WorkspaceLayoutInner = ({
|
|||||||
padding={appSettings.clientBorder}
|
padding={appSettings.clientBorder}
|
||||||
>
|
>
|
||||||
<Suspense>
|
<Suspense>
|
||||||
{migration ? (
|
{upgradeStatus?.needUpgrade || upgradeStatus?.upgrading ? (
|
||||||
<WorkspaceUpgrade migration={migration} />
|
<WorkspaceUpgrade />
|
||||||
) : (
|
) : (
|
||||||
children
|
children
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { SignOutModal } from '../components/affine/sign-out-modal';
|
|||||||
import { RouteLogic, useNavigateHelper } from '../hooks/use-navigate-helper';
|
import { RouteLogic, useNavigateHelper } from '../hooks/use-navigate-helper';
|
||||||
import { signOutCloud } from '../utils/cloud-utils';
|
import { signOutCloud } from '../utils/cloud-utils';
|
||||||
|
|
||||||
export const Component = (): ReactElement => {
|
export const PageNotFound = (): ReactElement => {
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
const { jumpToIndex } = useNavigateHelper();
|
const { jumpToIndex } = useNavigateHelper();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
@@ -52,3 +52,5 @@ export const Component = (): ReactElement => {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const Component = PageNotFound;
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import { Menu } from '@affine/component/ui/menu';
|
import { Menu } from '@affine/component/ui/menu';
|
||||||
import { DebugLogger } from '@affine/debug';
|
import { workspaceListAtom } from '@affine/workspace/atom';
|
||||||
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
import { useAtomValue } from 'jotai';
|
||||||
import { getWorkspace } from '@toeverything/infra/__internal__/workspace';
|
import { lazy, useEffect } from 'react';
|
||||||
import { getCurrentStore } from '@toeverything/infra/atom';
|
|
||||||
import { lazy } from 'react';
|
|
||||||
import type { LoaderFunction } from 'react-router-dom';
|
|
||||||
import { redirect } from 'react-router-dom';
|
|
||||||
|
|
||||||
|
import { createFirstAppData } from '../bootstrap/first-app-data';
|
||||||
import { UserWithWorkspaceList } from '../components/pure/workspace-slider-bar/user-with-workspace-list';
|
import { UserWithWorkspaceList } from '../components/pure/workspace-slider-bar/user-with-workspace-list';
|
||||||
|
import { useNavigateHelper } from '../hooks/use-navigate-helper';
|
||||||
|
import { WorkspaceSubPath } from '../shared';
|
||||||
|
|
||||||
const AllWorkspaceModals = lazy(() =>
|
const AllWorkspaceModals = lazy(() =>
|
||||||
import('../providers/modal-provider').then(({ AllWorkspaceModals }) => ({
|
import('../providers/modal-provider').then(({ AllWorkspaceModals }) => ({
|
||||||
@@ -15,44 +14,27 @@ const AllWorkspaceModals = lazy(() =>
|
|||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
const logger = new DebugLogger('index-page');
|
|
||||||
|
|
||||||
export const loader: LoaderFunction = async () => {
|
|
||||||
const rootStore = getCurrentStore();
|
|
||||||
const { createFirstAppData } = await import('../bootstrap/setup');
|
|
||||||
createFirstAppData(rootStore);
|
|
||||||
const meta = await rootStore.get(rootWorkspacesMetadataAtom);
|
|
||||||
const lastId = localStorage.getItem('last_workspace_id');
|
|
||||||
const lastPageId = localStorage.getItem('last_page_id');
|
|
||||||
const target = (lastId && meta.find(({ id }) => id === lastId)) || meta.at(0);
|
|
||||||
if (target) {
|
|
||||||
const targetWorkspace = getWorkspace(target.id);
|
|
||||||
|
|
||||||
const nonTrashPages = targetWorkspace.meta.pageMetas.filter(
|
|
||||||
({ trash }) => !trash
|
|
||||||
);
|
|
||||||
const helloWorldPage = nonTrashPages.find(({ jumpOnce }) => jumpOnce)?.id;
|
|
||||||
const pageId =
|
|
||||||
nonTrashPages.find(({ id }) => id === lastPageId)?.id ??
|
|
||||||
nonTrashPages.at(0)?.id;
|
|
||||||
if (helloWorldPage) {
|
|
||||||
logger.debug(
|
|
||||||
'Found target workspace. Jump to hello world page',
|
|
||||||
helloWorldPage
|
|
||||||
);
|
|
||||||
return redirect(`/workspace/${targetWorkspace.id}/${helloWorldPage}`);
|
|
||||||
} else if (pageId) {
|
|
||||||
logger.debug('Found target workspace. Jump to page', pageId);
|
|
||||||
return redirect(`/workspace/${targetWorkspace.id}/${pageId}`);
|
|
||||||
} else {
|
|
||||||
logger.debug('Found target workspace. Jump to all page');
|
|
||||||
return redirect(`/workspace/${targetWorkspace.id}/all`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Component = () => {
|
export const Component = () => {
|
||||||
|
const list = useAtomValue(workspaceListAtom);
|
||||||
|
const { openPage } = useNavigateHelper();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (list.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// open last workspace
|
||||||
|
const lastId = localStorage.getItem('last_workspace_id');
|
||||||
|
const openWorkspace = list.find(w => w.id === lastId) ?? list[0];
|
||||||
|
openPage(openWorkspace.id, WorkspaceSubPath.ALL);
|
||||||
|
}, [list, openPage]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
createFirstAppData().catch(err => {
|
||||||
|
console.error('Failed to create first app data', err);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
// TODO: We need a no workspace page
|
// TODO: We need a no workspace page
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import { authAtom } from '../atoms';
|
|||||||
import { setOnceSignedInEventAtom } from '../atoms/event';
|
import { setOnceSignedInEventAtom } from '../atoms/event';
|
||||||
import { useCurrentLoginStatus } from '../hooks/affine/use-current-login-status';
|
import { useCurrentLoginStatus } from '../hooks/affine/use-current-login-status';
|
||||||
import { RouteLogic, useNavigateHelper } from '../hooks/use-navigate-helper';
|
import { RouteLogic, useNavigateHelper } from '../hooks/use-navigate-helper';
|
||||||
import { useAppHelper } from '../hooks/use-workspaces';
|
|
||||||
|
|
||||||
export const loader: LoaderFunction = async args => {
|
export const loader: LoaderFunction = async args => {
|
||||||
const inviteId = args.params.inviteId || '';
|
const inviteId = args.params.inviteId || '';
|
||||||
@@ -49,7 +48,6 @@ export const loader: LoaderFunction = async args => {
|
|||||||
export const Component = () => {
|
export const Component = () => {
|
||||||
const loginStatus = useCurrentLoginStatus();
|
const loginStatus = useCurrentLoginStatus();
|
||||||
const { jumpToSignIn } = useNavigateHelper();
|
const { jumpToSignIn } = useNavigateHelper();
|
||||||
const { addCloudWorkspace } = useAppHelper();
|
|
||||||
const { jumpToSubPath } = useNavigateHelper();
|
const { jumpToSubPath } = useNavigateHelper();
|
||||||
|
|
||||||
const setOnceSignedInEvent = useSetAtom(setOnceSignedInEventAtom);
|
const setOnceSignedInEvent = useSetAtom(setOnceSignedInEventAtom);
|
||||||
@@ -61,13 +59,12 @@ export const Component = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const openWorkspace = useCallback(() => {
|
const openWorkspace = useCallback(() => {
|
||||||
addCloudWorkspace(inviteInfo.workspace.id);
|
|
||||||
jumpToSubPath(
|
jumpToSubPath(
|
||||||
inviteInfo.workspace.id,
|
inviteInfo.workspace.id,
|
||||||
WorkspaceSubPath.ALL,
|
WorkspaceSubPath.ALL,
|
||||||
RouteLogic.REPLACE
|
RouteLogic.REPLACE
|
||||||
);
|
);
|
||||||
}, [addCloudWorkspace, inviteInfo.workspace.id, jumpToSubPath]);
|
}, [inviteInfo.workspace.id, jumpToSubPath]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (loginStatus === 'unauthenticated') {
|
if (loginStatus === 'unauthenticated') {
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { MainContainer } from '@affine/component/workspace';
|
import { MainContainer } from '@affine/component/workspace';
|
||||||
import { DebugLogger } from '@affine/debug';
|
import { DebugLogger } from '@affine/debug';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
import { fetchWithTraceReport } from '@affine/graphql';
|
||||||
import type { CloudDoc } from '@affine/workspace/affine/download';
|
import {
|
||||||
import { downloadBinaryFromCloud } from '@affine/workspace/affine/download';
|
createAffineCloudBlobStorage,
|
||||||
import { getOrCreateWorkspace } from '@affine/workspace/manager';
|
createStaticBlobStorage,
|
||||||
|
globalBlockSuiteSchema,
|
||||||
|
} from '@affine/workspace';
|
||||||
import { assertExists } from '@blocksuite/global/utils';
|
import { assertExists } from '@blocksuite/global/utils';
|
||||||
import type { Page } from '@blocksuite/store';
|
import { type Page, Workspace } from '@blocksuite/store';
|
||||||
import { noop } from 'foxact/noop';
|
import { noop } from 'foxact/noop';
|
||||||
import type { ReactElement } from 'react';
|
import type { ReactElement } from 'react';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
@@ -24,6 +26,36 @@ import { PageDetailEditor } from '../../components/page-detail-editor';
|
|||||||
import { SharePageNotFoundError } from '../../components/share-page-not-found-error';
|
import { SharePageNotFoundError } from '../../components/share-page-not-found-error';
|
||||||
import { ShareHeader } from './share-header';
|
import { ShareHeader } from './share-header';
|
||||||
|
|
||||||
|
type DocPublishMode = 'edgeless' | 'page';
|
||||||
|
|
||||||
|
export type CloudDoc = {
|
||||||
|
arrayBuffer: ArrayBuffer;
|
||||||
|
publishMode: DocPublishMode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function downloadBinaryFromCloud(
|
||||||
|
rootGuid: string,
|
||||||
|
pageGuid: string
|
||||||
|
): Promise<CloudDoc | null> {
|
||||||
|
const response = await fetchWithTraceReport(
|
||||||
|
runtimeConfig.serverUrlPrefix +
|
||||||
|
`/api/workspaces/${rootGuid}/docs/${pageGuid}`,
|
||||||
|
{
|
||||||
|
priority: 'high',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (response.ok) {
|
||||||
|
const publishMode = (response.headers.get('publish-mode') ||
|
||||||
|
'page') as DocPublishMode;
|
||||||
|
const arrayBuffer = await response.arrayBuffer();
|
||||||
|
|
||||||
|
// return both arrayBuffer and publish mode
|
||||||
|
return { arrayBuffer, publishMode };
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
type LoaderData = {
|
type LoaderData = {
|
||||||
page: Page;
|
page: Page;
|
||||||
publishMode: PageMode;
|
publishMode: PageMode;
|
||||||
@@ -49,10 +81,18 @@ export const loader: LoaderFunction = async ({ params }) => {
|
|||||||
if (!workspaceId || !pageId) {
|
if (!workspaceId || !pageId) {
|
||||||
return redirect('/404');
|
return redirect('/404');
|
||||||
}
|
}
|
||||||
const workspace = getOrCreateWorkspace(
|
const workspace = new Workspace({
|
||||||
workspaceId,
|
id: workspaceId,
|
||||||
WorkspaceFlavour.AFFINE_PUBLIC
|
blobStorages: [
|
||||||
);
|
() => ({
|
||||||
|
crud: createAffineCloudBlobStorage(workspaceId),
|
||||||
|
}),
|
||||||
|
() => ({
|
||||||
|
crud: createStaticBlobStorage(),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
schema: globalBlockSuiteSchema,
|
||||||
|
});
|
||||||
// download root workspace
|
// download root workspace
|
||||||
{
|
{
|
||||||
const response = await downloadBinaryFromCloud(workspaceId, workspaceId);
|
const response = await downloadBinaryFromCloud(workspaceId, workspaceId);
|
||||||
@@ -84,9 +124,9 @@ export const Component = (): ReactElement => {
|
|||||||
<AppContainer>
|
<AppContainer>
|
||||||
<MainContainer>
|
<MainContainer>
|
||||||
<ShareHeader
|
<ShareHeader
|
||||||
workspace={page.workspace}
|
|
||||||
pageId={page.id}
|
pageId={page.id}
|
||||||
publishMode={publishMode}
|
publishMode={publishMode}
|
||||||
|
blockSuiteWorkspace={page.workspace}
|
||||||
/>
|
/>
|
||||||
<PageDetailEditor
|
<PageDetailEditor
|
||||||
isPublic
|
isPublic
|
||||||
|
|||||||
@@ -1,30 +1,27 @@
|
|||||||
import type { Workspace } from '@blocksuite/store';
|
import type { Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
|
||||||
|
|
||||||
import type { PageMode } from '../../atoms';
|
import type { PageMode } from '../../atoms';
|
||||||
import { BlockSuiteHeaderTitle } from '../../components/blocksuite/block-suite-header-title';
|
import { BlockSuiteHeaderTitle } from '../../components/blocksuite/block-suite-header-title';
|
||||||
import ShareHeaderLeftItem from '../../components/cloud/share-header-left-item';
|
import ShareHeaderLeftItem from '../../components/cloud/share-header-left-item';
|
||||||
import ShareHeaderRightItem from '../../components/cloud/share-header-right-item';
|
import ShareHeaderRightItem from '../../components/cloud/share-header-right-item';
|
||||||
import { Header } from '../../components/pure/header';
|
import { Header } from '../../components/pure/header';
|
||||||
import { useWorkspace } from '../../hooks/use-workspace';
|
|
||||||
|
|
||||||
export function ShareHeader({
|
export function ShareHeader({
|
||||||
workspace,
|
|
||||||
pageId,
|
pageId,
|
||||||
publishMode,
|
publishMode,
|
||||||
|
blockSuiteWorkspace,
|
||||||
}: {
|
}: {
|
||||||
workspace: Workspace;
|
|
||||||
pageId: string;
|
pageId: string;
|
||||||
publishMode: PageMode;
|
publishMode: PageMode;
|
||||||
|
blockSuiteWorkspace: BlockSuiteWorkspace;
|
||||||
}) {
|
}) {
|
||||||
const currentWorkspace = useWorkspace(workspace.id);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Header
|
<Header
|
||||||
isFloat={publishMode === 'edgeless'}
|
isFloat={publishMode === 'edgeless'}
|
||||||
left={<ShareHeaderLeftItem />}
|
left={<ShareHeaderLeftItem />}
|
||||||
center={
|
center={
|
||||||
<BlockSuiteHeaderTitle
|
<BlockSuiteHeaderTitle
|
||||||
workspace={currentWorkspace}
|
blockSuiteWorkspace={blockSuiteWorkspace}
|
||||||
pageId={pageId}
|
pageId={pageId}
|
||||||
isPublic={true}
|
isPublic={true}
|
||||||
publicMode={publishMode}
|
publicMode={publishMode}
|
||||||
@@ -32,7 +29,7 @@ export function ShareHeader({
|
|||||||
}
|
}
|
||||||
right={
|
right={
|
||||||
<ShareHeaderRightItem
|
<ShareHeaderRightItem
|
||||||
workspaceId={workspace.id}
|
workspaceId={blockSuiteWorkspace.id}
|
||||||
pageId={pageId}
|
pageId={pageId}
|
||||||
publishMode={publishMode}
|
publishMode={publishMode}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user