@formicoidea/labre-core 1.0.0 → 1.1.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.
Files changed (28) hide show
  1. package/architecture/code.c4 +13 -12
  2. package/dist/_pkgs/affine-block-root/configs/toolbar.d.ts +1 -1
  3. package/dist/_pkgs/affine-block-root/configs/toolbar.js +4 -2
  4. package/dist/_pkgs/affine-components/context-menu/menu-all.d.ts +1 -0
  5. package/dist/_pkgs/affine-components/context-menu/sub-menu.d.ts +3 -0
  6. package/dist/_pkgs/affine-components/context-menu/sub-menu.js +14 -2
  7. package/dist/_pkgs/affine-database-block/configs/slash-menu.d.ts +24 -0
  8. package/dist/_pkgs/affine-database-block/configs/slash-menu.js +38 -31
  9. package/dist/_pkgs/affine-database-block/creation-menu.d.ts +6 -0
  10. package/dist/_pkgs/affine-database-block/data-source.d.ts +6 -0
  11. package/dist/_pkgs/affine-database-block/data-source.js +16 -0
  12. package/dist/_pkgs/affine-database-block/database-block.d.ts +23 -0
  13. package/dist/_pkgs/affine-database-block/database-block.js +125 -3
  14. package/dist/_pkgs/affine-database-block/external-data-source.d.ts +7 -0
  15. package/dist/_pkgs/affine-database-block/external-data-source.js +30 -4
  16. package/dist/_pkgs/affine-database-block/index.d.ts +3 -0
  17. package/dist/_pkgs/affine-database-block/index.js +5 -0
  18. package/dist/_pkgs/affine-database-block/linked-view.d.ts +59 -0
  19. package/dist/_pkgs/affine-database-block/linked-view.js +131 -1
  20. package/dist/_pkgs/affine-database-block/translations.d.ts +18 -0
  21. package/dist/_pkgs/affine-database-block/translations.js +47 -0
  22. package/dist/_pkgs/affine-database-block/turn-into-source.d.ts +38 -0
  23. package/dist/_pkgs/affine-database-block/turn-into-source.js +187 -0
  24. package/dist/_pkgs/affine-database-block/view-deletion.d.ts +15 -0
  25. package/dist/_pkgs/affine-database-block/view-deletion.js +113 -0
  26. package/dist/_pkgs/affine-shared/services/data-source-picker-service.d.ts +99 -1
  27. package/dist/_pkgs/affine-widget-keyboard-toolbar/config.js +10 -21
  28. package/package.json +1 -1
