mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-04 19:15:33 +08:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b15a2a9638 |
@@ -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)
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+2
-2
@@ -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
-2
@@ -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 {
|
||||
|
||||
+21
-18
@@ -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';
|
||||
+21
-19
@@ -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);
|
||||
|
||||
+106
-1
@@ -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,
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
};
|
||||
+12
-11
@@ -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
-4
@@ -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();
|
||||
@@ -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
|
||||
>;
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
+1
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
+32
-24
@@ -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;
|
||||
|
||||
+2
-2
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
+3
-6
@@ -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,
|
||||
},
|
||||
}}
|
||||
|
||||
+21
-3
@@ -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',
|
||||
});
|
||||
|
||||
+7
-23
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+4
-11
@@ -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',
|
||||
|
||||
+2
-4
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+2
-18
@@ -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%',
|
||||
|
||||
+22
-22
@@ -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} />;
|
||||
};
|
||||
|
||||
+63
-58
@@ -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',
|
||||
});
|
||||
|
||||
+112
-162
@@ -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)',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
+3
-10
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+1
-6
@@ -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({
|
||||
|
||||
+1
-1
@@ -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();
|
||||
|
||||
+35
-39
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+1
-6
@@ -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
Reference in New Issue
Block a user