@antelopejs/dms-database 0.0.14 → 0.1.1
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/README.md +30 -5
- package/dist/db/models/query_history.model.d.ts +1 -0
- package/dist/db/models/query_history.model.js +6 -0
- package/dist/db/models/query_history.model.js.map +1 -1
- package/dist/db/tables/query_history.table.d.ts +3 -0
- package/dist/db/tables/query_history.table.js +12 -0
- package/dist/db/tables/query_history.table.js.map +1 -1
- package/dist/db/tables/saved_queries.table.d.ts +1 -0
- package/dist/db/tables/saved_queries.table.js +4 -0
- package/dist/db/tables/saved_queries.table.js.map +1 -1
- package/dist/i18n/messages.d.ts +39 -0
- package/dist/i18n/messages.js +64 -0
- package/dist/i18n/messages.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -7
- package/dist/index.js.map +1 -1
- package/dist/module.d.ts +11 -0
- package/dist/module.js +55 -0
- package/dist/module.js.map +1 -0
- package/dist/pages/data.d.ts +1 -1
- package/dist/pages/data.js +9 -6
- package/dist/pages/data.js.map +1 -1
- package/dist/pages/diagram.d.ts +9 -0
- package/dist/pages/diagram.js +45 -0
- package/dist/pages/diagram.js.map +1 -0
- package/dist/pages/index.d.ts +1 -0
- package/dist/pages/index.js +1 -0
- package/dist/pages/index.js.map +1 -1
- package/dist/pages/overview.d.ts +4 -1
- package/dist/pages/overview.js +113 -6
- package/dist/pages/overview.js.map +1 -1
- package/dist/pages/query.d.ts +1 -1
- package/dist/pages/query.js +22 -6
- package/dist/pages/query.js.map +1 -1
- package/dist/pages/schemas.d.ts +2 -1
- package/dist/pages/schemas.js +143 -6
- package/dist/pages/schemas.js.map +1 -1
- package/dist/routes/data.d.ts +8 -0
- package/dist/routes/data.js +84 -21
- package/dist/routes/data.js.map +1 -1
- package/dist/routes/index.d.ts +1 -0
- package/dist/routes/index.js +1 -0
- package/dist/routes/index.js.map +1 -1
- package/dist/routes/overview.d.ts +12 -0
- package/dist/routes/overview.js +55 -0
- package/dist/routes/overview.js.map +1 -0
- package/dist/routes/query.d.ts +5 -1
- package/dist/routes/query.js +162 -27
- package/dist/routes/query.js.map +1 -1
- package/dist/routes/tables.d.ts +15 -12
- package/dist/routes/tables.js +95 -55
- package/dist/routes/tables.js.map +1 -1
- package/dist/service/catalog.d.ts +3 -0
- package/dist/service/catalog.js +26 -0
- package/dist/service/catalog.js.map +1 -0
- package/dist/service/health.d.ts +3 -3
- package/dist/service/health.js +17 -14
- package/dist/service/health.js.map +1 -1
- package/dist/service/introspect.d.ts +3 -1
- package/dist/service/introspect.js +131 -43
- package/dist/service/introspect.js.map +1 -1
- package/dist/service/overview.d.ts +9 -0
- package/dist/service/overview.js +113 -0
- package/dist/service/overview.js.map +1 -0
- package/dist/service/queryInspection.d.ts +15 -0
- package/dist/service/queryInspection.js +107 -0
- package/dist/service/queryInspection.js.map +1 -0
- package/dist/service/references.d.ts +9 -0
- package/dist/service/references.js +52 -0
- package/dist/service/references.js.map +1 -0
- package/dist/service/schemaFilters.d.ts +26 -0
- package/dist/service/schemaFilters.js +42 -0
- package/dist/service/schemaFilters.js.map +1 -0
- package/dist/service/stagedSerialization.d.ts +2 -0
- package/dist/service/stagedSerialization.js +38 -10
- package/dist/service/stagedSerialization.js.map +1 -1
- package/dist/service/types.d.ts +11 -1
- package/dist/types/constants.d.ts +2 -0
- package/dist/types/constants.js +3 -1
- package/dist/types/constants.js.map +1 -1
- package/dist/utils/query.d.ts +0 -2
- package/dist/utils/query.js +0 -20
- package/dist/utils/query.js.map +1 -1
- package/frontend-vue/app/{composables → build/composables}/useDataBrowserGrid.ts +65 -50
- package/frontend-vue/app/build/composables/useDataBrowserTabs.ts +366 -0
- package/frontend-vue/app/build/composables/useDatabaseHealth.ts +50 -0
- package/frontend-vue/app/build/composables/useDatabaseSchemas.ts +140 -0
- package/frontend-vue/app/{composables → build/composables}/useDiagramPersistence.ts +47 -39
- package/frontend-vue/app/build/composables/useQueryRuntime.ts +217 -0
- package/frontend-vue/app/build/composables/useQueryStore.ts +162 -0
- package/frontend-vue/app/build/data/cellValues.ts +132 -0
- package/frontend-vue/app/build/data/instanceOptions.ts +36 -0
- package/frontend-vue/app/build/data/pickerMenus.ts +175 -0
- package/frontend-vue/app/build/data/recentTables.ts +48 -0
- package/frontend-vue/app/build/data/stagedEdits.ts +117 -0
- package/frontend-vue/app/build/data/stagedWrites.ts +64 -0
- package/frontend-vue/app/build/data/useStagedSave.ts +298 -0
- package/frontend-vue/app/build/diagram/autosave.ts +72 -0
- package/frontend-vue/app/build/diagram/graph.ts +585 -0
- package/frontend-vue/app/build/diagram/history.ts +178 -0
- package/frontend-vue/app/build/diagram/shortcuts.ts +62 -0
- package/frontend-vue/app/build/diagram/sync.ts +137 -0
- package/frontend-vue/app/build/query/completion.ts +100 -0
- package/frontend-vue/app/build/query/csv.ts +32 -0
- package/frontend-vue/app/build/query/examples.ts +38 -0
- package/frontend-vue/app/build/query/runs.ts +168 -0
- package/frontend-vue/app/build/query/tableColumns.ts +35 -0
- package/frontend-vue/app/build/query/target.ts +143 -0
- package/frontend-vue/app/build/schemas/filterState.ts +128 -0
- package/frontend-vue/app/build/schemas/useSchemasListing.ts +62 -0
- package/frontend-vue/app/build/utils/databaseLinks.ts +98 -0
- package/frontend-vue/app/build/utils/fieldTypes.ts +198 -0
- package/frontend-vue/app/build/utils/relativeTime.ts +19 -0
- package/frontend-vue/app/{utils → build/utils}/stagedSerialization.ts +7 -1
- package/frontend-vue/app/components/ConnectionStatus.vue +122 -0
- package/frontend-vue/app/components/DataBrowser.vue +126 -0
- package/frontend-vue/app/components/DataBrowserSidebar.vue +293 -210
- package/frontend-vue/app/components/DataBrowserTabBar.vue +185 -93
- package/frontend-vue/app/components/DataFilterBar.vue +163 -0
- package/frontend-vue/app/components/DataGrid.vue +737 -499
- package/frontend-vue/app/components/DataGridCell.vue +125 -77
- package/frontend-vue/app/components/DataGridHeaderCell.vue +119 -81
- package/frontend-vue/app/components/DiagramNoteNode.vue +81 -63
- package/frontend-vue/app/components/DiagramRelationEdge.vue +57 -27
- package/frontend-vue/app/components/DiagramStubNode.vue +16 -17
- package/frontend-vue/app/components/DiagramTableNode.vue +106 -68
- package/frontend-vue/app/components/InstancePicker.vue +195 -0
- package/frontend-vue/app/components/JsonCellPanel.vue +55 -41
- package/frontend-vue/app/components/QueryBuilderPanel.vue +290 -238
- package/frontend-vue/app/components/QueryConsole.vue +726 -0
- package/frontend-vue/app/components/QueryLibrary.vue +314 -0
- package/frontend-vue/app/components/QueryResultPanel.vue +556 -126
- package/frontend-vue/app/components/RowDrawer.vue +520 -0
- package/frontend-vue/app/components/SchemaDiagram.vue +1016 -1051
- package/frontend-vue/app/components/SchemaPicker.vue +86 -0
- package/frontend-vue/app/components/SchemasFilterBar.vue +201 -0
- package/frontend-vue/app/components/TableInspector.vue +741 -0
- package/frontend-vue/app/config/shortcuts-registry.ts +65 -0
- package/frontend-vue/dms.frontend.ts +5 -14
- package/frontend-vue/i18n/locales/database-en-GB.json +527 -193
- package/frontend-vue/i18n/locales/database-fr-FR.json +529 -195
- package/frontend-vue/package.json +2 -1
- package/frontend-vue/pnpm-lock.yaml +2601 -858
- package/frontend-vue/pnpm-workspace.yaml +1 -1
- package/frontend-vue/tests/dataBrowser.test.ts +206 -0
- package/frontend-vue/tests/diagram.test.ts +516 -0
- package/frontend-vue/tests/helpers.test.ts +205 -0
- package/frontend-vue/tests/instanceOptions.test.ts +53 -0
- package/frontend-vue/tests/locales.test.ts +56 -0
- package/frontend-vue/tests/persistence.test.ts +106 -5
- package/frontend-vue/tests/pickerMenus.test.ts +254 -0
- package/frontend-vue/tests/queryConsole.test.ts +282 -0
- package/frontend-vue/tests/queryRuntime.test.ts +2 -2
- package/frontend-vue/tests/registration.test.ts +36 -25
- package/frontend-vue/tests/schemasFilters.test.ts +115 -0
- package/package.json +7 -7
- package/frontend-vue/app/components/QueryFavoritesPanel.vue +0 -127
- package/frontend-vue/app/components/QueryHistoryPanel.vue +0 -134
- package/frontend-vue/app/components/SchemaTableInspector.vue +0 -278
- package/frontend-vue/app/composables/useDataBrowserTabs.ts +0 -255
- package/frontend-vue/app/composables/useDatabaseHealth.ts +0 -50
- package/frontend-vue/app/composables/useDatabaseSchemas.ts +0 -92
- package/frontend-vue/app/composables/useQueryRuntime.ts +0 -118
- package/frontend-vue/app/composables/useQueryStore.ts +0 -117
- package/frontend-vue/app/composables/useTableViewRowClick.ts +0 -22
- package/frontend-vue/app/custom-pages/database/data.vue +0 -41
- package/frontend-vue/app/custom-pages/database/overview.vue +0 -367
- package/frontend-vue/app/custom-pages/database/query.vue +0 -436
- package/frontend-vue/app/custom-pages/database/schemas.vue +0 -218
- package/frontend-vue/app/utils/databaseFieldModifiers.ts +0 -33
- package/frontend-vue/app/utils/databaseFieldTypeIcons.ts +0 -224
- package/frontend-vue/app/utils/dmsTableView.ts +0 -83
- package/frontend-vue/app/utils/relativeTime.ts +0 -18
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_INSTANCE_VALUE,
|
|
3
|
+
instanceBadge,
|
|
4
|
+
} from '../composables/useDataBrowserTabs'
|
|
5
|
+
import { browseSelectionQuery } from '../composables/useDataBrowserGrid'
|
|
6
|
+
import { h, onScopeDispose } from 'vue'
|
|
7
|
+
import type { BrowserTab } from '../composables/useDataBrowserTabs'
|
|
8
|
+
import { cellLabel } from './cellValues'
|
|
9
|
+
import { type StagedChange, undoBodies, useStagedEdits } from './stagedEdits'
|
|
10
|
+
import { changesOf, type RowWriteOutcome, writeRows } from './stagedWrites'
|
|
11
|
+
|
|
12
|
+
// Writes the staged edits of a tab after the user reviewed them, then offers
|
|
13
|
+
// to take them back for a few seconds, in the save bar of the current tab
|
|
14
|
+
// (another tab ends the offer). Each row is written with one call of the
|
|
15
|
+
// edit route; an undo writes the previous values back the same way. Only the
|
|
16
|
+
// rows written are announced and undone: a row gone meanwhile drops its
|
|
17
|
+
// changes, and a failure stops the save with the rows left still staged.
|
|
18
|
+
|
|
19
|
+
const EDIT_ENDPOINT = '/api/database/browse/edit'
|
|
20
|
+
const UNDO_WINDOW_MS = 10_000
|
|
21
|
+
const UNDONE_NOTICE_MS = 3_000
|
|
22
|
+
|
|
23
|
+
/** What the save bar says once a save is written, then once it is undone. */
|
|
24
|
+
interface SaveNotice {
|
|
25
|
+
state: 'saved' | 'undoing' | 'undone'
|
|
26
|
+
changes: StagedChange[]
|
|
27
|
+
scope: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface StagedSaveOptions {
|
|
31
|
+
tab: () => BrowserTab
|
|
32
|
+
onSaved: () => void
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The server's reason when it gave one, else the error's own message. */
|
|
36
|
+
function messageOf(error: unknown): string {
|
|
37
|
+
const data = (error as { data?: unknown } | null)?.data
|
|
38
|
+
if (typeof data === 'string' && data) return data
|
|
39
|
+
const message = (data as { message?: unknown } | null)?.message
|
|
40
|
+
if (typeof message === 'string' && message) return message
|
|
41
|
+
return error instanceof Error ? error.message : String(error)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function useStagedSave(options: StagedSaveOptions) {
|
|
45
|
+
const { t, n } = useI18n()
|
|
46
|
+
const { $authFetch } = useAuthFetch()
|
|
47
|
+
const { confirm } = useConfirm()
|
|
48
|
+
const toast = useToast()
|
|
49
|
+
const staged = useStagedEdits()
|
|
50
|
+
const saving = ref(false)
|
|
51
|
+
const notice = ref<SaveNotice | null>(null)
|
|
52
|
+
let expiry: ReturnType<typeof setTimeout> | undefined
|
|
53
|
+
|
|
54
|
+
/** Shows a notice, for `ms` when given, or ends it with null. */
|
|
55
|
+
function show(next: SaveNotice | null, ms?: number) {
|
|
56
|
+
clearTimeout(expiry)
|
|
57
|
+
notice.value = next
|
|
58
|
+
if (next && ms !== undefined)
|
|
59
|
+
expiry = setTimeout(() => {
|
|
60
|
+
notice.value = null
|
|
61
|
+
}, ms)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// The notice belongs to the tab it was written on.
|
|
65
|
+
watch(
|
|
66
|
+
() => options.tab().id,
|
|
67
|
+
() => show(null),
|
|
68
|
+
)
|
|
69
|
+
onScopeDispose(() => clearTimeout(expiry))
|
|
70
|
+
|
|
71
|
+
// A new edit ends the undo: written under it, the old values would make
|
|
72
|
+
// its review show the wrong "was", and its save would overwrite them.
|
|
73
|
+
watch(
|
|
74
|
+
() => staged.count(options.tab().id),
|
|
75
|
+
(count) => {
|
|
76
|
+
if (count > 0 && notice.value?.state === 'saved') show(null)
|
|
77
|
+
},
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
function scope(tab: BrowserTab): string {
|
|
81
|
+
const badge =
|
|
82
|
+
tab.instance === DEFAULT_INSTANCE_VALUE
|
|
83
|
+
? `@${t('dms_database.data.scope.default')}`
|
|
84
|
+
: instanceBadge(tab.instance, t('dms_database.data.scope.all'))
|
|
85
|
+
return `${tab.schema}.${tab.table} ${badge}`
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function writeRow(
|
|
89
|
+
tab: BrowserTab,
|
|
90
|
+
rowId: string,
|
|
91
|
+
body: Record<string, unknown>,
|
|
92
|
+
) {
|
|
93
|
+
await $authFetch(EDIT_ENDPOINT, {
|
|
94
|
+
method: 'PUT',
|
|
95
|
+
query: { ...browseSelectionQuery(tab), id: rowId },
|
|
96
|
+
body,
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// One change per row: the field and the row on a first line, the value
|
|
101
|
+
// before and after on its own line, wrapped, so neither hides the other.
|
|
102
|
+
function diffTable(changes: StagedChange[]) {
|
|
103
|
+
return h(
|
|
104
|
+
'div',
|
|
105
|
+
{
|
|
106
|
+
class:
|
|
107
|
+
'border-default divide-default max-h-72 divide-y overflow-auto rounded-md border font-mono text-[12px]',
|
|
108
|
+
},
|
|
109
|
+
changes.map((change) => {
|
|
110
|
+
const before = cellLabel(change.before)
|
|
111
|
+
const after = cellLabel(change.after)
|
|
112
|
+
return h('div', { class: 'grid gap-0.5 px-3 py-2' }, [
|
|
113
|
+
h('div', { class: 'flex min-w-0 items-baseline gap-2' }, [
|
|
114
|
+
h('span', { class: 'text-highlighted shrink-0' }, change.field),
|
|
115
|
+
h(
|
|
116
|
+
'span',
|
|
117
|
+
{ class: 'text-dimmed min-w-0 truncate', title: change.rowId },
|
|
118
|
+
change.rowId,
|
|
119
|
+
),
|
|
120
|
+
]),
|
|
121
|
+
h(
|
|
122
|
+
'div',
|
|
123
|
+
{
|
|
124
|
+
class: 'line-clamp-4 break-all',
|
|
125
|
+
title: `${before} → ${after}`,
|
|
126
|
+
},
|
|
127
|
+
[
|
|
128
|
+
h('s', { class: 'text-dimmed' }, before),
|
|
129
|
+
' → ',
|
|
130
|
+
h('span', { class: 'text-success' }, after),
|
|
131
|
+
],
|
|
132
|
+
),
|
|
133
|
+
])
|
|
134
|
+
}),
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Each row written (or gone) leaves the staged edits at once: a failure
|
|
139
|
+
// further on keeps only the rows not written.
|
|
140
|
+
function saveAll(tab: BrowserTab): Promise<RowWriteOutcome> {
|
|
141
|
+
const rows = staged
|
|
142
|
+
.rowIds(tab.id)
|
|
143
|
+
.map((rowId): [string, Record<string, unknown>] => [
|
|
144
|
+
rowId,
|
|
145
|
+
staged.rowBody(tab.id, rowId),
|
|
146
|
+
])
|
|
147
|
+
return writeRows(
|
|
148
|
+
rows,
|
|
149
|
+
(rowId, body) => writeRow(tab, rowId, body),
|
|
150
|
+
(rowId) => staged.discard(tab.id, [rowId]),
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// What the review dialog says once a save went only part of the way;
|
|
155
|
+
// nothing written at all is an error, shown by the dialog for a retry.
|
|
156
|
+
function partialOutcome(
|
|
157
|
+
outcome: RowWriteOutcome,
|
|
158
|
+
changes: StagedChange[],
|
|
159
|
+
savedCount: number,
|
|
160
|
+
) {
|
|
161
|
+
const nothingSettled =
|
|
162
|
+
outcome.written.length === 0 && outcome.gone.length === 0
|
|
163
|
+
if (outcome.error !== null && nothingSettled) throw outcome.error
|
|
164
|
+
if (outcome.error === null && outcome.gone.length === 0) return undefined
|
|
165
|
+
const details: string[] = []
|
|
166
|
+
if (outcome.gone.length > 0)
|
|
167
|
+
details.push(
|
|
168
|
+
t(
|
|
169
|
+
'dms_database.data.save.rows_gone',
|
|
170
|
+
{ count: n(outcome.gone.length) },
|
|
171
|
+
outcome.gone.length,
|
|
172
|
+
),
|
|
173
|
+
)
|
|
174
|
+
if (outcome.error !== null) {
|
|
175
|
+
const left = changesOf(changes, outcome.left).length
|
|
176
|
+
details.push(
|
|
177
|
+
t(
|
|
178
|
+
'dms_database.data.save.not_written',
|
|
179
|
+
{ count: n(left), error: messageOf(outcome.error) },
|
|
180
|
+
left,
|
|
181
|
+
),
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
partial: {
|
|
186
|
+
title: t(
|
|
187
|
+
'dms_database.data.save.partial_title',
|
|
188
|
+
{ saved: n(savedCount), total: n(changes.length) },
|
|
189
|
+
changes.length,
|
|
190
|
+
),
|
|
191
|
+
description: details.join(' '),
|
|
192
|
+
},
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function undo() {
|
|
197
|
+
const tab = options.tab()
|
|
198
|
+
const current = notice.value
|
|
199
|
+
if (current?.state !== 'saved') return
|
|
200
|
+
// Kept while the values are written back, however long that takes.
|
|
201
|
+
show({ ...current, state: 'undoing' })
|
|
202
|
+
const outcome = await writeRows(
|
|
203
|
+
undoBodies(current.changes),
|
|
204
|
+
(rowId, body) => writeRow(tab, rowId, body),
|
|
205
|
+
)
|
|
206
|
+
const restored = changesOf(current.changes, outcome.written)
|
|
207
|
+
const gone = changesOf(current.changes, outcome.gone)
|
|
208
|
+
const left = changesOf(current.changes, outcome.left)
|
|
209
|
+
// Another tab since: its bar says nothing of this one, the toasts do.
|
|
210
|
+
const here = options.tab().id === tab.id
|
|
211
|
+
if (left.length > 0) {
|
|
212
|
+
// The rows the failure left keep their undo, for a retry.
|
|
213
|
+
if (here)
|
|
214
|
+
show({ ...current, state: 'saved', changes: left }, UNDO_WINDOW_MS)
|
|
215
|
+
toast.add({
|
|
216
|
+
title: t(
|
|
217
|
+
'dms_database.data.save.undo_partial',
|
|
218
|
+
{ count: n(left.length + gone.length) },
|
|
219
|
+
left.length + gone.length,
|
|
220
|
+
),
|
|
221
|
+
description: messageOf(outcome.error),
|
|
222
|
+
color: 'error',
|
|
223
|
+
})
|
|
224
|
+
} else if (here) {
|
|
225
|
+
show(
|
|
226
|
+
restored.length > 0
|
|
227
|
+
? { ...current, state: 'undone', changes: restored }
|
|
228
|
+
: null,
|
|
229
|
+
UNDONE_NOTICE_MS,
|
|
230
|
+
)
|
|
231
|
+
}
|
|
232
|
+
if (gone.length > 0 && left.length === 0)
|
|
233
|
+
toast.add({
|
|
234
|
+
title: t(
|
|
235
|
+
'dms_database.data.save.undo_partial',
|
|
236
|
+
{ count: n(gone.length) },
|
|
237
|
+
gone.length,
|
|
238
|
+
),
|
|
239
|
+
description: t(
|
|
240
|
+
'dms_database.data.save.rows_missing',
|
|
241
|
+
{ count: n(outcome.gone.length) },
|
|
242
|
+
outcome.gone.length,
|
|
243
|
+
),
|
|
244
|
+
color: 'warning',
|
|
245
|
+
})
|
|
246
|
+
options.onSaved()
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function announce(tab: BrowserTab, saved: StagedChange[]) {
|
|
250
|
+
if (options.tab().id !== tab.id) return
|
|
251
|
+
show({ state: 'saved', changes: saved, scope: scope(tab) }, UNDO_WINDOW_MS)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function review() {
|
|
255
|
+
const tab = options.tab()
|
|
256
|
+
const changes = staged.list(tab.id)
|
|
257
|
+
if (changes.length === 0 || saving.value) return
|
|
258
|
+
// Filled as rows land, whatever happens next: a failure, a retry, or
|
|
259
|
+
// the dialog closed, what was written is announced and undoable.
|
|
260
|
+
const saved: StagedChange[] = []
|
|
261
|
+
let settled = false
|
|
262
|
+
try {
|
|
263
|
+
await confirm({
|
|
264
|
+
title: t(
|
|
265
|
+
'dms_database.data.save.title',
|
|
266
|
+
{ table: tab.table },
|
|
267
|
+
changes.length,
|
|
268
|
+
),
|
|
269
|
+
description: t(
|
|
270
|
+
'dms_database.data.save.description',
|
|
271
|
+
{ scope: scope(tab) },
|
|
272
|
+
changes.length,
|
|
273
|
+
),
|
|
274
|
+
confirmLabel: t('dms_database.data.save.confirm', changes.length),
|
|
275
|
+
cancelLabel: t('dms_database.data.save.keep_editing'),
|
|
276
|
+
color: 'warning',
|
|
277
|
+
icon: 'i-ph-pencil-simple',
|
|
278
|
+
body: () => diffTable(changes),
|
|
279
|
+
onConfirm: async () => {
|
|
280
|
+
saving.value = true
|
|
281
|
+
try {
|
|
282
|
+
const outcome = await saveAll(tab)
|
|
283
|
+
saved.push(...changesOf(changes, outcome.written))
|
|
284
|
+
if (outcome.written.length + outcome.gone.length > 0) settled = true
|
|
285
|
+
return partialOutcome(outcome, changes, saved.length)
|
|
286
|
+
} finally {
|
|
287
|
+
saving.value = false
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
})
|
|
291
|
+
} finally {
|
|
292
|
+
if (settled) options.onSaved()
|
|
293
|
+
if (saved.length > 0) announce(tab, saved)
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return { saving, review, notice, undo }
|
|
298
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { clonePositions, type DiagramState } from './history'
|
|
2
|
+
|
|
3
|
+
// When the diagram of one schema is written (D-2): a change waits for a short
|
|
4
|
+
// pause, then its differences go out through that schema's writer. The state
|
|
5
|
+
// is copied the moment a write is asked for, so the canvas loading another
|
|
6
|
+
// schema afterwards never reaches it; writes run one after the other.
|
|
7
|
+
|
|
8
|
+
export type SaveStatus = 'idle' | 'saving' | 'saved' | 'error'
|
|
9
|
+
|
|
10
|
+
/** What writes one schema's diagram: a DiagramSync. */
|
|
11
|
+
export interface DiagramWriter {
|
|
12
|
+
hasChanges(state: DiagramState): boolean
|
|
13
|
+
save(state: DiagramState): Promise<void>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function snapshotState(state: DiagramState): DiagramState {
|
|
17
|
+
return {
|
|
18
|
+
positions: clonePositions(state.positions),
|
|
19
|
+
notes: state.notes.map((note) => ({ ...note })),
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class DiagramAutosave {
|
|
24
|
+
private timer: ReturnType<typeof setTimeout> | null = null
|
|
25
|
+
private writes: Promise<void> = Promise.resolve()
|
|
26
|
+
|
|
27
|
+
constructor(
|
|
28
|
+
private readonly writer: DiagramWriter,
|
|
29
|
+
/** The state to write, read when the write is asked for. */
|
|
30
|
+
private readonly read: () => DiagramState,
|
|
31
|
+
private readonly delayMs: number,
|
|
32
|
+
private readonly onStatus: (status: SaveStatus) => void,
|
|
33
|
+
) {}
|
|
34
|
+
|
|
35
|
+
/** Saves once edits pause. */
|
|
36
|
+
schedule() {
|
|
37
|
+
this.stopTimer()
|
|
38
|
+
this.timer = setTimeout(() => void this.flush(), this.delayMs)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Writes a scheduled save now, from a copy of the state taken at once.
|
|
43
|
+
* Resolves once every write asked for so far has ended.
|
|
44
|
+
*/
|
|
45
|
+
flush(): Promise<void> {
|
|
46
|
+
if (this.timer === null) return this.writes
|
|
47
|
+
this.stopTimer()
|
|
48
|
+
const state = snapshotState(this.read())
|
|
49
|
+
this.writes = this.writes.then(() => this.write(state))
|
|
50
|
+
return this.writes
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private stopTimer() {
|
|
54
|
+
if (this.timer !== null) clearTimeout(this.timer)
|
|
55
|
+
this.timer = null
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private async write(state: DiagramState) {
|
|
59
|
+
// Nothing left to write, say after an undo: an earlier error is moot.
|
|
60
|
+
if (!this.writer.hasChanges(state)) {
|
|
61
|
+
this.onStatus('idle')
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
this.onStatus('saving')
|
|
65
|
+
try {
|
|
66
|
+
await this.writer.save(state)
|
|
67
|
+
this.onStatus('saved')
|
|
68
|
+
} catch {
|
|
69
|
+
this.onStatus('error')
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|