@@ -0,0 +1,113 @@
1
+ /**
2
+ * A host decides what a deleted view takes with it (ADR 0035 amendment of
3
+ * 2026-10-29). Labre sends a source to its trash with the last view reading
4
+ * it, and must confirm that BEFORE the view goes, then restore it when the
5
+ * removal is undone. So the delete gesture of a view of a host database
6
+ * becomes a request (`DataSourcePickerService.deleteView`) whose `remove()`
7
+ * is the 1.0 removal, and the undo step it makes is tagged so an undo or a
8
+ * redo of it reaches `viewHistory`.
9
+ *
10
+ * The tag rides in the undo step's `meta`. Yjs builds a NEW step for the
11
+ * opposite stack while it pops one ('stack-item-added' with the undo manager
12
+ * as origin, fired inside the pop's transaction, before 'stack-item-popped'),
13
+ * so the tag is carried over to that step, and an undo, redo, undo… chain
14
+ * keeps reporting. Remote updates never pop a step: they report nothing.
15
+ */
16
+ const VIEW_REMOVED = 'labre:database-view-removed';
17
+ // One listener pair per undo manager, whatever the number of blocks: the
18
+ // step outlives the block's data source (a block remounted after a mode
19
+ // switch, or unmounted, still has its removals undone).
20
+ const watches = new WeakMap();
21
+ function isViewRef(value) {
22
+ if (typeof value !== 'object' || value === null)
23
+ return false;
24
+ const ref = value;
25
+ return (typeof ref.databaseId === 'string' &&
26
+ typeof ref.viewId === 'string' &&
27
+ typeof ref.docId === 'string' &&
28
+ typeof ref.blockId === 'string');
29
+ }
30
+ function watchHistory(undoManager, picker) {
31
+ const known = watches.get(undoManager);
32
+ if (known) {
33
+ known.picker = picker;
34
+ return;
35
+ }
36
+ const watch = { picker };
37
+ watches.set(undoManager, watch);
38
+ let replay = null;
39
+ undoManager.on('stack-item-added', event => {
40
+ if (event.origin === undoManager)
41
+ replay = event.stackItem;
42
+ });
43
+ undoManager.on('stack-item-popped', event => {
44
+ const opposite = replay;
45
+ replay = null;
46
+ const ref = event.stackItem.meta.get(VIEW_REMOVED);
47
+ if (!isViewRef(ref))
48
+ return;
49
+ opposite?.meta.set(VIEW_REMOVED, ref);
50
+ try {
51
+ watch.picker.viewHistory?.(ref, event.type);
52
+ }
53
+ catch (error) {
54
+ // A host failure must not break the undo itself.
55
+ console.error(error);
56
+ }
57
+ });
58
+ }
59
+ /**
60
+ * Deletes view `viewId` of `model`, through the host's `deleteView` when it
61
+ * has one and the block shows one of its databases; at once otherwise.
62
+ * `removeNow` is the 1.0 removal (one undo step); `afterRemove` runs after a
63
+ * removal the host asked for (the view to show next).
64
+ */
65
+ export function deleteViewThroughHost(options) {
66
+ const { model, viewId, picker, removeNow, afterRemove } = options;
67
+ const store = model.store;
68
+ const databaseId = model.props.externalSourceId;
69
+ const deleteView = picker?.deleteView;
70
+ if (!picker || !deleteView || !databaseId || store.readonly) {
71
+ removeNow();
72
+ return;
73
+ }
74
+ const view = model.props.views.find(v => v.id === viewId);
75
+ if (!view)
76
+ return;
77
+ const ref = {
78
+ databaseId,
79
+ viewId,
80
+ docId: store.id,
81
+ blockId: model.id,
82
+ ...(view.sourceId === undefined ? {} : { sourceId: view.sourceId }),
83
+ };
84
+ let used = false;
85
+ const request = {
86
+ ...ref,
87
+ viewName: typeof view.name === 'string' ? view.name : '',
88
+ remove: () => {
89
+ if (used || store.readonly)
90
+ return false;
91
+ if (!model.props.views.some(v => v.id === viewId))
92
+ return false;
93
+ used = true;
94
+ const undoManager = store.history.undoManager;
95
+ watchHistory(undoManager, picker);
96
+ let added = null;
97
+ const grab = (event) => {
98
+ added = event.stackItem;
99
+ };
100
+ undoManager.on('stack-item-added', grab);
101
+ try {
102
+ removeNow();
103
+ }
104
+ finally {
105
+ undoManager.off('stack-item-added', grab);
106
+ }
107
+ added?.meta.set(VIEW_REMOVED, ref);
108
+ afterRemove();
109
+ return true;
110
+ },
111
+ };
112
+ deleteView.call(picker, request);
113
+ }
@@ -1,4 +1,4 @@
1
- import type { ExtensionType } from '../../store/index.js';
1
+ import type { BlockSnapshot, DeltaInsert, ExtensionType } from '../../store/index.js';
2
2
  import type { ReadonlySignal } from '@preact/signals-core';
3
3
  /** One source of a host database, as the picker lists it. */
