@antelopejs/dms-database 0.0.13 → 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 +9 -6
  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,105 +1,114 @@
1
1
  <script setup lang="ts">
2
+ import {
3
+ cellLabel,
4
+ cellText,
5
+ draftText,
6
+ isStructured,
7
+ } from '../build/data/cellValues'
8
+ import type { StagedCell } from '../build/data/stagedEdits'
9
+ import type { ColumnRole } from '../build/utils/fieldTypes'
10
+
11
+ // One cell of the data grid. A staged cell is tinted, with the stored value
12
+ // struck through before the new one; nothing is written until the changes
13
+ // are saved. An empty cell reads null, an empty text "".
14
+
2
15
  const props = defineProps<{
3
- value: unknown;
4
- editable: boolean;
5
- editing: boolean;
6
- pending: boolean;
7
- invalid: boolean;
8
- focused: boolean;
9
- }>();
16
+ /** The column's name, which names the editor for assistive technology. */
17
+ column: string
18
+ value: unknown
19
+ staged?: StagedCell
20
+ role: ColumnRole
21
+ editable: boolean
22
+ editing: boolean
23
+ /** Why the draft was refused, shown under the editor. */
24
+ invalid?: string
25
+ focused: boolean
26
+ }>()
10
27
 
11
28
  const emit = defineEmits<{
12
- select: [];
13
- "start-edit": [];
14
- commit: [draft: string | boolean, source: "enter" | "blur"];
15
- cancel: [];
16
- "open-json": [];
17
- }>();
18
-
19
- const isJson = computed(
20
- () => props.value !== null && props.value !== undefined && typeof props.value === "object",
21
- );
22
- const isNull = computed(() => props.value === null || props.value === undefined);
29
+ select: []
30
+ 'start-edit': []
31
+ commit: [draft: string | boolean, source: 'enter' | 'blur']
32
+ cancel: []
33
+ }>()
23
34
 
24
- const display = computed(() => {
25
- if (isNull.value) return "";
26
- if (isJson.value) {
27
- try {
28
- return JSON.stringify(props.value);
29
- } catch {
30
- return String(props.value);
31
- }
32
- }
33
- return String(props.value);
34
- });
35
+ const shown = computed(() => (props.staged ? props.staged.after : props.value))
36
+ const isNull = computed(() => shown.value === null || shown.value === undefined)
37
+ const isEmptyText = computed(() => shown.value === '')
38
+ const structured = computed(() => isStructured(shown.value))
39
+ const isNumber = computed(() => typeof shown.value === 'number')
40
+ // "{3}" for an object of three keys, "[2]" for a list of two items.
41
+ const sizeTag = computed(() => {
42
+ if (!structured.value) return ''
43
+ const size = Object.keys(shown.value as object).length
44
+ return Array.isArray(shown.value) ? `[${size}]` : `{${size}}`
45
+ })
35
46
 
36
- // --- inline editing ---
37
- const draft = ref("");
38
- const boolDraft = ref(false);
39
- const cellInput = useTemplateRef<HTMLInputElement | HTMLSelectElement>("cellInput");
40
- // Set before blurring on Escape so the blur-commit does not fire after a cancel.
41
- const cancelling = ref(false);
47
+ const draft = ref('')
48
+ const boolDraft = ref(false)
49
+ const cellInput = useTemplateRef<HTMLInputElement | HTMLSelectElement>(
50
+ 'cellInput',
51
+ )
52
+ // Set before blurring on Escape so the blur does not commit after a cancel.
53
+ const cancelling = ref(false)
42
54
 
43
55
  watch(
44
56
  () => props.editing,
45
57
  (editing) => {
46
- if (!editing) return;
47
- cancelling.value = false;
48
- if (typeof props.value === "boolean") {
49
- boolDraft.value = props.value;
50
- } else {
51
- draft.value = isNull.value ? "" : String(props.value);
52
- }
58
+ if (!editing) return
59
+ cancelling.value = false
60
+ if (typeof shown.value === 'boolean') boolDraft.value = shown.value
61
+ else draft.value = draftText(shown.value)
53
62
  nextTick(() => {
54
- cellInput.value?.focus();
55
- if (cellInput.value instanceof HTMLInputElement) cellInput.value.select();
56
- });
63
+ cellInput.value?.focus()
64
+ if (cellInput.value instanceof HTMLInputElement) cellInput.value.select()
65
+ })
57
66
  },
58
- );
67
+ )
59
68
 
