@antelopejs/dms-database 0.0.14 → 0.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 (173) hide show
  1. package/README.md +30 -5
  2. package/dist/db/models/query_history.model.d.ts +1 -0
  3. package/dist/db/models/query_history.model.js +6 -0
  4. package/dist/db/models/query_history.model.js.map +1 -1
  5. package/dist/db/tables/query_history.table.d.ts +3 -0
  6. package/dist/db/tables/query_history.table.js +12 -0
  7. package/dist/db/tables/query_history.table.js.map +1 -1
  8. package/dist/db/tables/saved_queries.table.d.ts +1 -0
  9. package/dist/db/tables/saved_queries.table.js +4 -0
  10. package/dist/db/tables/saved_queries.table.js.map +1 -1
  11. package/dist/i18n/messages.d.ts +39 -0
  12. package/dist/i18n/messages.js +64 -0
  13. package/dist/i18n/messages.js.map +1 -0
  14. package/dist/index.d.ts +1 -0
  15. package/dist/index.js +1 -7
  16. package/dist/index.js.map +1 -1
  17. package/dist/module.d.ts +11 -0
  18. package/dist/module.js +55 -0
  19. package/dist/module.js.map +1 -0
  20. package/dist/pages/data.d.ts +1 -1
  21. package/dist/pages/data.js +9 -6
  22. package/dist/pages/data.js.map +1 -1
  23. package/dist/pages/diagram.d.ts +9 -0
  24. package/dist/pages/diagram.js +45 -0
  25. package/dist/pages/diagram.js.map +1 -0
  26. package/dist/pages/index.d.ts +1 -0
  27. package/dist/pages/index.js +1 -0
  28. package/dist/pages/index.js.map +1 -1
  29. package/dist/pages/overview.d.ts +4 -1
  30. package/dist/pages/overview.js +113 -6
  31. package/dist/pages/overview.js.map +1 -1
  32. package/dist/pages/query.d.ts +1 -1
  33. package/dist/pages/query.js +22 -6
  34. package/dist/pages/query.js.map +1 -1
  35. package/dist/pages/schemas.d.ts +2 -1
  36. package/dist/pages/schemas.js +143 -6
  37. package/dist/pages/schemas.js.map +1 -1
  38. package/dist/routes/data.d.ts +8 -0
  39. package/dist/routes/data.js +84 -21
  40. package/dist/routes/data.js.map +1 -1
  41. package/dist/routes/index.d.ts +1 -0
  42. package/dist/routes/index.js +1 -0
  43. package/dist/routes/index.js.map +1 -1
  44. package/dist/routes/overview.d.ts +12 -0
  45. package/dist/routes/overview.js +55 -0
  46. package/dist/routes/overview.js.map +1 -0
  47. package/dist/routes/query.d.ts +5 -1
  48. package/dist/routes/query.js +162 -27
  49. package/dist/routes/query.js.map +1 -1
  50. package/dist/routes/tables.d.ts +15 -12
  51. package/dist/routes/tables.js +95 -55
  52. package/dist/routes/tables.js.map +1 -1
  53. package/dist/service/catalog.d.ts +3 -0
  54. package/dist/service/catalog.js +26 -0
  55. package/dist/service/catalog.js.map +1 -0
  56. package/dist/service/health.d.ts +3 -3
  57. package/dist/service/health.js +17 -14
  58. package/dist/service/health.js.map +1 -1
  59. package/dist/service/introspect.d.ts +3 -1
  60. package/dist/service/introspect.js +131 -43
  61. package/dist/service/introspect.js.map +1 -1
  62. package/dist/service/overview.d.ts +9 -0
  63. package/dist/service/overview.js +113 -0
  64. package/dist/service/overview.js.map +1 -0
  65. package/dist/service/queryInspection.d.ts +15 -0
  66. package/dist/service/queryInspection.js +107 -0
  67. package/dist/service/queryInspection.js.map +1 -0
  68. package/dist/service/references.d.ts +9 -0
  69. package/dist/service/references.js +52 -0
  70. package/dist/service/references.js.map +1 -0
  71. package/dist/service/schemaFilters.d.ts +26 -0
  72. package/dist/service/schemaFilters.js +42 -0
  73. package/dist/service/schemaFilters.js.map +1 -0
  74. package/dist/service/stagedSerialization.d.ts +2 -0
  75. package/dist/service/stagedSerialization.js +38 -10
  76. package/dist/service/stagedSerialization.js.map +1 -1
  77. package/dist/service/types.d.ts +11 -1
  78. package/dist/types/constants.d.ts +2 -0
  79. package/dist/types/constants.js +3 -1
  80. package/dist/types/constants.js.map +1 -1
  81. package/dist/utils/query.d.ts +0 -2
  82. package/dist/utils/query.js +0 -20
  83. package/dist/utils/query.js.map +1 -1
  84. package/frontend-vue/app/{composables → build/composables}/useDataBrowserGrid.ts +65 -50
  85. package/frontend-vue/app/build/composables/useDataBrowserTabs.ts +366 -0
  86. package/frontend-vue/app/build/composables/useDatabaseHealth.ts +50 -0
  87. package/frontend-vue/app/build/composables/useDatabaseSchemas.ts +140 -0
  88. package/frontend-vue/app/{composables → build/composables}/useDiagramPersistence.ts +47 -39
  89. package/frontend-vue/app/build/composables/useQueryRuntime.ts +217 -0
  90. package/frontend-vue/app/build/composables/useQueryStore.ts +162 -0
  91. package/frontend-vue/app/build/data/cellValues.ts +132 -0
  92. package/frontend-vue/app/build/data/instanceOptions.ts +36 -0
  93. package/frontend-vue/app/build/data/pickerMenus.ts +175 -0
  94. package/frontend-vue/app/build/data/recentTables.ts +48 -0
  95. package/frontend-vue/app/build/data/stagedEdits.ts +117 -0
  96. package/frontend-vue/app/build/data/stagedWrites.ts +64 -0
  97. package/frontend-vue/app/build/data/useStagedSave.ts +298 -0
  98. package/frontend-vue/app/build/diagram/autosave.ts +72 -0
  99. package/frontend-vue/app/build/diagram/graph.ts +585 -0
  100. package/frontend-vue/app/build/diagram/history.ts +178 -0
  101. package/frontend-vue/app/build/diagram/shortcuts.ts +62 -0
  102. package/frontend-vue/app/build/diagram/sync.ts +137 -0
  103. package/frontend-vue/app/build/query/completion.ts +100 -0
  104. package/frontend-vue/app/build/query/csv.ts +32 -0
  105. package/frontend-vue/app/build/query/examples.ts +38 -0
  106. package/frontend-vue/app/build/query/runs.ts +168 -0
  107. package/frontend-vue/app/build/query/tableColumns.ts +35 -0
  108. package/frontend-vue/app/build/query/target.ts +143 -0
  109. package/frontend-vue/app/build/schemas/filterState.ts +128 -0
  110. package/frontend-vue/app/build/schemas/useSchemasListing.ts +62 -0
  111. package/frontend-vue/app/build/utils/databaseLinks.ts +98 -0
  112. package/frontend-vue/app/build/utils/fieldTypes.ts +198 -0
  113. package/frontend-vue/app/build/utils/relativeTime.ts +19 -0
  114. package/frontend-vue/app/{utils → build/utils}/stagedSerialization.ts +7 -1
  115. package/frontend-vue/app/components/ConnectionStatus.vue +122 -0
  116. package/frontend-vue/app/components/DataBrowser.vue +126 -0
  117. package/frontend-vue/app/components/DataBrowserSidebar.vue +293 -210
  118. package/frontend-vue/app/components/DataBrowserTabBar.vue +185 -93
  119. package/frontend-vue/app/components/DataFilterBar.vue +163 -0
  120. package/frontend-vue/app/components/DataGrid.vue +737 -499
  121. package/frontend-vue/app/components/DataGridCell.vue +125 -77
  122. package/frontend-vue/app/components/DataGridHeaderCell.vue +119 -81
  123. package/frontend-vue/app/components/DiagramNoteNode.vue +81 -63
  124. package/frontend-vue/app/components/DiagramRelationEdge.vue +57 -27
  125. package/frontend-vue/app/components/DiagramStubNode.vue +16 -17
  126. package/frontend-vue/app/components/DiagramTableNode.vue +106 -68
  127. package/frontend-vue/app/components/InstancePicker.vue +195 -0
  128. package/frontend-vue/app/components/JsonCellPanel.vue +30 -29
  129. package/frontend-vue/app/components/QueryBuilderPanel.vue +290 -238
  130. package/frontend-vue/app/components/QueryConsole.vue +726 -0
  131. package/frontend-vue/app/components/QueryLibrary.vue +314 -0
  132. package/frontend-vue/app/components/QueryResultPanel.vue +556 -126
  133. package/frontend-vue/app/components/RowDrawer.vue +520 -0
  134. package/frontend-vue/app/components/SchemaDiagram.vue +1016 -1051
  135. package/frontend-vue/app/components/SchemaPicker.vue +86 -0
  136. package/frontend-vue/app/components/SchemasFilterBar.vue +201 -0
  137. package/frontend-vue/app/components/TableInspector.vue +741 -0
  138. package/frontend-vue/app/config/shortcuts-registry.ts +65 -0
  139. package/frontend-vue/dms.frontend.ts +5 -14
  140. package/frontend-vue/i18n/locales/database-en-GB.json +526 -193
  141. package/frontend-vue/i18n/locales/database-fr-FR.json +528 -195
  142. package/frontend-vue/package.json +2 -1
  143. package/frontend-vue/pnpm-lock.yaml +2601 -858
  144. package/frontend-vue/pnpm-workspace.yaml +1 -1
  145. package/frontend-vue/tests/dataBrowser.test.ts +206 -0
  146. package/frontend-vue/tests/diagram.test.ts +516 -0
  147. package/frontend-vue/tests/helpers.test.ts +205 -0
  148. package/frontend-vue/tests/instanceOptions.test.ts +53 -0
  149. package/frontend-vue/tests/locales.test.ts +56 -0
  150. package/frontend-vue/tests/persistence.test.ts +106 -5
  151. package/frontend-vue/tests/pickerMenus.test.ts +254 -0
  152. package/frontend-vue/tests/queryConsole.test.ts +282 -0
  153. package/frontend-vue/tests/queryRuntime.test.ts +2 -2
  154. package/frontend-vue/tests/registration.test.ts +36 -25
  155. package/frontend-vue/tests/schemasFilters.test.ts +115 -0
  156. package/package.json +7 -7
  157. package/frontend-vue/app/components/QueryFavoritesPanel.vue +0 -127
  158. package/frontend-vue/app/components/QueryHistoryPanel.vue +0 -134
  159. package/frontend-vue/app/components/SchemaTableInspector.vue +0 -278
  160. package/frontend-vue/app/composables/useDataBrowserTabs.ts +0 -255
  161. package/frontend-vue/app/composables/useDatabaseHealth.ts +0 -50
  162. package/frontend-vue/app/composables/useDatabaseSchemas.ts +0 -92
  163. package/frontend-vue/app/composables/useQueryRuntime.ts +0 -118
  164. package/frontend-vue/app/composables/useQueryStore.ts +0 -117
  165. package/frontend-vue/app/composables/useTableViewRowClick.ts +0 -22
  166. package/frontend-vue/app/custom-pages/database/data.vue +0 -41
  167. package/frontend-vue/app/custom-pages/database/overview.vue +0 -367
  168. package/frontend-vue/app/custom-pages/database/query.vue +0 -436
  169. package/frontend-vue/app/custom-pages/database/schemas.vue +0 -218
  170. package/frontend-vue/app/utils/databaseFieldModifiers.ts +0 -33
  171. package/frontend-vue/app/utils/databaseFieldTypeIcons.ts +0 -224
  172. package/frontend-vue/app/utils/dmsTableView.ts +0 -83
  173. package/frontend-vue/app/utils/relativeTime.ts +0 -18