4
4
  export interface DataSourcePickerSource {
@@ -83,6 +83,62 @@ export interface DataSourcePickerViewTemplate {
83
83
  view: Readonly<Record<string, unknown>>;
84
84
  properties: readonly DataSourcePickerProperty[];
85
85
  }
86
+ /** One row of a table turned into a source (`createDatabaseFrom`). */
87
+ export interface DataSourcePickerRow {
88
+ /**
89
+ * The row's title as a rich-text delta: links, mentions (`reference`),
90
+ * bold… Keep what your title property allows.
91
+ */
92
+ title: readonly DeltaInsert[];
93
+ /**
94
+ * Property id (of `DataSourcePickerContent.properties`) → the value as the
95
+ * library stores it: a number, a boolean, a date's epoch milliseconds, a
96
+ * select's option id (or ids), a rich text as a delta. A property left
97
+ * out has no value.
98
+ */
99
+ values: Readonly<Record<string, unknown>>;
100
+ /** The row's nested blocks, for its page body; empty when it has none. */
101
+ body: readonly BlockSnapshot[];
102
+ }
103
+ /** What `createDatabaseFrom` turns into a database. */
104
+ export interface DataSourcePickerContent {
105
+ /** The database's name: the table's title, or empty for you to name it. */
106
+ title: string;
107
+ /**
108
+ * Its properties in order, the title first (type `title`). Their types are
109
+ * the library's (`title`, `rich-text`, `number`, `select`,
110
+ * `multi-select`, `checkbox`, `date`, `link`, `progress`…): map each to
111
+ * one of yours, or drop it.
112
+ */
113
+ properties: readonly DataSourcePickerProperty[];
114
+ /** The rows, in order. */
115
+ rows: readonly DataSourcePickerRow[];
116
+ }
117
+ /**
118
+ * A view of a host database, where it is stored (ADR 0035 amendment of
119
+ * 2026-10-29, "a host decides what a deleted view takes with it").
120
+ */
121
+ export interface DataSourcePickerViewRef {
122
+ /** The block's `externalSourceId`, as `list()` names the database. */
123
+ databaseId: string;
124
+ /** The view's stored `sourceId`; absent, the host's implicit source. */
125
+ sourceId?: string;
126
+ viewId: string;
127
+ /** The document (store id) and the `affine:database` block holding it. */
128
+ docId: string;
129
+ blockId: string;
130
+ }
131
+ /** What `deleteView` hands the host: the view, and the verb that removes it. */
132
+ export interface DataSourcePickerViewDeletion extends DataSourcePickerViewRef {
133
+ /** The view's name, as stored ('' when it has none). */
134
+ viewName: string;
135
+ /**
136
+ * Remove the view from its block, as one undo step, and show the next
137
+ * one. `false` when nothing was removed: the page turned read-only, the
138
+ * view is already gone, or `remove` was already called.
139
+ */
140
+ remove(): boolean;
141
+ }
86
142
  /**
87
143
  * The data-source picker (ADR 0035 §9c, issue B12): the host lists the
88
144
  * databases and sources a view may read, and creates them. The library draws
@@ -107,6 +163,33 @@ export interface DataSourcePickerService {
107
163
  * leave the menu.
108
164
  */
109
165
  createDatabase?(): Promise<DataSourcePickerDatabase | null>;
166
+ /**
167
+ * No native table beside your databases (ADR 0035 amendment of
168
+ * 2026-10-29). The source decides, never the row count: a database block
169
+ * with no source (`externalSourceId`) and no row, or whose source you no
170
+ * longer resolve, draws an empty view with "Connect a source" (it
171
+ * reopens the creation popover); one with no source but native rows shows
172
+ * them read-only; a connected one with no row is an ordinary table.
173
+ * Closing the popover writes nothing. Absent or `false`: as in 1.0.
174
+ */
175
+ requireSource?: boolean;
176
+ /**
177
+ * Create a database holding `content` and resolve to it, with at least one
178
+ * source, or `null` when nothing was created (ADR 0035 amendment of
179
+ * 2026-10-29, "turn into a source"). One call, so the rows land together
180
+ * or not at all. With `requireSource`, "Create Table" on selected
181
+ * paragraphs and "Move into a database" on a database with native rows
182
+ * call it, then connect the block to the database's first source in one
183
+ * undo step; an undo leaves your database in place. Absent, both are
184
+ * hidden when a source is required.
185
+ */
186
+ createDatabaseFrom?(content: DataSourcePickerContent): Promise<DataSourcePickerDatabase | null>;
187
+ /**
188
+ * With `createDatabaseFrom`: you keep a row's nested blocks as its page
189
+ * body (`DataSourcePickerRow.body`). Absent, a conversion whose rows have
190
+ * nested blocks is refused with a notice and writes nothing.
191
+ */
192
+ rowBodies?: boolean;
110
193
  /**
111
194
  * Create a source in `databaseId`; it resolves to the new source, or
112
195
  * `null`. Absent, the picker offers no "New source".
@@ -126,6 +209,21 @@ export interface DataSourcePickerService {
126
209
  * offer it. Resolves `true` once saved. Absent, no "Save as template".
127
210
  */
128
211
  saveViewTemplate?(template: DataSourcePickerViewTemplate): Promise<boolean>;
212
+ /**
213
+ * A reader asked to delete a view of one of your databases (a block with
214
+ * an `externalSourceId`, on a writable document): decide what goes with
215
+ * it, ask what you must, then call `request.remove()`; not calling it
216
+ * keeps the view. Called once per gesture ("Delete" in the view's tab
217
+ * menu or its settings), never for a deleted block, an undo, a redo or a
218
+ * remote change. Absent, the view is removed at once, as in 1.0.
219
+ */
220
+ deleteView?(request: DataSourcePickerViewDeletion): void;
221
+ /**
222
+ * An undo (`'undo'`: the view is back) or a redo (`'redo'`: it is gone
223
+ * again) of a removal made by `request.remove()`, on this client. Never
224
+ * for a remote change. Nothing to confirm: it already happened.
225
+ */
226
+ viewHistory?(view: DataSourcePickerViewRef, step: 'undo' | 'redo'): void;
129
227
  }