60
- // The parent needs the trigger: Enter keeps an invalid draft open for fixing,
61
- // while a blur (click-away) discards it so the grid never holds an unfocused
62
- // editor hostage.
63
- function commit(source: "enter" | "blur") {
64
- if (cancelling.value) return;
69
+ function commit(source: 'enter' | 'blur') {
70
+ if (cancelling.value) return
65
71
  emit(
66
- "commit",
67
- typeof props.value === "boolean" ? boolDraft.value : draft.value,
72
+ 'commit',
73
+ typeof shown.value === 'boolean' ? boolDraft.value : draft.value,
68
74
  source,
69
- );
75
+ )
70
76
  }
71
77
 
72
78
  function cancel() {
73
- cancelling.value = true;
74
- emit("cancel");
75
- }
76
-
77
- function onDoubleClick() {
78
- if (isJson.value) emit("open-json");
79
- else if (props.editable) emit("start-edit");
79
+ cancelling.value = true
80
+ emit('cancel')
80
81
  }
81
82
  </script>
82
83
 
83
84
  <template>
84
85
  <td
85
- class="relative cursor-default border-r border-b border-default p-0 align-middle"
86
+ class="border-default relative cursor-default border-b border-r p-0 align-middle"
86
87
  :class="[
87
88
  invalid
88
- ? 'ring-2 ring-error ring-inset'
89
+ ? 'ring-error ring-2 ring-inset'
89
90
  : focused
90
- ? 'ring-2 ring-primary ring-inset'
91
+ ? 'ring-primary ring-2 ring-inset'
91
92
  : '',
92
- editing ? 'bg-primary/5' : '',
93
+ staged ? 'bg-warning/10' : editing ? 'bg-primary/5' : '',
93
94
  ]"
95
+ :title="
96
+ staged
97
+ ? $t('dms_database.data.grid.staged_title', {
98
+ before: cellLabel(staged.before),
99
+ })
100
+ : undefined
101
+ "
94
102
  @click="emit('select')"
95
- @dblclick="onDoubleClick"
103
+ @dblclick="editable && emit('start-edit')"
96
104
  >
97
105
  <template v-if="editing">
98
106
  <select
99
- v-if="typeof value === 'boolean'"
107
+ v-if="typeof shown === 'boolean'"
100
108
  ref="cellInput"
101
109
  v-model="boolDraft"
102
- class="w-full bg-default px-2 py-1 font-mono text-[12.5px] outline-none"
110
+ class="bg-default w-full px-2 py-1 font-mono text-[12.5px] outline-none"
111
+ :aria-label="$t('dms_database.data.grid.edit_cell', { column })"
103
112
  @change="commit('enter')"
104
113
  @keydown.esc.stop="cancel"
105
114
  @keydown.stop
@@ -113,34 +122,73 @@ function onDoubleClick() {
113
122
  ref="cellInput"
114
123
  v-model="draft"
115
124
  type="text"
116
- class="w-full bg-default px-2 py-1 font-mono text-[12.5px] outline-none"
125
+ class="bg-default w-full px-2 py-1 font-mono text-[12.5px] outline-none"
126
+ :aria-label="$t('dms_database.data.grid.edit_cell', { column })"
127
+ :title="$t('dms_database.data.grid.null_hint')"
128
+ :aria-invalid="invalid ? true : undefined"
117
129
  @keydown.enter.prevent="commit('enter')"
118
130
  @keydown.esc.stop="cancel"
119
131
  @keydown.stop
120
132
  @blur="commit('blur')"
133
+ />
134
+ <span
135
+ v-if="invalid"
136
+ class="bg-error text-inverted absolute left-0 top-full z-20 mt-1 whitespace-nowrap rounded px-2 py-1 text-[11px] shadow"
121
137
  >
138
+ {{ invalid }}
139
+ </span>
122
140
  </template>
123
141
  <div
124
142
  v-else
125
- class="flex items-center gap-1 overflow-hidden px-2.5 py-1 whitespace-nowrap"
143
+ class="flex items-center gap-1.5 overflow-hidden whitespace-nowrap px-2.5 py-1"
144
+ :class="isNumber ? 'justify-end' : ''"
126
145
  >
146
+ <s v-if="staged" class="text-dimmed font-mono text-[11.5px]">
147
+ {{ cellLabel(staged.before) }}
148
+ </s>
127
149
  <span
128
150
  v-if="isNull"
129
- class="rounded bg-elevated px-1 font-mono text-[10px] tracking-wide text-dimmed uppercase"
151
+ class="text-dimmed rounded border border-dashed border-current px-1 font-mono text-[10px]"
130
152
  >
131
153
  null
132
154
  </span>
155
+ <span
156
+ v-else-if="isEmptyText"
157
+ class="text-dimmed font-mono text-[12.5px]"
158
+ :title="$t('dms_database.data.grid.empty_text')"
159
+ >
160
+ ""
161
+ </span>
162
+ <template v-else-if="structured">
163
+ <span
164
+ class="bg-elevated text-muted shrink-0 rounded px-1 font-mono text-[10px]"
165
+ >
166
+ {{ sizeTag }}
167
+ </span>
168
+ <span
169
+ class="text-muted overflow-hidden text-ellipsis font-mono text-[12px]"
170
+ >
171
+ {{ cellText(shown) }}
172
+ </span>
173
+ </template>
133
174
  <span
134
175
  v-else
135
- class="overflow-hidden font-mono text-[12.5px] text-ellipsis"
136
- :class="isJson ? 'cursor-pointer text-info' : 'text-toned'"
176
+ class="overflow-hidden text-ellipsis font-mono text-[12.5px] tabular-nums"
177
+ :class="[
178
+ role === 'key'
179
+ ? 'text-muted'
180
+ : role === 'relation'
181
+ ? 'text-primary'
182
+ : 'text-toned',
183
+ staged ? 'text-highlighted font-medium' : '',
184
+ ]"
137
185
  >
