@formicoidea/labre-core 0.26.1 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_pkgs/affine-block-database/data-source.d.ts +28 -40
- package/dist/_pkgs/affine-block-database/data-source.js +116 -178
- package/dist/_pkgs/affine-block-database/database-block.d.ts +10 -3
- package/dist/_pkgs/affine-block-database/database-block.js +110 -70
- package/dist/_pkgs/affine-block-database/external-data-source.d.ts +69 -0
- package/dist/_pkgs/affine-block-database/external-data-source.js +144 -0
- package/dist/_pkgs/affine-block-database/index.d.ts +1 -0
- package/dist/_pkgs/affine-block-database/index.js +1 -0
- package/dist/_pkgs/affine-block-root/edgeless/configs/toolbar/misc.d.ts +18 -0
- package/dist/_pkgs/affine-block-root/edgeless/configs/toolbar/more.d.ts +18 -0
- package/dist/_pkgs/affine-block-root/edgeless/configs/toolbar/more.js +104 -2
- package/dist/_pkgs/affine-widget-edgeless-selected-rect/edgeless-element-link.d.ts +29 -0
- package/dist/_pkgs/affine-widget-edgeless-selected-rect/edgeless-element-link.js +233 -0
- package/dist/_pkgs/affine-widget-edgeless-selected-rect/effects.js +2 -0
- package/dist/_pkgs/affine-widget-edgeless-selected-rect/spec.d.ts +1 -0
- package/dist/_pkgs/affine-widget-edgeless-selected-rect/spec.js +2 -0
- package/dist/_pkgs/affine-widget-edgeless-selected-rect/view.js +2 -1
- package/dist/_pkgs/data-view/core/data-source/base.d.ts +6 -0
- package/dist/_pkgs/data-view/core/data-source/base.js +6 -0
- package/dist/_pkgs/std/gfx/model/surface/element-model.d.ts +7 -0
- package/dist/_pkgs/std/gfx/model/surface/element-model.js +24 -1
- package/dist/_pkgs/std/inline/components/v-element.js +5 -2
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ import { toast } from '../affine-components/toast/index.js';
|
|
|
6
6
|
import { EDGELESS_TOP_CONTENTEDITABLE_SELECTOR } from '../affine-shared/consts/index.js';
|
|
7
7
|
import { BlockCommentManager, CommentProviderIdentifier, DocModeProvider, NotificationProvider, TelemetryProvider, } from '../affine-shared/services/index.js';
|
|
8
8
|
import { getDropResult } from '../affine-widget-drag-handle/index.js';
|
|
9
|
-
import { createRecordDetail, createUniComponentFromWebComponent, DataViewRootUILogic, defineUniComponent, ExternalGroupByConfigProvider,
|
|
9
|
+
import { createRecordDetail, createUniComponentFromWebComponent, DataViewRootUILogic, defineUniComponent, ExternalGroupByConfigProvider, renderUniLit, uniMap, } from '../data-view/index.js';
|
|
10
10
|
import { widgetPresets } from '../data-view/widget-presets/index.js';
|
|
11
11
|
import { Rect } from '../global/gfx/index.js';
|
|
12
12
|
import { CommentIcon, CopyIcon, DeleteIcon, MoreHorizontalIcon, } from '@blocksuite/icons/lit';
|
|
@@ -16,6 +16,7 @@ import { Slice } from '../store/index.js';
|
|
|
16
16
|
import { autoUpdate } from '@floating-ui/dom';
|
|
17
17
|
import { computed, signal } from '@preact/signals-core';
|
|
18
18
|
import { html, nothing } from 'lit';
|
|
19
|
+
import { keyed } from 'lit/directives/keyed.js';
|
|
19
20
|
import { popSideDetail } from './components/layout.js';
|
|
20
21
|
import { DatabaseConfigExtension } from './config.js';
|
|
21
22
|
import { EditorHostKey } from './context/host-context.js';
|
|
@@ -88,25 +89,10 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent {
|
|
|
88
89
|
options,
|
|
89
90
|
});
|
|
90
91
|
};
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
.getAll(ExternalGroupByConfigProvider)
|
|
96
|
-
.forEach(config => {
|
|
97
|
-
dataSource.serviceSet(ExternalGroupByConfigProvider(config.name), config);
|
|
98
|
-
});
|
|
99
|
-
};
|
|
100
|
-
const provider = this.std.getOptional(DatabaseDataSourceProvider);
|
|
101
|
-
const dataSource = provider && this.model.props.externalSourceId
|
|
102
|
-
? provider.createDataSource(this.model, initFn)
|
|
103
|
-
: new DatabaseBlockDataSource(this.model, initFn);
|
|
104
|
-
const id = currentViewStorage.getCurrentView(this.model.id);
|
|
105
|
-
if (id && dataSource.viewManager.viewGet(id)) {
|
|
106
|
-
dataSource.viewManager.setCurrentView(id);
|
|
107
|
-
}
|
|
108
|
-
return dataSource;
|
|
109
|
-
});
|
|
92
|
+
// Bumps on every rebuild so `renderBlock` can `keyed()` the renderer element,
|
|
93
|
+
// forcing Lit to tear the old one down (disposing its listeners) and mount a
|
|
94
|
+
// fresh one bound to the new source.
|
|
95
|
+
this._sourceVersion = 0;
|
|
110
96
|
this.renderTitle = (dataViewLogic) => {
|
|
111
97
|
return html ` <affine-database-title
|
|
112
98
|
class="${databaseTitleStyles}"
|
|
@@ -219,7 +205,107 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent {
|
|
|
219
205
|
return databaseSelection?.viewSelection;
|
|
220
206
|
});
|
|
221
207
|
this.virtualPadding$ = signal(0);
|
|
222
|
-
this
|
|
208
|
+
this.#useZeroWidth_accessor_storage = true;
|
|
209
|
+
}
|
|
210
|
+
_buildDataSource() {
|
|
211
|
+
const initFn = (dataSource) => {
|
|
212
|
+
dataSource.serviceSet(EditorHostKey, this.host);
|
|
213
|
+
this.std.provider
|
|
214
|
+
.getAll(ExternalGroupByConfigProvider)
|
|
215
|
+
.forEach(config => {
|
|
216
|
+
dataSource.serviceSet(ExternalGroupByConfigProvider(config.name), config);
|
|
217
|
+
});
|
|
218
|
+
};
|
|
219
|
+
const provider = this.std.getOptional(DatabaseDataSourceProvider);
|
|
220
|
+
const dataSource = provider && this.model.props.externalSourceId
|
|
221
|
+
? provider.createDataSource(this.model, initFn)
|
|
222
|
+
: new DatabaseBlockDataSource(this.model, initFn);
|
|
223
|
+
const id = currentViewStorage.getCurrentView(this.model.id);
|
|
224
|
+
if (id && dataSource.viewManager.viewGet(id)) {
|
|
225
|
+
dataSource.viewManager.setCurrentView(id);
|
|
226
|
+
}
|
|
227
|
+
return dataSource;
|
|
228
|
+
}
|
|
229
|
+
get dataSource() {
|
|
230
|
+
if (!this._dataSource) {
|
|
231
|
+
this._dataSource = this._buildDataSource();
|
|
232
|
+
}
|
|
233
|
+
return this._dataSource;
|
|
234
|
+
}
|
|
235
|
+
rebuildDataSource() {
|
|
236
|
+
this._dataSource?.dispose();
|
|
237
|
+
this._dataSource = undefined;
|
|
238
|
+
this._dataViewRootLogic = undefined;
|
|
239
|
+
this._sourceVersion++;
|
|
240
|
+
this.requestUpdate();
|
|
241
|
+
}
|
|
242
|
+
get optionsConfig() {
|
|
243
|
+
return {
|
|
244
|
+
configure: (_model, options) => options,
|
|
245
|
+
...this.std.getOptional(DatabaseConfigExtension.identifier),
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
get isCommentHighlighted() {
|
|
249
|
+
return (this.std
|
|
250
|
+
.getOptional(BlockCommentManager)
|
|
251
|
+
?.isBlockCommentHighlighted(this.model) ?? false);
|
|
252
|
+
}
|
|
253
|
+
get topContenteditableElement() {
|
|
254
|
+
if (this.std.get(DocModeProvider).getEditorMode() === 'edgeless') {
|
|
255
|
+
return this.closest(EDGELESS_TOP_CONTENTEDITABLE_SELECTOR);
|
|
256
|
+
}
|
|
257
|
+
return this.rootComponent;
|
|
258
|
+
}
|
|
259
|
+
renderDatabaseOps() {
|
|
260
|
+
if (this.dataSource.readonly$.value) {
|
|
261
|
+
return nothing;
|
|
262
|
+
}
|
|
263
|
+
return html ` <div
|
|
264
|
+
data-testid="database-ops"
|
|
265
|
+
class="${databaseOpsStyles}"
|
|
266
|
+
@click="${this.clickDatabaseOps}"
|
|
267
|
+
>
|
|
268
|
+
${MoreHorizontalIcon()}
|
|
269
|
+
</div>`;
|
|
270
|
+
}
|
|
271
|
+
connectedCallback() {
|
|
272
|
+
super.connectedCallback();
|
|
273
|
+
this.setAttribute(RANGE_SYNC_EXCLUDE_ATTR, 'true');
|
|
274
|
+
this.classList.add(databaseBlockStyles);
|
|
275
|
+
this.listenFullWidthChange();
|
|
276
|
+
// Rebuild the source whenever the block is re-pointed at (or detached from)
|
|
277
|
+
// an external collection. The first (synchronous) emission is skipped: the
|
|
278
|
+
// source is still lazy at this point, so there is nothing to rebuild.
|
|
279
|
+
this.disposables.add(this.model.props.externalSourceId$.subscribe(() => {
|
|
280
|
+
if (this._dataSource) {
|
|
281
|
+
this.rebuildDataSource();
|
|
282
|
+
}
|
|
283
|
+
}));
|
|
284
|
+
}
|
|
285
|
+
disconnectedCallback() {
|
|
286
|
+
this._dataSource?.dispose();
|
|
287
|
+
this._dataSource = undefined;
|
|
288
|
+
this._dataViewRootLogic = undefined;
|
|
289
|
+
super.disconnectedCallback();
|
|
290
|
+
}
|
|
291
|
+
listenFullWidthChange() {
|
|
292
|
+
if (this.std.get(DocModeProvider).getEditorMode() === 'edgeless') {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
this.disposables.add(autoUpdate(this.host, this, () => {
|
|
296
|
+
const padding = this.getBoundingClientRect().left -
|
|
297
|
+
this.host.getBoundingClientRect().left;
|
|
298
|
+
this.virtualPadding$.value = Math.max(0, padding - 72);
|
|
299
|
+
}));
|
|
300
|
+
}
|
|
301
|
+
get dataViewRootLogic() {
|
|
302
|
+
if (!this._dataViewRootLogic) {
|
|
303
|
+
this._dataViewRootLogic = this._buildDataViewRootLogic();
|
|
304
|
+
}
|
|
305
|
+
return this._dataViewRootLogic;
|
|
306
|
+
}
|
|
307
|
+
_buildDataViewRootLogic() {
|
|
308
|
+
return new DataViewRootUILogic({
|
|
223
309
|
virtualPadding$: this.virtualPadding$,
|
|
224
310
|
bindHotkey: hotkeys => {
|
|
225
311
|
return {
|
|
@@ -237,7 +323,7 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent {
|
|
|
237
323
|
},
|
|
238
324
|
selection$: this.viewSelection$,
|
|
239
325
|
setSelection: this.setSelection,
|
|
240
|
-
dataSource: this.dataSource
|
|
326
|
+
dataSource: this.dataSource,
|
|
241
327
|
headerWidget: this.headerWidget,
|
|
242
328
|
onDrag: this.onDrag,
|
|
243
329
|
clipboard: this.std.clipboard,
|
|
@@ -297,58 +383,12 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent {
|
|
|
297
383
|
}
|
|
298
384
|
},
|
|
299
385
|
},
|
|
300
|
-
})
|
|
301
|
-
this.#useZeroWidth_accessor_storage = true;
|
|
302
|
-
}
|
|
303
|
-
get optionsConfig() {
|
|
304
|
-
return {
|
|
305
|
-
configure: (_model, options) => options,
|
|
306
|
-
...this.std.getOptional(DatabaseConfigExtension.identifier),
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
get isCommentHighlighted() {
|
|
310
|
-
return (this.std
|
|
311
|
-
.getOptional(BlockCommentManager)
|
|
312
|
-
?.isBlockCommentHighlighted(this.model) ?? false);
|
|
313
|
-
}
|
|
314
|
-
get topContenteditableElement() {
|
|
315
|
-
if (this.std.get(DocModeProvider).getEditorMode() === 'edgeless') {
|
|
316
|
-
return this.closest(EDGELESS_TOP_CONTENTEDITABLE_SELECTOR);
|
|
317
|
-
}
|
|
318
|
-
return this.rootComponent;
|
|
319
|
-
}
|
|
320
|
-
renderDatabaseOps() {
|
|
321
|
-
if (this.dataSource.value.readonly$.value) {
|
|
322
|
-
return nothing;
|
|
323
|
-
}
|
|
324
|
-
return html ` <div
|
|
325
|
-
data-testid="database-ops"
|
|
326
|
-
class="${databaseOpsStyles}"
|
|
327
|
-
@click="${this.clickDatabaseOps}"
|
|
328
|
-
>
|
|
329
|
-
${MoreHorizontalIcon()}
|
|
330
|
-
</div>`;
|
|
331
|
-
}
|
|
332
|
-
connectedCallback() {
|
|
333
|
-
super.connectedCallback();
|
|
334
|
-
this.setAttribute(RANGE_SYNC_EXCLUDE_ATTR, 'true');
|
|
335
|
-
this.classList.add(databaseBlockStyles);
|
|
336
|
-
this.listenFullWidthChange();
|
|
337
|
-
}
|
|
338
|
-
listenFullWidthChange() {
|
|
339
|
-
if (this.std.get(DocModeProvider).getEditorMode() === 'edgeless') {
|
|
340
|
-
return;
|
|
341
|
-
}
|
|
342
|
-
this.disposables.add(autoUpdate(this.host, this, () => {
|
|
343
|
-
const padding = this.getBoundingClientRect().left -
|
|
344
|
-
this.host.getBoundingClientRect().left;
|
|
345
|
-
this.virtualPadding$.value = Math.max(0, padding - 72);
|
|
346
|
-
}));
|
|
386
|
+
});
|
|
347
387
|
}
|
|
348
388
|
renderBlock() {
|
|
349
389
|
return html `
|
|
350
390
|
<div contenteditable="false" class="${databaseContentStyles}">
|
|
351
|
-
${this.dataViewRootLogic.
|
|
391
|
+
${keyed(this._sourceVersion, this.dataViewRootLogic.render())}
|
|
352
392
|
</div>
|
|
353
393
|
`;
|
|
354
394
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { DatabaseBlockModel } from '../affine-model/index.js';
|
|
2
|
+
import type { InsertToPosition } from '../affine-shared/utils/index.js';
|
|
3
|
+
import { type DatabaseFlags, DataSourceBase, type DataViewDataType, type PropertyMetaConfig, type TypeInstance, type ViewManager, type ViewMeta } from '../data-view/index.js';
|
|
4
|
+
import { DisposableGroup } from '../global/disposable/index.js';
|
|
5
|
+
import { type ReadonlySignal } from '@preact/signals-core';
|
|
6
|
+
/**
|
|
7
|
+
* Intermediate base for `affine:database` data sources whose **view config**
|
|
8
|
+
* keeps living in the block model blob (`model.props.views`) while the **data
|
|
9
|
+
* layer** (rows / cells / columns) is redirected elsewhere — the inline blob
|
|
10
|
+
* source, or a host-provided external collection.
|
|
11
|
+
*
|
|
12
|
+
* It implements everything generic — the view manager / metas / converts, the
|
|
13
|
+
* view-data CRUD, feature flags, readonly, and the reactive plumbing — so a
|
|
14
|
+
* concrete source only provides a small synchronous data contract (`getRows`,
|
|
15
|
+
* `getCellValue`, …) plus the mutations, and calls {@link invalidate} from its
|
|
16
|
+
* own change observers.
|
|
17
|
+
*
|
|
18
|
+
* Reactivity: the data-layer reads ({@link rows$}, {@link properties$} and the
|
|
19
|
+
* `*Get` getters that `DataSourceBase` wraps in `computed()`) all depend on
|
|
20
|
+
* {@link revision}. A source backed by reactive blocksuite signals (the blob
|
|
21
|
+
* source) tracks those naturally and never bumps `revision` — cost is nil. A
|
|
22
|
+
* source reading raw `Y.Map`s bumps `revision` via {@link invalidate} so the
|
|
23
|
+
* computeds re-run. Defining `rows$`/`properties$` here (not as instance fields
|
|
24
|
+
* reassigned in a subclass constructor) also avoids the "reassign after super"
|
|
25
|
+
* dance.
|
|
26
|
+
*/
|
|
27
|
+
export declare abstract class ExternalDataSourceBase extends DataSourceBase {
|
|
28
|
+
protected readonly _model: DatabaseBlockModel;
|
|
29
|
+
/** Bumped by {@link invalidate} to re-run the data-layer computeds. */
|
|
30
|
+
protected readonly revision: import("@preact/signals-core").Signal<number>;
|
|
31
|
+
protected readonly disposables: DisposableGroup;
|
|
32
|
+
constructor(model: DatabaseBlockModel);
|
|
33
|
+
/** Notify the view layer that the underlying data changed. */
|
|
34
|
+
protected invalidate(): void;
|
|
35
|
+
dispose(): void;
|
|
36
|
+
get doc(): import("../store/index.js").Store;
|
|
37
|
+
get parentProvider(): import("../global/di/provider.js").ServiceProvider;
|
|
38
|
+
protected abstract getRows(): string[];
|
|
39
|
+
protected abstract getProperties(): string[];
|
|
40
|
+
protected abstract getCellValue(rowId: string, propertyId: string): unknown;
|
|
41
|
+
protected abstract getPropertyType(propertyId: string): string | undefined;
|
|
42
|
+
protected abstract getPropertyName(propertyId: string): string;
|
|
43
|
+
protected abstract getPropertyData(propertyId: string): Record<string, unknown>;
|
|
44
|
+
protected abstract getPropertyDataType(propertyId: string): TypeInstance | undefined;
|
|
45
|
+
protected abstract getPropertyMetas(): PropertyMetaConfig[];
|
|
46
|
+
rows$: ReadonlySignal<string[]>;
|
|
47
|
+
properties$: ReadonlySignal<string[]>;
|
|
48
|
+
allPropertyMetas$: ReadonlySignal<PropertyMetaConfig[]>;
|
|
49
|
+
propertyMetas$: ReadonlySignal<PropertyMetaConfig[]>;
|
|
50
|
+
cellValueGet(rowId: string, propertyId: string): unknown;
|
|
51
|
+
propertyTypeGet(propertyId: string): string | undefined;
|
|
52
|
+
propertyNameGet(propertyId: string): string;
|
|
53
|
+
propertyDataGet(propertyId: string): Record<string, unknown>;
|
|
54
|
+
propertyDataTypeGet(propertyId: string): TypeInstance | undefined;
|
|
55
|
+
featureFlags$: ReadonlySignal<DatabaseFlags>;
|
|
56
|
+
readonly$: ReadonlySignal<boolean>;
|
|
57
|
+
viewConverts: import("./index.js").ViewConvertConfig[];
|
|
58
|
+
viewMetas: ViewMeta[];
|
|
59
|
+
viewManager: ViewManager;
|
|
60
|
+
viewDataList$: ReadonlySignal<DataViewDataType[]>;
|
|
61
|
+
viewDataGet(viewId: string): DataViewDataType | undefined;
|
|
62
|
+
viewDataAdd(viewData: DataViewDataType): string;
|
|
63
|
+
viewDataDelete(viewId: string): void;
|
|
64
|
+
viewDataDuplicate(id: string): string;
|
|
65
|
+
viewDataMoveTo(id: string, position: InsertToPosition): void;
|
|
66
|
+
viewDataUpdate<ViewData extends DataViewDataType>(id: string, updater: (data: ViewData) => Partial<ViewData>): void;
|
|
67
|
+
viewMetaGet(type: string): ViewMeta;
|
|
68
|
+
viewMetaGetById(viewId: string): ViewMeta | undefined;
|
|
69
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { FeatureFlagService } from '../affine-shared/services/index.js';
|
|
2
|
+
import { DataSourceBase, ViewManagerBase, } from '../data-view/index.js';
|
|
3
|
+
import { DisposableGroup } from '../global/disposable/index.js';
|
|
4
|
+
import { IS_MOBILE } from '../global/env/index.js';
|
|
5
|
+
import { BlockSuiteError, ErrorCode } from '../global/exceptions/index.js';
|
|
6
|
+
import { computed, signal, } from '@preact/signals-core';
|
|
7
|
+
import { deleteView, duplicateView, moveViewTo, updateView, } from './utils/block-utils.js';
|
|
8
|
+
import { databaseBlockViewConverts, databaseBlockViewMap, databaseBlockViews, } from './views/index.js';
|
|
9
|
+
/**
|
|
10
|
+
* Intermediate base for `affine:database` data sources whose **view config**
|
|
11
|
+
* keeps living in the block model blob (`model.props.views`) while the **data
|
|
12
|
+
* layer** (rows / cells / columns) is redirected elsewhere — the inline blob
|
|
13
|
+
* source, or a host-provided external collection.
|
|
14
|
+
*
|
|
15
|
+
* It implements everything generic — the view manager / metas / converts, the
|
|
16
|
+
* view-data CRUD, feature flags, readonly, and the reactive plumbing — so a
|
|
17
|
+
* concrete source only provides a small synchronous data contract (`getRows`,
|
|
18
|
+
* `getCellValue`, …) plus the mutations, and calls {@link invalidate} from its
|
|
19
|
+
* own change observers.
|
|
20
|
+
*
|
|
21
|
+
* Reactivity: the data-layer reads ({@link rows$}, {@link properties$} and the
|
|
22
|
+
* `*Get` getters that `DataSourceBase` wraps in `computed()`) all depend on
|
|
23
|
+
* {@link revision}. A source backed by reactive blocksuite signals (the blob
|
|
24
|
+
* source) tracks those naturally and never bumps `revision` — cost is nil. A
|
|
25
|
+
* source reading raw `Y.Map`s bumps `revision` via {@link invalidate} so the
|
|
26
|
+
* computeds re-run. Defining `rows$`/`properties$` here (not as instance fields
|
|
27
|
+
* reassigned in a subclass constructor) also avoids the "reassign after super"
|
|
28
|
+
* dance.
|
|
29
|
+
*/
|
|
30
|
+
export class ExternalDataSourceBase extends DataSourceBase {
|
|
31
|
+
constructor(model) {
|
|
32
|
+
super();
|
|
33
|
+
/** Bumped by {@link invalidate} to re-run the data-layer computeds. */
|
|
34
|
+
this.revision = signal(0);
|
|
35
|
+
this.disposables = new DisposableGroup();
|
|
36
|
+
// --- reactive bridge over the data contract -----------------------------
|
|
37
|
+
this.rows$ = computed(() => {
|
|
38
|
+
void this.revision.value;
|
|
39
|
+
return this.getRows();
|
|
40
|
+
});
|
|
41
|
+
this.properties$ = computed(() => {
|
|
42
|
+
void this.revision.value;
|
|
43
|
+
return this.getProperties();
|
|
44
|
+
});
|
|
45
|
+
this.allPropertyMetas$ = computed(() => {
|
|
46
|
+
void this.revision.value;
|
|
47
|
+
return this.getPropertyMetas();
|
|
48
|
+
});
|
|
49
|
+
this.propertyMetas$ = computed(() => {
|
|
50
|
+
return this.allPropertyMetas$.value.filter(v => !v.config.fixed && !v.config.hide);
|
|
51
|
+
});
|
|
52
|
+
// --- view surface (config stays in the model blob) ----------------------
|
|
53
|
+
this.featureFlags$ = computed(() => {
|
|
54
|
+
const featureFlagService = this.doc.get(FeatureFlagService);
|
|
55
|
+
return {
|
|
56
|
+
enable_number_formatting: featureFlagService.getFlag('enable_database_number_formatting') ??
|
|
57
|
+
false,
|
|
58
|
+
enable_table_virtual_scroll: featureFlagService.getFlag('enable_table_virtual_scroll') ?? false,
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
this.readonly$ = computed(() => {
|
|
62
|
+
return (this._model.store.readonly ||
|
|
63
|
+
// TODO(@L-Sun): use block level readonly
|
|
64
|
+
IS_MOBILE);
|
|
65
|
+
});
|
|
66
|
+
this.viewConverts = databaseBlockViewConverts;
|
|
67
|
+
this.viewMetas = databaseBlockViews;
|
|
68
|
+
this.viewManager = new ViewManagerBase(this);
|
|
69
|
+
this.viewDataList$ = computed(() => {
|
|
70
|
+
return this._model.props.views$.value;
|
|
71
|
+
});
|
|
72
|
+
this._model = model;
|
|
73
|
+
}
|
|
74
|
+
/** Notify the view layer that the underlying data changed. */
|
|
75
|
+
invalidate() {
|
|
76
|
+
this.revision.value++;
|
|
77
|
+
}
|
|
78
|
+
dispose() {
|
|
79
|
+
this.disposables.dispose();
|
|
80
|
+
}
|
|
81
|
+
get doc() {
|
|
82
|
+
return this._model.store;
|
|
83
|
+
}
|
|
84
|
+
get parentProvider() {
|
|
85
|
+
return this._model.store.provider;
|
|
86
|
+
}
|
|
87
|
+
cellValueGet(rowId, propertyId) {
|
|
88
|
+
void this.revision.value;
|
|
89
|
+
return this.getCellValue(rowId, propertyId);
|
|
90
|
+
}
|
|
91
|
+
propertyTypeGet(propertyId) {
|
|
92
|
+
void this.revision.value;
|
|
93
|
+
return this.getPropertyType(propertyId);
|
|
94
|
+
}
|
|
95
|
+
propertyNameGet(propertyId) {
|
|
96
|
+
void this.revision.value;
|
|
97
|
+
return this.getPropertyName(propertyId);
|
|
98
|
+
}
|
|
99
|
+
propertyDataGet(propertyId) {
|
|
100
|
+
void this.revision.value;
|
|
101
|
+
return this.getPropertyData(propertyId);
|
|
102
|
+
}
|
|
103
|
+
propertyDataTypeGet(propertyId) {
|
|
104
|
+
void this.revision.value;
|
|
105
|
+
return this.getPropertyDataType(propertyId);
|
|
106
|
+
}
|
|
107
|
+
viewDataGet(viewId) {
|
|
108
|
+
return this.viewDataList$.value.find(data => data.id === viewId);
|
|
109
|
+
}
|
|
110
|
+
viewDataAdd(viewData) {
|
|
111
|
+
this._model.store.captureSync();
|
|
112
|
+
this._model.store.transact(() => {
|
|
113
|
+
this._model.props.views = [...this._model.props.views, viewData];
|
|
114
|
+
});
|
|
115
|
+
return viewData.id;
|
|
116
|
+
}
|
|
117
|
+
viewDataDelete(viewId) {
|
|
118
|
+
this._model.store.captureSync();
|
|
119
|
+
deleteView(this._model, viewId);
|
|
120
|
+
}
|
|
121
|
+
viewDataDuplicate(id) {
|
|
122
|
+
return duplicateView(this._model, id);
|
|
123
|
+
}
|
|
124
|
+
viewDataMoveTo(id, position) {
|
|
125
|
+
moveViewTo(this._model, id, position);
|
|
126
|
+
}
|
|
127
|
+
viewDataUpdate(id, updater) {
|
|
128
|
+
updateView(this._model, id, updater);
|
|
129
|
+
}
|
|
130
|
+
viewMetaGet(type) {
|
|
131
|
+
const view = databaseBlockViewMap[type];
|
|
132
|
+
if (!view) {
|
|
133
|
+
throw new BlockSuiteError(ErrorCode.DatabaseBlockError, `Unknown view type: ${type}`);
|
|
134
|
+
}
|
|
135
|
+
return view;
|
|
136
|
+
}
|
|
137
|
+
viewMetaGetById(viewId) {
|
|
138
|
+
const view = this.viewDataGet(viewId);
|
|
139
|
+
if (!view) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
return this.viewMetaGet(view.mode);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -4,6 +4,7 @@ export * from './config';
|
|
|
4
4
|
export * from './context';
|
|
5
5
|
export * from './data-source';
|
|
6
6
|
export * from './database-block';
|
|
7
|
+
export * from './external-data-source';
|
|
7
8
|
export * from './detail-panel/block-renderer';
|
|
8
9
|
export * from './detail-panel/note-renderer';
|
|
9
10
|
export * from './properties';
|
|
@@ -4,6 +4,7 @@ export * from './config';
|
|
|
4
4
|
export * from './context';
|
|
5
5
|
export * from './data-source';
|
|
6
6
|
export * from './database-block';
|
|
7
|
+
export * from './external-data-source';
|
|
7
8
|
export * from './detail-panel/block-renderer';
|
|
8
9
|
export * from './detail-panel/note-renderer';
|
|
9
10
|
export * from './properties';
|
|
@@ -118,6 +118,24 @@ export declare const builtinMiscToolbarConfig: {
|
|
|
118
118
|
readonly icon: import("lit-html").TemplateResult<1>;
|
|
119
119
|
readonly when: (ctx: ToolbarContext) => boolean;
|
|
120
120
|
readonly run: (ctx: ToolbarContext) => void;
|
|
121
|
+
}, {
|
|
122
|
+
readonly id: "c.link-element";
|
|
123
|
+
readonly label: "Link";
|
|
124
|
+
readonly icon: import("lit-html").TemplateResult<1>;
|
|
125
|
+
readonly when: (ctx: ToolbarContext) => boolean;
|
|
126
|
+
readonly run: (ctx: ToolbarContext) => void;
|
|
127
|
+
}, {
|
|
128
|
+
readonly id: "c.edit-element-link";
|
|
129
|
+
readonly label: "Edit link";
|
|
130
|
+
readonly icon: import("lit-html").TemplateResult<1>;
|
|
131
|
+
readonly when: (ctx: ToolbarContext) => boolean;
|
|
132
|
+
readonly run: (ctx: ToolbarContext) => void;
|
|
133
|
+
}, {
|
|
134
|
+
readonly id: "c.remove-element-link";
|
|
135
|
+
readonly label: "Remove link";
|
|
136
|
+
readonly icon: import("lit-html").TemplateResult<1>;
|
|
137
|
+
readonly when: (ctx: ToolbarContext) => boolean;
|
|
138
|
+
readonly run: (ctx: ToolbarContext) => void;
|
|
121
139
|
}];
|
|
122
140
|
} | {
|
|
123
141
|
placement: ActionPlacement;
|
|
@@ -69,6 +69,24 @@ export declare const moreActions: [{
|
|
|
69
69
|
readonly icon: import("lit-html").TemplateResult<1>;
|
|
70
70
|
readonly when: (ctx: ToolbarContext) => boolean;
|
|
71
71
|
readonly run: (ctx: ToolbarContext) => void;
|
|
72
|
+
}, {
|
|
73
|
+
readonly id: "c.link-element";
|
|
74
|
+
readonly label: "Link";
|
|
75
|
+
readonly icon: import("lit-html").TemplateResult<1>;
|
|
76
|
+
readonly when: (ctx: ToolbarContext) => boolean;
|
|
77
|
+
readonly run: (ctx: ToolbarContext) => void;
|
|
78
|
+
}, {
|
|
79
|
+
readonly id: "c.edit-element-link";
|
|
80
|
+
readonly label: "Edit link";
|
|
81
|
+
readonly icon: import("lit-html").TemplateResult<1>;
|
|
82
|
+
readonly when: (ctx: ToolbarContext) => boolean;
|
|
83
|
+
readonly run: (ctx: ToolbarContext) => void;
|
|
84
|
+
}, {
|
|
85
|
+
readonly id: "c.remove-element-link";
|
|
86
|
+
readonly label: "Remove link";
|
|
87
|
+
readonly icon: import("lit-html").TemplateResult<1>;
|
|
88
|
+
readonly when: (ctx: ToolbarContext) => boolean;
|
|
89
|
+
readonly run: (ctx: ToolbarContext) => void;
|
|
72
90
|
}];
|
|
73
91
|
}, {
|
|
74
92
|
readonly id: "e.delete";
|
|
@@ -6,10 +6,11 @@ import { ImageBlockComponent } from '../../../../affine-block-image/index.js';
|
|
|
6
6
|
import { EdgelessCRUDIdentifier, getSurfaceComponent, } from '../../../../affine-block-surface/index.js';
|
|
7
7
|
import { createGroupFromSelectedCommand } from '../../../../affine-gfx-group/index.js';
|
|
8
8
|
import { AttachmentBlockModel, BookmarkBlockModel, EmbedLinkedDocBlockSchema, EmbedLinkedDocModel, EmbedSyncedDocBlockSchema, EmbedSyncedDocModel, FrameBlockModel, ImageBlockModel, isExternalEmbedModel, NoteBlockModel, ParagraphBlockModel, } from '../../../../affine-model/index.js';
|
|
9
|
+
import { QuickSearchProvider } from '../../../../affine-shared/services/index.js';
|
|
9
10
|
import { matchModels, } from '../../../../affine-shared/utils/index.js';
|
|
10
11
|
import { Bound, getCommonBoundWithRotation } from '../../../../global/gfx/index.js';
|
|
11
|
-
import { ArrowDownBigBottomIcon, ArrowDownBigIcon, ArrowUpBigIcon, ArrowUpBigTopIcon, CopyIcon, DeleteIcon, DuplicateIcon, FrameIcon, GroupIcon, LinkedPageIcon, ResetIcon, } from '@blocksuite/icons/lit';
|
|
12
|
-
import { GfxBlockElementModel } from '../../../../std/gfx/index.js';
|
|
12
|
+
import { ArrowDownBigBottomIcon, ArrowDownBigIcon, ArrowUpBigIcon, ArrowUpBigTopIcon, CopyIcon, DeleteIcon, DuplicateIcon, FrameIcon, GroupIcon, LinkIcon, LinkedPageIcon, ResetIcon, UnlinkIcon, } from '@blocksuite/icons/lit';
|
|
13
|
+
import { GfxBlockElementModel, GfxPrimitiveElementModel, } from '../../../../std/gfx/index.js';
|
|
13
14
|
import { EdgelessClipboardController } from '../../clipboard/clipboard';
|
|
14
15
|
import { duplicate } from '../../utils/clipboard-utils';
|
|
15
16
|
import { getSortedCloneElements } from '../../utils/clone-utils';
|
|
@@ -298,6 +299,54 @@ export const moreActions = [
|
|
|
298
299
|
create().catch(console.error);
|
|
299
300
|
},
|
|
300
301
|
},
|
|
302
|
+
{
|
|
303
|
+
id: 'c.link-element',
|
|
304
|
+
// Attach a link (existing doc or external URL) to a single drawing
|
|
305
|
+
// element. A hover arrow (edgeless-element-link widget) opens it.
|
|
306
|
+
label: 'Link',
|
|
307
|
+
icon: LinkIcon(),
|
|
308
|
+
when(ctx) {
|
|
309
|
+
const el = getLinkableElement(ctx);
|
|
310
|
+
return el !== null && !hasElementLink(el) && canPickLink(ctx);
|
|
311
|
+
},
|
|
312
|
+
run(ctx) {
|
|
313
|
+
const el = getLinkableElement(ctx);
|
|
314
|
+
if (el)
|
|
315
|
+
linkElementViaQuickSearch(ctx, el);
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
id: 'c.edit-element-link',
|
|
320
|
+
label: 'Edit link',
|
|
321
|
+
icon: LinkIcon(),
|
|
322
|
+
when(ctx) {
|
|
323
|
+
const el = getLinkableElement(ctx);
|
|
324
|
+
return el !== null && hasElementLink(el) && canPickLink(ctx);
|
|
325
|
+
},
|
|
326
|
+
run(ctx) {
|
|
327
|
+
const el = getLinkableElement(ctx);
|
|
328
|
+
if (el)
|
|
329
|
+
linkElementViaQuickSearch(ctx, el);
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
id: 'c.remove-element-link',
|
|
334
|
+
label: 'Remove link',
|
|
335
|
+
icon: UnlinkIcon(),
|
|
336
|
+
when(ctx) {
|
|
337
|
+
const el = getLinkableElement(ctx);
|
|
338
|
+
return el !== null && hasElementLink(el);
|
|
339
|
+
},
|
|
340
|
+
run(ctx) {
|
|
341
|
+
const el = getLinkableElement(ctx);
|
|
342
|
+
if (!el)
|
|
343
|
+
return;
|
|
344
|
+
ctx.std.get(EdgelessCRUDIdentifier).updateElement(el.id, {
|
|
345
|
+
linkedDocId: undefined,
|
|
346
|
+
externalLink: undefined,
|
|
347
|
+
});
|
|
348
|
+
},
|
|
349
|
+
},
|
|
301
350
|
],
|
|
302
351
|
},
|
|
303
352
|
// Deleting Group
|
|
@@ -321,6 +370,59 @@ export const moreActions = [
|
|
|
321
370
|
},
|
|
322
371
|
},
|
|
323
372
|
];
|
|
373
|
+
/** The single selected drawing element a link can be attached to, or null. */
|
|
374
|
+
function getLinkableElement(ctx) {
|
|
375
|
+
const models = ctx.getSurfaceModels();
|
|
376
|
+
return models.length === 1 && models[0] instanceof GfxPrimitiveElementModel
|
|
377
|
+
? models[0]
|
|
378
|
+
: null;
|
|
379
|
+
}
|
|
380
|
+
function hasElementLink(el) {
|
|
381
|
+
return Boolean(el.linkedDocId || el.externalLink);
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Picking a doc/URL needs the host-provided quick-search modal. Hide the
|
|
385
|
+
* Link / Edit link items where it is absent (they would no-op); Remove link
|
|
386
|
+
* stays available since clearing needs no picker.
|
|
387
|
+
*/
|
|
388
|
+
function canPickLink(ctx) {
|
|
389
|
+
return Boolean(ctx.std.getOptional(QuickSearchProvider));
|
|
390
|
+
}
|
|
391
|
+
/** Pick a doc or URL via the quick-search modal and store it on the element. */
|
|
392
|
+
function linkElementViaQuickSearch(ctx, el) {
|
|
393
|
+
const quickSearch = ctx.std.getOptional(QuickSearchProvider);
|
|
394
|
+
if (!quickSearch)
|
|
395
|
+
return;
|
|
396
|
+
quickSearch
|
|
397
|
+
.openQuickSearch()
|
|
398
|
+
.then(result => {
|
|
399
|
+
if (!result)
|
|
400
|
+
return;
|
|
401
|
+
const crud = ctx.std.get(EdgelessCRUDIdentifier);
|
|
402
|
+
if ('docId' in result) {
|
|
403
|
+
crud.updateElement(el.id, {
|
|
404
|
+
linkedDocId: result.docId,
|
|
405
|
+
externalLink: undefined,
|
|
406
|
+
});
|
|
407
|
+
ctx.track('LinkedDocCreated', {
|
|
408
|
+
control: 'link element',
|
|
409
|
+
type: 'element-ref',
|
|
410
|
+
other: 'existing doc',
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
else if ('externalUrl' in result) {
|
|
414
|
+
crud.updateElement(el.id, {
|
|
415
|
+
externalLink: result.externalUrl,
|
|
416
|
+
linkedDocId: undefined,
|
|
417
|
+
});
|
|
418
|
+
ctx.track('Link', {
|
|
419
|
+
control: 'link element',
|
|
420
|
+
type: 'external url',
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
})
|
|
424
|
+
.catch(console.error);
|
|
425
|
+
}
|
|
324
426
|
function reorderElements(ctx, models, type) {
|
|
325
427
|
if (!models.length)
|
|
326
428
|
return;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { RootBlockModel } from '../affine-model/index.js';
|
|
2
|
+
import { WidgetComponent } from '../std/index.js';
|
|
3
|
+
import { nothing } from 'lit';
|
|
4
|
+
export declare const EDGELESS_ELEMENT_LINK_WIDGET = "edgeless-element-link";
|
|
5
|
+
/**
|
|
6
|
+
* Shows a clickable arrow when hovering a drawing element that carries a link
|
|
7
|
+
* ({@link GfxPrimitiveElementModel.linkedDocId} or `.externalLink`). Clicking it
|
|
8
|
+
* opens the linked doc (host side-view via `docLinkClicked`) or the URL.
|
|
9
|
+
*
|
|
10
|
+
* ponytail: hover is resolved via `getElementByPoint`, which returns the topmost
|
|
11
|
+
* leaf — a link set on a group only shows when a child is hovered. Good enough
|
|
12
|
+
* for v1 (shapes/text/connectors are hit directly).
|
|
13
|
+
*/
|
|
14
|
+
export declare class EdgelessElementLinkWidget extends WidgetComponent<RootBlockModel> {
|
|
15
|
+
static styles: import("lit").CSSResult;
|
|
16
|
+
private _hideTimer;
|
|
17
|
+
private accessor _target;
|
|
18
|
+
private accessor _rect;
|
|
19
|
+
get gfx(): import("../std/gfx/controller.js").GfxController;
|
|
20
|
+
private readonly _updateRect;
|
|
21
|
+
private _hasLink;
|
|
22
|
+
private _setTarget;
|
|
23
|
+
/** The hovered element if it carries a link, else its nearest linked group. */
|
|
24
|
+
private _resolveLinked;
|
|
25
|
+
private readonly _open;
|
|
26
|
+
private readonly _onKeydown;
|
|
27
|
+
firstUpdated(): void;
|
|
28
|
+
render(): import("lit-html").TemplateResult<1> | typeof nothing;
|
|
29
|
+
}
|