@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.
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 +55 -41
  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 +527 -193
  141. package/frontend-vue/i18n/locales/database-fr-FR.json +529 -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
@@ -1,5 +1,14 @@
1
1
  <script setup lang="ts">
2
- import type { BrowserTab } from "../composables/useDataBrowserTabs";
2
+ import {
3
+ instanceBadge as badgeOf,
4
+ useDataBrowserTabs,
5
+ } from '../build/composables/useDataBrowserTabs'
6
+ import { useStagedEdits } from '../build/data/stagedEdits'
7
+ import type { BrowserTab } from '../build/composables/useDataBrowserTabs'
8
+
9
+ // The open tables (D-05). A preview tab, in italics, is replaced by the next
10
+ // table opened from the list; its pin keeps it, and the bar says so. A tab
11
+ // holding staged edits carries a dot and asks before it is closed.
3
12
 
4
13
  const {
5
14
  tabs,
@@ -9,20 +18,23 @@ const {
9
18
  closeTab,
10
19
  pinTab,
11
20
  moveTab,
12
- } = useDataBrowserTabs();
13
- const { t } = useI18n();
21
+ } = useDataBrowserTabs()
22
+ const { t } = useI18n()
23
+ const staged = useStagedEdits()
24
+ const { confirm } = useConfirm()
14
25
 
15
- // Shared by the pin and close buttons — they are a hover-reveal pair on the
16
- // same tab and must render identically.
17
- const tabActionClass =
18
- "rounded p-0.5 text-dimmed opacity-0 transition-opacity group-hover:opacity-100 focus-visible:opacity-100 hover:bg-elevated hover:text-default";
26
+ const closeButtonClass =
27
+ 'rounded p-0.5 text-dimmed opacity-0 transition-opacity group-hover:opacity-100 focus-visible:opacity-100 hover:bg-elevated hover:text-default'
28
+ const pinButtonClass =
29
+ 'rounded p-0.5 text-muted hover:bg-elevated hover:text-highlighted'
30
+ const hasPreview = computed(() => tabs.value.some((tab) => tab.preview))
19
31
 
20
32
  // The label doubles as the accessible name; a preview tab announces its
21
33
  // ephemeral state, which the italic styling alone cannot convey.
22
34
  function tabAriaLabel(tab: BrowserTab): string {
23
35
  return tab.preview
24
- ? t("dms_database.data.previewTab", { name: label(tab) })
25
- : label(tab);
36
+ ? t('dms_database.data.tabs.preview', { name: label(tab) })
37
+ : label(tab)
26
38
  }
27
39
 
28
40
  // Pinning from the pin button unmounts the button under focus (v-if), which
@@ -30,32 +42,32 @@ function tabAriaLabel(tab: BrowserTab): string {
30
42
  // label button — but only when focus actually fell to the body, so a mouse
31
43
  // click doesn't get focus stolen elsewhere.
32
44
  function pinFromButton(tab: BrowserTab, event: MouseEvent) {
33
- const tabEl = (event.currentTarget as HTMLElement).closest("[data-tab]");
34
- pinTab(tab.id);
45
+ const tabEl = (event.currentTarget as HTMLElement).closest('[data-tab]')
46
+ pinTab(tab.id)
35
47
  nextTick(() => {
36
48
  if (document.activeElement === document.body) {
37
- tabEl?.querySelector("button")?.focus();
49
+ tabEl?.querySelector('button')?.focus()
38
50
  }
39
- });
51
+ })
40
52
  }
41
53
 
42
54
  // --- drag & drop reorder ---
43
- const draggedId = ref<string | null>(null);
44
- const dropTargetId = ref<string | null>(null);
55
+ const draggedId = ref<string | null>(null)
56
+ const dropTargetId = ref<string | null>(null)
45
57
 
46
58
  function onDragStart(tab: BrowserTab, event: DragEvent) {
47
- draggedId.value = tab.id;
59
+ draggedId.value = tab.id
48
60
  // Firefox refuses to start a drag without data attached.
49
- event.dataTransfer?.setData("text/plain", tab.id);
50
- if (event.dataTransfer) event.dataTransfer.effectAllowed = "move";
61
+ event.dataTransfer?.setData('text/plain', tab.id)
62
+ if (event.dataTransfer) event.dataTransfer.effectAllowed = 'move'
51
63
  }
52
64
 