@@ -0,0 +1,520 @@
1
+ <script setup lang="ts">
2
+ import {
3
+ CROSS_INSTANCE_VALUE,
4
+ DEFAULT_INSTANCE_VALUE,
5
+ instanceBadge,
6
+ } from '../build/composables/useDataBrowserTabs'
7
+ import { browseSelectionQuery } from '../build/composables/useDataBrowserGrid'
8
+ import { onKeyStroke } from '@vueuse/core'
9
+ import {
10
+ cellLabel,
11
+ cellText,
12
+ draftText,
13
+ isStructured,
14
+ parseDraft,
15
+ } from '../build/data/cellValues'
16
+ import { useStagedEdits } from '../build/data/stagedEdits'
17
+ import type { TableSummary } from '../build/composables/useDatabaseSchemas'
18
+ import type { BrowserTab } from '../build/composables/useDataBrowserTabs'
19
+ import type { GridColumn } from '../build/composables/useDataBrowserGrid'
20
+ import { tableLink } from '../build/utils/databaseLinks'
21
+ import { describeRuntimeType } from '../build/utils/fieldTypes'
22
+
23
+ // One row of the data browser in a drawer: its fields, editable and staged
24
+ // like the grid's cells, its JSON, and the rows of other tables pointing at
25
+ // it. J and K step to the next and previous row of the page.
26
+
27
+ interface RowNavigation {
28
+ index: number
29
+ total: number
30
+ hasPrev: boolean
31
+ hasNext: boolean
32
+ prev: () => void
33
+ next: () => void
34
+ }
35
+
36
+ interface RowReference {
37
+ schema: string
38
+ table: string
39
+ field: string
40
+ count: number
41
+ }
42
+
43
+ type DrawerTab = 'fields' | 'json' | 'references'
44
+
45
+ const props = defineProps<{
46
+ tab: BrowserTab
47
+ row: Record<string, unknown>
48
+ rowId: string | null
49
+ columns: GridColumn[]
50
+ table?: TableSummary
51
+ readOnly: boolean
52
+ navigation?: RowNavigation
53
+ onStage: (rowId: string, field: string, value: unknown) => void
54
+ onReview: () => void
55
+ }>()
56
+
57
+ // A link out closes the drawer: the page it opens may be this one.
58
+ const emit = defineEmits<{ success: [] }>()
59
+
60
+ const { t, n } = useI18n()
61
+ const { $authFetch } = useAuthFetch()
62
+ const staged = useStagedEdits()
63
+
64
+ const active = ref<DrawerTab>('fields')
65
+ const drafts = reactive<Record<string, string>>({})
66
+ const errors = reactive<Record<string, string>>({})
67
+
68
+ const relations = computed(
69
+ () =>
70
+ new Map(
71
+ (props.table?.relations ?? []).map((relation) => [
72
+ relation.fromField,
73
+ relation,
74
+ ]),
75
+ ),
76
+ )
77
+
78
+ function valueOf(field: string): unknown {
79
+ if (!props.rowId) return props.row[field]
80
+ const cell = staged.get(props.tab.id, props.rowId, field)
81
+ return cell ? cell.after : props.row[field]
82
+ }
83
+
84
+ const fields = computed(() =>
85
+ props.columns.map((column) => {
86
+ const relation = relations.value.get(column.name)
87
+ const value = valueOf(column.name)
88
+ return {
89
+ name: column.name,
90
+ column,
91
+ value,
92
+ key: column.isPrimaryKey,
93
+ relation,
94
+ structured: isStructured(value),
95
+ type: relation
96
+ ? relation.toTable
97
+ : describeRuntimeType(column.type, column.name).label,
98
+ icon: column.isPrimaryKey
99
+ ? 'i-ph-key'
100
+ : relation
101
+ ? 'i-ph-arrow-right'
102
+ : describeRuntimeType(column.type, column.name).icon,
103
+ staged: props.rowId
104
+ ? staged.get(props.tab.id, props.rowId, column.name)
105
+ : undefined,
106
+ editable:
107
+ !props.readOnly &&
108
+ !column.isPrimaryKey &&
109
+ column.name !== '_instance' &&
110
+ props.rowId !== null,
111
+ }
112
+ }),
113
+ )
114
+
115
+ const pending = computed(() =>
116
+ props.rowId ? staged.rowCount(props.tab.id, props.rowId) : 0,
117
+ )
118
+ // The review writes every staged change of the table, this row's and others'.
119
+ const tabPending = computed(() => staged.count(props.tab.id))
120
+ const stagedRow = computed(() =>
121
+ Object.fromEntries(
122
+ props.columns.map((column) => [column.name, valueOf(column.name)]),
123
+ ),
124
+ )
125
+ const json = computed(() => JSON.stringify(stagedRow.value, null, 2))
126
+
127
+ function draftOf(field: string, value: unknown): string {
128
+ return (
129
+ drafts[field] ??
130
+ (isStructured(value) ? JSON.stringify(value, null, 2) : draftText(value))
131
+ )
132
+ }
133
+
134
+ function commit(field: (typeof fields.value)[number]) {
135
+ const draft = drafts[field.name]
136
+ if (draft === undefined || !props.rowId) return
137
+ Reflect.deleteProperty(errors, field.name)
138
+ if (field.structured) {
139
+ try {
140
+ props.onStage(props.rowId, field.name, JSON.parse(draft))
141
+ } catch {
142
+ errors[field.name] = t('dms_database.data.row.invalid_json')
143
+ return
144
+ }
145
+ } else {
146
+ const result = parseDraft(draft, field.value, field.column.type)
147
+ if (!result.ok) {
148
+ errors[field.name] = t(`dms_database.data.grid.invalid_${result.reason}`)
149
+ return
150
+ }
151
+ if (!result.unchanged) props.onStage(props.rowId, field.name, result.value)
152
+ }
153
+ Reflect.deleteProperty(drafts, field.name)
154
+ }
155
+
156
+ // The id the row's label names its control by.
157
+ function controlId(field: string): string {
158
+ return `dms-database-row-field-${field.replace(/[^\w-]/g, '_')}`
159
+ }
160
+
161
+ function setNull(field: string) {
162
+ if (!props.rowId) return
163
+ Reflect.deleteProperty(drafts, field)
164
+ props.onStage(props.rowId, field, null)
165
+ }
166
+
167
+ function setBoolean(field: string, value: boolean) {
168
+ if (props.rowId) props.onStage(props.rowId, field, value)
169
+ }
170
+
171
+ // --- referenced by ---
172
+ const references = ref<RowReference[] | null>(null)
173
+ const referencesFailed = ref(false)
174
+
175
+ async function loadReferences() {
176
+ if (!props.rowId || props.readOnly) return
177
+ referencesFailed.value = false
178
+ try {
179
+ const res = await $authFetch<{ items: RowReference[] }>(
180
+ '/api/database/browse/references',
181
+ {
182
+ query: { ...browseSelectionQuery(props.tab), id: props.rowId },
183
+ },
184
+ )
185
+ references.value = res.items
186
+ } catch {
187
+ referencesFailed.value = true
188
+ }
189
+ }
190
+
191
+ onMounted(loadReferences)
192
+
193
+ const referenceTotal = computed(() =>
194
+ (references.value ?? []).reduce((sum, reference) => sum + reference.count, 0),
195
+ )
196
+
197
+ const tabs = computed(() => [
198
+ {
199
+ value: 'fields',
200
+ label: t('dms_database.data.row.fields'),
201
+ badge: props.columns.length,
202
+ },
203
+ { value: 'json', label: t('dms_database.data.row.json') },
204
+ ...(props.readOnly
205
+ ? []
206
+ : [
207
+ {
208
+ value: 'references',
209
+ label: t('dms_database.data.row.references'),
210
+ badge: referenceTotal.value,
211
+ },
212
+ ]),
213
+ ])
214
+
215
+ const scope = computed(() => {
216
+ const badge = instanceBadge(
217
+ props.tab.instance,
218
+ t('dms_database.data.scope.all'),
219
+ )
220
+ return `${props.tab.schema}.${props.tab.table}${badge ? ` ${badge}` : ''}`
221
+ })
222
+
223
+ // The instance a link to a table of the same schema keeps.
224
+ const scopedInstance = computed(() =>
225
+ props.tab.instance === DEFAULT_INSTANCE_VALUE ||
226
+ props.tab.instance === CROSS_INSTANCE_VALUE
227
+ ? undefined
228
+ : props.tab.instance,
229
+ )
230
+
231
+ function typing(event: KeyboardEvent): boolean {
232
+ return Boolean(
233
+ (event.target as HTMLElement | null)?.closest('input, textarea, select'),
234
+ )
235
+ }
236
+
237
+ onKeyStroke('j', (event) => {
238
+ if (!typing(event) && props.navigation?.hasNext) props.navigation.next()
239
+ })
240
+ onKeyStroke('k', (event) => {
241
+ if (!typing(event) && props.navigation?.hasPrev) props.navigation.prev()
242
+ })
243
+ </script>
244
+
245
+ <template>
246
+ <!-- As wide as the side drawer, as the table inspector. -->
247
+ <div class="flex h-full w-full min-w-0 flex-col gap-4">
248
+ <header class="grid gap-2">
249
+ <div class="flex items-center gap-1">
250
+ <DmsEyebrow :label="t('dms_database.data.row.eyebrow', { scope })" />
251
+ <span class="flex-1" />
252
+ <template v-if="navigation">
253
+ <span class="text-dimmed mr-1 font-mono text-[11px] tabular-nums">
254
+ {{ navigation.index + 1 }} / {{ navigation.total }}
255
+ </span>
256
+ <UButton
257
+ icon="i-ph-caret-up"
258
+ color="neutral"
259
+ variant="ghost"
260
+ size="xs"
261
+ :disabled="!navigation.hasPrev"
262
+ :title="t('dms_database.data.row.previous')"
263
+ :aria-label="t('dms_database.data.row.previous')"
264
+ @click="navigation.prev()"
265
+ />
266
+ <UButton
267
+ icon="i-ph-caret-down"
268
+ color="neutral"
269
+ variant="ghost"
270
+ size="xs"
271
+ :disabled="!navigation.hasNext"
272
+ :title="t('dms_database.data.row.next')"
273
+ :aria-label="t('dms_database.data.row.next')"
274
+ @click="navigation.next()"
275
+ />
276
+ </template>
277
+ </div>
278
+ <h3 class="text-highlighted font-mono text-lg font-semibold">
279
+ {{ rowId ?? t('dms_database.data.row.no_id') }}
280
+ </h3>
281
+ <UTabs
282
+ v-model="active"
283
+ :items="tabs"
284
+ variant="link"
285
+ size="sm"
286
+ :content="false"
287
+ />
288
+ </header>
289
+
290
+ <div class="min-h-0 flex-1 overflow-y-auto">
291
+ <!-- Each field is a DMS form row: the column's name in the label
292
+ column, its type and staged change as help, the error under the
293
+ control. The drawer is narrower than the label column's step, so
294
+ the label sits above the control, as in a DMS form drawer. -->
295
+ <div v-if="active === 'fields'" class="flex flex-col">
296
+ <DmsFieldRow
297
+ v-for="field in fields"
298
+ :key="field.name"
299
+ layout="form"
300
+ :inset="false"
301
+ spacing="row"
302
+ :label-for="field.editable ? controlId(field.name) : undefined"
303
+ >
304
+ <template #label>
305
+ <span class="font-mono">{{ field.name }}</span>
306
+ </template>
307
+ <template #details>
308
+ <p
309
+ class="mt-0.5 flex items-center gap-1 font-mono text-[12px] leading-normal"
310
+ :class="
311
+ field.key
312
+ ? 'text-warning'
313
+ : field.relation
314
+ ? 'text-primary'
315
+ : 'text-muted'
316
+ "
317
+ >
318
+ <UIcon :name="field.icon" class="size-3 shrink-0" />
319
+ {{
320
+ field.key ? t('dms_database.inspector.primary_key') : field.type
321
+ }}
322
+ </p>
323
+ <p
324
+ v-if="field.staged"
325
+ class="text-warning mt-0.5 break-all font-mono text-[12px] leading-normal"
326
+ >
327
+ {{
328
+ t('dms_database.data.row.was', {
329
+ value: cellLabel(field.staged.before),
330
+ })
331
+ }}
332
+ </p>
333
+ </template>
334
+
335
+ <p
336
+ v-if="!field.editable"
337
+ class="text-muted break-all pt-1.5 font-mono text-[12.5px]"
338
+ >
339
+ {{ cellLabel(field.value) }}
340
+ <span v-if="field.key" class="text-dimmed font-sans">
341
+ · {{ t('dms_database.data.row.not_editable') }}
342
+ </span>
343
+ </p>
344
+ <div v-else class="grid min-w-0 gap-1.5">
345
+ <UFormField
346
+ :name="field.name"
347
+ :error="errors[field.name] ?? false"
348
+ :data-field="field.name"
349
+ >
350
+ <DmsSwitch
351
+ v-if="typeof field.value === 'boolean'"
352
+ :id="controlId(field.name)"
353
+ :model-value="field.value"
354
+ class="pt-1.5"
355
+ @update:model-value="setBoolean(field.name, Boolean($event))"
356
+ />
357
+ <DmsTextarea
358
+ v-else-if="field.structured"
359
+ :id="controlId(field.name)"
360
+ :model-value="draftOf(field.name, field.value)"
361
+ :rows="4"
362
+ autoresize
363
+ class="w-full font-mono text-xs"
364
+ :color="field.staged ? 'warning' : 'primary'"
365
+ :highlight="Boolean(field.staged || errors[field.name])"
366
+ @update:model-value="drafts[field.name] = String($event)"
367
+ @blur="commit(field)"
368
+ />
369
+ <div v-else class="flex min-w-0 items-center gap-2">
370
+ <DmsInputText
371
+ :id="controlId(field.name)"
372
+ :model-value="draftOf(field.name, field.value)"
373
+ :title="t('dms_database.data.grid.null_hint')"
374
+ :placeholder="
375
+ field.value === null || field.value === undefined
376
+ ? 'null'
377
+ : undefined
378
+ "
379
+ class="min-w-0 flex-1 font-mono"
380
+ :color="
381
+ errors[field.name]
382
+ ? 'error'
383
+ : field.staged
384
+ ? 'warning'
385
+ : 'primary'
386
+ "
387
+ :highlight="Boolean(field.staged || errors[field.name])"
388
+ @update:model-value="drafts[field.name] = String($event)"
389
+ @blur="commit(field)"
390
+ @keydown.enter.prevent="commit(field)"
391
+ />
392
+ <UButton
393
+ v-if="field.value !== null && field.value !== undefined"
394
+ size="sm"
395
+ color="neutral"
396
+ variant="ghost"
397
+ :label="t('dms_database.data.row.set_null')"
398
+ @click="setNull(field.name)"
399
+ />
400
+ </div>
401
+ <template v-if="errors[field.name]" #error="{ error }">
402
+ <DmsFormErrorText :error />
403
+ </template>
404
+ </UFormField>
405
+ </div>
406
+ <DmsAutoLink
407
+ @click="emit('success')"
408
+ v-if="
409
+ field.relation &&
410
+ field.value !== null &&
411
+ field.value !== undefined
412
+ "
413
+ :to="
414
+ tableLink('data', {
415
+ schema: field.relation.toSchema,
416
+ table: field.relation.toTable,
417
+ instance:
418
+ field.relation.toSchema === tab.schema
419
+ ? scopedInstance
420
+ : undefined,
421
+ match: {
422
+ field: field.relation.toField,
423
+ value: cellText(field.value),
424
+ },
425
+ })
426
+ "
427
+ class="text-primary inline-flex items-center gap-1 justify-self-start text-xs hover:underline"
428
+ >
429
+ <UIcon name="i-ph-arrow-square-out" class="size-3" />
430
+ {{
431
+ t('dms_database.data.row.open_relation', {
432
+ table: field.relation.toTable,
433
+ })
434
+ }}
435
+ </DmsAutoLink>
436
+ </DmsFieldRow>
437
+ </div>
438
+
439
+ <DmsCodeSnippet
440
+ v-else-if="active === 'json'"
441
+ :code="json"
442
+ language="json"
443
+ />
444
+
445
+ <div v-else class="grid gap-2">
446
+ <DmsEmptyState
447
+ v-if="referencesFailed"
448
+ size="sm"
449
+ variant="error"
450
+ :title="t('dms_database.data.row.references_failed')"
451
+ :actions="[
452
+ { label: t('dms_database.common.retry'), onClick: loadReferences },
453
+ ]"
454
+ />
455
+ <DmsEmptyState
456
+ v-else-if="references && references.length === 0"
457
+ size="sm"
458
+ :title="t('dms_database.data.row.references_none')"
459
+ />
460
+ <DmsAutoLink
461
+ @click="emit('success')"
462
+ v-for="reference in references ?? []"
463
+ :key="`${reference.schema}.${reference.table}.${reference.field}`"
464
+ :to="
465
+ tableLink('data', {
466
+ schema: reference.schema,
467
+ table: reference.table,
468
+ instance:
469
+ reference.schema === tab.schema ? scopedInstance : undefined,
470
+ match: rowId
471
+ ? { field: reference.field, value: rowId }
472
+ : undefined,
473
+ })
474
+ "
475
+ class="border-default hover:bg-elevated/50 flex items-center gap-2 rounded-md border px-3 py-2 font-mono text-[12.5px]"
476
+ >
477
+ <UIcon name="i-ph-table" class="text-primary size-3.5" />
478
+ <span class="text-toned">
479
+ {{
480
+ reference.schema === tab.schema
481
+ ? reference.table
482
+ : `${reference.schema}.${reference.table}`
483
+ }}
484
+ </span>
485
+ <span class="text-dimmed ml-auto text-xs">
486
+ {{
487
+ t(
488
+ 'dms_database.data.row.reference_count',
489
+ { count: n(reference.count), field: reference.field },
490
+ reference.count,
491
+ )
492
+ }}
493
+ </span>
494
+ </DmsAutoLink>
495
+ </div>
496
+ </div>
497
+
498
+ <footer
499
+ v-if="!readOnly"
500
+ class="border-default flex items-center gap-2 border-t pt-3"
501
+ >
502
+ <span class="text-dimmed flex items-center gap-1.5 text-xs">
503
+ <span v-if="pending" class="bg-warning size-1.5 rounded-full" />
504
+ {{
505
+ pending
506
+ ? t('dms_database.data.tabs.unsaved', pending)
507
+ : t('dms_database.data.row.no_change')
508
+ }}
509
+ </span>
510
+ <UButton
511
+ v-if="tabPending > 0"
512
+ class="ml-auto"
513
+ size="sm"
514
+ icon="i-ph-check"
515
+ :label="t('dms_database.data.save.review', tabPending)"
516
+ @click="onReview"
517
+ />
518
+ </footer>
519
+ </div>
520
+ </template>