@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.
- package/architecture/code.c4 +13 -12
- package/dist/_pkgs/affine-block-root/configs/toolbar.d.ts +1 -1
- package/dist/_pkgs/affine-block-root/configs/toolbar.js +4 -2
- package/dist/_pkgs/affine-components/context-menu/menu-all.d.ts +1 -0
- package/dist/_pkgs/affine-components/context-menu/sub-menu.d.ts +3 -0
- package/dist/_pkgs/affine-components/context-menu/sub-menu.js +14 -2
- package/dist/_pkgs/affine-database-block/configs/slash-menu.d.ts +24 -0
- package/dist/_pkgs/affine-database-block/configs/slash-menu.js +38 -31
- package/dist/_pkgs/affine-database-block/creation-menu.d.ts +6 -0
- package/dist/_pkgs/affine-database-block/data-source.d.ts +6 -0
- package/dist/_pkgs/affine-database-block/data-source.js +16 -0
- package/dist/_pkgs/affine-database-block/database-block.d.ts +23 -0
- package/dist/_pkgs/affine-database-block/database-block.js +125 -3
- package/dist/_pkgs/affine-database-block/external-data-source.d.ts +7 -0
- package/dist/_pkgs/affine-database-block/external-data-source.js +30 -4
- package/dist/_pkgs/affine-database-block/index.d.ts +3 -0
- package/dist/_pkgs/affine-database-block/index.js +5 -0
- package/dist/_pkgs/affine-database-block/linked-view.d.ts +59 -0
- package/dist/_pkgs/affine-database-block/linked-view.js +131 -1
- package/dist/_pkgs/affine-database-block/translations.d.ts +18 -0
- package/dist/_pkgs/affine-database-block/translations.js +47 -0
- package/dist/_pkgs/affine-database-block/turn-into-source.d.ts +38 -0
- package/dist/_pkgs/affine-database-block/turn-into-source.js +187 -0
- package/dist/_pkgs/affine-database-block/view-deletion.d.ts +15 -0
- package/dist/_pkgs/affine-database-block/view-deletion.js +113 -0
- package/dist/_pkgs/affine-shared/services/data-source-picker-service.d.ts +99 -1
- package/dist/_pkgs/affine-widget-keyboard-toolbar/config.js +10 -21
- package/package.json +1 -1
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { FeatureFlagService } from '../affine-shared/services/index.js';
|
|
1
|
+
import { DataSourcePickerProvider, FeatureFlagService, } from '../affine-shared/services/index.js';
|
|
2
2
|
import { DataSourceBase, ViewManagerBase, } from '../affine-database-engine/index.js';
|
|
3
3
|
import { DisposableGroup } from '../global/disposable/index.js';
|
|
4
4
|
import { computed, signal } from '@preact/signals-core';
|
|
5
5
|
import { deleteView, duplicateView, moveViewTo, updateView, } from './utils/block-utils.js';
|
|
6
|
+
import { deleteViewThroughHost } from './view-deletion.js';
|
|
6
7
|
import { databaseBlockViewConverts, databaseBlockViewMetaGet, databaseBlockViews, } from './views/index.js';
|
|
7
8
|
/**
|
|
8
9
|
* Intermediate base for `affine:database` data sources whose **view config**
|
|
@@ -87,10 +88,17 @@ export class HostedDataSourceBase extends DataSourceBase {
|
|
|
87
88
|
enable_table_virtual_scroll: featureFlagService.getFlag('enable_table_virtual_scroll') ?? false,
|
|
88
89
|
};
|
|
89
90
|
});
|
|
91
|
+
/**
|
|
92
|
+
* Refuses every edit on a writable store: a database with no source, under
|
|
93
|
+
* a host that requires one, shows its native rows read-only (ADR 0035
|
|
94
|
+
* amendment of 2026-10-29). Every write path of the view layer reads
|
|
95
|
+
* `readonly$` (cells, paste, drag, "New", view and property menus).
|
|
96
|
+
*/
|
|
97
|
+
this.locked$ = signal(false);
|
|
90
98
|
// Mobile edits too (ADR 0035 §1, B5): the table and kanban presets ship
|
|
91
99
|
// their own touch renderers, chosen by `DataViewRootUILogic`.
|
|
92
100
|
this.readonly$ = computed(() => {
|
|
93
|
-
return this._model.store.readonly;
|
|
101
|
+
return this._model.store.readonly || this.locked$.value;
|
|
94
102
|
});
|
|
95
103
|
this.viewConverts = databaseBlockViewConverts;
|
|
96
104
|
this.viewMetas = databaseBlockViews;
|
|
@@ -185,8 +193,26 @@ export class HostedDataSourceBase extends DataSourceBase {
|
|
|
185
193
|
return viewData.id;
|
|
186
194
|
}
|
|
187
195
|
viewDataDelete(viewId) {
|
|
188
|
-
|
|
189
|
-
|
|
196
|
+
deleteViewThroughHost({
|
|
197
|
+
model: this._model,
|
|
198
|
+
viewId,
|
|
199
|
+
picker: this.serviceGet(DataSourcePickerProvider),
|
|
200
|
+
removeNow: () => {
|
|
201
|
+
this._model.store.captureSync();
|
|
202
|
+
deleteView(this._model, viewId);
|
|
203
|
+
},
|
|
204
|
+
// The view manager moved on when the gesture was made; a removal the
|
|
205
|
+
// host made later must not leave a vanished view current.
|
|
206
|
+
afterRemove: () => {
|
|
207
|
+
const manager = this.viewManager;
|
|
208
|
+
const views = manager.views$.peek();
|
|
209
|
+
const current = manager.currentViewId$.peek();
|
|
210
|
+
const next = views[0];
|
|
211
|
+
if (current !== undefined && !views.includes(current) && next) {
|
|
212
|
+
manager.setCurrentView(next);
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
});
|
|
190
216
|
}
|
|
191
217
|
viewDataDuplicate(id) {
|
|
192
218
|
return duplicateView(this._model, id);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export * from './adapters/index.js';
|
|
2
2
|
export * from './commands.js';
|
|
3
|
+
export { canCreateDatabase, createDatabaseByGesture, } from './configs/slash-menu.js';
|
|
3
4
|
export * from './config.js';
|
|
4
5
|
export * from './context/index.js';
|
|
5
6
|
export * from './creation-menu.js';
|
|
@@ -17,3 +18,5 @@ export * from './view-creation.js';
|
|
|
17
18
|
export * from './views/index.js';
|
|
18
19
|
export * from '../affine-database-engine/index.js';
|
|
19
20
|
export * from './linked-view.js';
|
|
21
|
+
export * from './turn-into-source.js';
|
|
22
|
+
export { DATABASE_CREATION_TITLE } from './translations.js';
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export * from './adapters/index.js';
|
|
2
2
|
export * from './commands.js';
|
|
3
|
+
export { canCreateDatabase, createDatabaseByGesture, } from './configs/slash-menu.js';
|
|
3
4
|
export * from './config.js';
|
|
4
5
|
export * from './context/index.js';
|
|
5
6
|
export * from './creation-menu.js';
|
|
@@ -17,3 +18,7 @@ export * from './view-creation.js';
|
|
|
17
18
|
export * from './views/index.js';
|
|
18
19
|
export * from '../affine-database-engine/index.js';
|
|
19
20
|
export * from './linked-view.js';
|
|
21
|
+
export * from './turn-into-source.js';
|
|
22
|
+
// The creation popover (`@labre/affine-database-editor`) titles itself with
|
|
23
|
+
// the wording core declares for its "Connect a source" button.
|
|
24
|
+
export { DATABASE_CREATION_TITLE } from './translations.js';
|
|
@@ -47,6 +47,65 @@ export declare function connectDatabaseToSource(std: BlockStdScope, model: Datab
|
|
|
47
47
|
database: Pick<DataSourcePickerDatabase, 'id' | 'title'>;
|
|
48
48
|
sourceId: string;
|
|
49
49
|
}): boolean;
|
|
50
|
+
/**
|
|
51
|
+
* What a database shows under a host that requires a source
|
|
52
|
+
* (`requireSource`, ADR 0035 amendment of 2026-10-29, product owner's
|
|
53
|
+
* decision of the same day). The SOURCE decides, never the row count:
|
|
54
|
+
*
|
|
55
|
+
* - `'none'`: the host does not require one, or the block reads a source it
|
|
56
|
+
* resolves. A hosted database with no row is an ordinary empty table.
|
|
57
|
+
* - `'connect'`: no source and no row (the creation gesture left
|
|
58
|
+
* unfinished), or a hosted source the host no longer resolves (the source
|
|
59
|
+
* or its base deleted). An empty view offering "Connect a source".
|
|
60
|
+
* - `'read-only'`: no source but native rows (a database written before
|
|
61
|
+
* 1.0, by a host without a picker, or pasted from one). The rows render,
|
|
62
|
+
* since documents render forever, and nothing native can be edited.
|
|
63
|
+
*
|
|
64
|
+
* Nothing is stored: a native database and the gesture left unfinished are
|
|
65
|
+
* the same bytes, so neither may be edited natively.
|
|
66
|
+
*/
|
|
67
|
+
export type SourceRequirement = 'none' | 'connect' | 'read-only';
|
|
68
|
+
export declare function sourceRequirement(std: BlockStdScope, model: DatabaseBlockModel, sourceMissing?: boolean): SourceRequirement;
|
|
69
|
+
/**
|
|
70
|
+
* Re-points one view of a hosted database whose source the host no longer
|
|
71
|
+
* resolves (ADR 0035 amendment of 2026-10-29): the block takes the picked
|
|
72
|
+
* database's `externalSourceId` when it differs, and the view reads
|
|
73
|
+
* `sourceId` with its data re-derived over it (`defaultViewData`: its id
|
|
74
|
+
* and name kept, its layout too where the preset can build over the new
|
|
75
|
+
* source). The other views are left as they are. One undo step. Returns
|
|
76
|
+
* `false` and writes nothing on a read-only document, on a block with no
|
|
77
|
+
* source (`connectDatabaseToSource` connects those), without a host data
|
|
78
|
+
* source or without that view.
|
|
79
|
+
*/
|
|
80
|
+
export declare function reconnectDatabaseView(std: BlockStdScope, model: DatabaseBlockModel, choice: {
|
|
81
|
+
database: Pick<DataSourcePickerDatabase, 'id'>;
|
|
82
|
+
sourceId: string;
|
|
83
|
+
viewId: string;
|
|
84
|
+
}): boolean;
|
|
85
|
+
/**
|
|
86
|
+
* Whether "Create Table" (`convertToDatabase`) may run. Where no source is
|
|
87
|
+
* required, it moves the blocks into a native database, as in 1.0. Where
|
|
88
|
+
* one is, it turns them into a source of the host's (`createDatabaseFrom`,
|
|
89
|
+
* ADR 0035 amendment of 2026-10-29), and is hidden without that method: a
|
|
90
|
+
* native database would be shown read-only at once.
|
|
91
|
+
*/
|
|
92
|
+
export declare function canConvertToDatabase(std: BlockStdScope): boolean;
|
|
93
|
+
/**
|
|
94
|
+
* Moves a database with native rows into the host source just created from
|
|
95
|
+
* them (`createDatabaseFrom`, ADR 0035 amendment of 2026-10-29): the block
|
|
96
|
+
* takes the database's `externalSourceId`, its title when it has none, an
|
|
97
|
+
* empty schema of its own; EVERY view is kept by id, name and layout and
|
|
98
|
+
* reads `sourceId`, its data re-derived over the source (its filters, sorts
|
|
99
|
+
* and columns named native property ids, which the host's are not); the
|
|
100
|
+
* native rows are deleted. One undo step, which gives the native database
|
|
101
|
+
* back; the host's database stays. Returns `false` and writes nothing on a
|
|
102
|
+
* read-only document, on a block already connected, or without a host data
|
|
103
|
+
* source.
|
|
104
|
+
*/
|
|
105
|
+
export declare function moveDatabaseIntoSource(std: BlockStdScope, model: DatabaseBlockModel, choice: {
|
|
106
|
+
database: Pick<DataSourcePickerDatabase, 'id' | 'title'>;
|
|
107
|
+
sourceId: string;
|
|
108
|
+
}): boolean;
|
|
50
109
|
/**
|
|
51
110
|
* The creating slash entries once the host fills the picker (ADR 0035 §5):
|
|
52
111
|
* ask the host for a new database, then insert a view of its first source
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TelemetryProvider, } from '../affine-shared/services/index.js';
|
|
1
|
+
import { DataSourcePickerProvider, TelemetryProvider, } from '../affine-shared/services/index.js';
|
|
2
2
|
import { nanoid, Text } from '../store/index.js';
|
|
3
3
|
import { DatabaseDataSourceProvider } from './service/index.js';
|
|
4
4
|
/**
|
|
@@ -131,6 +131,136 @@ export function connectDatabaseToSource(std, model, choice) {
|
|
|
131
131
|
});
|
|
132
132
|
return true;
|
|
133
133
|
}
|
|
134
|
+
export function sourceRequirement(std, model, sourceMissing = false) {
|
|
135
|
+
if (std.getOptional(DataSourcePickerProvider)?.requireSource !== true) {
|
|
136
|
+
return 'none';
|
|
137
|
+
}
|
|
138
|
+
if (model.props.externalSourceId === undefined) {
|
|
139
|
+
return model.children.length === 0 ? 'connect' : 'read-only';
|
|
140
|
+
}
|
|
141
|
+
return sourceMissing ? 'connect' : 'none';
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Re-points one view of a hosted database whose source the host no longer
|
|
145
|
+
* resolves (ADR 0035 amendment of 2026-10-29): the block takes the picked
|
|
146
|
+
* database's `externalSourceId` when it differs, and the view reads
|
|
147
|
+
* `sourceId` with its data re-derived over it (`defaultViewData`: its id
|
|
148
|
+
* and name kept, its layout too where the preset can build over the new
|
|
149
|
+
* source). The other views are left as they are. One undo step. Returns
|
|
150
|
+
* `false` and writes nothing on a read-only document, on a block with no
|
|
151
|
+
* source (`connectDatabaseToSource` connects those), without a host data
|
|
152
|
+
* source or without that view.
|
|
153
|
+
*/
|
|
154
|
+
export function reconnectDatabaseView(std, model, choice) {
|
|
155
|
+
const { store } = std;
|
|
156
|
+
if (store.readonly)
|
|
157
|
+
return false;
|
|
158
|
+
if (model.props.externalSourceId === undefined)
|
|
159
|
+
return false;
|
|
160
|
+
const provider = std.getOptional(DatabaseDataSourceProvider);
|
|
161
|
+
if (!provider)
|
|
162
|
+
return false;
|
|
163
|
+
const { database, sourceId, viewId } = choice;
|
|
164
|
+
const index = model.props.views.findIndex(view => view.id === viewId);
|
|
165
|
+
const current = model.props.views[index];
|
|
166
|
+
if (!current)
|
|
167
|
+
return false;
|
|
168
|
+
const view = { id: current.id, name: current.name, mode: current.mode };
|
|
169
|
+
const withView = (data) => model.props.views.map((old, i) => (i === index ? data : old));
|
|
170
|
+
store.captureSync();
|
|
171
|
+
store.transact(() => {
|
|
172
|
+
if (model.props.externalSourceId !== database.id) {
|
|
173
|
+
store.updateBlock(model, { externalSourceId: database.id });
|
|
174
|
+
}
|
|
175
|
+
store.updateBlock(model, { views: withView({ ...view, sourceId }) });
|
|
176
|
+
const dataSource = provider.createDataSource(model);
|
|
177
|
+
try {
|
|
178
|
+
dataSource.viewManager.setCurrentView(view.id);
|
|
179
|
+
store.updateBlock(model, {
|
|
180
|
+
views: withView({
|
|
181
|
+
...defaultViewData(dataSource, view.mode),
|
|
182
|
+
id: view.id,
|
|
183
|
+
name: view.name,
|
|
184
|
+
sourceId,
|
|
185
|
+
}),
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
finally {
|
|
189
|
+
dataSource.dispose();
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Whether "Create Table" (`convertToDatabase`) may run. Where no source is
|
|
196
|
+
* required, it moves the blocks into a native database, as in 1.0. Where
|
|
197
|
+
* one is, it turns them into a source of the host's (`createDatabaseFrom`,
|
|
198
|
+
* ADR 0035 amendment of 2026-10-29), and is hidden without that method: a
|
|
199
|
+
* native database would be shown read-only at once.
|
|
200
|
+
*/
|
|
201
|
+
export function canConvertToDatabase(std) {
|
|
202
|
+
const picker = std.getOptional(DataSourcePickerProvider);
|
|
203
|
+
return picker?.requireSource !== true || !!picker.createDatabaseFrom;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Moves a database with native rows into the host source just created from
|
|
207
|
+
* them (`createDatabaseFrom`, ADR 0035 amendment of 2026-10-29): the block
|
|
208
|
+
* takes the database's `externalSourceId`, its title when it has none, an
|
|
209
|
+
* empty schema of its own; EVERY view is kept by id, name and layout and
|
|
210
|
+
* reads `sourceId`, its data re-derived over the source (its filters, sorts
|
|
211
|
+
* and columns named native property ids, which the host's are not); the
|
|
212
|
+
* native rows are deleted. One undo step, which gives the native database
|
|
213
|
+
* back; the host's database stays. Returns `false` and writes nothing on a
|
|
214
|
+
* read-only document, on a block already connected, or without a host data
|
|
215
|
+
* source.
|
|
216
|
+
*/
|
|
217
|
+
export function moveDatabaseIntoSource(std, model, choice) {
|
|
218
|
+
const { store } = std;
|
|
219
|
+
if (store.readonly)
|
|
220
|
+
return false;
|
|
221
|
+
if (model.props.externalSourceId !== undefined)
|
|
222
|
+
return false;
|
|
223
|
+
const provider = std.getOptional(DatabaseDataSourceProvider);
|
|
224
|
+
if (!provider)
|
|
225
|
+
return false;
|
|
226
|
+
const { database, sourceId } = choice;
|
|
227
|
+
const views = model.props.views.map(view => ({
|
|
228
|
+
id: view.id,
|
|
229
|
+
name: view.name,
|
|
230
|
+
mode: view.mode,
|
|
231
|
+
}));
|
|
232
|
+
store.captureSync();
|
|
233
|
+
store.transact(() => {
|
|
234
|
+
const title = model.props.title;
|
|
235
|
+
if (title.length === 0 && database.title)
|
|
236
|
+
title.insert(database.title, 0);
|
|
237
|
+
store.updateBlock(model, {
|
|
238
|
+
externalSourceId: database.id,
|
|
239
|
+
columns: [],
|
|
240
|
+
views: views.map(view => ({ ...view, sourceId })),
|
|
241
|
+
});
|
|
242
|
+
const dataSource = provider.createDataSource(model);
|
|
243
|
+
try {
|
|
244
|
+
store.updateBlock(model, {
|
|
245
|
+
views: views.map(view => {
|
|
246
|
+
dataSource.viewManager.setCurrentView(view.id);
|
|
247
|
+
return {
|
|
248
|
+
...defaultViewData(dataSource, view.mode),
|
|
249
|
+
id: view.id,
|
|
250
|
+
name: view.name,
|
|
251
|
+
sourceId,
|
|
252
|
+
};
|
|
253
|
+
}),
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
finally {
|
|
257
|
+
dataSource.dispose();
|
|
258
|
+
}
|
|
259
|
+
for (const row of [...model.children])
|
|
260
|
+
store.deleteBlock(row);
|
|
261
|
+
});
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
134
264
|
/**
|
|
135
265
|
* A preset's default view data over `dataSource`, named and typed. A preset
|
|
136
266
|
* that cannot build one over this source (a board with no property to group
|
|
@@ -2,6 +2,24 @@ import type { ChromeWording } from '../affine-shared/services/index.js';
|
|
|
2
2
|
/** Drawn in place of a view whose `sourceId` the host cannot resolve. */
|
|
3
3
|
export declare const DATABASE_SOURCE_MISSING: ChromeWording;
|
|
4
4
|
export declare const DATABASE_SOURCE_MISSING_HINT: ChromeWording;
|
|
5
|
+
/**
|
|
6
|
+
* Drawn in place of a database that waits for its source, when the host
|
|
7
|
+
* requires one (`requireSource`, ADR 0035 amendment of 2026-10-29).
|
|
8
|
+
*/
|
|
9
|
+
export declare const DATABASE_SOURCE_REQUIRED: ChromeWording;
|
|
10
|
+
export declare const DATABASE_SOURCE_REQUIRED_HINT: ChromeWording;
|
|
11
|
+
/** Over native rows shown read-only, under the same host. */
|
|
12
|
+
export declare const DATABASE_SOURCE_REQUIRED_READ_ONLY: ChromeWording;
|
|
13
|
+
/**
|
|
14
|
+
* The creation popover's title (`@labre/affine-database-editor`) and the
|
|
15
|
+
* button of a database waiting for its source: one chrome word, one key,
|
|
16
|
+
* declared in core, the lowest reader.
|
|
17
|
+
*/
|
|
18
|
+
export declare const DATABASE_CREATION_TITLE: ChromeWording;
|
|
19
|
+
/** Over native rows shown read-only: moves them into a new host database. */
|
|
20
|
+
export declare const DATABASE_MOVE_INTO_SOURCE: ChromeWording;
|
|
21
|
+
export declare const DATABASE_TURN_INTO_FAILED: ChromeWording;
|
|
22
|
+
export declare const DATABASE_TURN_INTO_NESTED: ChromeWording;
|
|
5
23
|
/** After 3 s of loading (`VALUES_SLOW_AFTER_MS`). */
|
|
6
24
|
export declare const DATABASE_VALUES_LOADING: ChromeWording;
|
|
7
25
|
export declare const DATABASE_VALUES_ERROR: ChromeWording;
|
|
@@ -8,6 +8,46 @@ export const DATABASE_SOURCE_MISSING_HINT = [
|
|
|
8
8
|
'com.labre.database.source-missing.hint',
|
|
9
9
|
'The source this view reads was deleted or cannot be reached. Pick another view.',
|
|
10
10
|
];
|
|
11
|
+
/**
|
|
12
|
+
* Drawn in place of a database that waits for its source, when the host
|
|
13
|
+
* requires one (`requireSource`, ADR 0035 amendment of 2026-10-29).
|
|
14
|
+
*/
|
|
15
|
+
export const DATABASE_SOURCE_REQUIRED = [
|
|
16
|
+
'com.labre.database.source-required',
|
|
17
|
+
'No source connected',
|
|
18
|
+
];
|
|
19
|
+
export const DATABASE_SOURCE_REQUIRED_HINT = [
|
|
20
|
+
'com.labre.database.source-required.hint',
|
|
21
|
+
'Connect a source to show its rows here.',
|
|
22
|
+
];
|
|
23
|
+
/** Over native rows shown read-only, under the same host. */
|
|
24
|
+
export const DATABASE_SOURCE_REQUIRED_READ_ONLY = [
|
|
25
|
+
'com.labre.database.source-required.read-only',
|
|
26
|
+
'This table has no source: its rows are read-only.',
|
|
27
|
+
];
|
|
28
|
+
/**
|
|
29
|
+
* The creation popover's title (`@labre/affine-database-editor`) and the
|
|
30
|
+
* button of a database waiting for its source: one chrome word, one key,
|
|
31
|
+
* declared in core, the lowest reader.
|
|
32
|
+
*/
|
|
33
|
+
export const DATABASE_CREATION_TITLE = [
|
|
34
|
+
'com.labre.database.creation.title',
|
|
35
|
+
'Connect a source',
|
|
36
|
+
];
|
|
37
|
+
/* ── Turning a table into a source (ADR 0035, amendment of 2026-10-29) ── */
|
|
38
|
+
/** Over native rows shown read-only: moves them into a new host database. */
|
|
39
|
+
export const DATABASE_MOVE_INTO_SOURCE = [
|
|
40
|
+
'com.labre.database.turn-into.move',
|
|
41
|
+
'Move into a database',
|
|
42
|
+
];
|
|
43
|
+
export const DATABASE_TURN_INTO_FAILED = [
|
|
44
|
+
'com.labre.database.turn-into.failed',
|
|
45
|
+
'The table could not be created. Nothing was changed.',
|
|
46
|
+
];
|
|
47
|
+
export const DATABASE_TURN_INTO_NESTED = [
|
|
48
|
+
'com.labre.database.turn-into.nested',
|
|
49
|
+
'Rows with nested blocks cannot be moved here. Nothing was changed.',
|
|
50
|
+
];
|
|
11
51
|
/* ── Cell hydration (ADR 0035 §9e, issue B2) ───────────────────────────── */
|
|
12
52
|
/** After 3 s of loading (`VALUES_SLOW_AFTER_MS`). */
|
|
13
53
|
export const DATABASE_VALUES_LOADING = [
|
|
@@ -27,9 +67,16 @@ export const DATABASE_NEW_RECORD_MORE = [
|
|
|
27
67
|
'Templates and options',
|
|
28
68
|
];
|
|
29
69
|
export const DATABASE_WORDINGS = [
|
|
70
|
+
DATABASE_CREATION_TITLE,
|
|
71
|
+
DATABASE_MOVE_INTO_SOURCE,
|
|
30
72
|
DATABASE_NEW_RECORD_MORE,
|
|
31
73
|
DATABASE_SOURCE_MISSING,
|
|
32
74
|
DATABASE_SOURCE_MISSING_HINT,
|
|
75
|
+
DATABASE_SOURCE_REQUIRED,
|
|
76
|
+
DATABASE_SOURCE_REQUIRED_HINT,
|
|
77
|
+
DATABASE_SOURCE_REQUIRED_READ_ONLY,
|
|
78
|
+
DATABASE_TURN_INTO_FAILED,
|
|
79
|
+
DATABASE_TURN_INTO_NESTED,
|
|
33
80
|
DATABASE_VALUES_ERROR,
|
|
34
81
|
DATABASE_VALUES_LOADING,
|
|
35
82
|
];
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { DatabaseBlockModel } from '../affine-model/index.js';
|
|
2
|
+
import { type DataSourcePickerContent } from '../affine-shared/services/index.js';
|
|
3
|
+
import type { BlockStdScope } from '../std/index.js';
|
|
4
|
+
import type { BlockModel } from '../store/index.js';
|
|
5
|
+
/**
|
|
6
|
+
* "Turn into a source" (ADR 0035 amendment of 2026-10-29): under a host that
|
|
7
|
+
* requires a source (`requireSource`), "Create Table" on selected paragraphs
|
|
8
|
+
* and "Move into a database" on a database with native rows hand their
|
|
9
|
+
* content to the host's `createDatabaseFrom`, then connect the block to the
|
|
10
|
+
* database it answers, in one undo step. Nothing is written until the host
|
|
11
|
+
* answers; a refusal, a rejection, a read-only document or content changed
|
|
12
|
+
* in the meantime writes nothing and says so with a fixed message.
|
|
13
|
+
*/
|
|
14
|
+
/** The id of the title property a conversion of paragraphs sends. */
|
|
15
|
+
export declare const TURN_INTO_TITLE_PROPERTY = "title";
|
|
16
|
+
/** The blocks, less those nested in another of them: they ride in its body. */
|
|
17
|
+
export declare function outermostBlocks(blocks: readonly BlockModel[]): BlockModel[];
|
|
18
|
+
/** One row per block, its title the block's text, its body its children. */
|
|
19
|
+
export declare function blocksContent(std: BlockStdScope, blocks: readonly BlockModel[]): DataSourcePickerContent;
|
|
20
|
+
/**
|
|
21
|
+
* A native database's columns and rows. A value is sent as stored, a rich
|
|
22
|
+
* text as its delta; the title column is the row's own text.
|
|
23
|
+
*/
|
|
24
|
+
export declare function databaseContent(std: BlockStdScope, model: DatabaseBlockModel): DataSourcePickerContent;
|
|
25
|
+
/** Whether the host turns content into a source where one is required. */
|
|
26
|
+
export declare function canTurnIntoSource(std: BlockStdScope): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* "Create Table" under a host that requires a source: the selected blocks
|
|
29
|
+
* become the rows of a new host database, and a view of its first source
|
|
30
|
+
* takes their place. Returns whether it was written.
|
|
31
|
+
*/
|
|
32
|
+
export declare function turnBlocksIntoSource(std: BlockStdScope, selected: readonly BlockModel[], viewType: string): Promise<boolean>;
|
|
33
|
+
/**
|
|
34
|
+
* "Move into a database" over native rows shown read-only: the database's
|
|
35
|
+
* columns and rows become a new host database, and the block reads its
|
|
36
|
+
* first source (`moveDatabaseIntoSource`). Returns whether it was written.
|
|
37
|
+
*/
|
|
38
|
+
export declare function moveDatabaseRowsIntoSource(std: BlockStdScope, model: DatabaseBlockModel): Promise<boolean>;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { toast } from '../affine-components/toast/index.js';
|
|
2
|
+
import { DataSourcePickerProvider, NotificationProvider, translateKey, } from '../affine-shared/services/index.js';
|
|
3
|
+
import { insertLinkedDatabaseView, moveDatabaseIntoSource, } from './linked-view.js';
|
|
4
|
+
import { DATABASE_TURN_INTO_FAILED, DATABASE_TURN_INTO_NESTED, } from './translations.js';
|
|
5
|
+
/**
|
|
6
|
+
* "Turn into a source" (ADR 0035 amendment of 2026-10-29): under a host that
|
|
7
|
+
* requires a source (`requireSource`), "Create Table" on selected paragraphs
|
|
8
|
+
* and "Move into a database" on a database with native rows hand their
|
|
9
|
+
* content to the host's `createDatabaseFrom`, then connect the block to the
|
|
10
|
+
* database it answers, in one undo step. Nothing is written until the host
|
|
11
|
+
* answers; a refusal, a rejection, a read-only document or content changed
|
|
12
|
+
* in the meantime writes nothing and says so with a fixed message.
|
|
13
|
+
*/
|
|
14
|
+
/** The id of the title property a conversion of paragraphs sends. */
|
|
15
|
+
export const TURN_INTO_TITLE_PROPERTY = 'title';
|
|
16
|
+
function notify(std, wording) {
|
|
17
|
+
const message = translateKey(std, ...wording);
|
|
18
|
+
const notification = std.getOptional(NotificationProvider);
|
|
19
|
+
if (notification)
|
|
20
|
+
notification.toast(message);
|
|
21
|
+
else
|
|
22
|
+
toast(std.host, message);
|
|
23
|
+
}
|
|
24
|
+
function rowOf(std, block, values = {}) {
|
|
25
|
+
const transformer = std.store.getTransformer();
|
|
26
|
+
return {
|
|
27
|
+
// A Y.Text delta holds inserts only; the guard narrows its type.
|
|
28
|
+
title: (block.text?.toDelta() ?? []).filter((op) => typeof op.insert === 'string'),
|
|
29
|
+
values,
|
|
30
|
+
body: block.children.flatMap(child => {
|
|
31
|
+
const snapshot = transformer.blockToSnapshot(child);
|
|
32
|
+
return snapshot ? [snapshot] : [];
|
|
33
|
+
}),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/** The blocks, less those nested in another of them: they ride in its body. */
|
|
37
|
+
export function outermostBlocks(blocks) {
|
|
38
|
+
const ids = new Set(blocks.map(block => block.id));
|
|
39
|
+
return blocks.filter(block => {
|
|
40
|
+
for (let parent = block.parent; parent; parent = parent.parent) {
|
|
41
|
+
if (ids.has(parent.id))
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
return true;
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/** One row per block, its title the block's text, its body its children. */
|
|
48
|
+
export function blocksContent(std, blocks) {
|
|
49
|
+
return {
|
|
50
|
+
title: '',
|
|
51
|
+
properties: [{ id: TURN_INTO_TITLE_PROPERTY, name: '', type: 'title' }],
|
|
52
|
+
rows: blocks.map(block => rowOf(std, block)),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* A native database's columns and rows. A value is sent as stored, a rich
|
|
57
|
+
* text as its delta; the title column is the row's own text.
|
|
58
|
+
*/
|
|
59
|
+
export function databaseContent(std, model) {
|
|
60
|
+
const columns = model.props.columns;
|
|
61
|
+
const title = columns.find(column => column.type === 'title');
|
|
62
|
+
const properties = [
|
|
63
|
+
title
|
|
64
|
+
? { id: title.id, name: title.name, type: 'title' }
|
|
65
|
+
: { id: TURN_INTO_TITLE_PROPERTY, name: '', type: 'title' },
|
|
66
|
+
...columns
|
|
67
|
+
.filter(column => column.type !== 'title')
|
|
68
|
+
.map(column => {
|
|
69
|
+
const options = column.data.options;
|
|
70
|
+
return {
|
|
71
|
+
id: column.id,
|
|
72
|
+
name: column.name,
|
|
73
|
+
type: column.type,
|
|
74
|
+
...(Array.isArray(options)
|
|
75
|
+
? {
|
|
76
|
+
options: options.map((option) => ({
|
|
77
|
+
id: option.id,
|
|
78
|
+
value: option.value,
|
|
79
|
+
})),
|
|
80
|
+
}
|
|
81
|
+
: {}),
|
|
82
|
+
};
|
|
83
|
+
}),
|
|
84
|
+
];
|
|
85
|
+
const rows = model.children.map(row => {
|
|
86
|
+
const values = {};
|
|
87
|
+
for (const column of columns) {
|
|
88
|
+
if (column.type === 'title')
|
|
89
|
+
continue;
|
|
90
|
+
const value = model.props.cells[row.id]?.[column.id]?.value;
|
|
91
|
+
if (value === undefined)
|
|
92
|
+
continue;
|
|
93
|
+
values[column.id] =
|
|
94
|
+
value && typeof value === 'object' && 'toDelta' in value
|
|
95
|
+
? value.toDelta()
|
|
96
|
+
: value;
|
|
97
|
+
}
|
|
98
|
+
return rowOf(std, row, values);
|
|
99
|
+
});
|
|
100
|
+
return { title: model.props.title.toString(), properties, rows };
|
|
101
|
+
}
|
|
102
|
+
/** Whether the host turns content into a source where one is required. */
|
|
103
|
+
export function canTurnIntoSource(std) {
|
|
104
|
+
const picker = std.getOptional(DataSourcePickerProvider);
|
|
105
|
+
return picker?.requireSource === true && !!picker.createDatabaseFrom;
|
|
106
|
+
}
|
|
107
|
+
/** The host's database for `content`, or `null` once the reader was told why. */
|
|
108
|
+
async function createFrom(std, content) {
|
|
109
|
+
const picker = std.getOptional(DataSourcePickerProvider);
|
|
110
|
+
const createDatabaseFrom = picker?.createDatabaseFrom;
|
|
111
|
+
if (!picker || !createDatabaseFrom || std.store.readonly)
|
|
112
|
+
return null;
|
|
113
|
+
if (!picker.rowBodies && content.rows.some(row => row.body.length > 0)) {
|
|
114
|
+
notify(std, DATABASE_TURN_INTO_NESTED);
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
const database = await createDatabaseFrom.call(picker, content);
|
|
119
|
+
if (database?.sources[0])
|
|
120
|
+
return database;
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
console.error(error);
|
|
124
|
+
}
|
|
125
|
+
notify(std, DATABASE_TURN_INTO_FAILED);
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* "Create Table" under a host that requires a source: the selected blocks
|
|
130
|
+
* become the rows of a new host database, and a view of its first source
|
|
131
|
+
* takes their place. Returns whether it was written.
|
|
132
|
+
*/
|
|
133
|
+
export async function turnBlocksIntoSource(std, selected, viewType) {
|
|
134
|
+
const blocks = outermostBlocks(selected);
|
|
135
|
+
const first = blocks[0];
|
|
136
|
+
if (!first || std.store.readonly)
|
|
137
|
+
return false;
|
|
138
|
+
const content = blocksContent(std, blocks);
|
|
139
|
+
const database = await createFrom(std, content);
|
|
140
|
+
const source = database?.sources[0];
|
|
141
|
+
if (!database || !source)
|
|
142
|
+
return false;
|
|
143
|
+
const unchanged = !std.store.readonly &&
|
|
144
|
+
blocks.every(block => std.store.getBlock(block.id)) &&
|
|
145
|
+
JSON.stringify(blocksContent(std, blocks)) === JSON.stringify(content);
|
|
146
|
+
if (!unchanged) {
|
|
147
|
+
notify(std, DATABASE_TURN_INTO_FAILED);
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
// `insertLinkedDatabaseView` opens the undo step; the deletions join it.
|
|
151
|
+
const id = insertLinkedDatabaseView(std, {
|
|
152
|
+
after: first,
|
|
153
|
+
database,
|
|
154
|
+
sourceId: source.id,
|
|
155
|
+
viewType,
|
|
156
|
+
});
|
|
157
|
+
if (!id) {
|
|
158
|
+
notify(std, DATABASE_TURN_INTO_FAILED);
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
for (const block of blocks)
|
|
162
|
+
std.store.deleteBlock(block);
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* "Move into a database" over native rows shown read-only: the database's
|
|
167
|
+
* columns and rows become a new host database, and the block reads its
|
|
168
|
+
* first source (`moveDatabaseIntoSource`). Returns whether it was written.
|
|
169
|
+
*/
|
|
170
|
+
export async function moveDatabaseRowsIntoSource(std, model) {
|
|
171
|
+
if (std.store.readonly || model.props.externalSourceId !== undefined) {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
const content = databaseContent(std, model);
|
|
175
|
+
const database = await createFrom(std, content);
|
|
176
|
+
const source = database?.sources[0];
|
|
177
|
+
if (!database || !source)
|
|
178
|
+
return false;
|
|
179
|
+
const unchanged = !!std.store.getBlock(model.id) &&
|
|
180
|
+
JSON.stringify(databaseContent(std, model)) === JSON.stringify(content);
|
|
181
|
+
if (!unchanged ||
|
|
182
|
+
!moveDatabaseIntoSource(std, model, { database, sourceId: source.id })) {
|
|
183
|
+
notify(std, DATABASE_TURN_INTO_FAILED);
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { DatabaseBlockModel } from '../affine-model/index.js';
|
|
2
|
+
import type { DataSourcePickerService } from '../affine-shared/services/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Deletes view `viewId` of `model`, through the host's `deleteView` when it
|
|
5
|
+
* has one and the block shows one of its databases; at once otherwise.
|
|
6
|
+
* `removeNow` is the 1.0 removal (one undo step); `afterRemove` runs after a
|
|
7
|
+
* removal the host asked for (the view to show next).
|
|
8
|
+
*/
|
|
9
|
+
export declare function deleteViewThroughHost(options: {
|
|
10
|
+
model: DatabaseBlockModel;
|
|
11
|
+
viewId: string;
|
|
12
|
+
picker: DataSourcePickerService | null;
|
|
13
|
+
removeNow: () => void;
|
|
14
|
+
afterRemove: () => void;
|
|
15
|
+
}): void;
|