53
65
  function onDragOver(tab: BrowserTab, event: DragEvent) {
54
- if (!draggedId.value || draggedId.value === tab.id) return;
66
+ if (!draggedId.value || draggedId.value === tab.id) return
55
67
  // preventDefault marks the tab as a valid drop target.
56
- event.preventDefault();
57
- if (event.dataTransfer) event.dataTransfer.dropEffect = "move";
58
- dropTargetId.value = tab.id;
68
+ event.preventDefault()
69
+ if (event.dataTransfer) event.dataTransfer.dropEffect = 'move'
70
+ dropTargetId.value = tab.id
59
71
  }
60
72
 
61
73
  function onDrop(tab: BrowserTab) {
@@ -63,104 +75,184 @@ function onDrop(tab: BrowserTab) {
63
75
  // the dragged tab as a valid target, so this is belt-and-braces.)
64
76
  if (draggedId.value && draggedId.value !== tab.id) {
65
77
  // An arranged tab is kept (VS Code): a completed drag-reorder pins.
66
- pinTab(draggedId.value);
67
- moveTab(draggedId.value, tab.id);
78
+ pinTab(draggedId.value)
79
+ moveTab(draggedId.value, tab.id)
68
80
  }
69
- onDragEnd();
81
+ onDragEnd()
70
82
  }
71
83
 
72
84
  function onDragEnd() {
73
- draggedId.value = null;
74
- dropTargetId.value = null;
85
+ draggedId.value = null
86
+ dropTargetId.value = null
87
+ }
88
+
89
+ // Labels name the schema while tabs span several. Closing the last tab of a
90
+ // schema would drop the prefix from every other label at once, so the tabs
91
+ // shrink and slide under the pointer that is closing them: as browsers keep
92
+ // tab widths, the prefix stays until the pointer leaves the bar.
93
+ const pointerInBar = ref(false)
94
+ const showSchema = ref(hasMultipleSchemas.value)
95
+ watch(hasMultipleSchemas, (several) => {
96
+ if (several || !pointerInBar.value) showSchema.value = several
97
+ })
98
+
99
+ function leaveBar() {
100
+ pointerInBar.value = false
101
+ showSchema.value = hasMultipleSchemas.value
75
102
  }
76
103
 
104
+ // Closing the last tab removes the bar before the pointer can leave it.
105
+ watch(
106
+ () => tabs.value.length === 0,
107
+ (empty) => {
108
+ if (empty) leaveBar()
109
+ },
110
+ )
111
+
77
112
  function label(tab: BrowserTab): string {
78
- return hasMultipleSchemas.value ? `${tab.schema}.${tab.table}` : tab.table;
113
+ return showSchema.value ? `${tab.schema}.${tab.table}` : tab.table
79
114
  }
80
115
 
81
116
  function instanceBadge(tab: BrowserTab): string | null {
82
- if (tab.instance === DEFAULT_INSTANCE_VALUE) return null;
83
- if (tab.instance === CROSS_INSTANCE_VALUE) return "*";
84
- return `@${tab.instance}`;
117
+ return badgeOf(tab.instance, t('dms_database.data.scope.all'))
85
118
  }
86
119
 
87
- function close(tab: BrowserTab) {
88
- closeTab(tab.id);
120
+ // The active tab stays in view when it changes or the bar fills up: a tab
121
+ // opened at the end of a full bar would otherwise open out of sight.
122
+ const strip = useTemplateRef<HTMLDivElement>('strip')
123
+ watch(
124
+ [activeId, () => tabs.value.length],
125
+ () => {
126
+ nextTick(() => {
127
+ const id = activeId.value
128
+ if (!id) return
129
+ const active = [
130
+ ...(strip.value?.querySelectorAll<HTMLElement>('[data-tab]') ?? []),
131
+ ].find((element) => element.dataset.tabId === id)
132
+ active?.scrollIntoView({ block: 'nearest', inline: 'nearest' })
133
+ })
134
+ },
135
+ { immediate: true, flush: 'post' },
136
+ )
137
+
138
+ async function close(tab: BrowserTab) {
139
+ const pending = staged.count(tab.id)
140
+ if (pending > 0) {
141
+ const discard = await confirm({
142
+ title: t('dms_database.data.tabs.close_title', { table: tab.table }),
143
+ description: t('dms_database.data.tabs.close_description', pending),
144
+ confirmLabel: t('dms_database.data.tabs.close_discard'),
145
+ cancelLabel: t('dms_database.data.tabs.close_keep'),
146
+ color: 'warning',
147
+ initialFocus: 'cancel',
148
+ })
149
+ if (!discard) return
150
+ staged.discard(tab.id)
151
+ }
152
+ closeTab(tab.id)
89
153
  }
90
154
  </script>
91
155
 
92
156
  <template>
93
157
  <div
94
158
  v-if="tabs.length > 0"
95
- class="flex items-end gap-1 overflow-x-auto border-b border-default bg-elevated/50 px-2 pt-1.5"
159
+ class="border-default bg-elevated/50 flex items-end border-b"
160
+ @pointerenter="pointerInBar = true"
161
+ @pointerleave="leaveBar"
96
162
  >
97
163
  <div
98
- v-for="tab in tabs"
99
- :key="tab.id"
100
- data-tab
101
- draggable="true"
102
- class="group flex shrink-0 select-none items-center gap-1.5 rounded-t-md border border-b-0 px-2.5 py-1.5"
103
- :class="[
104
- tab.id === activeId
105
- ? 'border-default bg-default'
106
- : 'border-transparent bg-transparent hover:bg-default/60',
107
- tab.id === draggedId ? 'opacity-40' : '',
108
- tab.id === dropTargetId ? 'ring-1 ring-primary ring-inset' : '',
109
- ]"
110
- @dragstart="onDragStart(tab, $event)"
111
- @dragover="onDragOver(tab, $event)"
112
- @drop.prevent="onDrop(tab)"
113
- @dragend="onDragEnd"
164
+ ref="strip"
165
+ class="flex min-w-0 flex-1 items-end gap-1 overflow-x-auto px-2 pt-1.5 [scrollbar-width:thin]"
114
166
  >
115
- <button
116
- type="button"
117
- class="flex items-center gap-1.5"
118
- :aria-label="tabAriaLabel(tab)"
119
- @click="activateTab(tab.id)"
120
- @dblclick="pinTab(tab.id)"
167
+ <div
168
+ v-for="tab in tabs"
169
+ :key="tab.id"
170
+ data-tab
171
+ :data-tab-id="tab.id"
172
+ draggable="true"
173
+ class="group flex shrink-0 select-none items-center gap-1.5 rounded-t-md border border-b-0 px-2.5 py-1.5"
174
+ :class="[
175
+ tab.id === activeId
176
+ ? 'border-default bg-default'
177
+ : 'hover:bg-default/60 border-transparent bg-transparent',
178
+ tab.id === draggedId ? 'opacity-40' : '',
179
+ tab.id === dropTargetId ? 'ring-primary ring-1 ring-inset' : '',
180
+ ]"
181
+ @dragstart="onDragStart(tab, $event)"
182
+ @dragover="onDragOver(tab, $event)"
183
+ @drop.prevent="onDrop(tab)"
184
+ @dragend="onDragEnd"
121
185
  >