138
- {{ display }}
186
+ {{ cellText(shown) }}
139
187
  </span>
140
188
  <UIcon
141
- v-if="pending"
142
- name="i-ph-circle-notch"
143
- class="size-3 shrink-0 animate-spin text-primary"
189
+ v-if="role === 'relation' && !isNull"
190
+ name="i-ph-arrow-up-right"
191
+ class="text-primary size-3 shrink-0"
144
192
  />
145
193
  </div>
146
194
  </td>
@@ -1,147 +1,185 @@
1
1
  <script setup lang="ts">
2
- import type { ColumnFilterState, GridColumn } from "../composables/useDataBrowserGrid";
2
+ import { isReservedFilterField } from '../build/composables/useDataBrowserGrid'
3
+ import type {
4
+ ColumnFilterState,
5
+ GridColumn,
6
+ } from '../build/composables/useDataBrowserGrid'
7
+ import {
8
+ COLUMN_ROLE_CLASSES,
9
+ type ColumnRole,
10
+ describeRuntimeType,
11
+ } from '../build/utils/fieldTypes'
12
+
13
+ // A column header (D-14): its type as a neutral icon, colour only for the key
14
+ // and relation columns, and the sort arrow and funnel shown whenever they are
15
+ // on. The funnel opens the column's filter; the active filters are also chips
16
+ // under the toolbar.
3
17
 
4
18
  const props = defineProps<{
5
- column: GridColumn;
6
- sortKey: string | null;
7
- sortDirection: "asc" | "desc";
8
- filter: ColumnFilterState | null;
9
- width: number;
10
- }>();
19
+ column: GridColumn
20
+ role: ColumnRole
21
+ sortKey: string | null
22
+ sortDirection: 'asc' | 'desc'
23
+ filter: ColumnFilterState | null
24
+ width: number
25
+ }>()
11
26
 
12
27
  const emit = defineEmits<{
13
- sort: [];
14
- filter: [value: ColumnFilterState | null];
15
- resize: [width: number];
16
- }>();
28
+ sort: []
29
+ filter: [value: ColumnFilterState | null]
30
+ resize: [width: number]
31
+ }>()
32
+
33
+ const { t } = useI18n()
17
34
 
18
- const { t } = useI18n();
35
+ const ROLE_ICONS: Record<ColumnRole, string | null> = {
36
+ key: 'i-ph-key',
37
+ relation: 'i-ph-arrow-right',
38
+ plain: null,
39
+ }
19
40
 
20
- // Shared icon set (utils/databaseFieldTypeIcons) so runtime-sampled columns
21
- // carry the same glyph, color and named-field overrides as the inspector.
22
- const typeIcon = computed(() =>
23
- runtimeTypeIcon(props.column.type, props.column.name),
24
- );
41
+ const type = computed(() =>
42
+ describeRuntimeType(props.column.type, props.column.name),
43
+ )
44
+ const icon = computed(() => ROLE_ICONS[props.role] ?? type.value.icon)
25
45
 
26
46
  const sortIcon = computed(() => {
27
- if (props.sortKey !== props.column.name) return null;
28
- return props.sortDirection === "asc" ? "i-ph-caret-up" : "i-ph-caret-down";
29
- });
47
+ if (props.sortKey !== props.column.name) return null
48
+ return props.sortDirection === 'asc' ? 'i-ph-arrow-up' : 'i-ph-arrow-down'
49
+ })
30
50
 
