@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.
- 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 +30 -29
- 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 +526 -193
- package/frontend-vue/i18n/locales/database-fr-FR.json +528 -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 +9 -6
- 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,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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
14
|
-
commit: [draft: string | boolean, source:
|
|
15
|
-
cancel: []
|
|
16
|
-
|
|
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
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return
|
|
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
|
-
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
49
|
-
|
|
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
|
-
|
|
61
|
-
|
|
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
|
-
|
|
67
|
-
typeof
|
|
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(
|
|
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-
|
|
86
|
+
class="border-default relative cursor-default border-b border-r p-0 align-middle"
|
|
86
87
|
:class="[
|
|
87
88
|
invalid
|
|
88
|
-
? 'ring-
|
|
89
|
+
? 'ring-error ring-2 ring-inset'
|
|
89
90
|
: focused
|
|
90
|
-
? 'ring-
|
|
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="
|
|
103
|
+
@dblclick="editable && emit('start-edit')"
|
|
96
104
|
>
|
|
97
105
|
<template v-if="editing">
|
|
98
106
|
<select
|
|
99
|
-
v-if="typeof
|
|
107
|
+
v-if="typeof shown === 'boolean'"
|
|
100
108
|
ref="cellInput"
|
|
101
109
|
v-model="boolDraft"
|
|
102
|
-
class="w-full
|
|
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
|
|
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
|
|
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
|
|
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]
|
|
136
|
-
:class="
|
|
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
|
-
{{
|
|
186
|
+
{{ cellText(shown) }}
|
|
139
187
|
</span>
|
|
140
188
|
<UIcon
|
|
141
|
-
v-if="
|
|
142
|
-
name="i-ph-
|
|
143
|
-
class="size-3 shrink-0
|
|
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
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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 ===
|
|
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<
|
|
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 ??
|
|
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(
|
|
44
|
-
{ label: t(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
87
|
-
window.removeEventListener(
|
|
88
|
-
detachResize = null
|
|
89
|
-
}
|
|
90
|
-
window.addEventListener(
|
|
91
|
-
window.addEventListener(
|
|
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-
|
|
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
|
-
|
|
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="
|
|
113
|
-
:title="
|
|
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
|
|
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
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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-
|
|
143
|
-
<p class="font-mono text-xs
|
|
144
|
-
<USelect
|
|
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.
|
|
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.
|
|
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
|
|
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>
|