Compare commits

..

1 Commits

Author SHA1 Message Date
zzj3720 b15a2a9638 fix(editor): adjust the style of the table block 2025-02-21 17:10:06 +08:00
107 changed files with 1043 additions and 1270 deletions
@@ -1,9 +1,9 @@
import { DatabaseBlockSchema } from '@blocksuite/affine-model';
import {
AdapterTextUtils,
BlockNotionHtmlAdapterExtension,
type BlockNotionHtmlAdapterMatcher,
HastUtils,
TextUtils,
} from '@blocksuite/affine-shared/adapters';
import { getTagColor } from '@blocksuite/data-view';
import { type BlockSnapshot, nanoid } from '@blocksuite/store';
@@ -219,7 +219,7 @@ export const databaseBlockNotionHtmlAdapterMatcher: BlockNotionHtmlAdapterMatche
column.type = 'rich-text';
row[column.id] = {
columnId: column.id,
value: AdapterTextUtils.createText(text),
value: TextUtils.createText(text),
};
} else {
row[column.id] = {
@@ -235,11 +235,11 @@ export const databaseBlockNotionHtmlAdapterMatcher: BlockNotionHtmlAdapterMatche
}
if (
column.type === 'rich-text' &&
!AdapterTextUtils.isText(row[column.id].value)
!TextUtils.isText(row[column.id].value)
) {
row[column.id] = {
columnId: column.id,
value: AdapterTextUtils.createText(row[column.id].value),
value: TextUtils.createText(row[column.id].value),
};
}
});
@@ -197,7 +197,7 @@ async function renderNoteContent(
match: ids.map(id => ({ id, viewType: 'display' })),
};
const previewDoc = doc.doc.getStore({ query });
const previewSpec = SpecProvider.getInstance().getSpec('preview:page');
const previewSpec = SpecProvider.getInstance().getSpec('page:preview');
const previewStd = new BlockStdScope({
store: previewDoc,
extensions: previewSpec.value,
@@ -1,8 +1,8 @@
import { EmbedLinkedDocBlockSchema } from '@blocksuite/affine-model';
import {
AdapterTextUtils,
BlockHtmlAdapterExtension,
type BlockHtmlAdapterMatcher,
TextUtils,
} from '@blocksuite/affine-shared/adapters';
export const embedLinkedDocBlockHtmlAdapterMatcher: BlockHtmlAdapterMatcher = {
@@ -18,7 +18,7 @@ export const embedLinkedDocBlockHtmlAdapterMatcher: BlockHtmlAdapterMatcher = {
return;
}
const title = configs.get('title:' + o.node.props.pageId) ?? 'untitled';
const url = AdapterTextUtils.generateDocUrl(
const url = TextUtils.generateDocUrl(
configs.get('docLinkBaseUrl') ?? '',
String(o.node.props.pageId),
o.node.props.params ?? Object.create(null)
@@ -1,8 +1,8 @@
import { EmbedLinkedDocBlockSchema } from '@blocksuite/affine-model';
import {
AdapterTextUtils,
BlockMarkdownAdapterExtension,
type BlockMarkdownAdapterMatcher,
TextUtils,
} from '@blocksuite/affine-shared/adapters';
export const embedLinkedDocBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher =
@@ -19,7 +19,7 @@ export const embedLinkedDocBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatc
return;
}
const title = configs.get('title:' + o.node.props.pageId) ?? 'untitled';
const url = AdapterTextUtils.generateDocUrl(
const url = TextUtils.generateDocUrl(
configs.get('docLinkBaseUrl') ?? '',
String(o.node.props.pageId),
o.node.props.params ?? Object.create(null)
@@ -1,8 +1,8 @@
import { EmbedLinkedDocBlockSchema } from '@blocksuite/affine-model';
import {
AdapterTextUtils,
BlockPlainTextAdapterExtension,
type BlockPlainTextAdapterMatcher,
TextUtils,
} from '@blocksuite/affine-shared/adapters';
export const embedLinkedDocBlockPlainTextAdapterMatcher: BlockPlainTextAdapterMatcher =
@@ -19,7 +19,7 @@ export const embedLinkedDocBlockPlainTextAdapterMatcher: BlockPlainTextAdapterMa
return;
}
const title = configs.get('title:' + o.node.props.pageId) ?? 'untitled';
const url = AdapterTextUtils.generateDocUrl(
const url = TextUtils.generateDocUrl(
configs.get('docLinkBaseUrl') ?? '',
String(o.node.props.pageId),
o.node.props.params ?? Object.create(null)
@@ -70,7 +70,7 @@ export class EmbedEdgelessSyncedDocBlockComponent extends toEdgelessEmbedBlock(
<div class="affine-page-viewport" data-theme=${appTheme}>
${new BlockStdScope({
store: syncedDoc,
extensions: this._buildPreviewSpec('preview:page'),
extensions: this._buildPreviewSpec('page:preview'),
}).render()}
</div>
`,
@@ -81,7 +81,7 @@ export class EmbedEdgelessSyncedDocBlockComponent extends toEdgelessEmbedBlock(
<div class="affine-edgeless-viewport" data-theme=${edgelessTheme}>
${new BlockStdScope({
store: syncedDoc,
extensions: this._buildPreviewSpec('preview:edgeless'),
extensions: this._buildPreviewSpec('edgeless:preview'),
}).render()}
</div>
`,
@@ -116,7 +116,7 @@ export class EmbedSyncedDocBlockComponent extends EmbedBlockComponent<EmbedSynce
],
};
protected _buildPreviewSpec = (name: 'preview:page' | 'preview:edgeless') => {
protected _buildPreviewSpec = (name: 'page:preview' | 'edgeless:preview') => {
const nextDepth = this.depth + 1;
const previewSpecBuilder = SpecProvider.getInstance().getSpec(name);
const currentDisposables = this.disposables;
@@ -203,7 +203,7 @@ export class EmbedSyncedDocBlockComponent extends EmbedBlockComponent<EmbedSynce
<div class="affine-page-viewport" data-theme=${appTheme}>
${new BlockStdScope({
store: syncedDoc,
extensions: this._buildPreviewSpec('preview:page'),
extensions: this._buildPreviewSpec('page:preview'),
}).render()}
</div>
`,
@@ -214,7 +214,7 @@ export class EmbedSyncedDocBlockComponent extends EmbedBlockComponent<EmbedSynce
<div class="affine-edgeless-viewport" data-theme=${edgelessTheme}>
${new BlockStdScope({
store: syncedDoc,
extensions: this._buildPreviewSpec('preview:edgeless'),
extensions: this._buildPreviewSpec('edgeless:preview'),
}).render()}
</div>
`,
@@ -1,4 +1,3 @@
export * from './frame-block.js';
export * from './frame-manager.js';
export * from './frame-spec.js';
export * from './tool.js';
@@ -1,4 +1,3 @@
export * from './html.js';
export * from './markdown.js';
export * from './middleware.js';
export * from './notion-html.js';
@@ -1,9 +1,9 @@
import { ListBlockSchema } from '@blocksuite/affine-model';
import {
AdapterTextUtils,
BlockHtmlAdapterExtension,
type BlockHtmlAdapterMatcher,
HastUtils,
TextUtils,
} from '@blocksuite/affine-shared/adapters';
import type { DeltaInsert } from '@blocksuite/inline';
import { nanoid } from '@blocksuite/store';
@@ -124,7 +124,7 @@ export const listBlockHtmlAdapterMatcher: BlockHtmlAdapterMatcher = {
Array.isArray(currentTNode.properties.className) &&
currentTNode.properties.className.includes('todo-list')
) ===
AdapterTextUtils.isNullish(
TextUtils.isNullish(
o.node.props.type === 'todo'
? (o.node.props.checked as boolean)
: undefined
@@ -177,7 +177,7 @@ export const listBlockHtmlAdapterMatcher: BlockHtmlAdapterMatcher = {
Array.isArray(previousTNode.properties.className) &&
previousTNode.properties.className.includes('todo-list')
) ===
AdapterTextUtils.isNullish(
TextUtils.isNullish(
o.node.props.type === 'todo'
? (o.node.props.checked as boolean)
: undefined
@@ -1,9 +1,9 @@
import { ListBlockSchema } from '@blocksuite/affine-model';
import {
AdapterTextUtils,
BlockMarkdownAdapterExtension,
type BlockMarkdownAdapterMatcher,
type MarkdownAST,
TextUtils,
} from '@blocksuite/affine-shared/adapters';
import type { DeltaInsert } from '@blocksuite/inline';
import { nanoid } from '@blocksuite/store';
@@ -75,8 +75,8 @@ export const listBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher = {
walkerContext.getNodeContext('affine:list:parent') === o.parent &&
currentTNode.type === 'list' &&
currentTNode.ordered === (o.node.props.type === 'numbered') &&
AdapterTextUtils.isNullish(currentTNode.children[0].checked) ===
AdapterTextUtils.isNullish(
TextUtils.isNullish(currentTNode.children[0].checked) ===
TextUtils.isNullish(
o.node.props.type === 'todo'
? (o.node.props.checked as boolean)
: undefined
@@ -129,8 +129,8 @@ export const listBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher = {
currentTNode.type === 'listItem' &&
previousTNode?.type === 'list' &&
previousTNode.ordered === (o.node.props.type === 'numbered') &&
AdapterTextUtils.isNullish(currentTNode.checked) ===
AdapterTextUtils.isNullish(
TextUtils.isNullish(currentTNode.checked) ===
TextUtils.isNullish(
o.node.props.type === 'todo'
? (o.node.props.checked as boolean)
: undefined
@@ -118,7 +118,7 @@ export class SurfaceRefNotePortal extends WithDisposable(ShadowlessElement) {
query: this.query,
readonly: true,
});
const previewSpec = SpecProvider.getInstance().getSpec('preview:page');
const previewSpec = SpecProvider.getInstance().getSpec('page:preview');
return new BlockStdScope({
store: doc,
extensions: previewSpec.value.slice(),
@@ -240,7 +240,7 @@ export class SurfaceRefBlockComponent extends BlockComponent<SurfaceRefBlockMode
private _previewDoc: Store | null = null;
private readonly _previewSpec =
SpecProvider.getInstance().getSpec('preview:edgeless');
SpecProvider.getInstance().getSpec('edgeless:preview');
private _referencedModel: GfxModel | null = null;
@@ -4,10 +4,7 @@ import type {
TableColumn,
TableRow,
} from '@blocksuite/affine-model';
import {
AdapterTextUtils,
HastUtils,
} from '@blocksuite/affine-shared/adapters';
import { HastUtils, TextUtils } from '@blocksuite/affine-shared/adapters';
import { generateFractionalIndexingKeyBetween } from '@blocksuite/affine-shared/utils';
import type { DeltaInsert } from '@blocksuite/inline';
import { nanoid } from '@blocksuite/store';
@@ -158,7 +155,7 @@ export const createTableProps = (rowTextLists: string[][]) => {
const cellId = `${row.rowId}:${column.columnId}`;
const text = rowTextLists[i]?.[j];
cells[cellId] = {
text: AdapterTextUtils.createText(text ?? ''),
text: TextUtils.createText(text ?? ''),
};
}
}
@@ -1,7 +1,7 @@
import type { InlineHtmlAST } from '@blocksuite/affine-shared/adapters';
import {
AdapterTextUtils,
InlineDeltaToHtmlAdapterExtension,
TextUtils,
} from '@blocksuite/affine-shared/adapters';
import { ThemeProvider } from '@blocksuite/affine-shared/services';
@@ -90,7 +90,7 @@ export const referenceDeltaToHtmlAdapterMatcher =
const { configs } = context;
const title = configs.get(`title:${reference.pageId}`);
const url = AdapterTextUtils.generateDocUrl(
const url = TextUtils.generateDocUrl(
configs.get('docLinkBaseUrl') ?? '',
String(reference.pageId),
reference.params ?? Object.create(null)
@@ -1,7 +1,7 @@
import {
AdapterTextUtils,
FOOTNOTE_DEFINITION_PREFIX,
InlineDeltaToMarkdownAdapterExtension,
TextUtils,
} from '@blocksuite/affine-shared/adapters';
import type { PhrasingContent } from 'mdast';
import type RemarkMath from 'remark-math';
@@ -74,7 +74,7 @@ export const referenceDeltaToMarkdownAdapterMatcher =
const { configs } = context;
const title = configs.get(`title:${reference.pageId}`);
const params = reference.params ?? {};
const url = AdapterTextUtils.generateDocUrl(
const url = TextUtils.generateDocUrl(
configs.get('docLinkBaseUrl') ?? '',
String(reference.pageId),
params
@@ -1,7 +1,7 @@
import {
AdapterTextUtils,
InlineDeltaToPlainTextAdapterExtension,
type TextBuffer,
TextUtils,
} from '@blocksuite/affine-shared/adapters';
import type { ExtensionType } from '@blocksuite/store';
@@ -20,7 +20,7 @@ export const referenceDeltaMarkdownAdapterMatch =
const { configs } = context;
const title = configs.get(`title:${reference.pageId}`) ?? '';
const url = AdapterTextUtils.generateDocUrl(
const url = TextUtils.generateDocUrl(
configs.get('docLinkBaseUrl') ?? '',
String(reference.pageId),
reference.params ?? Object.create(null)
@@ -1 +1,2 @@
export * from './frame-panel';
export * from './tool';
@@ -1,7 +1,6 @@
import type { NavigatorMode } from '@blocksuite/affine-block-frame';
import { BaseTool } from '@blocksuite/block-std/gfx';
import type { NavigatorMode } from './frame-manager';
type PresentToolOption = {
mode?: NavigatorMode;
};
@@ -1,11 +1,3 @@
import type { GfxModel } from '@blocksuite/block-std/gfx';
import type {
BrushElementModel,
ConnectorElementModel,
GroupElementModel,
} from '../elements';
export type EmbedCardStyle =
| 'horizontal'
| 'horizontalThin'
@@ -25,8 +17,3 @@ export type LinkPreviewData = {
image: string | null;
title: string | null;
};
export type Connectable = Exclude<
GfxModel,
ConnectorElementModel | BrushElementModel | GroupElementModel
>;
@@ -14,7 +14,7 @@ import {
type InlineDeltaMatcher,
} from '../types/adapter.js';
import type { HtmlAST, InlineHtmlAST } from '../types/hast.js';
import { AdapterTextUtils } from '../utils/text.js';
import { TextUtils } from '../utils/text.js';
export type InlineDeltaToHtmlAdapterMatcher = InlineDeltaMatcher<InlineHtmlAST>;
@@ -119,7 +119,7 @@ export class HtmlDeltaConverter extends DeltaASTConverter<
options: DeltaASTConverterOptions = Object.create(null)
): DeltaInsert<AffineTextAttributes>[] {
return this._spreadAstToDelta(ast, options).reduce((acc, cur) => {
return AdapterTextUtils.mergeDeltas(acc, cur);
return TextUtils.mergeDeltas(acc, cur);
}, [] as DeltaInsert<AffineTextAttributes>[]);
}
@@ -1,44 +0,0 @@
import type { TransformerMiddleware } from '@blocksuite/store';
const customDocLinkBaseUrlMiddleware = (
baseUrl: string,
collectionId: string
): TransformerMiddleware => {
return ({ adapterConfigs }) => {
const docLinkBaseUrl = baseUrl
? `${baseUrl}/workspace/${collectionId}`
: '';
adapterConfigs.set('docLinkBaseUrl', docLinkBaseUrl);
};
};
export const docLinkBaseURLMiddlewareBuilder = (
baseUrl: string,
collectionId: string
) => {
let middleware = customDocLinkBaseUrlMiddleware(baseUrl, collectionId);
return {
get: () => middleware,
set: (url: string) => {
middleware = customDocLinkBaseUrlMiddleware(url, collectionId);
},
};
};
const defaultDocLinkBaseURLMiddlewareBuilder = (collectionId: string) =>
docLinkBaseURLMiddlewareBuilder(
typeof window !== 'undefined' ? window.location.origin : '.',
collectionId
);
export const docLinkBaseURLMiddleware = (collectionId: string) =>
defaultDocLinkBaseURLMiddlewareBuilder(collectionId).get();
export const setDocLinkBaseURLMiddleware = (collectionId: string) =>
defaultDocLinkBaseURLMiddlewareBuilder(collectionId).set;
export const embedSyncedDocMiddleware =
(type: 'content'): TransformerMiddleware =>
({ adapterConfigs }) => {
adapterConfigs.set('embedSyncedDocExportType', type);
};
@@ -1,23 +0,0 @@
import type { TransformerMiddleware } from '@blocksuite/store';
export const fileNameMiddleware =
(fileName?: string): TransformerMiddleware =>
({ slots }) => {
slots.beforeImport.on(payload => {
if (payload.type !== 'page') {
return;
}
if (!fileName) {
return;
}
payload.snapshot.meta.title = fileName;
payload.snapshot.blocks.props.title = {
'$blocksuite:internal:text$': true,
delta: [
{
insert: fileName,
},
],
};
});
};
@@ -1,8 +1,4 @@
export * from './code';
export * from './copy';
export * from './doc-link';
export * from './file-name';
export * from './paste';
export * from './replace-id';
export * from './surface-ref-to-embed';
export * from './title';
@@ -1,11 +0,0 @@
import type { DocMeta, TransformerMiddleware } from '@blocksuite/store';
export const titleMiddleware =
(metas: DocMeta[]): TransformerMiddleware =>
({ slots, adapterConfigs }) => {
slots.beforeExport.on(() => {
for (const meta of metas) {
adapterConfigs.set('title:' + meta.id, meta.title);
}
});
};
@@ -86,7 +86,7 @@ function generateDocUrl(
return url;
}
export const AdapterTextUtils = {
export const TextUtils = {
mergeDeltas,
isNullish,
createText,
@@ -3,17 +3,10 @@ import type { ExtensionType } from '@blocksuite/store';
import { SpecBuilder } from './spec-builder.js';
type SpecId =
| 'store'
| 'page'
| 'edgeless'
| 'preview:page'
| 'preview:edgeless';
export class SpecProvider {
static instance: SpecProvider;
private readonly specMap = new Map<SpecId, ExtensionType[]>();
private readonly specMap = new Map<string, ExtensionType[]>();
private constructor() {}
@@ -24,17 +17,17 @@ export class SpecProvider {
return SpecProvider.instance;
}
addSpec(id: SpecId, spec: ExtensionType[]) {
addSpec(id: string, spec: ExtensionType[]) {
if (!this.specMap.has(id)) {
this.specMap.set(id, spec);
}
}
clearSpec(id: SpecId) {
clearSpec(id: string) {
this.specMap.delete(id);
}
extendSpec(id: SpecId, newSpec: ExtensionType[]) {
extendSpec(id: string, newSpec: ExtensionType[]) {
const existingSpec = this.specMap.get(id);
if (!existingSpec) {
console.error(`Spec not found for ${id}`);
@@ -43,17 +36,26 @@ export class SpecProvider {
this.specMap.set(id, [...existingSpec, ...newSpec]);
}
getSpec(id: SpecId) {
getSpec(id: string) {
const spec = this.specMap.get(id);
assertExists(spec, `Spec not found for ${id}`);
return new SpecBuilder(spec);
}
hasSpec(id: SpecId) {
hasSpec(id: string) {
return this.specMap.has(id);
}
omitSpec(id: SpecId, targetSpec: ExtensionType) {
cloneSpec(id: string, targetId: string) {
const existingSpec = this.specMap.get(id);
if (!existingSpec) {
console.error(`Spec not found for ${id}`);
return;
}
this.specMap.set(targetId, [...existingSpec]);
}
omitSpec(id: string, targetSpec: ExtensionType) {
const existingSpec = this.specMap.get(id);
if (!existingSpec) {
console.error(`Spec not found for ${id}`);
@@ -66,7 +68,7 @@ export class SpecProvider {
);
}
replaceSpec(id: SpecId, targetSpec: ExtensionType, newSpec: ExtensionType) {
replaceSpec(id: string, targetSpec: ExtensionType, newSpec: ExtensionType) {
const existingSpec = this.specMap.get(id);
if (!existingSpec) {
console.error(`Spec not found for ${id}`);
@@ -103,7 +103,7 @@ export class PreviewHelper {
const query = this._calculateQuery(blockIds as string[], mode);
const store = widget.doc.doc.getStore({ query });
const previewSpec = SpecProvider.getInstance().getSpec(
isEdgeless ? 'preview:edgeless' : 'preview:page'
isEdgeless ? 'edgeless:preview' : 'page:preview'
);
const settingSignal = signal({ ...editorSetting });
const extensions = [
@@ -3,10 +3,7 @@ import {
InlineDeltaToHtmlAdapterExtensions,
} from '@blocksuite/affine-components/rich-text';
import { DefaultTheme, NoteDisplayMode } from '@blocksuite/affine-model';
import {
embedSyncedDocMiddleware,
HtmlAdapter,
} from '@blocksuite/affine-shared/adapters';
import { HtmlAdapter } from '@blocksuite/affine-shared/adapters';
import { Container } from '@blocksuite/global/di';
import type {
BlockSnapshot,
@@ -17,6 +14,7 @@ import { AssetsManager, MemoryBlobCRUD } from '@blocksuite/store';
import { describe, expect, test } from 'vitest';
import { defaultBlockHtmlAdapterMatchers } from '../../_common/adapters/html/block-matcher.js';
import { embedSyncedDocMiddleware } from '../../_common/transformers/middlewares.js';
import { createJob } from '../utils/create-job.js';
import { nanoidReplacement } from '../utils/nanoid-replacement.js';
@@ -7,10 +7,7 @@ import {
NoteDisplayMode,
TableModelFlavour,
} from '@blocksuite/affine-model';
import {
embedSyncedDocMiddleware,
MarkdownAdapter,
} from '@blocksuite/affine-shared/adapters';
import { MarkdownAdapter } from '@blocksuite/affine-shared/adapters';
import { Container } from '@blocksuite/global/di';
import type {
BlockSnapshot,
@@ -22,6 +19,7 @@ import { AssetsManager, MemoryBlobCRUD } from '@blocksuite/store';
import { describe, expect, test } from 'vitest';
import { defaultBlockMarkdownAdapterMatchers } from '../../_common/adapters/markdown/block-matcher.js';
import { embedSyncedDocMiddleware } from '../../_common/transformers/middlewares.js';
import { createJob } from '../utils/create-job.js';
import { nanoidReplacement } from '../utils/nanoid-replacement.js';
@@ -1,9 +1,6 @@
import { InlineDeltaToPlainTextAdapterExtensions } from '@blocksuite/affine-components/rich-text';
import { DefaultTheme, NoteDisplayMode } from '@blocksuite/affine-model';
import {
embedSyncedDocMiddleware,
PlainTextAdapter,
} from '@blocksuite/affine-shared/adapters';
import { PlainTextAdapter } from '@blocksuite/affine-shared/adapters';
import { Container } from '@blocksuite/global/di';
import type {
BlockSnapshot,
@@ -13,6 +10,7 @@ import type {
import { describe, expect, test } from 'vitest';
import { defaultBlockPlainTextAdapterMatchers } from '../../_common/adapters/plain-text/block-matcher.js';
import { embedSyncedDocMiddleware } from '../../_common/transformers/middlewares.js';
import { createJob } from '../utils/create-job.js';
const container = new Container();
@@ -1,4 +1,3 @@
import { defaultImageProxyMiddleware } from '@blocksuite/affine-block-image';
import { FeatureFlagService } from '@blocksuite/affine-shared/services';
import {
Schema,
@@ -7,6 +6,7 @@ import {
} from '@blocksuite/store';
import { TestWorkspace } from '@blocksuite/store/test';
import { defaultImageProxyMiddleware } from '../../_common/transformers/middlewares.js';
import { AffineSchemas } from '../../schemas.js';
declare global {
@@ -1,16 +1,20 @@
import { defaultImageProxyMiddleware } from '@blocksuite/affine-block-image';
import {
docLinkBaseURLMiddleware,
fileNameMiddleware,
HtmlAdapter,
titleMiddleware,
} from '@blocksuite/affine-shared/adapters';
import { SpecProvider } from '@blocksuite/affine-shared/utils';
HtmlInlineToDeltaAdapterExtensions,
InlineDeltaToHtmlAdapterExtensions,
} from '@blocksuite/affine-components/rich-text';
import { HtmlAdapter } from '@blocksuite/affine-shared/adapters';
import { Container } from '@blocksuite/global/di';
import { sha } from '@blocksuite/global/utils';
import type { Store, Workspace } from '@blocksuite/store';
import { extMimeMap, Transformer } from '@blocksuite/store';
import { defaultBlockHtmlAdapterMatchers } from '../adapters/html/block-matcher.js';
import {
defaultImageProxyMiddleware,
docLinkBaseURLMiddleware,
fileNameMiddleware,
titleMiddleware,
} from './middlewares.js';
import { createAssetsArchive, download, Unzip } from './utils.js';
type ImportHTMLToDocOptions = {
@@ -24,14 +28,16 @@ type ImportHTMLZipOptions = {
imported: Blob;
};
function getProvider() {
const container = new Container();
const exts = SpecProvider.getInstance().getSpec('store').value;
exts.forEach(ext => {
ext.setup(container);
});
return container.provider();
}
const container = new Container();
[
...HtmlInlineToDeltaAdapterExtensions,
...defaultBlockHtmlAdapterMatchers,
...InlineDeltaToHtmlAdapterExtensions,
].forEach(ext => {
ext.setup(container);
});
const provider = container.provider();
/**
* Exports a doc to HTML format.
@@ -40,7 +46,6 @@ function getProvider() {
* @returns A Promise that resolves when the export is complete.
*/
async function exportDoc(doc: Store) {
const provider = getProvider();
const job = new Transformer({
schema: doc.schema,
blobCRUD: doc.blobSync,
@@ -96,7 +101,6 @@ async function importHTMLToDoc({
html,
fileName,
}: ImportHTMLToDocOptions) {
const provider = getProvider();
const job = new Transformer({
schema: collection.schema,
blobCRUD: collection.blobSync,
@@ -131,7 +135,6 @@ async function importHTMLToDoc({
* @returns A Promise that resolves to an array of IDs of the newly created docs.
*/
async function importHTMLZip({ collection, imported }: ImportHTMLZipOptions) {
const provider = getProvider();
const unzip = new Unzip();
await unzip.load(imported);
@@ -0,0 +1,15 @@
export { HtmlTransformer } from './html.js';
export { MarkdownTransformer } from './markdown.js';
export {
customImageProxyMiddleware,
defaultImageProxyMiddleware,
docLinkBaseURLMiddleware,
docLinkBaseURLMiddlewareBuilder,
embedSyncedDocMiddleware,
replaceIdMiddleware,
setImageProxyMiddlewareURL,
titleMiddleware,
} from './middlewares.js';
export { NotionHtmlTransformer } from './notion-html.js';
export { createAssetsArchive, download } from './utils.js';
export { ZipTransformer } from './zip.js';
@@ -1,27 +1,33 @@
import { defaultImageProxyMiddleware } from '@blocksuite/affine-block-image';
import {
docLinkBaseURLMiddleware,
fileNameMiddleware,
MarkdownAdapter,
titleMiddleware,
} from '@blocksuite/affine-shared/adapters';
import { SpecProvider } from '@blocksuite/affine-shared/utils';
InlineDeltaToMarkdownAdapterExtensions,
MarkdownInlineToDeltaAdapterExtensions,
} from '@blocksuite/affine-components/rich-text';
import { MarkdownAdapter } from '@blocksuite/affine-shared/adapters';
import { Container } from '@blocksuite/global/di';
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
import { assertExists, sha } from '@blocksuite/global/utils';
import type { Store, Workspace } from '@blocksuite/store';
import { extMimeMap, Transformer } from '@blocksuite/store';
import { defaultBlockMarkdownAdapterMatchers } from '../adapters/index.js';
import {
defaultImageProxyMiddleware,
docLinkBaseURLMiddleware,
fileNameMiddleware,
titleMiddleware,
} from './middlewares.js';
import { createAssetsArchive, download, Unzip } from './utils.js';
function getProvider() {
const container = new Container();
const exts = SpecProvider.getInstance().getSpec('store').value;
exts.forEach(ext => {
ext.setup(container);
});
return container.provider();
}
const container = new Container();
[
...MarkdownInlineToDeltaAdapterExtensions,
...defaultBlockMarkdownAdapterMatchers,
...InlineDeltaToMarkdownAdapterExtensions,
].forEach(ext => {
ext.setup(container);
});
const provider = container.provider();
type ImportMarkdownToBlockOptions = {
doc: Store;
@@ -46,7 +52,6 @@ type ImportMarkdownZipOptions = {
* @returns A Promise that resolves when the export is complete
*/
async function exportDoc(doc: Store) {
const provider = getProvider();
const job = new Transformer({
schema: doc.schema,
blobCRUD: doc.blobSync,
@@ -106,7 +111,6 @@ async function importMarkdownToBlock({
markdown,
blockId,
}: ImportMarkdownToBlockOptions) {
const provider = getProvider();
const job = new Transformer({
schema: doc.schema,
blobCRUD: doc.blobSync,
@@ -152,7 +156,6 @@ async function importMarkdownToDoc({
markdown,
fileName,
}: ImportMarkdownToDocOptions) {
const provider = getProvider();
const job = new Transformer({
schema: collection.schema,
blobCRUD: collection.blobSync,
@@ -189,7 +192,6 @@ async function importMarkdownZip({
collection,
imported,
}: ImportMarkdownZipOptions) {
const provider = getProvider();
const unzip = new Unzip();
await unzip.load(imported);
@@ -6,8 +6,13 @@ import type {
ParagraphBlockModel,
SurfaceRefBlockModel,
} from '@blocksuite/affine-model';
import { DEFAULT_IMAGE_PROXY_ENDPOINT } from '@blocksuite/affine-shared/consts';
import { assertExists } from '@blocksuite/global/utils';
import type { DeltaOperation, TransformerMiddleware } from '@blocksuite/store';
import type {
DeltaOperation,
DocMeta,
TransformerMiddleware,
} from '@blocksuite/store';
export const replaceIdMiddleware =
(idGenerator: () => string): TransformerMiddleware =>
@@ -198,3 +203,103 @@ export const replaceIdMiddleware =
}
});
};
export const customImageProxyMiddleware = (
imageProxyURL: string
): TransformerMiddleware => {
return ({ adapterConfigs }) => {
adapterConfigs.set('imageProxy', imageProxyURL);
};
};
const customDocLinkBaseUrlMiddleware = (
baseUrl: string,
collectionId: string
): TransformerMiddleware => {
return ({ adapterConfigs }) => {
const docLinkBaseUrl = baseUrl
? `${baseUrl}/workspace/${collectionId}`
: '';
adapterConfigs.set('docLinkBaseUrl', docLinkBaseUrl);
};
};
export const titleMiddleware =
(metas: DocMeta[]): TransformerMiddleware =>
({ slots, adapterConfigs }) => {
slots.beforeExport.on(() => {
for (const meta of metas) {
adapterConfigs.set('title:' + meta.id, meta.title);
}
});
};
export const docLinkBaseURLMiddlewareBuilder = (
baseUrl: string,
collectionId: string
) => {
let middleware = customDocLinkBaseUrlMiddleware(baseUrl, collectionId);
return {
get: () => middleware,
set: (url: string) => {
middleware = customDocLinkBaseUrlMiddleware(url, collectionId);
},
};
};
const defaultDocLinkBaseURLMiddlewareBuilder = (collectionId: string) =>
docLinkBaseURLMiddlewareBuilder(
typeof window !== 'undefined' ? window.location.origin : '.',
collectionId
);
export const docLinkBaseURLMiddleware = (collectionId: string) =>
defaultDocLinkBaseURLMiddlewareBuilder(collectionId).get();
export const setDocLinkBaseURLMiddleware = (collectionId: string) =>
defaultDocLinkBaseURLMiddlewareBuilder(collectionId).set;
const imageProxyMiddlewareBuilder = () => {
let middleware = customImageProxyMiddleware(DEFAULT_IMAGE_PROXY_ENDPOINT);
return {
get: () => middleware,
set: (url: string) => {
middleware = customImageProxyMiddleware(url);
},
};
};
const defaultImageProxyMiddlewarBuilder = imageProxyMiddlewareBuilder();
export const setImageProxyMiddlewareURL = defaultImageProxyMiddlewarBuilder.set;
export const defaultImageProxyMiddleware =
defaultImageProxyMiddlewarBuilder.get();
export const embedSyncedDocMiddleware =
(type: 'content'): TransformerMiddleware =>
({ adapterConfigs }) => {
adapterConfigs.set('embedSyncedDocExportType', type);
};
export const fileNameMiddleware =
(fileName?: string): TransformerMiddleware =>
({ slots }) => {
slots.beforeImport.on(payload => {
if (payload.type !== 'page') {
return;
}
if (!fileName) {
return;
}
payload.snapshot.meta.title = fileName;
payload.snapshot.blocks.props.title = {
'$blocksuite:internal:text$': true,
delta: [
{
insert: fileName,
},
],
};
});
};
@@ -1,10 +1,11 @@
import { defaultImageProxyMiddleware } from '@blocksuite/affine-block-image';
import { NotionHtmlInlineToDeltaAdapterExtensions } from '@blocksuite/affine-components/rich-text';
import { NotionHtmlAdapter } from '@blocksuite/affine-shared/adapters';
import { SpecProvider } from '@blocksuite/affine-shared/utils';
import { Container } from '@blocksuite/global/di';
import { sha } from '@blocksuite/global/utils';
import { extMimeMap, Transformer, type Workspace } from '@blocksuite/store';
import { defaultBlockNotionHtmlAdapterMatchers } from '../adapters/notion-html/block-matcher.js';
import { defaultImageProxyMiddleware } from './middlewares.js';
import { Unzip } from './utils.js';
type ImportNotionZipOptions = {
@@ -12,14 +13,15 @@ type ImportNotionZipOptions = {
imported: Blob;
};
function getProvider() {
const container = new Container();
const exts = SpecProvider.getInstance().getSpec('store').value;
exts.forEach(ext => {
ext.setup(container);
});
return container.provider();
}
const container = new Container();
[
...NotionHtmlInlineToDeltaAdapterExtensions,
...defaultBlockNotionHtmlAdapterMatchers,
].forEach(ext => {
ext.setup(container);
});
const provider = container.provider();
/**
* Imports a Notion zip file into the BlockSuite collection.
@@ -38,7 +40,6 @@ async function importNotionZip({
collection,
imported,
}: ImportNotionZipOptions) {
const provider = getProvider();
const pageIds: string[] = [];
let isWorkspaceFile = false;
let hasMarkdown = false;
@@ -1,12 +1,9 @@
import {
replaceIdMiddleware,
titleMiddleware,
} from '@blocksuite/affine-shared/adapters';
import { sha } from '@blocksuite/global/utils';
import type { DocSnapshot, Store, Workspace } from '@blocksuite/store';
import { extMimeMap, getAssetName, Transformer } from '@blocksuite/store';
import { download, Unzip, Zip } from '../transformers/utils.js';
import { replaceIdMiddleware, titleMiddleware } from './middlewares.js';
async function exportDocs(collection: Workspace, docs: Store[]) {
const zip = new Zip();
+11
View File
@@ -0,0 +1,11 @@
import type {
BrushElementModel,
ConnectorElementModel,
GroupElementModel,
} from '@blocksuite/affine-model';
import type { GfxModel } from '@blocksuite/block-std/gfx';
export type Connectable = Exclude<
GfxModel,
ConnectorElementModel | BrushElementModel | GroupElementModel
>;
-9
View File
@@ -56,11 +56,6 @@ import {
import type { ExtensionType } from '@blocksuite/store';
import { AdapterFactoryExtensions } from '../_common/adapters/extension.js';
import {
HtmlAdapterExtension,
MarkdownAdapterExtension,
NotionHtmlAdapterExtension,
} from './preset/adapters.js';
export const CommonBlockSpecs: ExtensionType[] = [
DocDisplayMetaService,
@@ -115,8 +110,4 @@ export const StoreExtensions: ExtensionType[] = [
LinkPreviewerService,
FileSizeLimitService,
ImageStoreSpec,
HtmlAdapterExtension,
MarkdownAdapterExtension,
NotionHtmlAdapterExtension,
].flat();
@@ -1,31 +0,0 @@
import {
HtmlInlineToDeltaAdapterExtensions,
InlineDeltaToHtmlAdapterExtensions,
InlineDeltaToMarkdownAdapterExtensions,
MarkdownInlineToDeltaAdapterExtensions,
NotionHtmlInlineToDeltaAdapterExtensions,
} from '@blocksuite/affine-components/rich-text';
import type { ExtensionType } from '@blocksuite/store';
import {
defaultBlockHtmlAdapterMatchers,
defaultBlockMarkdownAdapterMatchers,
defaultBlockNotionHtmlAdapterMatchers,
} from '../../_common/adapters';
export const HtmlAdapterExtension: ExtensionType[] = [
...HtmlInlineToDeltaAdapterExtensions,
...defaultBlockHtmlAdapterMatchers,
...InlineDeltaToHtmlAdapterExtensions,
];
export const MarkdownAdapterExtension: ExtensionType[] = [
...MarkdownInlineToDeltaAdapterExtensions,
...defaultBlockMarkdownAdapterMatchers,
...InlineDeltaToMarkdownAdapterExtensions,
];
export const NotionHtmlAdapterExtension: ExtensionType[] = [
...NotionHtmlInlineToDeltaAdapterExtensions,
...defaultBlockNotionHtmlAdapterMatchers,
];
@@ -1,9 +1,9 @@
import {
EdgelessFrameManager,
FrameOverlay,
PresentTool,
} from '@blocksuite/affine-block-frame';
import { ConnectionOverlay } from '@blocksuite/affine-block-surface';
import { PresentTool } from '@blocksuite/affine-fragment-frame-panel';
import type { ExtensionType } from '@blocksuite/store';
import { EdgelessRootBlockSpec } from '../../root-block/edgeless/edgeless-root-spec.js';
@@ -1,6 +1,6 @@
import { SpecProvider } from '@blocksuite/affine-shared/utils';
import { StoreExtensions } from './common.js';
import { CommonBlockSpecs, StoreExtensions } from './common.js';
import { EdgelessEditorBlockSpecs } from './preset/edgeless-specs.js';
import { PageEditorBlockSpecs } from './preset/page-specs.js';
import {
@@ -10,14 +10,15 @@ import {
export function registerSpecs() {
SpecProvider.getInstance().addSpec('store', StoreExtensions);
SpecProvider.getInstance().addSpec('common', CommonBlockSpecs);
SpecProvider.getInstance().addSpec('page', PageEditorBlockSpecs);
SpecProvider.getInstance().addSpec('edgeless', EdgelessEditorBlockSpecs);
SpecProvider.getInstance().addSpec(
'preview:page',
'page:preview',
PreviewPageEditorBlockSpecs
);
SpecProvider.getInstance().addSpec(
'preview:edgeless',
'edgeless:preview',
PreviewEdgelessEditorBlockSpecs
);
}
+26 -1
View File
@@ -7,6 +7,7 @@ import { splitElements } from './root-block/edgeless/utils/clipboard-utils.js';
import { isCanvasElement } from './root-block/edgeless/utils/query.js';
export * from './_common/adapters/index.js';
export * from './_common/transformers/index.js';
export * from './_specs/index.js';
export { EdgelessTemplatePanel } from './root-block/edgeless/components/toolbar/template/template-panel.js';
export type {
@@ -92,7 +93,31 @@ export {
export * from '@blocksuite/affine-fragment-frame-panel';
export * from '@blocksuite/affine-fragment-outline';
export * from '@blocksuite/affine-model';
export * from '@blocksuite/affine-shared/adapters';
export {
AttachmentAdapter,
AttachmentAdapterFactoryExtension,
AttachmentAdapterFactoryIdentifier,
codeBlockWrapMiddleware,
FetchUtils,
HtmlAdapter,
HtmlAdapterFactoryExtension,
HtmlAdapterFactoryIdentifier,
ImageAdapter,
ImageAdapterFactoryExtension,
ImageAdapterFactoryIdentifier,
MarkdownAdapter,
MarkdownAdapterFactoryExtension,
MarkdownAdapterFactoryIdentifier,
MixTextAdapter,
MixTextAdapterFactoryExtension,
MixTextAdapterFactoryIdentifier,
NotionTextAdapter,
NotionTextAdapterFactoryExtension,
NotionTextAdapterFactoryIdentifier,
PlainTextAdapter,
PlainTextAdapterFactoryExtension,
PlainTextAdapterFactoryIdentifier,
} from '@blocksuite/affine-shared/adapters';
export * from '@blocksuite/affine-shared/commands';
export { HighlightSelection } from '@blocksuite/affine-shared/selection';
export * from '@blocksuite/affine-shared/services';
@@ -1,7 +1,6 @@
import { deleteTextCommand } from '@blocksuite/affine-components/rich-text';
import {
pasteMiddleware,
replaceIdMiddleware,
surfaceRefToEmbed,
} from '@blocksuite/affine-shared/adapters';
import {
@@ -18,6 +17,7 @@ import type { UIEventHandler } from '@blocksuite/block-std';
import { DisposableGroup } from '@blocksuite/global/utils';
import type { BlockSnapshot, Store } from '@blocksuite/store';
import { replaceIdMiddleware } from '../../_common/transformers/middlewares';
import { ReadOnlyClipboard } from './readonly-clipboard';
/**
@@ -1,4 +1,3 @@
import { defaultImageProxyMiddleware } from '@blocksuite/affine-block-image';
import {
AttachmentAdapter,
copyMiddleware,
@@ -6,7 +5,6 @@ import {
ImageAdapter,
MixTextAdapter,
NotionTextAdapter,
titleMiddleware,
} from '@blocksuite/affine-shared/adapters';
import {
copySelectedModelsCommand,
@@ -16,6 +14,10 @@ import {
import type { BlockComponent, UIEventHandler } from '@blocksuite/block-std';
import { DisposableGroup } from '@blocksuite/global/utils';
import {
defaultImageProxyMiddleware,
titleMiddleware,
} from '../../_common/transformers/middlewares.js';
import { ClipboardAdapter } from './adapter.js';
/**
@@ -81,7 +81,7 @@ export class FramePreview extends WithDisposable(ShadowlessElement) {
private _previewDoc: Store | null = null;
private readonly _previewSpec =
SpecProvider.getInstance().getSpec('preview:edgeless');
SpecProvider.getInstance().getSpec('edgeless:preview');
private readonly _updateFrameViewportWH = () => {
const [, , w, h] = deserializeXYWH(this.frame.xywh);
@@ -31,6 +31,11 @@ import {
import { IS_MAC } from '@blocksuite/global/env';
import { Bound, getCommonBound } from '@blocksuite/global/utils';
import {
getNearestTranslation,
isElementOutsideViewport,
isSingleMindMapNode,
} from '../../_common/edgeless/mindmap/index.js';
import { PageKeyboardManager } from '../keyboard/keyboard-manager.js';
import type { EdgelessRootBlockComponent } from './edgeless-root-block.js';
import { CopilotTool } from './gfx-tool/copilot-tool.js';
@@ -43,11 +48,6 @@ import {
} from './utils/consts.js';
import { deleteElements } from './utils/crud.js';
import { getNextShapeType } from './utils/hotkey-utils.js';
import {
getNearestTranslation,
isElementOutsideViewport,
isSingleMindMapNode,
} from './utils/mindmap.js';
import { isCanvasElement } from './utils/query.js';
import {
mountConnectorLabelEditor,
@@ -45,12 +45,12 @@ import { css, html } from 'lit';
import { query } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js';
import { isSingleMindMapNode } from '../../_common/edgeless/mindmap/index.js';
import type { EdgelessRootBlockWidgetName } from '../types.js';
import { EdgelessClipboardController } from './clipboard/clipboard.js';
import type { EdgelessSelectedRectWidget } from './components/rects/edgeless-selected-rect.js';
import { EdgelessPageKeyboardManager } from './edgeless-keyboard.js';
import type { EdgelessRootService } from './edgeless-root-service.js';
import { isSingleMindMapNode } from './utils/mindmap.js';
import { isCanvasElement } from './utils/query.js';
import { mountShapeTextEditor } from './utils/text.js';
@@ -19,7 +19,7 @@ import {
} from '@blocksuite/block-std/gfx';
import type { Bound, IVec } from '@blocksuite/global/utils';
import { isSingleMindMapNode } from '../../../utils/mindmap.js';
import { isSingleMindMapNode } from '../../../../../_common/edgeless/mindmap/index.js';
import { isMindmapNode } from '../../../utils/query.js';
import { DefaultModeDragType, DefaultToolExt, type DragState } from '../ext.js';
import { calculateResponseArea } from './drag-utils.js';
@@ -53,9 +53,9 @@ import {
} from '@blocksuite/global/utils';
import { effect } from '@preact/signals-core';
import { isSingleMindMapNode } from '../../../_common/edgeless/mindmap/index.js';
import type { EdgelessRootBlockComponent } from '../edgeless-root-block.js';
import { prepareCloneData } from '../utils/clone-utils.js';
import { isSingleMindMapNode } from '../utils/mindmap.js';
import { calPanDelta } from '../utils/panning-utils.js';
import { isCanvasElement, isEdgelessTextBlock } from '../utils/query.js';
import type { EdgelessSnapManager } from '../utils/snap-manager.js';
@@ -1,7 +1,7 @@
import { isNoteBlock } from '@blocksuite/affine-block-surface';
import type { Connectable } from '@blocksuite/affine-model';
import type { GfxModel } from '@blocksuite/block-std/gfx';
import type { Connectable } from '../../../_common/types.js';
import type { EdgelessRootBlockComponent } from '../index.js';
import { isConnectable } from './query.js';
@@ -2,7 +2,6 @@ import type { CanvasElementWithText } from '@blocksuite/affine-block-surface';
import {
type AttachmentBlockModel,
type BookmarkBlockModel,
type Connectable,
ConnectorElementModel,
type EdgelessTextBlockModel,
type EmbedBlockModel,
@@ -33,6 +32,8 @@ import type { PointLocation } from '@blocksuite/global/utils';
import { Bound } from '@blocksuite/global/utils';
import type { BlockModel } from '@blocksuite/store';
import type { Connectable } from '../../../_common/types';
export function isMindmapNode(element: GfxBlockElementModel | GfxModel | null) {
return element?.group instanceof MindmapElementModel;
}
@@ -11,7 +11,6 @@ export * from './page/page-root-spec.js';
export * from './preview/preview-root-block.js';
export * from './root-config.js';
export { RootService } from './root-service.js';
export * from './transformers/index.js';
export * from './types.js';
export * from './utils/index.js';
export * from './widgets/index.js';
@@ -8,11 +8,22 @@ import {
import type { BlockComponent } from '@blocksuite/block-std';
import { BlockService } from '@blocksuite/block-std';
import {
HtmlTransformer,
MarkdownTransformer,
ZipTransformer,
} from '../_common/transformers/index.js';
import type { RootBlockComponent } from './types.js';
export abstract class RootService extends BlockService {
static override readonly flavour = RootBlockSchema.model.flavour;
transformers = {
markdown: MarkdownTransformer,
html: HtmlTransformer,
zip: ZipTransformer,
};
get selectedBlocks() {
let result: BlockComponent[] = [];
this.std.command
@@ -1,5 +0,0 @@
export { HtmlTransformer } from './html.js';
export { MarkdownTransformer } from './markdown.js';
export { NotionHtmlTransformer } from './notion-html.js';
export { createAssetsArchive, download } from './utils.js';
export { ZipTransformer } from './zip.js';
@@ -12,9 +12,9 @@ import type { Workspace } from '@blocksuite/store';
import { html, LitElement, type PropertyValues } from 'lit';
import { query, state } from 'lit/decorators.js';
import { HtmlTransformer } from '../../../transformers/html.js';
import { MarkdownTransformer } from '../../../transformers/markdown.js';
import { NotionHtmlTransformer } from '../../../transformers/notion-html.js';
import { HtmlTransformer } from '../../../../_common/transformers/html.js';
import { MarkdownTransformer } from '../../../../_common/transformers/markdown.js';
import { NotionHtmlTransformer } from '../../../../_common/transformers/notion-html.js';
import { styles } from './styles.js';
export type OnSuccessHandler = (
@@ -1,4 +1,5 @@
import { type SurfaceBlockModel, ZipTransformer } from '@blocksuite/blocks';
import { BlockServiceIdentifier } from '@blocksuite/block-std';
import type { PageRootService, SurfaceBlockModel } from '@blocksuite/blocks';
import type { PointLocation } from '@blocksuite/global/utils';
import { beforeEach, expect, test } from 'vitest';
@@ -24,7 +25,13 @@ const fieldChecker: Record<string, (value: any) => boolean> = {
const skipFields = new Set(['_lastXYWH']);
const snapshotTest = async (snapshotUrl: string, elementsCount: number) => {
const transformer = ZipTransformer;
const pageService = window.editor.host!.std.getOptional(
BlockServiceIdentifier('affine:page')
) as PageRootService;
if (!pageService) {
throw new Error('page service not found');
}
const transformer = pageService.transformers.zip;
const snapshotFile = await fetch(snapshotUrl)
.then(res => res.blob())
@@ -11,6 +11,7 @@ import { ClsInterceptor } from 'nestjs-cls';
import { Socket } from 'socket.io';
import {
AlreadyInSpace,
CallMetric,
DocNotFound,
GatewayErrorWrapper,
@@ -616,17 +617,13 @@ abstract class SyncSocketAdapter {
}
async join(userId: string, spaceId: string, roomType: RoomType = 'sync') {
if (this.in(spaceId, roomType)) {
return;
}
this.assertNotIn(spaceId, roomType);
await this.assertAccessible(spaceId, userId, WorkspaceRole.Collaborator);
return this.client.join(this.room(spaceId, roomType));
}
async leave(spaceId: string, roomType: RoomType = 'sync') {
if (!this.in(spaceId, roomType)) {
return;
}
this.assertIn(spaceId, roomType);
return this.client.leave(this.room(spaceId, roomType));
}
@@ -634,6 +631,12 @@ abstract class SyncSocketAdapter {
return this.client.rooms.has(this.room(spaceId, roomType));
}
assertNotIn(spaceId: string, roomType: RoomType = 'sync') {
if (this.client.rooms.has(this.room(spaceId, roomType))) {
throw new AlreadyInSpace({ spaceId });
}
}
assertIn(spaceId: string, roomType: RoomType = 'sync') {
if (!this.client.rooms.has(this.room(spaceId, roomType))) {
throw new NotInSpace({ spaceId });
@@ -74,7 +74,7 @@ export class QuotaOverride {
);
break;
case SubscriptionPlan.Pro:
await this.models.userFeature.switchQuota(
await this.models.userFeature.add(
userId,
recurring === 'lifetime' ? 'lifetime_pro_plan_v1' : 'pro_plan_v1',
'subscription activated'
@@ -35,13 +35,11 @@ export async function decodeWithCharset(
});
const body = await rewriter.transform(response).arrayBuffer();
try {
if (charset) {
const decoder = new TextDecoder(charset);
res.charset = decoder.encoding;
return new Response(decoder.decode(body), response);
}
} catch {}
return new Response(body, response);
if (charset) {
const decoder = new TextDecoder(charset);
res.charset = decoder.encoding;
return new Response(decoder.decode(body), response);
} else {
return new Response(body, response);
}
}
+2 -10
View File
@@ -399,31 +399,23 @@ export class DocFrontend {
this.statusUpdatedSubject$.next(job.docId);
}
/**
* skip listen doc update when apply update
*/
private skipDocUpdate = false;
applyUpdate(docId: string, update: Uint8Array) {
const doc = this.status.docs.get(docId);
if (doc && !isEmptyUpdate(update)) {
try {
this.skipDocUpdate = true;
applyUpdate(doc, update, NBSTORE_ORIGIN);
} catch (err) {
console.error('failed to apply update yjs doc', err);
} finally {
this.skipDocUpdate = false;
}
}
}
private readonly handleDocUpdate = (
update: Uint8Array,
_origin: any,
origin: any,
doc: YDoc
) => {
if (this.skipDocUpdate) {
if (origin === NBSTORE_ORIGIN) {
return;
}
if (!this.status.docs.has(doc.guid)) {
+28 -41
View File
@@ -17,7 +17,7 @@ type Job =
| {
type: 'push';
docId: string;
update?: Uint8Array;
update: Uint8Array;
clock: Date;
}
| {
@@ -247,7 +247,7 @@ export class DocSyncPeer {
!this.remote.isReadonly &&
clock &&
(pushedClock === null ||
pushedClock.getTime() < clock.timestamp.getTime())
pushedClock.getTime() !== clock.timestamp.getTime())
) {
await this.jobs.pullAndPush(docId, signal);
} else {
@@ -255,10 +255,9 @@ export class DocSyncPeer {
const pulled =
(await this.syncMetadata.getPeerPulledRemoteClock(this.peerId, docId))
?.timestamp ?? null;
const remoteClock = this.status.remoteClocks.get(docId);
if (
remoteClock &&
(pulled === null || pulled.getTime() < remoteClock.getTime())
pulled === null ||
pulled.getTime() !== this.status.remoteClocks.get(docId)?.getTime()
) {
await this.jobs.pull(docId, signal);
}
@@ -279,9 +278,7 @@ export class DocSyncPeer {
);
const merged = await this.mergeUpdates(
jobs
.map(j => j.update ?? new Uint8Array())
.filter(update => !isEmptyUpdate(update))
jobs.map(j => j.update).filter(update => !isEmptyUpdate(update))
);
if (!isEmptyUpdate(merged)) {
const { timestamp } = await this.remote.pushDocUpdate(
@@ -319,6 +316,11 @@ export class DocSyncPeer {
state: serverStateVector,
timestamp: remoteClock,
} = remoteDocRecord;
this.schedule({
type: 'save',
docId,
remoteClock,
});
throwIfAborted(signal);
const { timestamp: localClock } = await this.local.pushDocUpdate(
{
@@ -357,10 +359,9 @@ export class DocSyncPeer {
});
}
throwIfAborted(signal);
this.schedule({
type: 'push',
await this.syncMetadata.setPeerPushedClock(this.peerId, {
docId,
clock: localClock,
timestamp: localClock,
});
} else {
if (localDocRecord) {
@@ -379,11 +380,6 @@ export class DocSyncPeer {
remoteClock,
});
}
this.schedule({
type: 'push',
docId,
clock: localDocRecord.timestamp,
});
await this.syncMetadata.setPeerPushedClock(this.peerId, {
docId,
timestamp: localDocRecord.timestamp,
@@ -404,7 +400,7 @@ export class DocSyncPeer {
}
const { missing: newData, timestamp: remoteClock } = serverDoc;
throwIfAborted(signal);
const { timestamp } = await this.local.pushDocUpdate(
await this.local.pushDocUpdate(
{
docId,
bin: newData,
@@ -417,9 +413,9 @@ export class DocSyncPeer {
timestamp: remoteClock,
});
this.schedule({
type: 'push',
type: 'save',
docId,
clock: timestamp,
remoteClock: remoteClock,
});
},
save: async (
@@ -442,20 +438,13 @@ export class DocSyncPeer {
throwIfAborted(signal);
if (!isEmptyUpdate(update)) {
const { timestamp } = await this.local.pushDocUpdate(
await this.local.pushDocUpdate(
{
docId,
bin: update,
},
this.uniqueId
);
// schedule push job to mark the timestamp as pushed timestamp
this.schedule({
type: 'push',
docId,
clock: timestamp,
});
}
throwIfAborted(signal);
@@ -468,9 +457,15 @@ export class DocSyncPeer {
});
private readonly actions = {
updateRemoteClock: (docId: string, remoteClock: Date) => {
this.status.remoteClocks.setIfBigger(docId, remoteClock);
this.statusUpdatedSubject$.next(docId);
updateRemoteClock: async (docId: string, remoteClock: Date) => {
const updated = this.status.remoteClocks.setIfBigger(docId, remoteClock);
if (updated) {
await this.syncMetadata.setPeerRemoteClock(this.peerId, {
docId,
timestamp: remoteClock,
});
this.statusUpdatedSubject$.next(docId);
}
},
addDoc: (docId: string) => {
if (!this.status.docs.has(docId)) {
@@ -516,7 +511,6 @@ export class DocSyncPeer {
}) => {
// try add doc for new doc
this.actions.addDoc(docId);
this.actions.updateRemoteClock(docId, remoteClock);
// schedule push job
this.schedule({
@@ -690,14 +684,7 @@ export class DocSyncPeer {
const maxClockValue = this.status.remoteClocks.max;
const newClocks = await this.remote.getDocTimestamps(maxClockValue);
for (const [id, v] of Object.entries(newClocks)) {
this.actions.updateRemoteClock(id, v);
}
for (const [id, v] of Object.entries(newClocks)) {
await this.syncMetadata.setPeerRemoteClock(this.peerId, {
docId: id,
timestamp: v,
});
await this.actions.updateRemoteClock(id, v);
}
// add all docs from remote
@@ -791,9 +778,9 @@ export class DocSyncPeer {
};
}
protected mergeUpdates = (updates: Uint8Array[]) => {
protected mergeUpdates(updates: Uint8Array[]) {
const merge = this.options?.mergeUpdates ?? mergeUpdates;
return merge(updates.filter(bin => !isEmptyUpdate(bin)));
};
}
}
@@ -25,8 +25,6 @@ export interface MenuItemProps
suffix?: ReactNode;
prefixIcon?: ReactNode;
suffixIcon?: ReactNode;
prefixIconClassName?: string;
suffixIconClassName?: string;
checked?: boolean;
selected?: boolean;
block?: boolean;
@@ -11,10 +11,8 @@ export const useMenuItem = <T extends MenuItemProps>({
className: propsClassName,
prefix,
prefixIcon,
prefixIconClassName,
suffix,
suffixIcon,
suffixIconClassName,
checked,
selected,
block,
@@ -40,17 +38,13 @@ export const useMenuItem = <T extends MenuItemProps>({
{prefix}
{prefixIcon ? (
<div className={clsx(styles.menuItemIcon, prefixIconClassName)}>
{prefixIcon}
</div>
<div className={styles.menuItemIcon}>{prefixIcon}</div>
) : null}
<span className={styles.menuSpan}>{propsChildren}</span>
{suffixIcon ? (
<div className={clsx(styles.menuItemIcon, suffixIconClassName)}>
{suffixIcon}
</div>
<div className={styles.menuItemIcon}>{suffixIcon}</div>
) : null}
{suffix}
@@ -12,7 +12,6 @@ export type ScrollableContainerProps = {
viewPortClassName?: string;
styles?: React.CSSProperties;
scrollBarClassName?: string;
scrollThumbClassName?: string;
};
export const ScrollableContainer = ({
@@ -23,7 +22,6 @@ export const ScrollableContainer = ({
styles: _styles,
viewPortClassName,
scrollBarClassName,
scrollThumbClassName,
}: PropsWithChildren<ScrollableContainerProps>) => {
const [setContainer, hasScrollTop] = useHasScrollTop();
return (
@@ -47,9 +45,7 @@ export const ScrollableContainer = ({
[styles.TableScrollbar]: inTableView,
})}
>
<ScrollArea.Thumb
className={clsx(styles.scrollbarThumb, scrollThumbClassName)}
/>
<ScrollArea.Thumb className={styles.scrollbarThumb} />
</ScrollArea.Scrollbar>
</ScrollArea.Root>
);
@@ -209,7 +209,7 @@ export class AISlidesRenderer extends WithDisposable(LitElement) {
${new BlockStdScope({
store: this._doc,
extensions:
SpecProvider.getInstance().getSpec('preview:edgeless').value,
SpecProvider.getInstance().getSpec('edgeless:preview').value,
}).render()}
</div>
<div class="mask"></div>
@@ -42,7 +42,7 @@ export interface AffineEditorContainer extends HTMLElement {
page: Store;
doc: Store;
docTitle: DocTitle;
host?: EditorHost;
host: EditorHost;
model: RootBlockModel | null;
updateComplete: Promise<boolean>;
mode: DocMode;
@@ -67,38 +67,46 @@ const BlockSuiteEditorImpl = ({
let canceled = false;
const disposableGroup = new DisposableGroup();
// Invoke onLoad once the editor has been mounted to the DOM.
if (canceled) {
return;
}
if (onEditorReady) {
// Invoke onLoad once the editor has been mounted to the DOM.
editor.updateComplete
.then(() => {
if (canceled) {
return;
}
// host should be ready
// provide image proxy endpoint to blocksuite
const imageProxyUrl = new URL(
BUILD_CONFIG.imageProxyUrl,
server.baseUrl
).toString();
// provide image proxy endpoint to blocksuite
const imageProxyUrl = new URL(
BUILD_CONFIG.imageProxyUrl,
server.baseUrl
).toString();
const linkPreviewUrl = new URL(
BUILD_CONFIG.linkPreviewUrl,
server.baseUrl
).toString();
const linkPreviewUrl = new URL(
BUILD_CONFIG.linkPreviewUrl,
server.baseUrl
).toString();
editor.host?.std.clipboard.use(
customImageProxyMiddleware(imageProxyUrl)
);
editor.std.clipboard.use(customImageProxyMiddleware(imageProxyUrl));
page.get(LinkPreviewerService).setEndpoint(linkPreviewUrl);
page.get(ImageProxyService).setImageProxyURL(imageProxyUrl);
page.get(LinkPreviewerService).setEndpoint(linkPreviewUrl);
editor.updateComplete
.then(() => {
if (onEditorReady) {
page.get(ImageProxyService).setImageProxyURL(imageProxyUrl);
return editor.host?.updateComplete;
})
.then(() => {
if (canceled) {
return;
}
const dispose = onEditorReady(editor);
if (dispose) {
disposableGroup.add(dispose);
}
}
})
.catch(error => {
console.error('Error updating editor', error);
});
})
.catch(console.error);
}
return () => {
canceled = true;
@@ -245,10 +245,10 @@ export const extendEdgelessPreviewSpec = (function () {
return _extension;
} else {
_extension &&
SpecProvider.getInstance().omitSpec('preview:edgeless', _extension);
SpecProvider.getInstance().omitSpec('edgeless:preview', _extension);
_extension = getThemeExtension(framework);
_framework = framework;
SpecProvider.getInstance().extendSpec('preview:edgeless', [_extension]);
SpecProvider.getInstance().extendSpec('edgeless:preview', [_extension]);
return _extension;
}
};
@@ -29,17 +29,14 @@ const CustomSpecs: ExtensionType[] = [
getFontConfigExtension(),
].flat();
function patchPreviewSpec(
id: 'preview:edgeless' | 'preview:page',
specs: ExtensionType[]
) {
function patchPreviewSpec(id: string, specs: ExtensionType[]) {
const specProvider = SpecProvider.getInstance();
specProvider.extendSpec(id, specs);
}
export function effects() {
// Patch edgeless preview spec for blocksuite surface-ref and embed-synced-doc
patchPreviewSpec('preview:edgeless', CustomSpecs);
patchPreviewSpec('edgeless:preview', CustomSpecs);
}
export function getPagePreviewThemeExtension(framework: FrameworkProvider) {
@@ -101,7 +98,7 @@ export function createPageModePreviewSpecs(
framework: FrameworkProvider
): SpecBuilder {
const specProvider = SpecProvider.getInstance();
const pagePreviewSpec = specProvider.getSpec('preview:page');
const pagePreviewSpec = specProvider.getSpec('page:preview');
// Enable theme extension, doc display meta extension and peek view service
const peekViewService = framework.get(PeekViewService);
pagePreviewSpec.extend([
@@ -97,8 +97,6 @@ export const WorkspaceSelector = ({
...menuContentOptions,
style: {
width: '300px',
maxHeight: 'min(800px, calc(100vh - 200px))',
padding: 0,
...menuContentOptions?.style,
},
}}
@@ -1,5 +1,23 @@
import { cssVar } from '@toeverything/theme';
import { style } from '@vanilla-extract/css';
export const addServerDividerWrapper = style({
padding: '0px 12px',
export const ItemContainer = style({
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-start',
padding: '8px 14px',
gap: '14px',
cursor: 'pointer',
borderRadius: '8px',
transition: 'background-color 0.2s',
fontSize: '24px',
color: cssVar('iconSecondary'),
});
export const ItemText = style({
fontSize: cssVar('fontSm'),
lineHeight: '22px',
color: cssVar('textSecondaryColor'),
fontWeight: 400,
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
});
@@ -1,50 +1,34 @@
import { Divider, MenuItem } from '@affine/component';
import { GlobalDialogService } from '@affine/core/modules/dialogs';
import { MenuItem } from '@affine/component/ui/menu';
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
import { useI18n } from '@affine/i18n';
import { PlusIcon } from '@blocksuite/icons/rc';
import { useLiveData, useService } from '@toeverything/infra';
import { useCallback } from 'react';
import {
ItemContainer,
ItemText,
prefixIcon,
} from '../add-workspace/index.css';
import { addServerDividerWrapper } from './index.css';
import * as styles from './index.css';
export const AddServer = () => {
export const AddServer = ({ onAddServer }: { onAddServer?: () => void }) => {
const t = useI18n();
const globalDialogService = useService(GlobalDialogService);
const featureFlagService = useService(FeatureFlagService);
const enableMultipleServer = useLiveData(
featureFlagService.flags.enable_multiple_cloud_servers.$
);
const onAddServer = useCallback(() => {
globalDialogService.open('sign-in', { step: 'addSelfhosted' });
}, [globalDialogService]);
if (!enableMultipleServer) {
return null;
}
return (
<>
<div className={addServerDividerWrapper}>
<Divider size="thinner" />
</div>
<div>
<MenuItem
block={true}
prefixIcon={<PlusIcon />}
prefixIconClassName={prefixIcon}
onClick={onAddServer}
data-testid="new-server"
className={ItemContainer}
className={styles.ItemContainer}
>
<div className={ItemText}>
<div className={styles.ItemText}>
{t['com.affine.workspaceList.addServer']()}
</div>
</MenuItem>
</>
</div>
);
};
@@ -1,28 +1,21 @@
import { cssVar } from '@toeverything/theme';
import { cssVarV2 } from '@toeverything/theme/v2';
import { style } from '@vanilla-extract/css';
export const ItemContainer = style({
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-start',
padding: '6px 16px 6px 11px',
gap: '12px',
padding: '8px 14px',
gap: '14px',
cursor: 'pointer',
borderRadius: '8px',
transition: 'background-color 0.2s',
fontSize: '24px',
});
export const prefixIcon = style({
width: 24,
height: 24,
fontSize: 24,
color: cssVarV2.icon.secondary,
color: cssVar('iconSecondary'),
});
export const ItemText = style({
fontSize: cssVar('fontSm'),
lineHeight: '22px',
color: cssVarV2.text.secondary,
color: cssVar('textSecondaryColor'),
fontWeight: 400,
whiteSpace: 'nowrap',
overflow: 'hidden',
@@ -20,12 +20,11 @@ export const AddWorkspace = ({
);
return (
<>
<div>
{BUILD_CONFIG.isElectron && (
<MenuItem
block={true}
prefixIcon={<ImportIcon />}
prefixIconClassName={styles.prefixIcon}
onClick={onAddWorkspace}
data-testid="add-workspace"
className={styles.ItemContainer}
@@ -38,7 +37,6 @@ export const AddWorkspace = ({
<MenuItem
block={true}
prefixIcon={<PlusIcon />}
prefixIconClassName={styles.prefixIcon}
onClick={onNewWorkspace}
data-testid="new-workspace"
className={styles.ItemContainer}
@@ -49,6 +47,6 @@ export const AddWorkspace = ({
: t['com.affine.workspaceList.addWorkspace.create-cloud']()}
</div>
</MenuItem>
</>
</div>
);
};
@@ -1,26 +1,10 @@
import { cssVar } from '@toeverything/theme';
import { style } from '@vanilla-extract/css';
export const workspaceScrollArea = style({
export const workspaceListWrapper = style({
display: 'flex',
width: '100%',
flexDirection: 'column',
});
export const workspaceScrollAreaViewport = style({
padding: '10px 8px 0px 8px',
});
export const workspaceFooter = style({
padding: '0px 8px 10px 8px',
});
export const scrollbar = style({
width: 9,
padding: '0px 2px',
':hover': {
padding: 0,
},
});
export const scrollbarThumb = style({
width: 5,
});
export const signInWrapper = style({
display: 'flex',
width: '100%',
@@ -1,4 +1,3 @@
import { ScrollableContainer } from '@affine/component';
import { MenuItem } from '@affine/component/ui/menu';
import { AuthService } from '@affine/core/modules/cloud';
import { GlobalDialogService } from '@affine/core/modules/dialogs';
@@ -10,6 +9,7 @@ import { Logo1Icon } from '@blocksuite/icons/rc';
import { useLiveData, useService } from '@toeverything/infra';
import { useCallback } from 'react';
import { AddServer } from './add-server';
import { AddWorkspace } from './add-workspace';
import * as styles from './index.css';
import { AFFiNEWorkspaceList } from './workspace-list';
@@ -58,7 +58,7 @@ interface UserWithWorkspaceListProps {
showEnableCloudButton?: boolean;
}
export const UserWithWorkspaceList = ({
const UserWithWorkspaceListInner = ({
onEventEnd,
onClickWorkspace,
onCreatedWorkspace,
@@ -109,26 +109,26 @@ export const UserWithWorkspaceList = ({
onEventEnd?.();
}, [globalDialogService, onCreatedWorkspace, onEventEnd]);
const onAddServer = useCallback(() => {
globalDialogService.open('sign-in', { step: 'addSelfhosted' });
}, [globalDialogService]);
return (
<>
<ScrollableContainer
className={styles.workspaceScrollArea}
viewPortClassName={styles.workspaceScrollAreaViewport}
scrollBarClassName={styles.scrollbar}
scrollThumbClassName={styles.scrollbarThumb}
>
<AFFiNEWorkspaceList
onEventEnd={onEventEnd}
onClickWorkspace={onClickWorkspace}
showEnableCloudButton={showEnableCloudButton}
/>
</ScrollableContainer>
<div className={styles.workspaceFooter}>
<AddWorkspace
onAddWorkspace={onAddWorkspace}
onNewWorkspace={onNewWorkspace}
/>
</div>
</>
<div className={styles.workspaceListWrapper}>
<AFFiNEWorkspaceList
onEventEnd={onEventEnd}
onClickWorkspace={onClickWorkspace}
showEnableCloudButton={showEnableCloudButton}
/>
<AddWorkspace
onAddWorkspace={onAddWorkspace}
onNewWorkspace={onNewWorkspace}
/>
<AddServer onAddServer={onAddServer} />
</div>
);
};
export const UserWithWorkspaceList = (props: UserWithWorkspaceListProps) => {
return <UserWithWorkspaceListInner {...props} />;
};
@@ -1,76 +1,81 @@
import { cssVar } from '@toeverything/theme';
import { cssVarV2 } from '@toeverything/theme/v2';
import { style } from '@vanilla-extract/css';
export const workspaceListsWrapper = style({
display: 'flex',
width: '100%',
flexDirection: 'column',
maxHeight: 'calc(100vh - 300px)',
});
export const workspaceListWrapper = style({
display: 'flex',
width: '100%',
flexDirection: 'column',
gap: 2,
});
export const workspaceServer = style({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '0px 8px',
gap: 8,
marginBottom: 6,
});
export const workspaceServerIcon = style({
border: `1px solid ${cssVarV2.layer.insideBorder.border}`,
borderRadius: 4,
color: cssVarV2.icon.primary,
fontSize: 18,
width: 30,
height: 30,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 4,
paddingLeft: '12px',
marginBottom: '4px',
});
export const workspaceServerContent = style({
display: 'flex',
flexDirection: 'column',
});
const ellipsis = style({
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
color: cssVarV2('text/secondary'),
gap: 4,
width: '100%',
overflow: 'hidden',
});
export const workspaceServerAccount = style([
ellipsis,
{
fontSize: cssVar('fontXs'),
lineHeight: '20px',
color: cssVarV2.text.secondary,
marginTop: -1.5,
},
]);
export const workspaceServerName = style([
ellipsis,
{
fontSize: cssVar('fontXs'),
lineHeight: '20px',
fontWeight: 500,
color: cssVarV2.text.primary,
selectors: {
[`&:has(~ ${workspaceServerAccount})`]: {
marginBottom: -1.5,
},
},
},
]);
export const infoMoreIcon = style({
color: cssVarV2.icon.secondary,
export const workspaceServerName = style({
display: 'flex',
alignItems: 'center',
gap: 4,
fontWeight: 500,
fontSize: cssVar('fontXs'),
lineHeight: '20px',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
});
export const workspaceServerSpacer = style({
width: 0,
flexGrow: 1,
export const account = style({
fontSize: cssVar('fontXs'),
overflow: 'hidden',
width: '100%',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
});
export const workspaceTypeIcon = style({
color: cssVarV2('icon/primary'),
fontSize: '16px',
});
export const scrollbar = style({
width: '4px',
});
export const workspaceCard = style({
height: 36,
padding: '7px 12px',
});
export const workspaceCardInfoContainer = style({
gap: 12,
height: '44px',
padding: '0 12px',
});
export const serverDivider = style({
marginTop: 8,
marginBottom: 12,
export const ItemContainer = style({
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-start',
padding: '8px 14px',
gap: '14px',
cursor: 'pointer',
borderRadius: '8px',
transition: 'background-color 0.2s',
fontSize: '24px',
color: cssVarV2('icon/secondary'),
});
export const ItemText = style({
fontSize: cssVar('fontSm'),
lineHeight: '22px',
color: cssVarV2('text/secondary'),
fontWeight: 400,
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
});
@@ -1,9 +1,14 @@
import { IconButton, Menu, MenuItem } from '@affine/component';
import {
IconButton,
Menu,
MenuItem,
ScrollableContainer,
} from '@affine/component';
import { Divider } from '@affine/component/ui/divider';
import { useEnableCloud } from '@affine/core/components/hooks/affine/use-enable-cloud';
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
import { useNavigateHelper } from '@affine/core/components/hooks/use-navigate-helper';
import type { AuthAccountInfo, Server } from '@affine/core/modules/cloud';
import type { Server } from '@affine/core/modules/cloud';
import { AuthService, ServersService } from '@affine/core/modules/cloud';
import { GlobalDialogService } from '@affine/core/modules/dialogs';
import { GlobalContextService } from '@affine/core/modules/global-context';
@@ -12,15 +17,14 @@ import {
WorkspaceService,
WorkspacesService,
} from '@affine/core/modules/workspace';
import { ServerDeploymentType } from '@affine/graphql';
import { useI18n } from '@affine/i18n';
import {
AccountIcon,
CloudWorkspaceIcon,
DeleteIcon,
LocalWorkspaceIcon,
MoreHorizontalIcon,
SelfhostIcon,
SignOutIcon,
PlusIcon,
TeamWorkspaceIcon,
} from '@blocksuite/icons/rc';
import {
FrameworkScope,
@@ -31,7 +35,6 @@ import {
import { useCallback, useMemo } from 'react';
import { WorkspaceCard } from '../../workspace-card';
import { AddServer } from '../add-server';
import * as styles from './index.css';
interface WorkspaceModalProps {
@@ -43,93 +46,6 @@ interface WorkspaceModalProps {
onAddWorkspace: () => void;
}
const WorkspaceServerInfo = ({
server,
name,
account,
accountStatus,
onDeleteServer,
onSignOut,
onSignIn,
}: {
server: string;
name: string;
account?: AuthAccountInfo | null;
accountStatus?: 'authenticated' | 'unauthenticated';
onDeleteServer?: () => void;
onSignOut?: () => void;
onSignIn?: () => void;
}) => {
const t = useI18n();
const isCloud = server !== 'local';
const isAffineCloud = server === 'affine-cloud';
const Icon = isAffineCloud
? CloudWorkspaceIcon
: isCloud
? SelfhostIcon
: LocalWorkspaceIcon;
const menuItems = useMemo(
() =>
[
server !== 'affine-cloud' && server !== 'local' && (
<MenuItem
prefixIcon={<DeleteIcon />}
type="danger"
key="delete-server"
onClick={onDeleteServer}
>
{t['com.affine.server.delete']()}
</MenuItem>
),
accountStatus === 'authenticated' && (
<MenuItem
prefixIcon={<SignOutIcon />}
key="sign-out"
onClick={onSignOut}
type="danger"
>
{t['Sign out']()}
</MenuItem>
),
accountStatus === 'unauthenticated' && (
<MenuItem
prefixIcon={<AccountIcon />}
key="sign-in"
onClick={onSignIn}
>
{t['Sign in']()}
</MenuItem>
),
].filter(Boolean),
[accountStatus, onDeleteServer, onSignIn, onSignOut, server, t]
);
return (
<div className={styles.workspaceServer}>
<div className={styles.workspaceServerIcon}>
<Icon />
</div>
<div className={styles.workspaceServerContent}>
<div className={styles.workspaceServerName}>{name}</div>
{isCloud ? (
<div className={styles.workspaceServerAccount}>
{account ? account.email : 'Not signed in'}
</div>
) : null}
</div>
<div className={styles.workspaceServerSpacer} />
{menuItems.length ? (
<Menu items={menuItems}>
<IconButton
icon={<MoreHorizontalIcon className={styles.infoMoreIcon} />}
/>
</Menu>
) : null}
</div>
);
};
const CloudWorkSpaceList = ({
server,
workspaces,
@@ -141,6 +57,7 @@ const CloudWorkSpaceList = ({
onClickWorkspace: (workspaceMetadata: WorkspaceMetadata) => void;
onClickEnableCloud?: (meta: WorkspaceMetadata) => void;
}) => {
const t = useI18n();
const globalContextService = useService(GlobalContextService);
const globalDialogService = useService(GlobalDialogService);
const serverName = useLiveData(server.config$.selector(c => c.serverName));
@@ -154,6 +71,8 @@ const CloudWorkSpaceList = ({
globalContextService.globalContext.workspaceFlavour.$
);
const serverType = server.config$.value.type;
const handleDeleteServer = useCallback(() => {
serversService.removeServer(server.id);
@@ -181,23 +100,78 @@ const CloudWorkSpaceList = ({
});
}, [globalDialogService, server.baseUrl]);
const onNewWorkspace = useCallback(() => {
globalDialogService.open(
'create-workspace',
{
serverId: server.id,
forcedCloud: true,
},
payload => {
if (payload) {
navigateHelper.openPage(payload.metadata.id, 'all');
}
}
);
}, [globalDialogService, navigateHelper, server.id]);
return (
<>
<WorkspaceServerInfo
server={server.id}
name={serverName}
account={account}
accountStatus={accountStatus}
onDeleteServer={handleDeleteServer}
onSignOut={handleSignOut}
onSignIn={handleSignIn}
/>
<div className={styles.workspaceListWrapper}>
<div className={styles.workspaceServer}>
<div className={styles.workspaceServerContent}>
<div className={styles.workspaceServerName}>
{serverType === ServerDeploymentType.Affine ? (
<CloudWorkspaceIcon className={styles.workspaceTypeIcon} />
) : (
<TeamWorkspaceIcon className={styles.workspaceTypeIcon} />
)}
<div className={styles.account}>{serverName}</div>
</div>
<div className={styles.account}>
{account ? account.email : 'Not signed in'}
</div>
</div>
<Menu
items={[
server.id !== 'affine-cloud' && (
<MenuItem key="delete-server" onClick={handleDeleteServer}>
{t['com.affine.server.delete']()}
</MenuItem>
),
accountStatus === 'authenticated' && (
<MenuItem key="sign-out" onClick={handleSignOut}>
{t['Sign out']()}
</MenuItem>
),
accountStatus === 'unauthenticated' && (
<MenuItem key="sign-in" onClick={handleSignIn}>
{t['Sign in']()}
</MenuItem>
),
]}
>
<div>
<IconButton icon={<MoreHorizontalIcon />} />
</div>
</Menu>
</div>
<WorkspaceList
items={workspaces}
onClick={onClickWorkspace}
onEnableCloudClick={onClickEnableCloud}
/>
</>
<MenuItem
block={true}
prefixIcon={<PlusIcon />}
onClick={onNewWorkspace}
className={styles.ItemContainer}
>
<div className={styles.ItemText}>
{t['com.affine.workspaceList.addWorkspace.create']()}
</div>
</MenuItem>
</div>
);
};
@@ -212,18 +186,25 @@ const LocalWorkspaces = ({
return null;
}
return (
<>
<WorkspaceServerInfo
server="local"
name={t['com.affine.workspaceList.workspaceListType.local']()}
/>
<div className={styles.workspaceListWrapper}>
<div className={styles.workspaceServer}>
<div className={styles.workspaceServerName}>
<LocalWorkspaceIcon
width={14}
height={14}
className={styles.workspaceTypeIcon}
/>
{t['com.affine.workspaceList.workspaceListType.local']()}
</div>
</div>
<WorkspaceList
items={workspaces}
onClick={onClickWorkspace}
onSettingClick={onClickWorkspaceSetting}
onEnableCloudClick={onClickEnableCloud}
/>
</>
<Divider size="thinner" />
</div>
);
};
@@ -243,14 +224,6 @@ export const AFFiNEWorkspaceList = ({
const serversService = useService(ServersService);
const servers = useLiveData(serversService.servers$);
const affineCloudServer = useMemo(
() => servers.find(s => s.id === 'affine-cloud') as Server,
[servers]
);
const selfhostServers = useMemo(
() => servers.filter(s => s.id !== 'affine-cloud'),
[servers]
);
const cloudWorkspaces = useMemo(
() =>
@@ -289,25 +262,24 @@ export const AFFiNEWorkspaceList = ({
);
return (
<>
{/* 1. affine-cloud */}
<FrameworkScope
key={affineCloudServer.id}
scope={affineCloudServer.scope}
>
<CloudWorkSpaceList
server={affineCloudServer}
workspaces={cloudWorkspaces.filter(
({ flavour }) => flavour === affineCloudServer.id
)}
onClickWorkspace={handleClickWorkspace}
/>
</FrameworkScope>
{(localWorkspaces.length > 0 || selfhostServers.length > 0) && (
<Divider size="thinner" className={styles.serverDivider} />
)}
{/* 2. local */}
<ScrollableContainer
className={styles.workspaceListsWrapper}
scrollBarClassName={styles.scrollbar}
>
<div>
{servers.map(server => (
<FrameworkScope key={server.id} scope={server.scope}>
<CloudWorkSpaceList
server={server}
workspaces={cloudWorkspaces.filter(
({ flavour }) => flavour === server.id
)}
onClickWorkspace={handleClickWorkspace}
/>
<Divider size="thinner" />
</FrameworkScope>
))}
</div>
<LocalWorkspaces
workspaces={localWorkspaces}
onClickWorkspace={handleClickWorkspace}
@@ -315,28 +287,7 @@ export const AFFiNEWorkspaceList = ({
showEnableCloudButton ? onClickEnableCloud : undefined
}
/>
{selfhostServers.length > 0 && (
<Divider size="thinner" className={styles.serverDivider} />
)}
{/* 3. selfhost */}
{selfhostServers.map((server, index) => (
<FrameworkScope key={server.id} scope={server.scope}>
<CloudWorkSpaceList
server={server}
workspaces={cloudWorkspaces.filter(
({ flavour }) => flavour === server.id
)}
onClickWorkspace={handleClickWorkspace}
/>
{index !== selfhostServers.length - 1 && (
<Divider size="thinner" className={styles.serverDivider} />
)}
</FrameworkScope>
))}
<AddServer />
<Divider size="thinner" />
</>
</ScrollableContainer>
);
};
@@ -366,10 +317,9 @@ const SortableWorkspaceItem = ({
return (
<WorkspaceCard
className={styles.workspaceCard}
infoClassName={styles.workspaceCardInfoContainer}
workspaceMetadata={workspaceMetadata}
onClick={handleClick}
avatarSize={22}
avatarSize={28}
active={currentWorkspace?.id === workspaceMetadata.id}
onClickOpenSettings={onSettingClick}
onClickEnableCloud={onEnableCloudClick}
@@ -249,7 +249,6 @@ export const WorkspaceCard = forwardRef<
hideCollaborationIcon?: boolean;
hideTeamWorkspaceIcon?: boolean;
active?: boolean;
infoClassName?: string;
onClickOpenSettings?: (workspaceMetadata: WorkspaceMetadata) => void;
onClickEnableCloud?: (workspaceMetadata: WorkspaceMetadata) => void;
}
@@ -263,7 +262,6 @@ export const WorkspaceCard = forwardRef<
onClickOpenSettings,
onClickEnableCloud,
className,
infoClassName,
disable,
hideCollaborationIcon,
hideTeamWorkspaceIcon,
@@ -298,7 +296,7 @@ export const WorkspaceCard = forwardRef<
ref={ref}
{...props}
>
<div className={clsx(styles.infoContainer, infoClassName)}>
<div className={styles.infoContainer}>
{information ? (
<WorkspaceAvatar
meta={workspaceMetadata}
@@ -334,35 +332,34 @@ export const WorkspaceCard = forwardRef<
Enable Cloud
</Button>
) : null}
{hideCollaborationIcon || information?.isOwner ? null : (
<Tooltip
content={t['com.affine.settings.workspace.state.joined']()}
>
<CollaborationIcon className={styles.collaborationIcon} />
</Tooltip>
)}
{hideTeamWorkspaceIcon || !information?.isTeam ? null : (
<Tooltip
content={t['com.affine.settings.workspace.state.team']()}
>
<TeamWorkspaceIcon className={styles.collaborationIcon} />
</Tooltip>
)}
{onClickOpenSettings && (
<div className={styles.settingButton} onClick={onOpenSettings}>
<SettingsIcon width={16} height={16} />
</div>
)}
</div>
</div>
<div className={styles.suffixIcons}>
{hideCollaborationIcon || information?.isOwner ? null : (
<Tooltip
content={t['com.affine.settings.workspace.state.joined']()}
>
<CollaborationIcon className={styles.collaborationIcon} />
</Tooltip>
)}
{hideTeamWorkspaceIcon || !information?.isTeam ? null : (
<Tooltip content={t['com.affine.settings.workspace.state.team']()}>
<TeamWorkspaceIcon className={styles.collaborationIcon} />
</Tooltip>
)}
{active && (
<div className={styles.activeContainer}>
<DoneIcon className={styles.activeIcon} />
</div>
)}
{showArrowDownIcon && <ArrowDownSmallIcon />}
</div>
{active && (
<div className={styles.activeContainer}>
<DoneIcon className={styles.activeIcon} />
</div>
)}
</div>
);
}
@@ -19,7 +19,6 @@ export const container = style({
width: '100%',
maxWidth: 500,
color: cssVarV2('text/primary'),
overflow: 'hidden',
':hover': {
cursor: 'pointer',
background: cssVar('hoverColor'),
@@ -35,7 +34,6 @@ export const infoContainer = style({
});
export const activeContainer = style({
flexShrink: 0,
lineHeight: 0,
});
export const disable = style({
@@ -189,9 +187,6 @@ export const showOnCardHover = style({
[`.${container}:hover &`]: {
position: 'relative',
},
'&:empty': {
display: 'none',
},
},
});
@@ -199,14 +194,3 @@ export const activeIcon = style({
fontSize: 14,
color: cssVarV2('icon/activated'),
});
export const suffixIcons = style({
display: 'flex',
gap: 8,
alignItems: 'center',
selectors: {
'&:empty': {
display: 'none',
},
},
});
@@ -0,0 +1,77 @@
import { cssVar } from '@toeverything/theme';
import { style } from '@vanilla-extract/css';
export const header = style({
position: 'relative',
marginTop: '44px',
});
export const subTitle = style({
fontSize: cssVar('fontSm'),
color: cssVar('textPrimaryColor'),
fontWeight: 600,
});
export const avatarWrapper = style({
display: 'flex',
margin: '10px 0',
});
export const workspaceNameWrapper = style({
display: 'flex',
flexDirection: 'column',
gap: '8px',
padding: '12px 0',
});
export const affineCloudWrapper = style({
display: 'flex',
flexDirection: 'column',
gap: '6px',
paddingTop: '10px',
});
export const card = style({
padding: '12px',
display: 'flex',
alignItems: 'center',
borderRadius: '8px',
backgroundColor: cssVar('backgroundSecondaryColor'),
minHeight: '114px',
position: 'relative',
});
export const cardText = style({
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
width: '100%',
gap: '12px',
});
export const cardTitle = style({
fontSize: cssVar('fontBase'),
color: cssVar('textPrimaryColor'),
display: 'flex',
justifyContent: 'space-between',
});
export const cardDescription = style({
fontSize: cssVar('fontXs'),
color: cssVar('textSecondaryColor'),
maxWidth: '288px',
});
export const cloudTips = style({
fontSize: cssVar('fontXs'),
color: cssVar('textSecondaryColor'),
});
export const cloudSvgContainer = style({
width: '146px',
display: 'flex',
justifyContent: 'flex-end',
alignItems: 'center',
position: 'absolute',
bottom: '0',
right: '0',
pointerEvents: 'none',
});
@@ -1,44 +0,0 @@
import { cssVar } from '@toeverything/theme';
import { cssVarV2 } from '@toeverything/theme/v2';
import { style } from '@vanilla-extract/css';
export const content = style({
// to avoid content clipped
width: `calc(100% + 20px)`,
padding: '10px 10px 20px 10px',
marginLeft: '-10px',
});
export const section = style({
display: 'flex',
flexDirection: 'column',
gap: 8,
padding: '12px 0px',
});
export const label = style({
fontSize: cssVar('fontSm'),
fontWeight: 600,
lineHeight: '22px',
color: cssVarV2.text.primary,
});
const baseFormInput = style({
fontSize: 15,
fontWeight: 500,
lineHeight: '24px',
color: cssVarV2.text.primary,
border: `1px solid ${cssVarV2.layer.insideBorder.blackBorder}`,
});
export const input = style([
baseFormInput,
{
borderRadius: 4,
padding: '8px 10px',
},
]);
export const select = style([
baseFormInput,
{
borderRadius: 8,
padding: '10px',
},
]);
@@ -1,184 +1,226 @@
import { Button, ConfirmModal, notify, RowInput } from '@affine/component';
import { Avatar, ConfirmModal, Input, notify, Switch } from '@affine/component';
import type { ConfirmModalProps } from '@affine/component/ui/modal';
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
import {
AuthService,
type Server,
ServersService,
} from '@affine/core/modules/cloud';
import { AuthService, ServersService } from '@affine/core/modules/cloud';
import {
type DialogComponentProps,
type GLOBAL_DIALOG_SCHEMA,
GlobalDialogService,
} from '@affine/core/modules/dialogs';
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
import { CloudSvg } from '@affine/core/modules/share-menu';
import { WorkspacesService } from '@affine/core/modules/workspace';
import { buildShowcaseWorkspace } from '@affine/core/utils/first-app-data';
import { useI18n } from '@affine/i18n';
import track from '@affine/track';
import { track } from '@affine/track';
import { FrameworkScope, useLiveData, useService } from '@toeverything/infra';
import { useCallback, useState } from 'react';
import * as styles from './index.css';
import { ServerSelector } from './server-selector';
import { buildShowcaseWorkspace } from '../../../utils/first-app-data';
import * as styles from './dialog.css';
const FormSection = ({
label,
input,
}: {
label: string;
input: React.ReactNode;
}) => {
interface NameWorkspaceContentProps extends ConfirmModalProps {
loading: boolean;
forcedCloud?: boolean;
serverId?: string;
onConfirmName: (
name: string,
workspaceFlavour: string,
avatar?: File
) => void;
}
const NameWorkspaceContent = ({
loading,
onConfirmName,
forcedCloud,
serverId,
...props
}: NameWorkspaceContentProps) => {
const t = useI18n();
const [workspaceName, setWorkspaceName] = useState('');
const [enable, setEnable] = useState(!!forcedCloud);
const session = useService(AuthService).session;
const loginStatus = useLiveData(session.status$);
const globalDialogService = useService(GlobalDialogService);
const openSignInModal = useCallback(() => {
globalDialogService.open('sign-in', {});
}, [globalDialogService]);
const onSwitchChange = useCallback(
(checked: boolean) => {
if (loginStatus !== 'authenticated') {
return openSignInModal();
}
return setEnable(checked);
},
[loginStatus, openSignInModal]
);
const handleCreateWorkspace = useCallback(() => {
if (loginStatus !== 'authenticated' && enable) {
return openSignInModal();
}
onConfirmName(workspaceName, enable ? serverId || 'affine-cloud' : 'local');
}, [
enable,
loginStatus,
onConfirmName,
openSignInModal,
serverId,
workspaceName,
]);
const onEnter = useCallback(() => {
if (workspaceName) {
handleCreateWorkspace();
}
}, [handleCreateWorkspace, workspaceName]);
// Currently, when we create a new workspace and upload an avatar at the same time,
// an error occurs after the creation is successful: get blob 404 not found
return (
<section className={styles.section}>
<label className={styles.label}>{label}</label>
{input}
</section>
<ConfirmModal
defaultOpen={true}
title={t['com.affine.nameWorkspace.title']()}
description={t['com.affine.nameWorkspace.description']()}
cancelText={t['com.affine.nameWorkspace.button.cancel']()}
confirmText={t['com.affine.nameWorkspace.button.create']()}
confirmButtonOptions={{
variant: 'primary',
loading,
disabled: !workspaceName,
'data-testid': 'create-workspace-create-button',
}}
closeButtonOptions={{
['data-testid' as string]: 'create-workspace-close-button',
}}
onConfirm={handleCreateWorkspace}
{...props}
>
<div className={styles.avatarWrapper}>
<Avatar size={56} name={workspaceName} colorfulFallback />
</div>
<div className={styles.workspaceNameWrapper}>
<div className={styles.subTitle}>
{t['com.affine.nameWorkspace.subtitle.workspace-name']()}
</div>
<Input
autoFocus
data-testid="create-workspace-input"
onEnter={onEnter}
placeholder={t['com.affine.nameWorkspace.placeholder']()}
maxLength={64}
minLength={0}
onChange={setWorkspaceName}
size="large"
/>
</div>
{!serverId || serverId === 'affine-cloud' ? (
<div className={styles.affineCloudWrapper}>
<div className={styles.subTitle}>{t['AFFiNE Cloud']()}</div>
<div className={styles.card}>
<div className={styles.cardText}>
<div className={styles.cardTitle}>
<span>
{t['com.affine.nameWorkspace.affine-cloud.title']()}
</span>
<Switch
checked={enable}
onChange={onSwitchChange}
disabled={forcedCloud}
/>
</div>
<div className={styles.cardDescription}>
{t['com.affine.nameWorkspace.affine-cloud.description']()}
</div>
</div>
<div className={styles.cloudSvgContainer}>
<CloudSvg />
</div>
</div>
{forcedCloud && BUILD_CONFIG.isWeb ? (
<a
className={styles.cloudTips}
href={BUILD_CONFIG.downloadUrl}
target="_blank"
rel="noreferrer"
>
{t['com.affine.nameWorkspace.affine-cloud.web-tips']()}
</a>
) : null}
</div>
) : null}
</ConfirmModal>
);
};
export const CreateWorkspaceDialog = ({
forcedCloud,
serverId,
close,
...props
}: DialogComponentProps<GLOBAL_DIALOG_SCHEMA['create-workspace']>) => {
const t = useI18n();
const [workspaceName, setWorkspaceName] = useState('');
const [inputServerId, setInputServerId] = useState(
serverId ?? 'affine-cloud'
);
const workspacesService = useService(WorkspacesService);
const serversService = useService(ServersService);
const featureFlagService = useService(FeatureFlagService);
const enableLocalWorkspace = useLiveData(
featureFlagService.flags.enable_local_workspace.$
);
const server = useLiveData(
inputServerId ? serversService.server$(inputServerId) : null
serverId ? serversService.server$(serverId) : null
);
const [loading, setLoading] = useState(false);
const onConfirmName = useAsyncCallback(
async (name: string, workspaceFlavour: string) => {
track.$.$.$.createWorkspace({ flavour: workspaceFlavour });
if (loading) return;
setLoading(true);
// this will be the last step for web for now
// fix me later
try {
const { meta, defaultDocId } = await buildShowcaseWorkspace(
workspacesService,
workspaceFlavour,
name
);
close({ metadata: meta, defaultDocId });
} catch (e) {
console.error(e);
notify.error({
title: 'Failed to create workspace',
message: 'please try again later.',
});
} finally {
setLoading(false);
}
},
[loading, workspacesService, close]
);
const onOpenChange = useCallback(
(open: boolean) => {
if (!open) close();
if (!open) {
close();
}
},
[close]
);
return (
<ConfirmModal
open
onOpenChange={onOpenChange}
title={t['com.affine.nameWorkspace.title']()}
description={t['com.affine.nameWorkspace.description']()}
cancelText={t['com.affine.nameWorkspace.button.cancel']()}
closeButtonOptions={{
['data-testid' as string]: 'create-workspace-close-button',
}}
contentOptions={{}}
childrenContentClassName={styles.content}
customConfirmButton={() => {
return (
<FrameworkScope scope={server?.scope}>
<CustomConfirmButton
workspaceName={workspaceName}
server={server}
onCreated={res =>
close({ metadata: res.meta, defaultDocId: res.defaultDocId })
}
/>
</FrameworkScope>
);
}}
{...props}
>
<FormSection
label={t['com.affine.nameWorkspace.subtitle.workspace-name']()}
input={
<RowInput
autoFocus
className={styles.input}
data-testid="create-workspace-input"
placeholder={t['com.affine.nameWorkspace.placeholder']()}
maxLength={64}
minLength={0}
onChange={setWorkspaceName}
/>
}
<FrameworkScope scope={server?.scope}>
<NameWorkspaceContent
loading={loading}
serverId={serverId}
open
forcedCloud={forcedCloud || !enableLocalWorkspace}
onOpenChange={onOpenChange}
onConfirmName={onConfirmName}
/>
<FormSection
label={t['com.affine.nameWorkspace.subtitle.workspace-type']()}
input={
<ServerSelector
className={styles.select}
selectedId={inputServerId}
onChange={setInputServerId}
/>
}
/>
</ConfirmModal>
);
};
const CustomConfirmButton = ({
workspaceName,
server,
onCreated,
}: {
workspaceName: string;
server?: Server | null;
onCreated: (res: Awaited<ReturnType<typeof buildShowcaseWorkspace>>) => void;
}) => {
const t = useI18n();
const [loading, setLoading] = useState(false);
const session = useService(AuthService).session;
const loginStatus = useLiveData(session.status$);
const globalDialogService = useService(GlobalDialogService);
const workspacesService = useService(WorkspacesService);
const openSignInModal = useCallback(() => {
globalDialogService.open('sign-in', { server: server?.baseUrl });
}, [globalDialogService, server?.baseUrl]);
const handleConfirm = useAsyncCallback(async () => {
if (loading) return;
setLoading(true);
track.$.$.$.createWorkspace({
flavour: !server ? 'local' : 'affine-cloud',
});
// this will be the last step for web for now
// fix me later
try {
const res = await buildShowcaseWorkspace(
workspacesService,
server?.id ?? 'local',
workspaceName
);
onCreated(res);
} catch (e) {
console.error(e);
notify.error({
title: 'Failed to create workspace',
message: 'please try again later.',
});
} finally {
setLoading(false);
}
}, [loading, onCreated, server, workspaceName, workspacesService]);
const handleCheckSessionAndConfirm = useCallback(() => {
if (server && loginStatus !== 'authenticated') {
return openSignInModal();
}
handleConfirm();
}, [handleConfirm, loginStatus, openSignInModal, server]);
return (
<Button
disabled={!workspaceName}
data-testid="create-workspace-create-button"
variant="primary"
onClick={handleCheckSessionAndConfirm}
loading={loading}
>
{t['com.affine.nameWorkspace.button.create']()}
</Button>
</FrameworkScope>
);
};
@@ -1,36 +0,0 @@
import { cssVarV2 } from '@toeverything/theme/v2';
import { style } from '@vanilla-extract/css';
export const trigger = style({
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
});
export const arrow = style({
transition: 'transform 0.2s ease',
transform: 'rotate(0deg)',
fontSize: 16,
color: cssVarV2.icon.primary,
selectors: {
'&.open': {
transform: 'rotate(180deg)',
},
},
});
export const list = style({
display: 'flex',
flexDirection: 'column',
gap: 4,
});
export const item = style({
padding: 4,
gap: 8,
});
export const done = style({
color: cssVarV2.icon.primary,
fontSize: 20,
});
@@ -1,147 +0,0 @@
import { Menu, MenuItem } from '@affine/component';
import { type Server, ServersService } from '@affine/core/modules/cloud';
import { useI18n } from '@affine/i18n';
import {
ArrowDownSmallIcon,
CloudWorkspaceIcon,
DoneIcon,
LocalWorkspaceIcon,
SelfhostIcon,
} from '@blocksuite/icons/rc';
import { useLiveData, useService } from '@toeverything/infra';
import clsx from 'clsx';
import {
type HTMLAttributes,
type ReactNode,
useCallback,
useMemo,
useState,
} from 'react';
import * as styles from './server-selector.css';
export interface ServerSelectorProps
extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> {
selectedId: Server['id'];
onChange: (id: Server['id']) => void;
placeholder?: ReactNode;
}
export const ServerSelector = ({
selectedId,
onChange,
placeholder,
className,
...props
}: ServerSelectorProps) => {
const t = useI18n();
const [open, setOpen] = useState(false);
const serversService = useService(ServersService);
const servers = useLiveData(serversService.servers$);
const selectedServer = useMemo(() => {
return servers.find(s => s.id === selectedId);
}, [selectedId, servers]);
const serverName = useLiveData(
selectedServer?.config$.selector(c => c.serverName)
);
const selectedServerName =
selectedId === 'local'
? t['com.affine.workspaceList.workspaceListType.local']()
: serverName;
return (
<Menu
rootOptions={{
open,
onOpenChange: setOpen,
}}
contentOptions={{
style: {
maxWidth: 432,
width: 'calc(100dvw - 68px)',
},
}}
items={
<ul className={styles.list} data-testid="server-selector-list">
<LocalSelectorItem
onSelect={onChange}
active={selectedId === 'local'}
/>
{servers.map(server => (
<ServerSelectorItem
key={server.id}
server={server}
onSelect={onChange}
active={selectedId === server.id}
/>
))}
</ul>
}
>
<div
data-testid="server-selector-trigger"
className={clsx(styles.trigger, className)}
{...props}
>
{selectedServerName ?? placeholder}
<ArrowDownSmallIcon className={clsx(styles.arrow, { open })} />
</div>
</Menu>
);
};
const LocalSelectorItem = ({
onSelect,
active,
}: {
onSelect?: (id: string) => void;
active?: boolean;
}) => {
const t = useI18n();
const handleSelect = useCallback(() => {
onSelect?.('local');
}, [onSelect]);
return (
<MenuItem
data-testid="local"
className={styles.item}
prefixIcon={<LocalWorkspaceIcon />}
onClick={handleSelect}
suffixIcon={active ? <DoneIcon className={styles.done} /> : null}
>
{t['com.affine.workspaceList.workspaceListType.local']()}
</MenuItem>
);
};
const ServerSelectorItem = ({
server,
onSelect,
active,
}: {
server: Server;
onSelect?: (id: string) => void;
active?: boolean;
}) => {
const name = useLiveData(server.config$.selector(c => c.serverName));
const Icon = server.id === 'affine-cloud' ? CloudWorkspaceIcon : SelfhostIcon;
const handleSelect = useCallback(() => {
onSelect?.(server.id);
}, [onSelect, server.id]);
return (
<MenuItem
data-testid={server.id}
className={styles.item}
prefixIcon={<Icon />}
onClick={handleSelect}
suffixIcon={active ? <DoneIcon className={styles.done} /> : null}
>
{name}
</MenuItem>
);
};
@@ -186,14 +186,6 @@ const Dialog = ({
className={styles.workspaceSelector}
showArrowDownIcon
disable={disabled}
menuContentOptions={{
side: 'top',
style: {
maxHeight: 'min(600px, calc(50vh + 50px))',
width: 352,
maxWidth: 'calc(100vw - 20px)',
},
}}
/>
</>
)}
@@ -214,8 +214,8 @@ export const BackupSettingPanel = () => {
/>
);
}
if (!backupWorkspaces) {
return null;
if (backupWorkspaces?.items.length === 0 || !backupWorkspaces) {
return <Empty />;
}
return (
<>
@@ -242,9 +242,6 @@ export const BackupSettingPanel = () => {
);
}, [isLoading, backupWorkspaces, pageNum]);
const isEmpty =
(backupWorkspaces?.items.length === 0 || !backupWorkspaces) && !isLoading;
return (
<>
<SettingHeader
@@ -252,11 +249,7 @@ export const BackupSettingPanel = () => {
subtitle={t['com.affine.settings.workspace.backup.subtitle']()}
data-testid="backup-title"
/>
{isEmpty ? (
<Empty />
) : (
<div className={styles.listContainer}>{innerElement}</div>
)}
<div className={styles.listContainer}>{innerElement}</div>
</>
);
};
@@ -62,12 +62,7 @@ export const listItemRightLabel = style({
});
export const empty = style({
paddingTop: '64px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: cssVarV2('text/secondary'),
fontSize: cssVar('fontSm'),
padding: '8px 16px',
});
export const pagination = style({
@@ -92,7 +92,7 @@ export const EdgelessSnapshot = (props: Props) => {
const editorHost = new BlockStdScope({
store: doc,
extensions: [
...SpecProvider.getInstance().getSpec('preview:edgeless').value,
...SpecProvider.getInstance().getSpec('edgeless:preview').value,
getThemeExtension(framework),
],
}).render();
@@ -261,8 +261,6 @@ export const BlobManagementPanel = () => {
return;
}, [unusedBlobs]);
const isEmpty = (unusedBlobs.length === 0 || !unusedBlobs) && !isLoading;
return (
<>
{selectedBlobs.length > 0 ? (
@@ -290,44 +288,42 @@ export const BlobManagementPanel = () => {
{`${t['com.affine.settings.workspace.storage.unused-blobs']()} (${unusedBlobs.length})`}
</div>
)}
{isEmpty ? (
<Empty />
) : (
<div className={styles.blobManagementContainer}>
{isLoading ? (
<div className={styles.loadingContainer}>
<Loading size={32} />
<div className={styles.blobManagementContainer}>
{isLoading ? (
<div className={styles.loadingContainer}>
<Loading size={32} />
</div>
) : unusedBlobs.length === 0 ? (
<Empty />
) : (
<>
<div className={styles.blobPreviewGrid} ref={blobPreviewGridRef}>
{unusedBlobsPage.map(blob => {
const selected = selectedBlobs.includes(blob);
return (
<BlobCard
key={blob.key}
blobRecord={blob}
onClick={e => handleBlobClick(blob, e)}
selected={selected}
/>
);
})}
</div>
) : (
<>
<div className={styles.blobPreviewGrid} ref={blobPreviewGridRef}>
{unusedBlobsPage.map(blob => {
const selected = selectedBlobs.includes(blob);
return (
<BlobCard
key={blob.key}
blobRecord={blob}
onClick={e => handleBlobClick(blob, e)}
selected={selected}
/>
);
})}
</div>
{unusedBlobs.length > PAGE_SIZE && (
<Pagination
pageNum={pageNum}
totalCount={unusedBlobs.length}
countPerPage={PAGE_SIZE}
onPageChange={(_, pageNum) => {
setPageNum(pageNum);
setSkip(pageNum * PAGE_SIZE);
}}
/>
)}
</>
)}
</div>
)}
{unusedBlobs.length > PAGE_SIZE && (
<Pagination
pageNum={pageNum}
totalCount={unusedBlobs.length}
countPerPage={PAGE_SIZE}
onPageChange={(_, pageNum) => {
setPageNum(pageNum);
setSkip(pageNum * PAGE_SIZE);
}}
/>
)}
</>
)}
</div>
</>
);
};
@@ -88,12 +88,7 @@ export const loadingContainer = style({
});
export const empty = style({
paddingTop: '64px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: cssVarV2('text/secondary'),
fontSize: cssVar('fontSm'),
padding: '8px 16px',
});
export const blobPreviewContainer = style({
@@ -167,7 +167,10 @@ const DetailPageImpl = memo(function DetailPageImpl() {
const onLoad = useCallback(
(editorContainer: AffineEditorContainer) => {
const std = editorContainer.std;
// blocksuite editor host
const editorHost = editorContainer.host;
const std = editorHost?.std;
const disposable = new DisposableGroup();
if (std) {
const refNodeSlots = std.getOptional(RefNodeSlotsProvider);
@@ -176,7 +179,7 @@ const DetailPageImpl = memo(function DetailPageImpl() {
// the event should not be emitted by AffineReference
refNodeSlots.docLinkClicked.on(
({ pageId, params, openMode, event, host }) => {
if (host !== editorContainer.host) {
if (host !== editorHost) {
return;
}
openMode ??=
@@ -139,6 +139,9 @@ const DetailPageImpl = () => {
const onLoad = useCallback(
(editorContainer: AffineEditorContainer) => {
// blocksuite editor host
const editorHost = editorContainer.host;
// provide image proxy endpoint to blocksuite
const imageProxyUrl = new URL(
BUILD_CONFIG.imageProxyUrl,
@@ -150,19 +153,14 @@ const DetailPageImpl = () => {
server.baseUrl
).toString();
editorContainer.std.clipboard.use(
customImageProxyMiddleware(imageProxyUrl)
);
editorContainer.doc
.get(ImageProxyService)
.setImageProxyURL(imageProxyUrl);
editorHost?.std.clipboard.use(customImageProxyMiddleware(imageProxyUrl));
editorHost?.doc.get(ImageProxyService).setImageProxyURL(imageProxyUrl);
// provide link preview endpoint to blocksuite
editorContainer.doc.get(LinkPreviewerService).setEndpoint(linkPreviewUrl);
editorHost?.doc.get(LinkPreviewerService).setEndpoint(linkPreviewUrl);
// provide page mode and updated date to blocksuite
const refNodeService =
editorContainer.std.getOptional(RefNodeSlotsProvider);
const refNodeService = editorHost?.std.getOptional(RefNodeSlotsProvider);
const disposable = new DisposableGroup();
if (refNodeService) {
disposable.add(
@@ -1,7 +1,7 @@
import { style } from '@vanilla-extract/css';
export const fallback = style({
padding: '4px 16px',
padding: '4px 20px',
height: '100%',
overflow: 'clip',
});
@@ -121,45 +121,20 @@ export class UnusedBlobs extends Entity {
}
private async getUsedBlobs(): Promise<string[]> {
const batchSize = 100;
let offset = 0;
const unusedBlobKeys: string[] = [];
while (true) {
const result = await this.docsSearchService.indexer.blockIndex.aggregate(
{
type: 'boolean',
occur: 'must',
queries: [
{
type: 'exists',
field: 'blob',
},
],
},
'blob',
{
pagination: {
limit: batchSize,
skip: offset,
const result = await this.docsSearchService.indexer.blockIndex.aggregate(
{
type: 'boolean',
occur: 'must',
queries: [
{
type: 'exists',
field: 'blob',
},
}
);
if (!result.buckets.length) {
break;
}
unusedBlobKeys.push(...result.buckets.map(bucket => bucket.key));
offset += batchSize;
// If we got less results than the batch size, we've reached the end
if (result.buckets.length < batchSize) {
break;
}
}
return unusedBlobKeys;
],
},
'blob'
);
return result.buckets.map(bucket => bucket.key);
}
async hydrateBlob(
@@ -15,7 +15,7 @@ export type SettingTab =
| `workspace:${'preference' | 'properties' | 'members' | 'storage' | 'billing' | 'license'}`;
export type GLOBAL_DIALOG_SCHEMA = {
'create-workspace': (props: { serverId?: string }) => {
'create-workspace': (props: { serverId?: string; forcedCloud?: boolean }) => {
metadata: WorkspaceMetadata;
defaultDocId?: string;
};

Some files were not shown because too many files have changed in this diff Show More