31
51
  // --- filter popover ---
32
- const filterOpen = ref(false);
33
- const draftMode = ref<"is" | "contains">("contains");
34
- const draftValue = ref("");
52
+ const filterOpen = ref(false)
53
+ const draftMode = ref<'is' | 'contains'>('contains')
54
+ const draftValue = ref('')
35
55
 
36
56
  watch(filterOpen, (open) => {
37
- if (!open) return;
38
- draftMode.value = props.filter?.mode ?? "contains";
39
- draftValue.value = props.filter?.value ?? "";
40
- });
57
+ if (!open) return
58
+ draftMode.value = props.filter?.mode ?? 'contains'
59
+ draftValue.value = props.filter?.value ?? ''
60
+ })
41
61
 
42
62
  const modeOptions = computed(() => [
43
- { label: t("dms_database.data.filterModeContains"), value: "contains" },
44
- { label: t("dms_database.data.filterModeIs"), value: "is" },
45
- ]);
63
+ { label: t('dms_database.data.filters.contains'), value: 'contains' },
64
+ { label: t('dms_database.data.filters.is'), value: 'is' },
65
+ ])
46
66
 
47
67
  function applyFilter() {
48
- const value = draftValue.value.trim();
49
- emit("filter", value ? { mode: draftMode.value, value } : null);
50
- filterOpen.value = false;
68
+ const value = draftValue.value.trim()
69
+ emit('filter', value ? { mode: draftMode.value, value } : null)
70
+ filterOpen.value = false
51
71
  }
52
72
 
53
73
  function clearFilter() {
54
- emit("filter", null);
55
- filterOpen.value = false;
74
+ emit('filter', null)
75
+ filterOpen.value = false
56
76
  }
57
77
 
58
78
  // --- column resize (drag the right edge) ---
59
- const MIN_WIDTH = 80;
60
- const MAX_WIDTH = 640;
79
+ const MIN_WIDTH = 80
80
+ const MAX_WIDTH = 640
61
81
 
62
82
  // Emits are rAF-throttled (one width update per frame, not per mousemove) and
63
83
  // the window listeners are detached if the header unmounts mid-drag.
64
- let resizeFrame = 0;
65
- let detachResize: (() => void) | null = null;
84
+ let resizeFrame = 0
85
+ let detachResize: (() => void) | null = null
66
86
 
67
87
  function startResize(event: MouseEvent) {
68
- event.preventDefault();
69
- const startX = event.clientX;
70
- const startWidth = props.width;
88
+ event.preventDefault()
89
+ const startX = event.clientX
90
+ const startWidth = props.width
71
91
  function onMove(move: MouseEvent) {
72
- cancelAnimationFrame(resizeFrame);
92
+ cancelAnimationFrame(resizeFrame)
73
93
  resizeFrame = requestAnimationFrame(() => {
74
94
  const next = Math.min(
75
95
  MAX_WIDTH,
76
96
  Math.max(MIN_WIDTH, startWidth + (move.clientX - startX)),
77
- );
78
- emit("resize", next);
79
- });
97
+ )
98
+ emit('resize', next)
99
+ })
80
100
  }
81
101
  function onUp() {
82
- detachResize?.();
102
+ detachResize?.()
83
103
  }
84
104
  detachResize = () => {
85
- cancelAnimationFrame(resizeFrame);
86
- window.removeEventListener("mousemove", onMove);
87
- window.removeEventListener("mouseup", onUp);
88
- detachResize = null;
89
- };
90
- window.addEventListener("mousemove", onMove);
91
- window.addEventListener("mouseup", onUp);
105
+ cancelAnimationFrame(resizeFrame)
106
+ window.removeEventListener('mousemove', onMove)
107
+ window.removeEventListener('mouseup', onUp)
108
+ detachResize = null
109
+ }
110
+ window.addEventListener('mousemove', onMove)
111
+ window.addEventListener('mouseup', onUp)
92
112
  }
93
113
 
94
- onBeforeUnmount(() => detachResize?.());
114
+ onBeforeUnmount(() => detachResize?.())
95
115
  </script>
96
116
 
97
117
  <template>
98
118
  <th
99
- class="group relative select-none border-r border-b border-default bg-elevated p-0 text-left font-medium"
119
+ class="border-default bg-elevated group relative select-none border-b border-r p-0 text-left font-medium"
100
120
  scope="col"
121
+ :aria-sort="
122
+ sortIcon
123
+ ? sortDirection === 'asc'
124
+ ? 'ascending'
125
+ : 'descending'
126
+ : undefined
127
+ "
101
128
  >