130
228
  export declare const DataSourcePickerProvider: import("../../global/di/types.js").ServiceIdentifier<DataSourcePickerService> & (<U extends DataSourcePickerService = DataSourcePickerService>(variant: import("../../global/di/types.js").ServiceVariant) => import("../../global/di/types.js").ServiceIdentifier<U>);
131
229
  /**
@@ -1,5 +1,5 @@
1
1
  import { pickAndAddSiblingAttachmentBlocks } from '../affine-block-attachment/index.js';
2
- import { insertDatabaseBlockCommand } from '../affine-database-block/index.js';
2
+ import { canCreateDatabase, createDatabaseByGesture, } from '../affine-database-block/index.js';
3
3
  import { insertEmptyEmbedIframeCommand } from '../affine-block-embed/index.js';
4
4
  import { insertImagesCommand } from '../affine-block-image/index.js';
5
5
  import { insertLatexBlockCommand } from '../affine-block-latex/index.js';
@@ -500,33 +500,22 @@ const databaseToolGroup = {
500
500
  {
501
501
  name: 'Table view',
502
502
  icon: DatabaseTableViewIcon(),
503
- showWhen: ({ std }) => std.store.schema.flavourSchemaMap.has('affine:database'),
503
+ showWhen: ({ std }) => std.store.schema.flavourSchemaMap.has('affine:database') &&
504
+ canCreateDatabase(std),
505
+ // The slash entries' gesture (ADR 0035 amendment of 2026-10-29): with
506
+ // a host picker, the popover over a rowless database, never 3 native
507
+ // rows the host would not see.
504
508
  action: ({ std }) => {
505
- std.command
506
- .chain()
507
- .pipe(getSelectedModelsCommand)
508
- .pipe(insertDatabaseBlockCommand, {
509
- viewType: viewPresets.tableViewMeta.type,
510
- place: 'after',
511
- removeEmptyLine: true,
512
- })
513
- .run();
509
+ createDatabaseByGesture(std, viewPresets.tableViewMeta.type);
514
510
  },
515
511
  },
516
512
  {
517
513
  name: 'Kanban view',
518
514
  icon: DatabaseKanbanViewIcon(),
519
- showWhen: ({ std }) => std.store.schema.flavourSchemaMap.has('affine:database'),
515
+ showWhen: ({ std }) => std.store.schema.flavourSchemaMap.has('affine:database') &&
516
+ canCreateDatabase(std),
520
517
  action: ({ std }) => {
521
- std.command
522
- .chain()
523
- .pipe(getSelectedModelsCommand)
524
- .pipe(insertDatabaseBlockCommand, {
525
- viewType: viewPresets.kanbanViewMeta.type,
526
- place: 'after',
527
- removeEmptyLine: true,
528
- })
529
- .run();
518
+ createDatabaseByGesture(std, viewPresets.kanbanViewMeta.type);
530
519
  },
531
520
  },
532
521
  ],
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@formicoidea/labre-core",
3
3
  "description": "Labre editor — the full BlockSuite-derived editor as one package.",
4
- "version": "1.0.0",
4
+ "version": "1.1.0",
5
5
  "type": "module",
6
6
  "sideEffects": false,
7
7
  "author": "lajola",