@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
@@ -1404,7 +1404,7 @@ model {
1404
1404
  link https://github.com/formicoidea/blocksuite-labre/tree/blocksuite-labre-main/packages/affine/database/block
1405
1405
  metadata {
1406
1406
  path 'packages/affine/database/block'
1407
- files '63'
1407
+ files '65'
1408
1408
  }
1409
1409
  adapters = folder 'adapters/' {
1410
1410
  link https://github.com/formicoidea/blocksuite-labre/tree/blocksuite-labre-main/packages/affine/database/block/src/adapters
@@ -1459,7 +1459,7 @@ model {
1459
1459
  link https://github.com/formicoidea/blocksuite-labre/tree/blocksuite-labre-main/packages/affine/database/block/src
1460
1460
  metadata {
1461
1461
  path 'packages/affine/database/block/src'
1462
- files '18'
1462
+ files '20'
1463
1463
  }
1464
1464
  }
1465
1465
  utils = folder 'utils/' {
@@ -3722,19 +3722,19 @@ model {
3722
3722
  features.components -[uses]-> framework.store '8 imports, 7 type-only'
3723
3723
  features.components -[uses]-> framework.sync '1 import, 1 type-only'
3724
3724
  features.components -[uses]-> model_.model_ '16 imports, 5 type-only'
3725
- features.database.block -[uses]-> features.components '8 imports, 1 type-only'
3725
+ features.database.block -[uses]-> features.components '10 imports, 1 type-only'
3726
3726
  features.database.block -[uses]-> features.database.engine '42 imports, 10 type-only'
3727
3727
  features.database.block -[uses]-> features.ext-loader '2 imports'
3728
3728
  features.database.block -[uses]-> features.inlines.preset '3 imports'
3729
3729
  features.database.block -[uses]-> features.inlines.reference '1 import'
3730
3730
  features.database.block -[uses]-> features.rich-text '2 imports, 2 type-only'
3731
- features.database.block -[uses]-> features.shared '41 imports, 7 type-only'
3731
+ features.database.block -[uses]-> features.shared '43 imports, 8 type-only'
3732
3732
  features.database.block -[uses]-> features.widgets.drag-handle '1 import'
3733
3733
  features.database.block -[uses]-> features.widgets.slash-menu '2 imports, 1 type-only'
3734
3734
  features.database.block -[uses]-> framework.global_ '16 imports'
3735
- features.database.block -[uses]-> framework.std '18 imports, 8 type-only'
3736
- features.database.block -[uses]-> framework.store '23 imports, 15 type-only'
3737
- features.database.block -[uses]-> model_.model_ '19 imports, 11 type-only'
3735
+ features.database.block -[uses]-> framework.std '19 imports, 9 type-only'
3736
+ features.database.block -[uses]-> framework.store '24 imports, 16 type-only'
3737
+ features.database.block -[uses]-> model_.model_ '21 imports, 13 type-only'
3738
3738
  features.database.editor -[uses]-> features.components '6 imports'
3739
3739
  features.database.editor -[uses]-> features.database.block '9 imports, 2 type-only'
3740
3740
  features.database.editor -[uses]-> features.database.engine '8 imports, 2 type-only'
@@ -5744,8 +5744,9 @@ model {
5744
5744
  features.database.block.src -[imports]-> features.components.caption '1 import'
5745
5745
  features.database.block.src -[imports]-> features.components.context-menu '2 imports, 1 type-only'
5746
5746
  features.database.block.src -[imports]-> features.components.drop-indicator '1 import'
5747
+ features.database.block.src -[imports]-> features.components.icons_ '1 import'
5747
5748
  features.database.block.src -[imports]-> features.components.peek '1 import'
5748
- features.database.block.src -[imports]-> features.components.toast '1 import'
5749
+ features.database.block.src -[imports]-> features.components.toast '2 imports'
5749
5750
  features.database.block.src -[imports]-> features.database.block.adapters '1 import'
5750
5751
  features.database.block.src -[imports]-> features.database.block.components '4 imports'
5751
5752
  features.database.block.src -[imports]-> features.database.block.configs '1 import'
@@ -5762,7 +5763,7 @@ model {
5762
5763
  features.database.block.src -[imports]-> features.ext-loader.src '2 imports'
5763
5764
  features.database.block.src -[imports]-> features.shared.commands '1 import'
5764
5765
  features.database.block.src -[imports]-> features.shared.consts '1 import'
5765
- features.database.block.src -[imports]-> features.shared.services '4 imports, 1 type-only'
5766
+ features.database.block.src -[imports]-> features.shared.services '6 imports, 2 type-only'
5766
5767
  features.database.block.src -[imports]-> features.shared.theme '1 import'
5767
5768
  features.database.block.src -[imports]-> features.shared.utils '4 imports, 1 type-only'
5768
5769
  features.database.block.src -[imports]-> features.widgets.drag-handle.src '1 import'
@@ -5771,9 +5772,9 @@ model {
5771
5772
  features.database.block.src -[imports]-> framework.global_.disposable '1 import'
5772
5773
  features.database.block.src -[imports]-> framework.global_.gfx '1 import'
5773
5774
  features.database.block.src -[imports]-> framework.std.inline '1 import'
5774
- features.database.block.src -[imports]-> framework.std.src '8 imports, 4 type-only'
5775
- features.database.block.src -[imports]-> framework.store.src '8 imports, 5 type-only'
5776
- features.database.block.src -[imports]-> model_.model_.src '9 imports, 7 type-only'
5775
+ features.database.block.src -[imports]-> framework.std.src '9 imports, 5 type-only'
5776
+ features.database.block.src -[imports]-> framework.store.src '9 imports, 6 type-only'
5777
+ features.database.block.src -[imports]-> model_.model_.src '11 imports, 9 type-only'
5777
5778
  features.database.block.utils -[imports]-> features.database.block.service '1 import, 1 type-only'
5778
5779
  features.database.block.utils -[imports]-> features.shared.types '1 import, 1 type-only'
5779
5780
  features.database.block.utils -[imports]-> features.shared.utils '1 import'
@@ -26,7 +26,7 @@ export declare const builtinToolbarConfig: {
26
26
  readonly tooltip: "Create Table";
27
27
  readonly tooltipWording: import("../../affine-shared/services/index.js").ChromeWording;
28
28
  readonly icon: import("lit-html").TemplateResult<1>;
29
- readonly when: ({ chain }: import("../../affine-shared/services/index.js").ToolbarContext) => boolean;
29
+ readonly when: ({ chain, std }: import("../../affine-shared/services/index.js").ToolbarContext) => boolean;
30
30
  readonly run: ({ host }: import("../../affine-shared/services/index.js").ToolbarContext) => void;
31
31
  }, {
32
32
  readonly id: "f.convert-to-linked-doc";
@@ -1,4 +1,4 @@
1
- import { convertToDatabase, DATABASE_CONVERT_WHITE_LIST, } from '../../affine-database-block/index.js';
1
+ import { canConvertToDatabase, convertToDatabase, DATABASE_CONVERT_WHITE_LIST, } from '../../affine-database-block/index.js';
2
2
  import { convertSelectedBlocksToLinkedDoc, getTitleFromSelectedModels, notifyDocCreated, promptDocTitle, } from '../../affine-block-embed/index.js';
3
3
  import { updateBlockType } from '../../affine-block-note/index.js';
4
4
  import { toast } from '../../affine-components/toast/index.js';
@@ -129,7 +129,9 @@ const turnIntoDatabase = {
129
129
  tooltip: 'Create Table',
130
130
  tooltipWording: ROOT_TOOLBAR_CREATE_TABLE,
131
131
  icon: DatabaseTableViewIcon(),
132
- when({ chain }) {
132
+ when({ chain, std }) {
133
+ if (!canConvertToDatabase(std))
134
+ return false;
133
135
  const middleware = (count = 0) => {
134
136
  return (ctx, next) => {
135
137
  const { selectedBlocks } = ctx;
@@ -23,6 +23,7 @@ export declare const menu: {
23
23
  postfix?: import("lit-html").TemplateResult;
24
24
  prefix?: import("lit-html").TemplateResult;
25
25
  class?: string;
26
+ testId?: string;
26
27
  options: import("./menu.js").MenuOptions;
27
28
  disableArrow?: boolean;
28
29
  hide?: () => boolean;
@@ -6,6 +6,8 @@ export type MenuSubMenuData = {
6
6
  options: MenuOptions;
7
7
  select?: () => void;
8
8
  class?: string;
9
+ /** Names the entry itself; `options.testId` names the menu it opens. */
10
+ testId?: string;
9
11
  };
10
12
  export declare const subMenuOffset: {
11
13
  name: string;
@@ -49,6 +51,7 @@ export declare const subMenuItems: {
49
51
  postfix?: TemplateResult;
50
52
  prefix?: TemplateResult;
51
53
  class?: string;
54
+ testId?: string;
52
55
  options: MenuOptions;
53
56
  disableArrow?: boolean;
54
57
  hide?: () => boolean;
@@ -38,6 +38,7 @@ import { autoPlacement, autoUpdate, computePosition, offset, } from '@floating-u
38
38
  import { html, nothing } from 'lit';
39
39
  import { property } from 'lit/decorators.js';
40
40
  import { classMap } from 'lit/directives/class-map.js';
41
+ import { ifDefined } from 'lit/directives/if-defined.js';
41
42
  import { MenuFocusable } from './focusable.js';
42
43
  import { Menu } from './menu.js';
43
44
  import { popMenu, popupTargetFromElement } from './menu-renderer.js';
@@ -121,7 +122,12 @@ let MenuSubMenu = (() => {
121
122
  'affine-menu-button': true,
122
123
  focused: this.isFocused$.value,
123
124
  });
124
- return html ` <div class="${classString}">${this.data.content()}</div>`;
125
+ return html ` <div
126
+ class="${classString}"
127
+ data-testid=${ifDefined(this.data.testId)}
128
+ >
129
+ ${this.data.content()}
130
+ </div>`;
125
131
  }
126
132
  #data_accessor_storage;
127
133
  get data() { return this.#data_accessor_storage; }
@@ -182,7 +188,12 @@ let MobileSubMenu = (() => {
182
188
  'mobile-menu-button': true,
183
189
  focused: this.isFocused$.value,
184
190
  });
185
- return html ` <div class="${classString}">${this.data.content()}</div>`;
191
+ return html ` <div
192
+ class="${classString}"
193
+ data-testid=${ifDefined(this.data.testId)}
194
+ >
195
+ ${this.data.content()}
196
+ </div>`;
186
197
  }
187
198
  #data_accessor_storage = __runInitializers(this, _data_initializers, void 0);
188
199
  get data() { return this.#data_accessor_storage; }
@@ -219,6 +230,7 @@ export const subMenuItems = {
219
230
  ${config.postfix}
220
231
  ${config.disableArrow ? nothing : ArrowRightSmallIcon()} `,
221
232
  class: config.class,
233
+ testId: config.testId,
222
234
  options: config.options,
223
235
  };
224
236
  return renderSubMenu(data, menu);
@@ -1,4 +1,28 @@
1
1
  import type { SlashMenuConfig } from '../../affine-widget-slash-menu/index.js';
2
+ import type { BlockStdScope } from '../../std/index.js';
3
+ /**
4
+ * With a data-source picker the host owns database creation (ADR 0035 §5):
5
+ * its `createDatabase` is the only way in, and without it no entry creates
6
+ * one. Without a picker, a native inline database, as in 0.48.
7
+ */
8
+ export declare function canCreateDatabase(std: BlockStdScope): boolean;
9
+ /**
10
+ * Three ways in, by what is registered (ADR 0035 §5, amendment of
11
+ * 2026-10-28):
12
+ *
13
+ * - a picker and the bundle's creation menu: the database lands at once,
14
+ * native and with no row, and the menu opens over it to connect it to a
15
+ * source (`connectDatabaseToSource`). Dismissed, the native database stays
16
+ * and renders: nothing is lost by closing the popover;
17
+ * - a picker alone (a core-only host): the host's `createDatabase` first,
18
+ * then a view of its first source, as in 0.61;
19
+ * - no picker: a native inline database with 3 rows, as in 0.48.
20
+ *
21
+ * The one creation gesture: the slash entries and the mobile keyboard
22
+ * toolbar's "Table view" / "Kanban view" both run it (ADR 0035 amendment of
23
+ * 2026-10-29), so neither inserts native rows a host's picker would not see.
24
+ */
25
+ export declare function createDatabaseByGesture(std: BlockStdScope, viewType: string): void;
2
26
  /**
3
27
  * The slash entries that CREATE a database, registered by the gated
4
28
  * `DatabaseViewExtension` only. No Calendar entry after 0.48 (ADR 0035 §6):
@@ -12,7 +12,7 @@ import { KanbanViewTooltip, TableViewTooltip } from './tooltips.js';
12
12
  * its `createDatabase` is the only way in, and without it no entry creates
13
13
  * one. Without a picker, a native inline database, as in 0.48.
14
14
  */
15
- function canCreateDatabase(std) {
15
+ export function canCreateDatabase(std) {
16
16
  const picker = std.getOptional(DataSourcePickerProvider);
17
17
  return !picker || !!picker.createDatabase;
18
18
  }
@@ -55,38 +55,45 @@ function insertNativeDatabase(std, viewType, initialRows) {
55
55
  * - a picker alone (a core-only host): the host's `createDatabase` first,
56
56
  * then a view of its first source, as in 0.61;
57
57
  * - no picker: a native inline database with 3 rows, as in 0.48.
58
+ *
59
+ * The one creation gesture: the slash entries and the mobile keyboard
60
+ * toolbar's "Table view" / "Kanban view" both run it (ADR 0035 amendment of
61
+ * 2026-10-29), so neither inserts native rows a host's picker would not see.
58
62
  */
59
- function createDatabaseAction(viewType) {
60
- return ({ std }) => {
61
- const picker = std.getOptional(DataSourcePickerProvider);
62
- if (!picker) {
63
- insertNativeDatabase(std, viewType);
64
- return;
65
- }
66
- const menu = std.getOptional(DatabaseCreationMenuIdentifier);
67
- if (!menu) {
68
- const [, { selectedModels }] = std.command.exec(getSelectedModelsCommand);
69
- const after = selectedModels?.at(-1);
70
- if (!after)
71
- return;
72
- createHostedDatabaseView(std, picker, {
73
- after,
74
- viewType,
75
- removeEmptyLine: true,
76
- }).catch(console.error);
77
- return;
78
- }
79
- const blockId = insertNativeDatabase(std, viewType, 0);
80
- if (!blockId)
63
+ export function createDatabaseByGesture(std, viewType) {
64
+ if (!canCreateDatabase(std))
65
+ return;
66
+ const picker = std.getOptional(DataSourcePickerProvider);
67
+ if (!picker) {
68
+ insertNativeDatabase(std, viewType);
69
+ return;
70
+ }
71
+ const menu = std.getOptional(DatabaseCreationMenuIdentifier);
72
+ if (!menu) {
73
+ const [, { selectedModels }] = std.command.exec(getSelectedModelsCommand);
74
+ const after = selectedModels?.at(-1);
75
+ if (!after)
81
76
  return;
82
- std.host.updateComplete
83
- .then(() => {
84
- const anchor = std.view.getBlock(blockId);
85
- if (anchor)
86
- menu.open({ std, blockId, anchor });
87
- })
88
- .catch(console.error);
89
- };
77
+ createHostedDatabaseView(std, picker, {
78
+ after,
79
+ viewType,
80
+ removeEmptyLine: true,
81
+ }).catch(console.error);
82
+ return;
83
+ }
84
+ const blockId = insertNativeDatabase(std, viewType, 0);
85
+ if (!blockId)
86
+ return;
87
+ std.host.updateComplete
88
+ .then(() => {
89
+ const anchor = std.view.getBlock(blockId);
90
+ if (anchor)
91
+ menu.open({ std, blockId, anchor });
92
+ })
93
+ .catch(console.error);
94
+ }
95
+ function createDatabaseAction(viewType) {
96
+ return ({ std }) => createDatabaseByGesture(std, viewType);
90
97
  }
91
98
  /**
92
99
  * The slash entries that CREATE a database, registered by the gated
@@ -7,6 +7,12 @@ export interface DatabaseCreationRequest {
7
7
  blockId: string;
8
8
  /** The block's element, to hang the menu from. */
9
9
  anchor: HTMLElement;
10
+ /**
11
+ * The view to connect, when the block already has a source the host no
12
+ * longer resolves ("Connect a source" of its empty view, ADR 0035
13
+ * amendment of 2026-10-29). Absent: the block's first view.
14
+ */
15
+ viewId?: string;
10
16
  }
11
17
  /**
12
18
  * What a database creation entry opens over the block it just inserted
@@ -86,5 +86,11 @@ export declare class DatabaseBlockDataSource extends HostedDataSourceBase {
86
86
  * to be chosen (the creation popover, ADR 0035 amendment of 2026-10-28).
87
87
  */
88
88
  export declare const databaseViewInitTemplate: (datasource: DatabaseBlockDataSource, viewType: string, std?: BlockStdScope, rows?: number) => void;
89
+ /**
90
+ * "Create Table": the selected blocks become a database's rows. Under a host
91
+ * that requires a source they become a source of the host's instead
92
+ * (`turnBlocksIntoSource`, ADR 0035 amendment of 2026-10-29), never a
93
+ * native database it would show read-only.
94
+ */
89
95
  export declare const convertToDatabase: (host: EditorHost, viewType: string) => void;
90
96
  export {};
@@ -7,6 +7,7 @@ import { computed, signal } from '@preact/signals-core';
7
7
  import { getIcon } from './block-icons.js';
8
8
  import { HostedDataSourceBase } from './external-data-source.js';
9
9
  import { databaseBlockProperties, databasePropertyConverts, } from './properties/index.js';
10
+ import { canTurnIntoSource, turnBlocksIntoSource } from './turn-into-source.js';
10
11
  import { addProperty, copyCellsByProperty, deleteRows, getCell, getProperty, updateCell, updateCells, updateProperty, } from './utils/block-utils.js';
11
12
  /**
12
13
  * The inline blob source: rows are the block's children, cells/columns live in
@@ -405,6 +406,12 @@ export const databaseViewInitTemplate = (datasource, viewType, std, rows = 3) =>
405
406
  });
406
407
  datasource.viewManager.viewAdd(viewType, viewDefaultName(datasource.viewMetaGet(viewType), std));
407
408
  };
409
+ /**
410
+ * "Create Table": the selected blocks become a database's rows. Under a host
411
+ * that requires a source they become a source of the host's instead
412
+ * (`turnBlocksIntoSource`, ADR 0035 amendment of 2026-10-29), never a
413
+ * native database it would show read-only.
414
+ */
408
415
  export const convertToDatabase = (host, viewType) => {
409
416
  const [_, ctx] = host.std.command.exec(getSelectedModelsCommand, {
410
417
  types: ['block', 'text'],
@@ -413,6 +420,15 @@ export const convertToDatabase = (host, viewType) => {
413
420
  const firstModel = selectedModels?.[0];
414
421
  if (!firstModel)
415
422
  return;
423
+ if (canTurnIntoSource(host.std)) {
424
+ turnBlocksIntoSource(host.std, selectedModels, viewType)
425
+ .then(written => {
426
+ if (written)
427
+ host.selection.clear();
428
+ })
429
+ .catch(console.error);
430
+ return;
431
+ }
416
432
  host.store.captureSync();
417
433
  const parentModel = host.store.getParent(firstModel);
418
434
  if (!parentModel) {
@@ -60,6 +60,29 @@ export declare class DatabaseBlockComponent extends CaptionedBlockComponent<Data
60
60
  get isCommentHighlighted(): boolean;
61
61
  get topContenteditableElement(): BlockComponent<import("../store/index.js").BlockModel<object>, import("../std/index.js").BlockService, string> | null;
62
62
  private readonly renderSourceMissing;
63
+ /**
64
+ * A database a host that requires a source must connect first
65
+ * (`sourceRequirement` is `'connect'`, ADR 0035 amendment of 2026-10-29):
66
+ * no source and no row, or a hosted source the host no longer resolves.
67
+ * Drawn in place of the tools and the body: no row, no "New", no cell to
68
+ * type or paste into, so nothing is written. A hosted block keeps its view
69
+ * bar, since another of its views may still read a source. The button
70
+ * reopens the creation popover over the block, for the current view; a
71
+ * read-only document, or a host without the bundle's creation menu, gets
72
+ * the notice alone. Takes precedence over "Source not found" (§7).
73
+ */
74
+ private readonly renderSourceRequired;
75
+ /** "Move into a database" is waiting for the host: one call at a time. */
76
+ private readonly moving$;
77
+ private readonly moveIntoSource;
78
+ /**
79
+ * Native rows under a host that requires a source (`sourceRequirement` is
80
+ * `'read-only'`): the rows render, locked (`locked$`), with this notice and,
81
+ * where the host turns content into a source (`createDatabaseFrom`) and
82
+ * the document is writable, "Move into a database"; a spinner while the
83
+ * host answers.
84
+ */
85
+ private renderReadOnlyNotice;
63
86
  private renderDatabaseOps;
64
87
  connectedCallback(): void;
65
88
  disconnectedCallback(): void;
@@ -2,10 +2,11 @@ import { CaptionedBlockComponent } from '../affine-components/caption/index.js';
2
2
  import { menu, popMenu, popupTargetFromElement, } from '../affine-components/context-menu/index.js';
3
3
  import { DropIndicator } from '../affine-components/drop-indicator/index.js';
4
4
  import { PeekViewProvider } from '../affine-components/peek/index.js';
5
+ import { LoadingIcon } from '../affine-components/icons/index.js';
5
6
  import { toast } from '../affine-components/toast/index.js';
6
7
  import { EDGELESS_TOP_CONTENTEDITABLE_SELECTOR } from '../affine-shared/consts/index.js';
7
8
  import { getScrollContainer } from '../affine-shared/utils/index.js';
8
- import { BlockCommentManager, CommentProviderIdentifier, DocModeProvider, NotificationProvider, PropertiesPanelProvider, TelemetryProvider, TOAST_COPIED_TO_CLIPBOARD, translateKey, } from '../affine-shared/services/index.js';
9
+ import { BlockCommentManager, CommentProviderIdentifier, DataSourcePickerProvider, DocModeProvider, NotificationProvider, PropertiesPanelProvider, TelemetryProvider, TOAST_COPIED_TO_CLIPBOARD, translateKey, } from '../affine-shared/services/index.js';
9
10
  import { getDropResult } from '../affine-widget-drag-handle/index.js';
10
11
  import { createRecordDetail, createUniComponentFromWebComponent, DataViewRootUILogic, DataViewEventTraceKey, defineUniComponent, ExternalGroupByConfigProvider, NewRowMenuOpenerKey, PageLayoutKey, PropertiesPanelOpenerKey, renderUniLit, traceViewEvent, uniMap, ViewCreationOffKey, } from '../affine-database-engine/index.js';
11
12
  import { CalendarExternalSourceProvider } from '../affine-database-engine/view-presets/index.js';
@@ -21,16 +22,19 @@ import { html, nothing } from 'lit';
21
22
  import { keyed } from 'lit/directives/keyed.js';
22
23
  import { popSideDetail } from './components/layout.js';
23
24
  import { DatabaseConfigExtension } from './config.js';
25
+ import { DatabaseCreationMenuIdentifier } from './creation-menu.js';
24
26
  import { EditorHostKey } from './context/host-context.js';
25
27
  import { DatabaseBlockDataSource } from './data-source.js';
26
28
  import { HostedDataSourceBase } from './external-data-source.js';
27
29
  import { databaseBlockStyles, databaseContentStyles, databaseHeaderBarStyles, databaseHeaderContainerStyles, databaseOpsStyles, databaseSourceMissingHintStyles, databaseSourceMissingStyles, databaseTitleRowStyles, databaseTitleStyles, databaseToolbarRowStyles, databaseViewBarContainerStyles, } from './database-block-styles.js';
28
30
  import { BlockRenderer } from './detail-panel/block-renderer.js';
29
31
  import { NoteRenderer } from './detail-panel/note-renderer.js';
32
+ import { sourceRequirement } from './linked-view.js';
30
33
  import { DatabaseSelection } from './selection.js';
31
34
  import { isStandaloneDatabase } from './standalone-layout.js';
32
35
  import { databaseOpenRowRequest, DatabaseDataSourceProvider, DatabaseInitialViewProvider, DatabaseNewRowMenuProvider, DatabaseOpenRowProvider, openDatabasePropertiesPanel, rowOpenedParams, } from './service/index.js';
33
- import { DATABASE_NEW_RECORD_MORE, DATABASE_SOURCE_MISSING, DATABASE_SOURCE_MISSING_HINT, } from './translations.js';
36
+ import { DATABASE_CREATION_TITLE, DATABASE_MOVE_INTO_SOURCE, DATABASE_NEW_RECORD_MORE, DATABASE_SOURCE_MISSING, DATABASE_SOURCE_MISSING_HINT, DATABASE_SOURCE_REQUIRED, DATABASE_SOURCE_REQUIRED_HINT, DATABASE_SOURCE_REQUIRED_READ_ONLY, } from './translations.js';
37
+ import { canTurnIntoSource, moveDatabaseRowsIntoSource, } from './turn-into-source.js';
34
38
  import { currentViewStorage, initialViewId } from './utils/current-view.js';
35
39
  import { getSingleDocIdFromText } from './utils/title-doc.js';
36
40
  import { DatabaseViewCreationIdentifier } from './view-creation.js';
@@ -146,6 +150,7 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent {
146
150
  ${renderUniLit(this.toolsWidget, props)}
147
151
  </div>
148
152
  ${renderUniLit(widgetPresets.quickSettingBar, props)}
153
+ ${this.renderReadOnlyNotice()}
149
154
  <affine-database-values-banner
150
155
  .dataSource="${this.dataSource}"
151
156
  .std="${this.std}"
@@ -277,6 +282,77 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent {
277
282
  </div>
278
283
  </div>`;
279
284
  };
285
+ /**
286
+ * A database a host that requires a source must connect first
287
+ * (`sourceRequirement` is `'connect'`, ADR 0035 amendment of 2026-10-29):
288
+ * no source and no row, or a hosted source the host no longer resolves.
289
+ * Drawn in place of the tools and the body: no row, no "New", no cell to
290
+ * type or paste into, so nothing is written. A hosted block keeps its view
291
+ * bar, since another of its views may still read a source. The button
292
+ * reopens the creation popover over the block, for the current view; a
293
+ * read-only document, or a host without the bundle's creation menu, gets
294
+ * the notice alone. Takes precedence over "Source not found" (§7).
295
+ */
296
+ this.renderSourceRequired = (dataViewLogic) => {
297
+ const dataSource = this.dataSource;
298
+ const sourceMissing = dataSource instanceof HostedDataSourceBase &&
299
+ dataSource.sourceMissing$.value;
300
+ if (sourceRequirement(this.std, this.model, sourceMissing) !== 'connect') {
301
+ return undefined;
302
+ }
303
+ const hosted = this.model.props.externalSourceId !== undefined;
304
+ const creationMenu = this.std.getOptional(DatabaseCreationMenuIdentifier);
305
+ const connect = creationMenu && !this.store.readonly$.value
306
+ ? () => creationMenu.open({
307
+ std: this.std,
308
+ blockId: this.blockId,
309
+ anchor: this,
310
+ viewId: dataViewLogic.view.id,
311
+ })
312
+ : undefined;
313
+ return html `<div class="${databaseHeaderContainerStyles}">
314
+ <div class="${databaseTitleRowStyles}">
315
+ ${this.renderTitle(dataViewLogic)}
316
+ </div>
317
+ ${hosted
318
+ ? html `<div
319
+ class="${databaseToolbarRowStyles} ${databaseHeaderBarStyles}"
320
+ >
321
+ ${this.renderViewBar({ dataViewLogic })}
322
+ </div>`
323
+ : nothing}
324
+ </div>
325
+ <div
326
+ class="${databaseSourceMissingStyles}"
327
+ data-testid="database-source-required"
328
+ role="status"
329
+ >
330
+ <div>${translateKey(this.std, ...DATABASE_SOURCE_REQUIRED)}</div>
331
+ <div class="${databaseSourceMissingHintStyles}">
332
+ ${translateKey(this.std, ...DATABASE_SOURCE_REQUIRED_HINT)}
333
+ </div>
334
+ ${connect
335
+ ? html `<data-view-component-button
336
+ data-testid="database-source-required-connect"
337
+ hoverType="border"
338
+ .onClick="${connect}"
339
+ .text="${translateKey(this.std, ...DATABASE_CREATION_TITLE)}"
340
+ ></data-view-component-button>`
341
+ : nothing}
342
+ </div>`;
343
+ };
344
+ /** "Move into a database" is waiting for the host: one call at a time. */
345
+ this.moving$ = signal(false);
346
+ this.moveIntoSource = () => {
347
+ if (this.moving$.peek())
348
+ return;
349
+ this.moving$.value = true;
350
+ moveDatabaseRowsIntoSource(this.std, this.model)
351
+ .catch(console.error)
352
+ .finally(() => {
353
+ this.moving$.value = false;
354
+ });
355
+ };
280
356
  this.#useZeroWidth_accessor_storage = true;
281
357
  }
282
358
  _buildDataSource() {
@@ -303,6 +379,12 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent {
303
379
  },
304
380
  });
305
381
  }
382
+ // A view of a host database is deleted through the host's request
383
+ // (`deleteView`, ADR 0035 amendment of 2026-10-29); the data source
384
+ // reads the picker from its own scope, below the editor's.
385
+ const picker = this.std.getOptional(DataSourcePickerProvider);
386
+ if (picker)
387
+ dataSource.serviceSet(DataSourcePickerProvider, picker);
306
388
  // `{ database: false }` takes view creation away, never the views.
307
389
  if (!this.std.getOptional(DatabaseViewCreationIdentifier)) {
308
390
  dataSource.serviceSet(ViewCreationOffKey, true);
@@ -325,6 +407,12 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent {
325
407
  const id = initialViewId(this.model.id, viewId => dataSource.viewManager.viewGet(viewId) != null, this.std.getOptional(DatabaseInitialViewProvider));
326
408
  if (id)
327
409
  dataSource.viewManager.setCurrentView(id);
410
+ // A native database under a host that requires a source is never edited
411
+ // natively, whatever its row count (`sourceRequirement`).
412
+ if (dataSource instanceof DatabaseBlockDataSource &&
413
+ sourceRequirement(this.std, this.model) !== 'none') {
414
+ dataSource.locked$.value = true;
415
+ }
328
416
  return dataSource;
329
417
  }
330
418
  get dataSource() {
@@ -429,6 +517,40 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent {
429
517
  }
430
518
  return this.rootComponent;
431
519
  }
520
+ /**
521
+ * Native rows under a host that requires a source (`sourceRequirement` is
522
+ * `'read-only'`): the rows render, locked (`locked$`), with this notice and,
523
+ * where the host turns content into a source (`createDatabaseFrom`) and
524
+ * the document is writable, "Move into a database"; a spinner while the
525
+ * host answers.
526
+ */
527
+ renderReadOnlyNotice() {
528
+ if (sourceRequirement(this.std, this.model) !== 'read-only') {
529
+ return nothing;
530
+ }
531
+ const canMove = canTurnIntoSource(this.std) && !this.store.readonly$.value;
532
+ const moving = this.moving$.value;
533
+ return html `<div
534
+ class="${databaseSourceMissingStyles}"
535
+ data-testid="database-source-required-read-only"
536
+ role="status"
537
+ >
538
+ <div>${translateKey(this.std, ...DATABASE_SOURCE_REQUIRED)}</div>
539
+ <div class="${databaseSourceMissingHintStyles}">
540
+ ${translateKey(this.std, ...DATABASE_SOURCE_REQUIRED_READ_ONLY)}
541
+ </div>
542
+ ${canMove
543
+ ? html `<data-view-component-button
544
+ data-testid="database-source-required-move"
545
+ hoverType="border"
546
+ aria-busy="${moving}"
547
+ .icon="${moving ? LoadingIcon() : undefined}"
548
+ .onClick="${this.moveIntoSource}"
549
+ .text="${translateKey(this.std, ...DATABASE_MOVE_INTO_SOURCE)}"
550
+ ></data-view-component-button>`
551
+ : nothing}
552
+ </div>`;
553
+ }
432
554
  renderDatabaseOps() {
433
555
  if (this.dataSource.readonly$.value) {
434
556
  return nothing;
@@ -509,7 +631,7 @@ export class DatabaseBlockComponent extends CaptionedBlockComponent {
509
631
  setSelection: this.setSelection,
510
632
  dataSource: this.dataSource,
511
633
  headerWidget: this.headerWidget,
512
- viewPlaceholder: this.renderSourceMissing,
634
+ viewPlaceholder: logic => this.renderSourceRequired(logic) ?? this.renderSourceMissing(logic),
513
635
  onDrag: this.onDrag,
514
636
  clipboard: this.std.clipboard,
515
637
  dnd: this.std.dnd,
@@ -114,6 +114,13 @@ export declare abstract class HostedDataSourceBase extends DataSourceBase {
114
114
  propertyDataGet(propertyId: string): Record<string, unknown>;
115
115
  propertyDataTypeGet(propertyId: string): TypeInstance | undefined;
116
116
  featureFlags$: ReadonlySignal<DatabaseFlags>;
117
+ /**
118
+ * Refuses every edit on a writable store: a database with no source, under
119
+ * a host that requires one, shows its native rows read-only (ADR 0035
120
+ * amendment of 2026-10-29). Every write path of the view layer reads
121
+ * `readonly$` (cells, paste, drag, "New", view and property menus).
122
+ */
123
+ readonly locked$: import("@preact/signals-core").Signal<boolean>;
117
124
  readonly$: ReadonlySignal<boolean>;
118
125
  viewConverts: import("./index.js").ViewConvertConfig[];
119
126
  viewMetas: ViewMeta[];