122
- <UIcon
123
- name="i-ph-table"
124
- class="size-3.5"
125
- :class="tab.id === activeId ? 'text-primary' : 'text-dimmed'"
126
- />
127
- <span
128
- class="max-w-48 truncate font-mono text-xs"
129
- :class="[
130
- tab.id === activeId ? 'text-highlighted' : 'text-muted',
131
- tab.preview ? 'italic' : '',
132
- ]"
186
+ <button
187
+ type="button"
188
+ class="flex items-center gap-1.5"
189
+ :aria-label="tabAriaLabel(tab)"
190
+ @click="activateTab(tab.id)"
191
+ @dblclick="pinTab(tab.id)"
133
192
  >
134
- {{ label(tab) }}
135
- </span>
136
- <span
137
- v-if="instanceBadge(tab)"
138
- class="rounded bg-primary/10 px-1 font-mono text-[10px] text-primary"
193
+ <UIcon
194
+ name="i-ph-table"
195
+ class="size-3.5"
196
+ :class="tab.id === activeId ? 'text-primary' : 'text-dimmed'"
197
+ />
198
+ <span
199
+ class="max-w-48 truncate font-mono text-xs"
200
+ :class="[
201
+ tab.id === activeId ? 'text-highlighted' : 'text-muted',
202
+ tab.preview ? 'italic' : '',
203
+ ]"
204
+ >
205
+ {{ label(tab) }}
206
+ </span>
207
+ <span
208
+ v-if="instanceBadge(tab)"
209
+ class="text-primary font-mono text-[10.5px]"
210
+ >
211
+ {{ instanceBadge(tab) }}
212
+ </span>
213
+ <span
214
+ v-if="staged.count(tab.id) > 0"
215
+ class="bg-warning size-1.5 rounded-full"
216
+ :title="t('dms_database.data.tabs.unsaved', staged.count(tab.id))"
217
+ />
218
+ </button>
219
+ <button
220
+ type="button"
221
+ :class="[closeButtonClass, { 'opacity-100': tab.id === activeId }]"
222
+ :aria-label="t('dms_database.data.tabs.close')"
223
+ :title="t('dms_database.data.tabs.close')"
224
+ @click.stop="close(tab)"
139
225
  >
