fix: add Shiki SQL syntax highlighting to all SQL display components
Add useSqlHighlighter composable and integrate Shiki highlighting into DangerConfirmDialog, QueryHistory, DataGrid (replacing regex-based highlighting), SqlFileExecutionDialog, SchemaDiffDialog, TreeItem, ObjectBrowser, and FieldLineageDialog. Also fix danger dialog overflow by adding overflow-hidden to DialogContent and wrap/scroll support.
This commit is contained in:
parent
a1f718d6bd
commit
f5f986cdd6
|
|
@ -16,6 +16,7 @@ import type { TableInfo } from "@/types/database";
|
|||
import { sqlMetadataRefreshTarget } from "@/lib/sqlMetadataRefresh";
|
||||
import { useToast } from "@/composables/useToast";
|
||||
import { Loader2, Copy, Play, GitCompareArrows, ArrowLeftRight } from "lucide-vue-next";
|
||||
import { useSqlHighlighter } from "@/composables/useSqlHighlighter";
|
||||
|
||||
interface SelectableTableDiff extends TableDiff {
|
||||
selected: boolean;
|
||||
|
|
@ -23,6 +24,7 @@ interface SelectableTableDiff extends TableDiff {
|
|||
|
||||
const { t } = useI18n();
|
||||
const { toast } = useToast();
|
||||
const { highlight } = useSqlHighlighter();
|
||||
const open = defineModel<boolean>("open", { default: false });
|
||||
const store = useConnectionStore();
|
||||
|
||||
|
|
@ -52,6 +54,7 @@ const executedCount = ref(0);
|
|||
const executeTotal = ref(0);
|
||||
const syncErrors = ref<{ sql: string; error: string }[]>([]);
|
||||
const syncSql = ref("");
|
||||
const highlightedSyncSql = computed(() => highlight(syncSql.value));
|
||||
|
||||
const allSelected = computed(() => diffs.value.length > 0 && diffs.value.every((d) => d.selected));
|
||||
const someSelected = computed(() => diffs.value.some((d) => d.selected) && !allSelected.value);
|
||||
|
|
@ -616,11 +619,10 @@ watch(
|
|||
<!-- SQL Preview -->
|
||||
<div class="space-y-1">
|
||||
<Label class="text-xs font-medium">{{ t("diff.generatedSql") }}</Label>
|
||||
<textarea
|
||||
:value="syncSql"
|
||||
readonly
|
||||
class="w-full h-48 rounded-lg border bg-muted/20 p-3 font-mono text-xs resize-none focus:outline-none focus:ring-1 focus:ring-ring"
|
||||
/>
|
||||
<pre
|
||||
class="w-full h-48 overflow-auto rounded-lg border bg-muted/20 p-3 font-mono text-xs whitespace-pre"
|
||||
v-html="highlightedSyncSql"
|
||||
></pre>
|
||||
</div>
|
||||
|
||||
<!-- Sync Errors -->
|
||||
|
|
|
|||
|
|
@ -1,14 +1,17 @@
|
|||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { AlertTriangle } from "lucide-vue-next";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
||||
import { useSqlHighlighter } from "@/composables/useSqlHighlighter";
|
||||
|
||||
const { t } = useI18n();
|
||||
const { highlight } = useSqlHighlighter();
|
||||
|
||||
const open = defineModel<boolean>("open", { default: false });
|
||||
|
||||
withDefaults(
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
sql?: string;
|
||||
title?: string;
|
||||
|
|
@ -29,6 +32,9 @@ const emit = defineEmits<{
|
|||
confirm: [];
|
||||
}>();
|
||||
|
||||
const code = computed(() => props.details || props.sql);
|
||||
const highlightedCode = computed(() => highlight(code.value));
|
||||
|
||||
function onConfirm() {
|
||||
open.value = false;
|
||||
emit("confirm");
|
||||
|
|
@ -45,13 +51,13 @@ function onConfirm() {
|
|||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div class="py-4">
|
||||
<div class="py-4 min-w-0">
|
||||
<p class="text-sm text-muted-foreground mb-3">{{ message || t("dangerDialog.message") }}</p>
|
||||
<pre
|
||||
v-if="details || sql"
|
||||
class="text-xs bg-muted p-3 rounded overflow-auto max-h-40 min-w-0 font-mono whitespace-pre-wrap"
|
||||
>{{ details || sql }}</pre
|
||||
>
|
||||
v-if="code"
|
||||
class="text-xs bg-muted p-3 rounded overflow-auto max-h-40 min-w-0 font-mono whitespace-pre"
|
||||
v-html="highlightedCode"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useSqlHighlighter } from "@/composables/useSqlHighlighter";
|
||||
import { Clock, Copy, Database, RotateCcw, Search, Sparkles, Trash2, X } from "lucide-vue-next";
|
||||
import { RecycleScroller } from "vue-virtual-scroller";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
|
@ -18,6 +19,7 @@ import * as api from "@/lib/api";
|
|||
|
||||
const { t } = useI18n();
|
||||
const { toast } = useToast();
|
||||
const { highlight } = useSqlHighlighter();
|
||||
const store = useHistoryStore();
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
|
@ -297,7 +299,8 @@ onMounted(() => store.load());
|
|||
</div>
|
||||
<pre
|
||||
class="max-h-48 overflow-auto rounded border bg-muted/30 p-3 text-xs"
|
||||
><code>{{ selectedEntry.sql }}</code></pre>
|
||||
v-html="highlight(selectedEntry.sql)"
|
||||
></pre>
|
||||
</div>
|
||||
<div v-if="selectedEntry.rollback_sql">
|
||||
<div class="mb-1 flex items-center justify-between">
|
||||
|
|
@ -309,7 +312,8 @@ onMounted(() => store.load());
|
|||
</div>
|
||||
<pre
|
||||
class="max-h-40 overflow-auto rounded border bg-muted/30 p-3 text-xs"
|
||||
><code>{{ selectedEntry.rollback_sql }}</code></pre>
|
||||
v-html="highlight(selectedEntry.rollback_sql || '')"
|
||||
></pre>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
|
|
|
|||
|
|
@ -157,11 +157,13 @@ import { useDataGridExport } from "@/composables/useDataGridExport";
|
|||
import { useDataGridColumnResize } from "@/composables/useDataGridColumnResize";
|
||||
import { useDataGridSelection } from "@/composables/useDataGridSelection";
|
||||
import { useDataGridEditor } from "@/composables/useDataGridEditor";
|
||||
import { useSqlHighlighter } from "@/composables/useSqlHighlighter";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
|
||||
const { t } = useI18n();
|
||||
const settingsStore = useSettingsStore();
|
||||
const { toast } = useToast();
|
||||
const { highlight } = useSqlHighlighter();
|
||||
|
||||
interface PreparedCopyValue {
|
||||
key: string;
|
||||
|
|
@ -3330,6 +3332,8 @@ onUnmounted(() => {
|
|||
const SQL_KEYWORDS =
|
||||
/\b(CREATE|TABLE|INDEX|UNIQUE|PRIMARY|KEY|FOREIGN|REFERENCES|CONSTRAINT|NOT|NULL|DEFAULT|INT|INTEGER|BIGINT|SMALLINT|VARCHAR|CHARACTER|VARYING|TEXT|BOOLEAN|DOUBLE|PRECISION|REAL|FLOAT|NUMERIC|DECIMAL|TIMESTAMP|DATE|TIME|SERIAL|AUTOINCREMENT|AUTO_INCREMENT|IF|EXISTS|ON|SET|CASCADE|RESTRICT|CHECK|WITH|WITHOUT|ZONE)\b/gi;
|
||||
|
||||
const highlightedDdlContent = computed(() => highlight(ddlContent.value));
|
||||
|
||||
function highlightSql(sql: string): string {
|
||||
const tokens: string[] = [];
|
||||
let rest = sql;
|
||||
|
|
@ -4850,7 +4854,7 @@ defineExpose({
|
|||
v-else-if="activeTableInfoTab === 'ddl' && !ddlLoading"
|
||||
class="flex-1 min-w-0 text-xs font-mono p-3 overflow-auto ddl-code leading-5 select-text"
|
||||
:class="ddlWrap ? 'whitespace-pre-wrap break-words' : 'whitespace-pre'"
|
||||
v-html="highlightSql(ddlContent)"
|
||||
v-html="highlightedDdlContent"
|
||||
></pre>
|
||||
<div v-else class="flex-1 flex items-center justify-center">
|
||||
<Loader2 class="w-4 h-4 animate-spin text-muted-foreground" />
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useSqlHighlighter } from "@/composables/useSqlHighlighter";
|
||||
import {
|
||||
ArrowUpRight,
|
||||
Check,
|
||||
|
|
@ -58,6 +59,7 @@ const emit = defineEmits<{
|
|||
|
||||
const { t } = useI18n();
|
||||
const { toast } = useToast();
|
||||
const { highlight } = useSqlHighlighter();
|
||||
const connectionStore = useConnectionStore();
|
||||
const dialogOpen = computed({
|
||||
get: () => props.open,
|
||||
|
|
@ -452,8 +454,8 @@ function openItemTarget(item: FieldLineageItem) {
|
|||
<pre
|
||||
v-if="item.sqlSnippet"
|
||||
class="mt-2 max-h-20 overflow-auto rounded-md bg-muted/40 p-2 text-xs whitespace-pre-wrap"
|
||||
>{{ item.sqlSnippet }}</pre
|
||||
>
|
||||
v-html="highlight(item.sqlSnippet)"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { RecycleScroller } from "vue-virtual-scroller";
|
||||
import { useSqlHighlighter } from "@/composables/useSqlHighlighter";
|
||||
import {
|
||||
ArrowDown,
|
||||
ArrowRightLeft,
|
||||
ArrowUp,
|
||||
Braces,
|
||||
CheckSquare,
|
||||
Code2,
|
||||
Copy,
|
||||
CopyPlus,
|
||||
|
|
@ -23,6 +25,7 @@ import {
|
|||
Scissors,
|
||||
Search,
|
||||
ScrollText,
|
||||
Square,
|
||||
Table2,
|
||||
TerminalSquare,
|
||||
Trash2,
|
||||
|
|
@ -105,6 +108,7 @@ const emit = defineEmits<{
|
|||
|
||||
const { t } = useI18n();
|
||||
const { toast } = useToast();
|
||||
const { highlight } = useSqlHighlighter();
|
||||
const connectionStore = useConnectionStore();
|
||||
const queryStore = useQueryStore();
|
||||
|
||||
|
|
@ -144,6 +148,9 @@ const emptyPreviewSql = ref("");
|
|||
const showDuplicateDialog = ref(false);
|
||||
const duplicateTarget = ref<ObjectBrowserRow | null>(null);
|
||||
const duplicateTableName = ref("");
|
||||
const selectedTableIds = ref<Set<string>>(new Set());
|
||||
const showBatchDropConfirm = ref(false);
|
||||
const batchDropPreviewSql = ref("");
|
||||
let loadId = 0;
|
||||
|
||||
const needsSchema = computed(() => isSchemaAware(props.connection.db_type));
|
||||
|
|
@ -189,7 +196,7 @@ const hasComments = computed(() => rows.value.some((row) => row.comment?.trim())
|
|||
const hasCreatedAt = computed(() => rows.value.some((row) => row.created_at?.trim()));
|
||||
const hasUpdatedAt = computed(() => rows.value.some((row) => row.updated_at?.trim()));
|
||||
const gridTemplateColumns = computed(() => {
|
||||
const columns = ["minmax(0,1fr)", "120px"];
|
||||
const columns = ["34px", "minmax(0,1fr)", "120px"];
|
||||
if (hasCreatedAt.value) columns.push("150px");
|
||||
if (hasUpdatedAt.value) columns.push("150px");
|
||||
if (hasComments.value) columns.push("minmax(160px,0.7fr)");
|
||||
|
|
@ -206,6 +213,18 @@ const filteredRows = computed(() => {
|
|||
if (objectFilter.value === "functions") rows = rows.filter((row) => row.type === "FUNCTION");
|
||||
return sortObjectBrowserRows(rows, sortKey.value, sortDirection.value);
|
||||
});
|
||||
const selectableRows = computed(() => rows.value.filter((row) => row.type === "TABLE"));
|
||||
const visibleSelectableRows = computed(() => filteredRows.value.filter((row) => row.type === "TABLE"));
|
||||
const selectedTableRows = computed(() => {
|
||||
const ids = selectedTableIds.value;
|
||||
return selectableRows.value.filter((row) => ids.has(row.id));
|
||||
});
|
||||
const selectedTableCount = computed(() => selectedTableRows.value.length);
|
||||
const allVisibleTablesSelected = computed(
|
||||
() =>
|
||||
visibleSelectableRows.value.length > 0 &&
|
||||
visibleSelectableRows.value.every((row) => selectedTableIds.value.has(row.id)),
|
||||
);
|
||||
|
||||
function iconFor(row: ObjectBrowserRow) {
|
||||
if (row.type === "VIEW") return Eye;
|
||||
|
|
@ -551,6 +570,89 @@ function openDatabaseExport(row: ObjectBrowserRow) {
|
|||
};
|
||||
}
|
||||
|
||||
function setSelectedTableIds(ids: Set<string>) {
|
||||
selectedTableIds.value = new Set(ids);
|
||||
}
|
||||
|
||||
function toggleTableSelection(row: ObjectBrowserRow) {
|
||||
if (row.type !== "TABLE") return;
|
||||
const next = new Set(selectedTableIds.value);
|
||||
if (next.has(row.id)) {
|
||||
next.delete(row.id);
|
||||
} else {
|
||||
next.add(row.id);
|
||||
}
|
||||
setSelectedTableIds(next);
|
||||
}
|
||||
|
||||
function toggleVisibleTableSelection() {
|
||||
const next = new Set(selectedTableIds.value);
|
||||
if (allVisibleTablesSelected.value) {
|
||||
for (const row of visibleSelectableRows.value) next.delete(row.id);
|
||||
} else {
|
||||
for (const row of visibleSelectableRows.value) next.add(row.id);
|
||||
}
|
||||
setSelectedTableIds(next);
|
||||
}
|
||||
|
||||
function clearTableSelection() {
|
||||
setSelectedTableIds(new Set());
|
||||
}
|
||||
|
||||
function openBatchDatabaseExport() {
|
||||
const selectedTables = selectedTableRows.value.map((row) => row.name);
|
||||
if (selectedTables.length === 0) return;
|
||||
connectionStore.databaseExportSource = {
|
||||
connectionId: props.connection.id,
|
||||
database: props.database,
|
||||
schema: selectedTableRows.value[0]?.schema || selectedSchema.value,
|
||||
tableNames: selectedTables,
|
||||
};
|
||||
}
|
||||
|
||||
async function refreshBatchDropPreviewSql() {
|
||||
const statements: string[] = [];
|
||||
for (const row of selectedTableRows.value) {
|
||||
const sql = await buildDropObjectSql({
|
||||
databaseType: props.connection.db_type,
|
||||
objectType: "TABLE",
|
||||
schema: row.schema || selectedSchema.value,
|
||||
name: row.name,
|
||||
}).catch(() => "");
|
||||
if (sql) statements.push(sql);
|
||||
}
|
||||
batchDropPreviewSql.value = statements.join("\n");
|
||||
}
|
||||
|
||||
function requestBatchDropTables() {
|
||||
if (selectedTableCount.value === 0) return;
|
||||
batchDropPreviewSql.value = "";
|
||||
void refreshBatchDropPreviewSql();
|
||||
showBatchDropConfirm.value = true;
|
||||
}
|
||||
|
||||
async function confirmBatchDropTables() {
|
||||
const targets = [...selectedTableRows.value];
|
||||
if (targets.length === 0) return;
|
||||
try {
|
||||
for (const row of targets) {
|
||||
const sql = await buildDropObjectSql({
|
||||
databaseType: props.connection.db_type,
|
||||
objectType: "TABLE",
|
||||
schema: row.schema || selectedSchema.value,
|
||||
name: row.name,
|
||||
});
|
||||
await api.executeQuery(props.connection.id, props.database, sql);
|
||||
}
|
||||
toast(t("objects.batchDropSuccess", { count: targets.length }));
|
||||
clearTableSelection();
|
||||
await reload();
|
||||
await connectionStore.refreshObjectListTreeNode(props.connection.id, props.database, selectedSchema.value);
|
||||
} catch (e: any) {
|
||||
toast(t("contextMenu.tableOperationFailed", { message: e?.message || String(e) }), 5000);
|
||||
}
|
||||
}
|
||||
|
||||
async function exportStructure(row: ObjectBrowserRow) {
|
||||
try {
|
||||
const schema = row.schema || selectedSchema.value || props.database;
|
||||
|
|
@ -839,6 +941,8 @@ async function loadObjects() {
|
|||
fallbackSchema: schema,
|
||||
needsSchema: needsSchema.value,
|
||||
});
|
||||
const availableTableIds = new Set(rows.value.filter((row) => row.type === "TABLE").map((row) => row.id));
|
||||
setSelectedTableIds(new Set([...selectedTableIds.value].filter((id) => availableTableIds.has(id))));
|
||||
} catch (e: any) {
|
||||
if (id !== loadId) return;
|
||||
error.value = e?.message || String(e);
|
||||
|
|
@ -913,6 +1017,7 @@ watch(
|
|||
selectedSchema.value = props.schema;
|
||||
userHasSelectedFilter.value = false;
|
||||
objectFilter.value = "all";
|
||||
clearTableSelection();
|
||||
void reload();
|
||||
},
|
||||
{ immediate: true },
|
||||
|
|
@ -973,6 +1078,23 @@ watch(
|
|||
<RefreshCw class="h-3.5 w-3.5" :class="{ 'animate-spin': loadingObjects }" />
|
||||
</Button>
|
||||
</div>
|
||||
<div v-if="selectedTableCount > 0" class="flex h-9 shrink-0 items-center gap-2 border-b bg-muted/30 px-3 text-xs">
|
||||
<div class="min-w-0 flex-1 truncate text-muted-foreground">
|
||||
{{ t("objects.selectedTables", { count: selectedTableCount }) }}
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" class="h-7 px-2 text-xs" @click="openBatchDatabaseExport">
|
||||
<Download class="mr-1.5 h-3.5 w-3.5" />
|
||||
{{ t("objects.exportSelected") }}
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" class="h-7 px-2 text-xs text-destructive" @click="requestBatchDropTables">
|
||||
<Trash2 class="mr-1.5 h-3.5 w-3.5" />
|
||||
{{ t("objects.dropSelected") }}
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" class="h-7 px-2 text-xs" @click="clearTableSelection">
|
||||
<X class="mr-1.5 h-3.5 w-3.5" />
|
||||
{{ t("objects.clearSelection") }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div v-if="loadingObjects" class="flex flex-1 items-center justify-center gap-2 text-sm text-muted-foreground">
|
||||
<Loader2 class="h-4 w-4 animate-spin" />
|
||||
|
|
@ -992,6 +1114,15 @@ watch(
|
|||
class="grid h-8 shrink-0 items-center gap-3 border-b bg-muted/40 px-3 text-xs font-medium text-muted-foreground"
|
||||
:style="{ gridTemplateColumns }"
|
||||
>
|
||||
<button
|
||||
class="flex h-6 w-6 items-center justify-center rounded-sm hover:bg-accent"
|
||||
type="button"
|
||||
:disabled="visibleSelectableRows.length === 0"
|
||||
@click="toggleVisibleTableSelection"
|
||||
>
|
||||
<CheckSquare v-if="allVisibleTablesSelected" class="h-3.5 w-3.5 text-primary" />
|
||||
<Square v-else class="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button class="flex min-w-0 items-center gap-1 truncate text-left" type="button" @click="toggleSort('name')">
|
||||
<span class="truncate">{{ t("objects.name") }}</span>
|
||||
<component :is="sortIconFor('name')" v-if="sortIconFor('name')" class="h-3 w-3 shrink-0" />
|
||||
|
|
@ -1041,10 +1172,22 @@ watch(
|
|||
<ContextMenuTrigger as-child>
|
||||
<div
|
||||
class="grid h-[38px] cursor-pointer items-center gap-3 border-b px-3 hover:bg-accent/50"
|
||||
:class="{ 'bg-accent/40': sourceRow?.id === item.id }"
|
||||
:class="{
|
||||
'bg-accent/40': sourceRow?.id === item.id,
|
||||
'bg-primary/5': selectedTableIds.has(item.id),
|
||||
}"
|
||||
:style="{ gridTemplateColumns }"
|
||||
@click="openRow(item)"
|
||||
>
|
||||
<button
|
||||
class="flex h-6 w-6 items-center justify-center rounded-sm text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
type="button"
|
||||
:class="{ invisible: item.type !== 'TABLE' }"
|
||||
@click.stop="toggleTableSelection(item)"
|
||||
>
|
||||
<CheckSquare v-if="selectedTableIds.has(item.id)" class="h-3.5 w-3.5 text-primary" />
|
||||
<Square v-else class="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
<component :is="iconFor(item)" class="h-3.5 w-3.5 shrink-0" :class="iconClass(item.type)" />
|
||||
<span class="truncate text-[13px] font-medium text-foreground">{{ item.name }}</span>
|
||||
|
|
@ -1298,6 +1441,15 @@ watch(
|
|||
@confirm="confirmDrop"
|
||||
/>
|
||||
|
||||
<DangerConfirmDialog
|
||||
v-model:open="showBatchDropConfirm"
|
||||
:title="t('objects.confirmBatchDropTitle')"
|
||||
:message="t('objects.confirmBatchDropMessage', { count: selectedTableCount })"
|
||||
:sql="batchDropPreviewSql"
|
||||
:confirm-label="t('objects.dropSelected')"
|
||||
@confirm="confirmBatchDropTables"
|
||||
/>
|
||||
|
||||
<Dialog v-model:open="showRenameDialog">
|
||||
<DialogContent class="sm:max-w-[420px]">
|
||||
<DialogHeader>
|
||||
|
|
@ -1312,8 +1464,8 @@ watch(
|
|||
<pre
|
||||
v-if="renamePreviewSqlText"
|
||||
class="max-h-32 overflow-auto rounded bg-muted p-3 text-xs whitespace-pre-wrap"
|
||||
>{{ renamePreviewSqlText }}</pre
|
||||
>
|
||||
v-html="highlight(renamePreviewSqlText)"
|
||||
></pre>
|
||||
<p v-if="renameError" class="text-sm text-destructive">{{ renameError }}</p>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, computed, nextTick, watch } from "vue";
|
||||
import { useSqlHighlighter } from "@/composables/useSqlHighlighter";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { translateBackendError } from "@/i18n/backend-errors";
|
||||
import {
|
||||
|
|
@ -137,6 +138,7 @@ const queryStore = useQueryStore();
|
|||
const savedSqlStore = useSavedSqlStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
const { toast } = useToast();
|
||||
const { highlight } = useSqlHighlighter();
|
||||
const { getDatabaseOptions } = useDatabaseOptions();
|
||||
const showVisibleDatabasesDialog = ref(false);
|
||||
|
||||
|
|
@ -2299,8 +2301,8 @@ const isDragging = computed(() => dragState.active && dragState.draggedId === pr
|
|||
<pre
|
||||
v-if="renameObjectPreviewSql"
|
||||
class="max-h-32 overflow-auto rounded bg-muted p-3 text-xs whitespace-pre-wrap"
|
||||
>{{ renameObjectPreviewSql }}</pre
|
||||
>
|
||||
v-html="highlight(renameObjectPreviewSql)"
|
||||
></pre>
|
||||
<p v-if="renameObjectError" class="text-sm text-destructive">{{ renameObjectError }}</p>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import { computed, ref, watch } from "vue";
|
||||
import { uuid } from "@/lib/utils";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useSqlHighlighter } from "@/composables/useSqlHighlighter";
|
||||
import { isTauriRuntime } from "@/lib/tauriRuntime";
|
||||
import { Dialog, DialogFooter, DialogHeader, DialogScrollContent, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
|
@ -25,6 +26,7 @@ import { Check, CheckSquare, FileCode, FolderOpen, Loader2, Play, Square, X } fr
|
|||
|
||||
const { t } = useI18n();
|
||||
const { toast } = useToast();
|
||||
const { highlight } = useSqlHighlighter();
|
||||
const open = defineModel<boolean>("open", { default: false });
|
||||
|
||||
const props = defineProps<{
|
||||
|
|
@ -434,9 +436,10 @@ watch(
|
|||
</div>
|
||||
<span class="text-muted-foreground shrink-0">{{ formatBytes(preview.sizeBytes) }}</span>
|
||||
</div>
|
||||
<pre class="max-h-40 max-w-full overflow-auto p-3 text-xs font-mono whitespace-pre bg-muted/15">{{
|
||||
preview.preview
|
||||
}}</pre>
|
||||
<pre
|
||||
class="max-h-40 max-w-full overflow-auto p-3 text-xs font-mono whitespace-pre bg-muted/15"
|
||||
v-html="highlight(preview.preview)"
|
||||
></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
|||
v-bind="{ ...$attrs, ...forwarded }"
|
||||
:class="
|
||||
cn(
|
||||
'bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 ring-foreground/10 grid max-w-[calc(100%-2rem)] gap-4 rounded-xl p-4 text-sm ring-1 duration-100 sm:max-w-sm fixed top-1/2 left-1/2 z-50 w-full -translate-x-1/2 -translate-y-1/2 outline-none',
|
||||
'bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 ring-foreground/10 grid max-w-[calc(100%-2rem)] gap-4 rounded-xl p-4 text-sm ring-1 duration-100 sm:max-w-sm fixed top-1/2 left-1/2 z-50 w-full -translate-x-1/2 -translate-y-1/2 outline-none overflow-hidden',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
import { computed, onMounted, ref } from "vue";
|
||||
import { useTheme } from "@/composables/useTheme";
|
||||
import { type SqlHighlighter, createShikiSqlHighlighter } from "@/lib/sqlHighlighter";
|
||||
|
||||
export function useSqlHighlighter() {
|
||||
const { isDark } = useTheme();
|
||||
const sqlHighlighter = ref<SqlHighlighter>();
|
||||
|
||||
onMounted(async () => {
|
||||
sqlHighlighter.value = await createShikiSqlHighlighter({
|
||||
appearance: () => (isDark.value ? "dark" : "light"),
|
||||
});
|
||||
});
|
||||
|
||||
function highlight(sql: string): string {
|
||||
return sqlHighlighter.value?.(sql) ?? sql;
|
||||
}
|
||||
|
||||
return { highlight, sqlHighlighter };
|
||||
}
|
||||
Loading…
Reference in New Issue