@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
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
50
|
-
if (event.dataTransfer) event.dataTransfer.effectAllowed =
|
|
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 =
|
|
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
|
|
113
|
+
return showSchema.value ? `${tab.schema}.${tab.table}` : tab.table
|
|
79
114
|
}
|
|
80
115
|
|
|
81
116
|
function instanceBadge(tab: BrowserTab): string | null {
|
|
82
|
-
|
|
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
|
-
|
|
88
|
-
|
|
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="
|
|
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
|
-
|
|
99
|
-
|
|
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
|
-
<
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
<
|
|
123
|
-
|
|
124
|
-
class="
|
|
125
|
-
:
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
141
|
-
</
|
|
142
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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>
|