140
- {{ instanceBadge(tab) }}
141
- </span>
142
- </button>
143
- <button
144
- type="button"
145
- :class="[tabActionClass, { 'opacity-100': tab.id === activeId }]"
146
- :aria-label="$t('dms_database.data.closeTab')"
147
- @click.stop="close(tab)"
148
- >
149
- <UIcon name="i-ph-x" class="size-3" />
150
- </button>
151
- <!-- The pin button sits AFTER the close button: pinning unmounts it, and
226
+ <UIcon name="i-ph-x" class="size-3" />
227
+ </button>
228
+ <!-- The pin button sits AFTER the close button: pinning unmounts it, and
152
229
  nothing may reflow into the pointer's position — a double-click's
153
230
  second click would otherwise land on the close button and destroy
154
231
  the tab the user just pinned. -->
155
- <button
156
- v-if="tab.preview"
157
- type="button"
158
- :class="[tabActionClass, { 'opacity-100': tab.id === activeId }]"
159
- :aria-label="$t('dms_database.data.keepTab')"
160
- @click.stop="pinFromButton(tab, $event)"
161
- >
162
- <UIcon name="i-ph-push-pin" class="size-3" />
163
- </button>
232
+ <UTooltip
233
+ v-if="tab.preview"
234
+ :text="t('dms_database.data.tabs.keep_hint')"
235
+ >
236
+ <button
237
+ type="button"
238
+ :class="pinButtonClass"
239
+ :aria-label="t('dms_database.data.tabs.keep')"
240
+ @click.stop="pinFromButton(tab, $event)"
241
+ >
242
+ <UIcon name="i-ph-push-pin" class="size-3" />
243
+ </button>
244
+ </UTooltip>
245
+ </div>
164
246
  </div>
247
+ <span
248
+ v-if="hasPreview"
249
+ class="text-dimmed hidden shrink-0 self-center whitespace-nowrap px-3 pb-1 pt-1.5 text-[11px] lg:inline"
250
+ >
251
+ <i18n-t keypath="dms_database.data.tabs.hint" tag="span">
252
+ <template #italic>
253
+ <em>{{ t('dms_database.data.tabs.italic') }}</em>
254
+ </template>
255
+ </i18n-t>
256
+ </span>
165
257
  </div>
166
258
  </template>