102
129
  <div class="flex items-center gap-1.5 px-2.5 py-1.5">
103
130
  <UIcon
104
- v-if="column.isPrimaryKey"
105
- name="i-ph-key"
106
- class="size-3 shrink-0 text-amber-500"
107
- />
108
- <UIcon
109
- v-else
110
- :name="typeIcon.icon"
131
+ :name="icon"
111
132
  class="size-3 shrink-0"
112
- :class="typeIcon.color"
113
- :title="typeIcon.label"
133
+ :class="role === 'plain' ? 'text-dimmed' : COLUMN_ROLE_CLASSES[role]"
134
+ :title="type.label"
114
135
  />
115
136
  <button
116
137
  type="button"
117
- class="min-w-0 flex-1 truncate text-left font-mono text-xs text-toned hover:text-highlighted"
138
+ class="hover:text-highlighted min-w-0 flex-1 truncate text-left font-mono text-xs"
139
+ :class="role === 'relation' ? 'text-primary' : 'text-toned'"
140
+ :title="t('dms_database.data.grid.sort_hint')"
118
141
  @click="emit('sort')"
119
142
  >
120
143
  {{ column.name }}
121
144
  </button>
122
- <UIcon v-if="sortIcon" :name="sortIcon" class="size-3 shrink-0 text-primary" />
123
- <!--
124
- Data filters on columns named after the wire protocol's selection
125
- params (schema / table / instance) are silently dropped server-side,
126
- so don't offer the filter button on those columns at all.
127
- -->
128
- <UPopover v-if="!isReservedFilterField(column.name)" v-model:open="filterOpen">
145
+ <UIcon
146
+ v-if="sortIcon"
147
+ :name="sortIcon"
148
+ class="text-primary size-3 shrink-0"
149
+ />
150
+ <!-- Columns named after the selection parameters (schema, table,
151
+ instance) cannot be filtered: the server reads those as the
152
+ table to browse. -->
153
+ <UPopover
154
+ v-if="!isReservedFilterField(column.name)"
155
+ v-model:open="filterOpen"
156
+ >
129
157
  <button
130
158
  type="button"
131
159
  class="rounded p-0.5 transition-opacity"
132
160
  :class="
133
161
  filter
134
162
  ? 'text-primary opacity-100'
135
- : 'text-dimmed opacity-0 group-hover:opacity-100'
163
+ : 'text-dimmed opacity-0 focus-visible:opacity-100 group-hover:opacity-100'
164
+ "
165
+ :aria-label="
166
+ t('dms_database.data.filters.column_title', { column: column.name })
167
+ "
168
+ :title="
169
+ t('dms_database.data.filters.column_title', { column: column.name })
136
170
  "
137
- :aria-label="t('dms_database.data.filterTitle', { column: column.name })"
138
171
  >
139
172
  <UIcon name="i-ph-funnel" class="size-3" />
140
173
  </button>
141
174
  <template #content>
142
- <div class="w-56 space-y-2 p-3">
143
- <p class="font-mono text-xs text-muted">{{ column.name }}</p>
144
- <USelect v-model="draftMode" :items="modeOptions" size="xs" class="w-full" />
175
+ <div class="w-60 space-y-2 p-3">
176
+ <p class="text-muted font-mono text-xs">{{ column.name }}</p>
177
+ <USelect
178
+ v-model="draftMode"
179
+ :items="modeOptions"
180
+ size="xs"
181
+ class="w-full"
182
+ />
145
183
  <UInput
146
184
  v-model="draftValue"
147
185
  size="xs"
@@ -154,12 +192,12 @@ onBeforeUnmount(() => detachResize?.());
154
192
  size="xs"
155
193
  variant="ghost"
156
194
  color="neutral"
157
- :label="t('dms_database.data.filterClear')"
195
+ :label="t('dms_database.data.filters.remove')"
158
196
  @click="clearFilter"
159
197
  />
160
198
  <UButton
161
199
  size="xs"
162
- :label="t('dms_database.data.filterApply')"
200
+ :label="t('dms_database.data.filters.apply')"
163
201
  @click="applyFilter"
164
202
  />
165
203
  </div>
@@ -168,7 +206,7 @@ onBeforeUnmount(() => detachResize?.());
168
206
  </UPopover>
169
207
  </div>
170
208
  <span
171
- class="absolute top-0 right-0 z-10 h-full w-1 cursor-col-resize hover:bg-primary/40"
209
+ class="hover:bg-primary/40 absolute right-0 top-0 z-10 h-full w-1 cursor-col-resize"
172
210
  @mousedown="startResize"
173
211
  />
174
212
  </th>