@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
|
@@ -5,115 +5,125 @@ import {
|
|
|
5
5
|
type CompletionContext,
|
|
6
6
|
type CompletionResult,
|
|
7
7
|
completionKeymap,
|
|
8
|
-
} from
|
|
8
|
+
} from '@codemirror/autocomplete'
|
|
9
9
|
import {
|
|
10
10
|
defaultKeymap,
|
|
11
11
|
history,
|
|
12
12
|
historyKeymap,
|
|
13
13
|
indentWithTab,
|
|
14
|
-
} from
|
|
15
|
-
import { javascript } from
|
|
14
|
+
} from '@codemirror/commands'
|
|
15
|
+
import { javascript } from '@codemirror/lang-javascript'
|
|
16
16
|
import {
|
|
17
17
|
bracketMatching,
|
|
18
18
|
defaultHighlightStyle,
|
|
19
19
|
indentOnInput,
|
|
20
20
|
syntaxHighlighting,
|
|
21
|
-
} from
|
|
22
|
-
import { Compartment, EditorState } from
|
|
23
|
-
import { oneDark } from
|
|
21
|
+
} from '@codemirror/language'
|
|
22
|
+
import { Compartment, EditorState } from '@codemirror/state'
|
|
23
|
+
import { oneDark } from '@codemirror/theme-one-dark'
|
|
24
24
|
import {
|
|
25
25
|
EditorView,
|
|
26
26
|
keymap,
|
|
27
27
|
lineNumbers,
|
|
28
28
|
placeholder as cmPlaceholder,
|
|
29
|
-
} from
|
|
30
|
-
import { useColorMode } from
|
|
31
|
-
import type { SchemaSummary } from
|
|
32
|
-
import
|
|
29
|
+
} from '@codemirror/view'
|
|
30
|
+
import { useColorMode } from '@vueuse/core'
|
|
31
|
+
import type { SchemaSummary } from '../build/composables/useDatabaseSchemas'
|
|
32
|
+
import {
|
|
33
|
+
buildCompletionIndex,
|
|
34
|
+
fieldsAt,
|
|
35
|
+
INSTANCE_METHODS,
|
|
36
|
+
QUERY_METHODS,
|
|
37
|
+
SCHEMA_METHODS,
|
|
38
|
+
} from '../build/query/completion'
|
|
39
|
+
import { targetFromSource } from '../build/query/target'
|
|
40
|
+
import { tableAccess } from '../build/utils/databaseLinks'
|
|
41
|
+
|
|
42
|
+
// The query editor (D-12): AQL with completion of the workspace's own
|
|
43
|
+
// schemas, tables and columns, snippets to insert at
|
|
44
|
+
// the cursor, and the cursor position.
|
|
33
45
|
|
|
34
46
|
interface Props {
|
|
35
|
-
modelValue: string
|
|
36
|
-
|
|
37
|
-
executing?: boolean;
|
|
47
|
+
modelValue: string
|
|
48
|
+
executing?: boolean
|
|
38
49
|
// Drives the schema-aware autocomplete (table/field names + the query DSL).
|
|
39
|
-
schemas?: SchemaSummary[]
|
|
50
|
+
schemas?: SchemaSummary[]
|
|
51
|
+
/** The saved query open in the editor, if any. */
|
|
52
|
+
savedName?: string
|
|
53
|
+
savedShared?: boolean
|
|
54
|
+
/** Whether the editor differs from the saved query it opened. */
|
|
55
|
+
edited?: boolean
|
|
40
56
|
}
|
|
41
57
|
|
|
42
58
|
const props = withDefaults(defineProps<Props>(), {
|
|
43
59
|
executing: false,
|
|
44
60
|
schemas: () => [],
|
|
45
|
-
|
|
61
|
+
savedName: undefined,
|
|
62
|
+
savedShared: false,
|
|
63
|
+
edited: false,
|
|
64
|
+
})
|
|
46
65
|
|
|
47
66
|
const emit = defineEmits<{
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}>()
|
|
67
|
+
'update:modelValue': [value: string]
|
|
68
|
+
execute: []
|
|
69
|
+
save: []
|
|
70
|
+
new: []
|
|
71
|
+
}>()
|
|
53
72
|
|
|
54
|
-
|
|
55
|
-
// footer hint (not translatable — the angle-bracket-free call trips no i18n guard).
|
|
56
|
-
const BUILDER_EXAMPLE = 'schemas.shop.instance().table("users").slice(0, 10)';
|
|
73
|
+
const { t } = useI18n()
|
|
57
74
|
|
|
58
75
|
const internalValue = computed<string>({
|
|
59
76
|
get: () => props.modelValue,
|
|
60
|
-
set: (value) => emit(
|
|
61
|
-
})
|
|
77
|
+
set: (value) => emit('update:modelValue', value),
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
// The placeholder starts from a table of the workspace, not a made-up one.
|
|
81
|
+
const placeholderExample = computed(() => {
|
|
82
|
+
const schema = props.schemas.find((candidate) => candidate.tables.length > 0)
|
|
83
|
+
const table = schema?.tables[0]?.name
|
|
84
|
+
return schema && table
|
|
85
|
+
? `${tableAccess({ schema: schema.id, table })}.slice(0, 10)`
|
|
86
|
+
: 'schemas.<schema>.instance().table("<table>").slice(0, 10)'
|
|
87
|
+
})
|
|
62
88
|
|
|
63
|
-
|
|
89
|
+
interface Snippet {
|
|
90
|
+
label: string
|
|
91
|
+
insert: string
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const SNIPPETS: Snippet[] = [
|
|
95
|
+
{ label: 'table()', insert: '.table("")' },
|
|
96
|
+
{ label: 'filter()', insert: '.filter((row) => row.key("").eq(""))' },
|
|
97
|
+
{ label: 'orderBy()', insert: '.orderBy("", "desc")' },
|
|
98
|
+
{ label: 'slice()', insert: '.slice(0, 50)' },
|
|
99
|
+
{ label: 'count()', insert: '.count()' },
|
|
100
|
+
{ label: 'get(id)', insert: '.get("")' },
|
|
101
|
+
]
|
|
64
102
|
|
|
65
|
-
const
|
|
66
|
-
get: () => props.language,
|
|
67
|
-
set: (value) => emit("update:language", value),
|
|
68
|
-
});
|
|
103
|
+
const cursor = ref({ line: 1, column: 1 })
|
|
69
104
|
|
|
70
105
|
function canExecute(): boolean {
|
|
71
|
-
return Boolean(internalValue.value.trim()) && !props.executing
|
|
106
|
+
return Boolean(internalValue.value.trim()) && !props.executing
|
|
72
107
|
}
|
|
73
108
|
|
|
74
109
|
// Light tidy: strip trailing whitespace and surrounding blank lines.
|
|
75
110
|
function runFormat() {
|
|
76
111
|
internalValue.value = internalValue.value
|
|
77
|
-
.split(
|
|
78
|
-
.map((line) => line.replace(/\s+$/,
|
|
79
|
-
.join(
|
|
80
|
-
.replace(/^\n+/,
|
|
81
|
-
.replace(/\n+$/,
|
|
112
|
+
.split('\n')
|
|
113
|
+
.map((line) => line.replace(/\s+$/, ''))
|
|
114
|
+
.join('\n')
|
|
115
|
+
.replace(/^\n+/, '')
|
|
116
|
+
.replace(/\n+$/, '')
|
|
82
117
|
}
|
|
83
118
|
|
|
84
119
|
// --- autocomplete -----------------------------------------------------------
|
|
85
120
|
// The DSL is a fluent chain — `schemas.<id>.instance().table("…").slice(…)` — so
|
|
86
121
|
// completions are offered by position in that chain rather than as one flat list.
|
|
87
|
-
// Schema also exposes createInstance/destroyInstance/listInstances, but only
|
|
88
|
-
// instance(id?) continues the browse chain, so it is the only one offered.
|
|
89
|
-
const SCHEMA_METHODS = ["instance"];
|
|
90
|
-
const INSTANCE_METHODS = ["table"]; // a SchemaInstance only exposes table(name)
|
|
91
|
-
const QUERY_METHODS = [
|
|
92
|
-
"slice",
|
|
93
|
-
"orderBy",
|
|
94
|
-
"filter",
|
|
95
|
-
"map",
|
|
96
|
-
"pluck",
|
|
97
|
-
"get",
|
|
98
|
-
"getAll",
|
|
99
|
-
"between",
|
|
100
|
-
"count",
|
|
101
|
-
"limit",
|
|
102
|
-
"insert",
|
|
103
|
-
"update",
|
|
104
|
-
"replace",
|
|
105
|
-
"delete",
|
|
106
|
-
"do",
|
|
107
|
-
"default",
|
|
108
|
-
"key",
|
|
109
|
-
"lookup",
|
|
110
|
-
"changes",
|
|
111
|
-
];
|
|
112
122
|
|
|
113
123
|
interface CompletionItem {
|
|
114
|
-
label: string
|
|
115
|
-
type: string
|
|
116
|
-
detail?: string
|
|
124
|
+
label: string
|
|
125
|
+
type: string
|
|
126
|
+
detail?: string
|
|
117
127
|
// What is actually inserted, when it differs from the label: a string (e.g.
|
|
118
128
|
// auto-quoting a table name) or a function (e.g. rewriting `schemas.<id>` to
|
|
119
129
|
// bracket form for ids that aren't valid identifiers).
|
|
@@ -124,65 +134,29 @@ interface CompletionItem {
|
|
|
124
134
|
completion: Completion,
|
|
125
135
|
from: number,
|
|
126
136
|
to: number,
|
|
127
|
-
) => void)
|
|
137
|
+
) => void)
|
|
128
138
|
}
|
|
129
139
|
|
|
130
140
|
const opt = (label: string, type: string, detail?: string): CompletionItem => ({
|
|
131
141
|
label,
|
|
132
142
|
type,
|
|
133
143
|
detail,
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
// Schema-derived names, indexed per schema
|
|
137
|
-
//
|
|
138
|
-
// completion source reads `.value` lazily, so no editor
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
instancesBySchema[schema.id] = schema.instances ?? [];
|
|
149
|
-
const tables: string[] = [];
|
|
150
|
-
const fields = new Set<string>();
|
|
151
|
-
for (const table of schema.tables ?? []) {
|
|
152
|
-
tables.push(table.name);
|
|
153
|
-
allTables.add(table.name);
|
|
154
|
-
for (const field of Object.keys(table.fields ?? {})) {
|
|
155
|
-
fields.add(field);
|
|
156
|
-
allFields.add(field);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
tablesBySchema[schema.id] = tables;
|
|
160
|
-
fieldsBySchema[schema.id] = [...fields];
|
|
161
|
-
}
|
|
162
|
-
return {
|
|
163
|
-
schemaIds,
|
|
164
|
-
tablesBySchema,
|
|
165
|
-
fieldsBySchema,
|
|
166
|
-
instancesBySchema,
|
|
167
|
-
allTables: [...allTables],
|
|
168
|
-
allFields: [...allFields],
|
|
169
|
-
};
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
// `schemas.demo` (dot) or `schemas["dms-core"]` (bracket — required for ids that
|
|
173
|
-
// aren't valid JS identifiers). Used to scope table/field/instance completions.
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
// Schema-derived names, indexed per schema and per table so completions are
|
|
147
|
+
// scoped to the chain's own schema and table. Rebuilt whenever the live schema
|
|
148
|
+
// list changes; the completion source reads `.value` lazily, so no editor
|
|
149
|
+
// reconfigure is needed.
|
|
150
|
+
const completionData = computed(() => buildCompletionIndex(props.schemas ?? []))
|
|
151
|
+
|
|
152
|
+
// How far back the chain the cursor is in is read: a long multi-line chain
|
|
153
|
+
// still names its schema, instance and table.
|
|
154
|
+
const CHAIN_WINDOW = 2000
|
|
155
|
+
|
|
156
|
+
// The schema the chain before the cursor names: `schemas.demo` or
|
|
157
|
+
// `schemas["dms-core"]`. Scopes table completions.
|
|
174
158
|
function currentSchemaId(before: string): string | null {
|
|
175
|
-
|
|
176
|
-
/schemas\s*(?:\.\s*([A-Za-z_$][\w$]*)|\[\s*["'`]([\w$-]+)["'`]\s*\])/g;
|
|
177
|
-
let last: string | null = null;
|
|
178
|
-
for (
|
|
179
|
-
let match = re.exec(before);
|
|
180
|
-
match !== null;
|
|
181
|
-
match = re.exec(before)
|
|
182
|
-
) {
|
|
183
|
-
last = match[1] ?? match[2] ?? null;
|
|
184
|
-
}
|
|
185
|
-
return last;
|
|
159
|
+
return targetFromSource(before).schema ?? null
|
|
186
160
|
}
|
|
187
161
|
|
|
188
162
|
function listFor(
|
|
@@ -190,98 +164,96 @@ function listFor(
|
|
|
190
164
|
schemaId: string | null,
|
|
191
165
|
fallback: string[],
|
|
192
166
|
): string[] {
|
|
193
|
-
return (schemaId && map[schemaId]) || fallback
|
|
167
|
+
return (schemaId && map[schemaId]) || fallback
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function chainText(ctx: CompletionContext): string {
|
|
171
|
+
return ctx.state.sliceDoc(Math.max(0, ctx.pos - CHAIN_WINDOW), ctx.pos)
|
|
194
172
|
}
|
|
195
173
|
|
|
196
174
|
function completionSource(ctx: CompletionContext): CompletionResult | null {
|
|
197
|
-
const data = completionData.value
|
|
175
|
+
const data = completionData.value
|
|
198
176
|
// A bounded look-behind window covers multi-line chains without scanning the
|
|
199
177
|
// whole document on every keystroke.
|
|
200
|
-
const before = ctx.state.sliceDoc(Math.max(0, ctx.pos - 240), ctx.pos)
|
|
178
|
+
const before = ctx.state.sliceDoc(Math.max(0, ctx.pos - 240), ctx.pos)
|
|
201
179
|
|
|
202
180
|
// `instance(<id>)` argument → that schema's instance ids (+ CROSS_INSTANCE).
|
|
203
181
|
// Acts as parameter help: it surfaces what instance() accepts. Quote-aware.
|
|
204
182
|
const inInstance =
|
|
205
183
|
/(?:^|[^\w$.])schemas\s*(?:\.\s*([A-Za-z_$][\w$]*)|\[\s*["'`]([\w$-]+)["'`]\s*\])\s*\.\s*instance\(\s*(["'`])?([\w$-]*)$/.exec(
|
|
206
184
|
before,
|
|
207
|
-
)
|
|
185
|
+
)
|
|
208
186
|
if (inInstance) {
|
|
209
|
-
const schemaId = inInstance[1] ?? inInstance[2] ??
|
|
210
|
-
const quote = inInstance[3]
|
|
211
|
-
const typed = inInstance[4] ??
|
|
187
|
+
const schemaId = inInstance[1] ?? inInstance[2] ?? ''
|
|
188
|
+
const quote = inInstance[3]
|
|
189
|
+
const typed = inInstance[4] ?? ''
|
|
212
190
|
const options: CompletionItem[] = (
|
|
213
191
|
data.instancesBySchema[schemaId] ?? []
|
|
214
192
|
).map((id) => ({
|
|
215
193
|
label: id,
|
|
216
|
-
type:
|
|
217
|
-
detail:
|
|
194
|
+
type: 'enum',
|
|
195
|
+
detail: 'instance id',
|
|
218
196
|
apply: quote ? id : `"${id}"`,
|
|
219
|
-
}))
|
|
197
|
+
}))
|
|
220
198
|
// CROSS_INSTANCE is a bare identifier, so only when not inside a quote.
|
|
221
199
|
if (!quote) {
|
|
222
200
|
options.push({
|
|
223
|
-
label:
|
|
224
|
-
type:
|
|
225
|
-
detail:
|
|
226
|
-
})
|
|
201
|
+
label: 'CROSS_INSTANCE',
|
|
202
|
+
type: 'constant',
|
|
203
|
+
detail: 'all instances (read-only)',
|
|
204
|
+
})
|
|
227
205
|
}
|
|
228
|
-
if (!options.length) return null
|
|
229
|
-
return { from: ctx.pos - typed.length, validFor: /[\w$-]*/, options }
|
|
206
|
+
if (!options.length) return null
|
|
207
|
+
return { from: ctx.pos - typed.length, validFor: /[\w$-]*/, options }
|
|
230
208
|
}
|
|
231
209
|
// Schema id inside bracket access: `schemas["…`.
|
|
232
|
-
const inSchemaBracket = /schemas\s*\[\s*["'`]([\w$-]*)$/.exec(before)
|
|
210
|
+
const inSchemaBracket = /schemas\s*\[\s*["'`]([\w$-]*)$/.exec(before)
|
|
233
211
|
if (inSchemaBracket) {
|
|
234
|
-
const typed = inSchemaBracket[1] ??
|
|
212
|
+
const typed = inSchemaBracket[1] ?? ''
|
|
235
213
|
return {
|
|
236
214
|
from: ctx.pos - typed.length,
|
|
237
215
|
validFor: /[\w$-]*/,
|
|
238
|
-
options: data.schemaIds.map((id) => opt(id,
|
|
239
|
-
}
|
|
216
|
+
options: data.schemaIds.map((id) => opt(id, 'namespace', 'schema')),
|
|
217
|
+
}
|
|
240
218
|
}
|
|
241
219
|
// `table(<name>)` argument → that schema's table names. Quote-aware.
|
|
242
|
-
const inTable = /\.table\(\s*(["'`])?([\w$-]*)$/.exec(before)
|
|
220
|
+
const inTable = /\.table\(\s*(["'`])?([\w$-]*)$/.exec(before)
|
|
243
221
|
if (inTable) {
|
|
244
|
-
const quote = inTable[1]
|
|
245
|
-
const typed = inTable[2] ??
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
currentSchemaId(before),
|
|
249
|
-
data.allTables,
|
|
250
|
-
);
|
|
222
|
+
const quote = inTable[1]
|
|
223
|
+
const typed = inTable[2] ?? ''
|
|
224
|
+
const schemaId = currentSchemaId(before)
|
|
225
|
+
const tables = listFor(data.tablesBySchema, schemaId, data.allTables)
|
|
251
226
|
return {
|
|
252
227
|
from: ctx.pos - typed.length,
|
|
253
228
|
validFor: /[\w$-]*/,
|
|
254
229
|
options: tables.map((name) => ({
|
|
255
230
|
label: name,
|
|
256
|
-
type:
|
|
257
|
-
detail:
|
|
231
|
+
type: 'class',
|
|
232
|
+
detail: 'table',
|
|
258
233
|
apply: quote ? name : `"${name}"`,
|
|
259
234
|
})),
|
|
260
|
-
}
|
|
235
|
+
}
|
|
261
236
|
}
|
|
262
|
-
// Quoted field name inside a field selector (`pluck` / `orderBy` / `key`)
|
|
263
|
-
|
|
237
|
+
// Quoted field name inside a field selector (`pluck` / `orderBy` / `key`):
|
|
238
|
+
// the fields of the chain's table.
|
|
239
|
+
const inField = /\.(?:pluck|orderBy|key)\([^)]*["'`]([\w$]*)$/.exec(before)
|
|
264
240
|
if (inField) {
|
|
265
|
-
const typed = inField[1] ??
|
|
266
|
-
const fields =
|
|
267
|
-
data.fieldsBySchema,
|
|
268
|
-
currentSchemaId(before),
|
|
269
|
-
data.allFields,
|
|
270
|
-
);
|
|
241
|
+
const typed = inField[1] ?? ''
|
|
242
|
+
const fields = fieldsAt(data, targetFromSource(chainText(ctx)))
|
|
271
243
|
return {
|
|
272
244
|
from: ctx.pos - typed.length,
|
|
273
245
|
validFor: /[\w$]*/,
|
|
274
|
-
options: fields.map((name) => opt(name,
|
|
275
|
-
}
|
|
246
|
+
options: fields.map((name) => opt(name, 'property', 'field')),
|
|
247
|
+
}
|
|
276
248
|
}
|
|
277
249
|
|
|
278
250
|
// Member access / bare identifier.
|
|
279
|
-
const typed = before.match(/[\w$]*$/)?.[0] ??
|
|
280
|
-
const upto = before.slice(0, before.length - typed.length)
|
|
281
|
-
const from = ctx.pos - typed.length
|
|
251
|
+
const typed = before.match(/[\w$]*$/)?.[0] ?? ''
|
|
252
|
+
const upto = before.slice(0, before.length - typed.length)
|
|
253
|
+
const from = ctx.pos - typed.length
|
|
282
254
|
|
|
283
|
-
if (upto.endsWith(
|
|
284
|
-
const chain = upto.slice(0, -1).trimEnd()
|
|
255
|
+
if (upto.endsWith('.')) {
|
|
256
|
+
const chain = upto.slice(0, -1).trimEnd()
|
|
285
257
|
if (/(?:^|[^\w$.])schemas$/.test(chain)) {
|
|
286
258
|
// `schemas.` → schema ids. Ids that aren't valid identifiers rewrite the
|
|
287
259
|
// dot to bracket form (`schemas["dms-core"]`).
|
|
@@ -290,12 +262,12 @@ function completionSource(ctx: CompletionContext): CompletionResult | null {
|
|
|
290
262
|
validFor: /[\w$]*/,
|
|
291
263
|
options: data.schemaIds.map((id) => {
|
|
292
264
|
if (/^[A-Z_$][\w$]*$/i.test(id)) {
|
|
293
|
-
return opt(id,
|
|
265
|
+
return opt(id, 'namespace', 'schema')
|
|
294
266
|
}
|
|
295
267
|
return {
|
|
296
268
|
label: id,
|
|
297
|
-
type:
|
|
298
|
-
detail:
|
|
269
|
+
type: 'namespace',
|
|
270
|
+
detail: 'schema',
|
|
299
271
|
apply: (view, _completion, fromPos, toPos) =>
|
|
300
272
|
view.dispatch({
|
|
301
273
|
changes: {
|
|
@@ -304,9 +276,9 @@ function completionSource(ctx: CompletionContext): CompletionResult | null {
|
|
|
304
276
|
insert: `[${JSON.stringify(id)}]`,
|
|
305
277
|
},
|
|
306
278
|
}),
|
|
307
|
-
}
|
|
279
|
+
}
|
|
308
280
|
}),
|
|
309
|
-
}
|
|
281
|
+
}
|
|
310
282
|
}
|
|
311
283
|
if (
|
|
312
284
|
/(?:^|[^\w$.])schemas\s*(?:\.\s*[A-Za-z_$][\w$]*|\[\s*["'`][\w$-]+["'`]\s*\])$/.test(
|
|
@@ -317,53 +289,58 @@ function completionSource(ctx: CompletionContext): CompletionResult | null {
|
|
|
317
289
|
return {
|
|
318
290
|
from,
|
|
319
291
|
validFor: /[\w$]*/,
|
|
320
|
-
options: SCHEMA_METHODS.map((m) => opt(m,
|
|
321
|
-
}
|
|
292
|
+
options: SCHEMA_METHODS.map((m) => opt(m, 'method', 'Schema')),
|
|
293
|
+
}
|
|
322
294
|
}
|
|
323
|
-
if (chain.endsWith(
|
|
295
|
+
if (chain.endsWith(')')) {
|
|
324
296
|
// A completed call → its result's methods.
|
|
325
297
|
const options = /\binstance\s*\([^()]*\)$/.test(chain)
|
|
326
|
-
? INSTANCE_METHODS.map((m) => opt(m,
|
|
327
|
-
: QUERY_METHODS.map((m) => opt(m,
|
|
328
|
-
return { from, validFor: /[\w$]*/, options }
|
|
298
|
+
? INSTANCE_METHODS.map((m) => opt(m, 'method', 'SchemaInstance'))
|
|
299
|
+
: QUERY_METHODS.map((m) => opt(m, 'method', 'Query'))
|
|
300
|
+
return { from, validFor: /[\w$]*/, options }
|
|
329
301
|
}
|
|
330
302
|
// A `.` after a bare, uncalled method (e.g. `…instance.`) is not a DSL step.
|
|
331
|
-
return null
|
|
303
|
+
return null
|
|
332
304
|
}
|
|
333
305
|
|
|
334
306
|
// Top level: the entry point only.
|
|
335
|
-
if (!typed && !ctx.explicit) return null
|
|
336
|
-
return {
|
|
307
|
+
if (!typed && !ctx.explicit) return null
|
|
308
|
+
return {
|
|
309
|
+
from,
|
|
310
|
+
validFor: /[\w$]*/,
|
|
311
|
+
options: [opt('schemas', 'variable', 'root')],
|
|
312
|
+
}
|
|
337
313
|
}
|
|
338
314
|
|
|
339
315
|
// --- editor -----------------------------------------------------------------
|
|
340
|
-
const editorContainer = ref<HTMLElement | null>(null)
|
|
341
|
-
let view: EditorView | null = null
|
|
342
|
-
const themeCompartment = new Compartment()
|
|
343
|
-
const
|
|
316
|
+
const editorContainer = ref<HTMLElement | null>(null)
|
|
317
|
+
let view: EditorView | null = null
|
|
318
|
+
const themeCompartment = new Compartment()
|
|
319
|
+
const placeholderCompartment = new Compartment()
|
|
320
|
+
const colorMode = useColorMode()
|
|
344
321
|
|
|
345
322
|
// Transparent surfaces so the editor blends into the DmsCard (bg-default).
|
|
346
323
|
const baseTheme = EditorView.theme({
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
324
|
+
'&': { backgroundColor: 'transparent', height: '100%', fontSize: '13px' },
|
|
325
|
+
'&.cm-focused': { outline: 'none' },
|
|
326
|
+
'.cm-scroller': {
|
|
350
327
|
fontFamily:
|
|
351
328
|
"ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace",
|
|
352
|
-
lineHeight:
|
|
329
|
+
lineHeight: '1.6',
|
|
353
330
|
},
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
})
|
|
331
|
+
'.cm-content': { padding: '12px 0' },
|
|
332
|
+
'.cm-gutters': { backgroundColor: 'transparent', border: 'none' },
|
|
333
|
+
'.cm-activeLine, .cm-activeLineGutter': { backgroundColor: 'transparent' },
|
|
334
|
+
})
|
|
358
335
|
|
|
359
336
|
function themeExtension() {
|
|
360
|
-
return colorMode.value ===
|
|
337
|
+
return colorMode.value === 'dark'
|
|
361
338
|
? oneDark
|
|
362
|
-
: syntaxHighlighting(defaultHighlightStyle, { fallback: true })
|
|
339
|
+
: syntaxHighlighting(defaultHighlightStyle, { fallback: true })
|
|
363
340
|
}
|
|
364
341
|
|
|
365
342
|
onMounted(() => {
|
|
366
|
-
if (!editorContainer.value) return
|
|
343
|
+
if (!editorContainer.value) return
|
|
367
344
|
const state = EditorState.create({
|
|
368
345
|
doc: internalValue.value,
|
|
369
346
|
extensions: [
|
|
@@ -373,15 +350,26 @@ onMounted(() => {
|
|
|
373
350
|
indentOnInput(),
|
|
374
351
|
javascript({ typescript: true }),
|
|
375
352
|
autocompletion({ override: [completionSource], activateOnTyping: true }),
|
|
376
|
-
cmPlaceholder(
|
|
353
|
+
placeholderCompartment.of(cmPlaceholder(placeholderExample.value)),
|
|
377
354
|
EditorView.lineWrapping,
|
|
355
|
+
EditorView.contentAttributes.of({
|
|
356
|
+
'aria-label': t('dms_database.query.editor.aria_label'),
|
|
357
|
+
}),
|
|
378
358
|
keymap.of([
|
|
379
359
|
{
|
|
380
|
-
key:
|
|
360
|
+
key: 'Mod-Enter',
|
|
381
361
|
preventDefault: true,
|
|
382
362
|
run: () => {
|
|
383
|
-
if (canExecute()) emit(
|
|
384
|
-
return true
|
|
363
|
+
if (canExecute()) emit('execute')
|
|
364
|
+
return true
|
|
365
|
+
},
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
key: 'Mod-s',
|
|
369
|
+
preventDefault: true,
|
|
370
|
+
run: () => {
|
|
371
|
+
if (internalValue.value.trim()) emit('save')
|
|
372
|
+
return true
|
|
385
373
|
},
|
|
386
374
|
},
|
|
387
375
|
indentWithTab,
|
|
@@ -392,14 +380,19 @@ onMounted(() => {
|
|
|
392
380
|
themeCompartment.of(themeExtension()),
|
|
393
381
|
baseTheme,
|
|
394
382
|
EditorView.updateListener.of((update) => {
|
|
395
|
-
if (
|
|
396
|
-
|
|
397
|
-
|
|
383
|
+
if (update.selectionSet || update.docChanged) {
|
|
384
|
+
const head = update.state.selection.main.head
|
|
385
|
+
const line = update.state.doc.lineAt(head)
|
|
386
|
+
cursor.value = { line: line.number, column: head - line.from + 1 }
|
|
387
|
+
}
|
|
388
|
+
if (!update.docChanged) return
|
|
389
|
+
const text = update.state.doc.toString()
|
|
390
|
+
if (text !== props.modelValue) emit('update:modelValue', text)
|
|
398
391
|
}),
|
|
399
392
|
],
|
|
400
|
-
})
|
|
401
|
-
view = new EditorView({ state, parent: editorContainer.value })
|
|
402
|
-
})
|
|
393
|
+
})
|
|
394
|
+
view = new EditorView({ state, parent: editorContainer.value })
|
|
395
|
+
})
|
|
403
396
|
|
|
404
397
|
// External edits (history load, format, clear) → push into the editor.
|
|
405
398
|
watch(
|
|
@@ -408,10 +401,16 @@ watch(
|
|
|
408
401
|
if (view && value !== view.state.doc.toString()) {
|
|
409
402
|
view.dispatch({
|
|
410
403
|
changes: { from: 0, to: view.state.doc.length, insert: value },
|
|
411
|
-
})
|
|
404
|
+
})
|
|
412
405
|
}
|
|
413
406
|
},
|
|
414
|
-
)
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
watch(placeholderExample, (example) => {
|
|
410
|
+
view?.dispatch({
|
|
411
|
+
effects: placeholderCompartment.reconfigure(cmPlaceholder(example)),
|
|
412
|
+
})
|
|
413
|
+
})
|
|
415
414
|
|
|
416
415
|
// Swap the syntax theme when the app toggles light/dark.
|
|
417
416
|
watch(
|
|
@@ -419,84 +418,137 @@ watch(
|
|
|
419
418
|
() => {
|
|
420
419
|
view?.dispatch({
|
|
421
420
|
effects: themeCompartment.reconfigure(themeExtension()),
|
|
422
|
-
})
|
|
421
|
+
})
|
|
423
422
|
},
|
|
424
|
-
)
|
|
423
|
+
)
|
|
425
424
|
|
|
426
425
|
onBeforeUnmount(() => {
|
|
427
|
-
view?.destroy()
|
|
428
|
-
view = null
|
|
429
|
-
})
|
|
426
|
+
view?.destroy()
|
|
427
|
+
view = null
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
// Inserts a snippet at the cursor, the cursor landing inside its first quotes.
|
|
431
|
+
function insertSnippet(snippet: Snippet) {
|
|
432
|
+
if (!view) return
|
|
433
|
+
const { from, to } = view.state.selection.main
|
|
434
|
+
const quote = snippet.insert.indexOf('""')
|
|
435
|
+
const anchor = from + (quote >= 0 ? quote + 1 : snippet.insert.length)
|
|
436
|
+
view.dispatch({
|
|
437
|
+
changes: { from, to, insert: snippet.insert },
|
|
438
|
+
selection: { anchor },
|
|
439
|
+
})
|
|
440
|
+
view.focus()
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
defineExpose({ focus: () => view?.focus() })
|
|
430
444
|
</script>
|
|
431
445
|
|
|
432
446
|
<template>
|
|
433
447
|
<DmsCard :padded="false" class="overflow-hidden">
|
|
434
|
-
<!-- editor bar -->
|
|
435
448
|
<div
|
|
436
|
-
class="flex items-center gap-2 px-3.5 py-
|
|
449
|
+
class="border-default flex flex-wrap items-center gap-2 border-b px-3.5 py-2.5"
|
|
437
450
|
>
|
|
438
|
-
<
|
|
439
|
-
v-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
class="
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
451
|
+
<span
|
|
452
|
+
v-if="savedName"
|
|
453
|
+
class="text-highlighted flex min-w-0 items-center gap-1.5 text-sm font-medium"
|
|
454
|
+
>
|
|
455
|
+
<UIcon name="i-ph-star-fill" class="text-warning size-4 shrink-0" />
|
|
456
|
+
<span class="truncate">{{ savedName }}</span>
|
|
457
|
+
<UBadge v-if="savedShared" color="neutral" variant="outline" size="sm">
|
|
458
|
+
{{ t('dms_database.query.editor.shared') }}
|
|
459
|
+
</UBadge>
|
|
460
|
+
<span v-if="edited" class="text-dimmed text-xs font-normal">
|
|
461
|
+
· {{ t('dms_database.query.editor.edited') }}
|
|
462
|
+
</span>
|
|
463
|
+
</span>
|
|
464
|
+
<span v-else class="text-muted text-sm">
|
|
465
|
+
{{ t('dms_database.query.editor.untitled') }}
|
|
448
466
|
</span>
|
|
449
467
|
<span class="flex-1" />
|
|
468
|
+
<UTooltip :text="t('dms_database.query.editor.new_hint')">
|
|
469
|
+
<UButton
|
|
470
|
+
size="sm"
|
|
471
|
+
color="neutral"
|
|
472
|
+
variant="ghost"
|
|
473
|
+
icon="i-ph-plus"
|
|
474
|
+
:label="t('dms_database.query.editor.new')"
|
|
475
|
+
@click="emit('new')"
|
|
476
|
+
/>
|
|
477
|
+
</UTooltip>
|
|
450
478
|
<UButton
|
|
451
479
|
size="sm"
|
|
452
480
|
color="neutral"
|
|
453
481
|
variant="ghost"
|
|
454
482
|
icon="i-ph-magic-wand"
|
|
455
|
-
:label="
|
|
483
|
+
:label="t('dms_database.query.editor.format')"
|
|
456
484
|
:disabled="!internalValue.trim()"
|
|
457
485
|
@click="runFormat"
|
|
458
486
|
/>
|
|
459
487
|
<UButton
|
|
460
488
|
size="sm"
|
|
461
489
|
color="neutral"
|
|
462
|
-
variant="
|
|
490
|
+
variant="outline"
|
|
463
491
|
icon="i-ph-floppy-disk"
|
|
464
|
-
:label="$t('dms_database.query.save')"
|
|
465
492
|
:disabled="!internalValue.trim()"
|
|
466
493
|
@click="emit('save')"
|
|
467
|
-
|
|
494
|
+
>
|
|
495
|
+
{{ t('dms_database.query.editor.save') }}
|
|
496
|
+
<!-- `meta` and `ctrl` read ⌘ or Ctrl from the browser, which the
|
|
497
|
+
server cannot tell: rendered on the client only. -->
|
|
498
|
+
<DmsClientOnly>
|
|
499
|
+
<UKbd value="meta" size="sm" class="ml-1" />
|
|
500
|
+
</DmsClientOnly>
|
|
501
|
+
<UKbd value="s" size="sm" />
|
|
502
|
+
</UButton>
|
|
468
503
|
<UButton
|
|
469
504
|
size="sm"
|
|
470
|
-
|
|
471
|
-
icon="i-ph-lightning"
|
|
505
|
+
icon="i-ph-play"
|
|
472
506
|
:loading="executing"
|
|
473
507
|
:disabled="!internalValue.trim() || executing"
|
|
474
508
|
@click="emit('execute')"
|
|
475
509
|
>
|
|
476
|
-
{{
|
|
477
|
-
<
|
|
510
|
+
{{ t('dms_database.query.editor.run') }}
|
|
511
|
+
<DmsClientOnly>
|
|
512
|
+
<UKbd value="meta" size="sm" class="ml-1" />
|
|
513
|
+
</DmsClientOnly>
|
|
478
514
|
<UKbd value="enter" size="sm" />
|
|
479
515
|
</UButton>
|
|
480
516
|
</div>
|
|
481
517
|
|
|
482
|
-
<!--
|
|
518
|
+
<!-- CodeMirror mounts here on the client. -->
|
|
483
519
|
<div
|
|
484
520
|
ref="editorContainer"
|
|
485
|
-
class="
|
|
521
|
+
class="bg-default text-toned h-60 overflow-auto"
|
|
486
522
|
/>
|
|
487
523
|
|
|
488
|
-
<!-- builder hint footer -->
|
|
489
524
|
<div
|
|
490
|
-
class="flex items-center gap-2 px-3.5 py-
|
|
525
|
+
class="border-default text-dimmed flex flex-wrap items-center gap-2 border-t px-3.5 py-2 text-[11.5px]"
|
|
491
526
|
>
|
|
492
|
-
<
|
|
493
|
-
|
|
527
|
+
<DmsEyebrow :label="t('dms_database.query.editor.insert')" />
|
|
528
|
+
<button
|
|
529
|
+
v-for="snippet in SNIPPETS"
|
|
530
|
+
:key="snippet.label"
|
|
531
|
+
type="button"
|
|
532
|
+
class="border-default bg-elevated text-toned hover:border-primary hover:text-highlighted rounded border px-1.5 py-0.5 font-mono text-[11px]"
|
|
533
|
+
@click="insertSnippet(snippet)"
|
|
494
534
|
>
|
|
495
|
-
{{
|
|
535
|
+
{{ snippet.label }}
|
|
536
|
+
</button>
|
|
537
|
+
<span class="ml-auto flex items-center gap-1">
|
|
538
|
+
<DmsClientOnly>
|
|
539
|
+
<UKbd value="ctrl" size="sm" />
|
|
540
|
+
</DmsClientOnly>
|
|
541
|
+
<UKbd value="space" size="sm" />
|
|
542
|
+
{{ t('dms_database.query.editor.complete') }}
|
|
543
|
+
</span>
|
|
544
|
+
<span class="font-mono">
|
|
545
|
+
{{
|
|
546
|
+
t('dms_database.query.editor.position', {
|
|
547
|
+
line: cursor.line,
|
|
548
|
+
column: cursor.column,
|
|
549
|
+
})
|
|
550
|
+
}}
|
|
496
551
|
</span>
|
|
497
|
-
<code class="text-[11.5px] font-mono text-muted truncate">
|
|
498
|
-
{{ BUILDER_EXAMPLE }}
|
|
499
|
-
</code>
|
|
500
552
|
</div>
|
|
501
553
|
</DmsCard>
|
|
502
554
|
</template>
|