@@ -0,0 +1,163 @@
1
+ <script setup lang="ts">
2
+ import { isReservedFilterField } from '../build/composables/useDataBrowserGrid'
3
+ import type {
4
+ ColumnFilterState,
5
+ GridColumn,
6
+ } from '../build/composables/useDataBrowserGrid'
7
+ import { describeRuntimeType } from '../build/utils/fieldTypes'
8
+
9
+ // The active column filters as removable chips (D-14), with a way to add one
10
+ // on any column and to clear them all.
11
+
12
+ type FilterMode = ColumnFilterState['mode']
13
+
14
+ const props = defineProps<{
15
+ columns: GridColumn[]
16
+ filters: Record<string, ColumnFilterState>
17
+ }>()
18
+
19
+ const emit = defineEmits<{
20
+ set: [field: string, filter: ColumnFilterState | null]
21
+ clear: []
22
+ }>()
23
+
24
+ const { t } = useI18n()
25
+
26
+ const chips = computed(() =>
27
+ Object.entries(props.filters)
28
+ .filter(([, filter]) => filter.value)
29
+ .map(([field, filter]) => {
30
+ const column = props.columns.find((candidate) => candidate.name === field)
31
+ return {
32
+ field,
33
+ filter,
34
+ icon: column
35
+ ? describeRuntimeType(column.type, column.name).icon
36
+ : 'i-ph-funnel',
37
+ }
38
+ }),
39
+ )
40
+
41
+ const MODE_LABELS: Record<FilterMode, string> = {
42
+ contains: 'dms_database.data.filters.contains',
43
+ is: 'dms_database.data.filters.is',
44
+ }
45
+
46
+ const addOpen = ref(false)
47
+ const draftField = ref<string | undefined>()
48
+ const draftMode = ref<FilterMode>('contains')
49
+ const draftValue = ref('')
50
+
51
+ const fieldItems = computed(() =>
52
+ props.columns
53
+ .filter((column) => !isReservedFilterField(column.name))
54
+ .map((column) => ({ label: column.name, value: column.name })),
55
+ )
56
+ const modeItems = computed(() =>
57
+ (Object.keys(MODE_LABELS) as FilterMode[]).map((mode) => ({
58
+ label: t(MODE_LABELS[mode]),
59
+ value: mode,
60
+ })),
61
+ )
62
+
63
+ watch(addOpen, (open) => {
64
+ if (!open) return
65
+ draftField.value = fieldItems.value[0]?.value
66
+ draftMode.value = 'contains'
67
+ draftValue.value = ''
68
+ })
69
+
70
+ function add() {
71
+ const value = draftValue.value.trim()
72
+ if (!draftField.value || !value) return
73
+ emit('set', draftField.value, { mode: draftMode.value, value })
74
+ addOpen.value = false
75
+ }
76
+ </script>
77
+
78
+ <template>
79
+ <div
80
+ class="border-default flex flex-wrap items-center gap-1.5 border-b px-3 py-1.5"
81
+ >
82
+ <span
83
+ v-for="chip in chips"
84
+ :key="chip.field"
85
+ class="border-accented bg-elevated inline-flex h-7 items-center overflow-hidden rounded-md border text-xs"
86
+ >
87
+ <span class="text-toned flex items-center gap-1 px-2 font-mono">
88
+ <UIcon :name="chip.icon" class="text-dimmed size-3" />
89
+ {{ chip.field }}
90
+ </span>
91
+ <span class="text-muted border-default border-l px-2">
92
+ {{ t(MODE_LABELS[chip.filter.mode]) }}
93
+ </span>
94
+ <span
95
+ class="text-highlighted border-default max-w-48 truncate border-l px-2 font-mono"
96
+ >
97
+ {{ chip.filter.value }}
98
+ </span>
99
+ <button
100
+ type="button"
101
+ class="text-dimmed hover:text-highlighted hover:bg-default border-default h-full border-l px-1.5"
102
+ :aria-label="
103
+ t('dms_database.data.filters.remove_chip', { column: chip.field })
104
+ "
105
+ :title="
106
+ t('dms_database.data.filters.remove_chip', { column: chip.field })
107
+ "
108
+ @click="emit('set', chip.field, null)"
109
+ >
110
+ <UIcon name="i-ph-x" class="size-3" />
111
+ </button>
112
+ </span>
113
+ <UPopover v-model:open="addOpen">
114
+ <UButton
115
+ size="xs"
116
+ color="neutral"
117
+ variant="ghost"
118
+ icon="i-ph-plus"
119
+ :label="t('dms_database.data.filters.add')"
120
+ :disabled="fieldItems.length === 0"
121
+ />
122
+ <template #content>
123
+ <form class="grid w-64 gap-2 p-3" @submit.prevent="add">
124
+ <USelect
125
+ v-model="draftField"
126
+ :items="fieldItems"
127
+ size="xs"
128
+ class="w-full font-mono"
129
+ />
130
+ <USelect
131
+ v-model="draftMode"
132
+ :items="modeItems"
133
+ size="xs"
134
+ class="w-full"
135
+ />
136
+ <UInput
137
+ v-model="draftValue"
138
+ size="xs"
139
+ class="w-full"
140
+ autofocus
141
+ :placeholder="t('dms_database.data.filters.value')"
142
+ />
143
+ <UButton
144
+ type="submit"
145
+ size="xs"
146
+ class="justify-self-end"
147
+ :label="t('dms_database.data.filters.apply')"
148
+ :disabled="!draftValue.trim()"
149
+ />
150
+ </form>
151
+ </template>
152
+ </UPopover>
153
+ <UButton
154
+ v-if="chips.length > 0"
155
+ size="xs"
156
+ color="neutral"
157
+ variant="ghost"
158
+ class="ml-auto"
159
+ :label="t('dms_database.data.filters.clear_all')"
160
+ @click="emit('clear')"
161
+ />
162
+ </div>
163
+ </template>