fix(editor): protect external SQL files from conflicting changes
This commit is contained in:
29 files changed
+1125
-65
No files matched your search
@@ -0,0 +1,64 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { AlertTriangle, FileWarning } from "@lucide/vue";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import type { ExternalSqlFilePrompt, ExternalSqlFilePromptDecision } from "@/composables/useExternalSqlFileChanges";
|
||||
|
||||
const props = defineProps<{
|
||||
prompt: ExternalSqlFilePrompt | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
decide: [decision: ExternalSqlFilePromptDecision];
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
const isOpen = computed(() => props.prompt !== null);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog :open="isOpen">
|
||||
<DialogContent :show-close-button="false" class="sm:max-w-[560px]" @interact-outside.prevent @escape-key-down.prevent>
|
||||
<template v-if="prompt">
|
||||
<DialogHeader>
|
||||
<div class="flex items-start gap-3 pr-2">
|
||||
<div class="mt-0.5 rounded-md bg-amber-500/12 p-2 text-amber-600 dark:text-amber-400">
|
||||
<FileWarning v-if="prompt.kind === 'deleted'" class="h-5 w-5" />
|
||||
<AlertTriangle v-else class="h-5 w-5" />
|
||||
</div>
|
||||
<div class="min-w-0 space-y-1 text-left">
|
||||
<DialogTitle>{{ t(prompt.kind === "modified" ? "externalSqlFile.modifiedTitle" : "externalSqlFile.deletedTitle") }}</DialogTitle>
|
||||
<DialogDescription class="break-all">
|
||||
{{ t(prompt.kind === "modified" ? "externalSqlFile.modifiedDescription" : "externalSqlFile.deletedDescription", { path: prompt.path }) }}
|
||||
</DialogDescription>
|
||||
</div>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
<p v-if="prompt.dirty" class="rounded-md border border-amber-500/30 bg-amber-500/8 px-3 py-2 text-xs text-amber-800 dark:text-amber-200">
|
||||
{{ t(prompt.kind === "deleted" ? "externalSqlFile.deletedUnsavedWarning" : "externalSqlFile.unsavedWarning") }}
|
||||
</p>
|
||||
|
||||
<DialogFooter v-if="prompt.kind === 'modified' && prompt.context === 'reload'" class="gap-2 sm:gap-2">
|
||||
<Button variant="outline" @click="emit('decide', 'keep')">{{ t("externalSqlFile.keepEditor") }}</Button>
|
||||
<Button @click="emit('decide', 'load')">{{ t("externalSqlFile.loadLatest") }}</Button>
|
||||
</DialogFooter>
|
||||
|
||||
<DialogFooter v-else-if="prompt.kind === 'modified'" class="flex-wrap gap-2 sm:gap-2">
|
||||
<Button variant="outline" @click="emit('decide', 'cancel')">{{ t("externalSqlFile.cancelSave") }}</Button>
|
||||
<Button variant="outline" @click="emit('decide', 'load')">{{ t("externalSqlFile.loadExternal") }}</Button>
|
||||
<Button variant="destructive" @click="emit('decide', 'overwrite')">{{ t("externalSqlFile.overwriteSave") }}</Button>
|
||||
</DialogFooter>
|
||||
|
||||
<DialogFooter v-else class="flex-wrap gap-2 sm:gap-2">
|
||||
<Button variant="ghost" @click="emit('decide', 'close')">{{ t("externalSqlFile.closeTab") }}</Button>
|
||||
<Button variant="outline" @click="emit('decide', 'keep')">{{ t("externalSqlFile.keepEditing") }}</Button>
|
||||
<Button variant="outline" @click="emit('decide', 'saveAs')">{{ t("externalSqlFile.saveAs") }}</Button>
|
||||
<Button @click="emit('decide', 'recreate')">{{ t("externalSqlFile.recreate") }}</Button>
|
||||
</DialogFooter>
|
||||
</template>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
@@ -490,6 +490,7 @@ function tabColorStyle(tab: QueryTab) {
|
||||
}
|
||||
|
||||
function tabIconClass(tab: QueryTab) {
|
||||
if (tab.externalSqlFileMissing) return "text-amber-600 dark:text-amber-400";
|
||||
if (tab.mode === "mq") return "";
|
||||
if (tab.mode === "objects") return "text-amber-500 dark:text-amber-400";
|
||||
if (tab.mode === "data" || tab.mode === "mongo" || tab.mode === "vector" || tab.mode === "redis" || tab.mode === "hbase" || tab.mode === "structure") return "text-emerald-600 dark:text-emerald-400";
|
||||
@@ -519,6 +520,7 @@ const tabBarClass = computed(() => [isClassicLayout.value ? "bg-muted" : "border
|
||||
const regularTabRowClass = computed(() => [isClassicLayout.value ? "h-9 items-stretch" : "h-10 items-center px-2", isClassicLayout.value && !hasFixedTabs.value ? "border-b" : ""]);
|
||||
|
||||
function tabMenuIcon(tab: QueryTab) {
|
||||
if (tab.externalSqlFileMissing) return AlertTriangle;
|
||||
if (tab.mode === "data" || tab.mode === "mongo" || tab.mode === "redis" || tab.mode === "hbase") return Table2;
|
||||
if (tab.mode === "vector") return TableProperties;
|
||||
if (tab.mode === "etcd" || tab.mode === "zookeeper") return KeyRound;
|
||||
@@ -663,7 +665,8 @@ function onOverflowItemKeydown(event: KeyboardEvent, tabId: string, kind: "regul
|
||||
@mouseleave="tabDrag.clearTarget(tab.id)"
|
||||
>
|
||||
<span class="shrink-0" :class="tabIconClass(tab)">
|
||||
<Table2 v-if="tab.mode === 'data' || tab.mode === 'mongo' || tab.mode === 'redis' || tab.mode === 'hbase'" class="h-3.5 w-3.5" />
|
||||
<AlertTriangle v-if="tab.externalSqlFileMissing" class="h-3.5 w-3.5" />
|
||||
<Table2 v-else-if="tab.mode === 'data' || tab.mode === 'mongo' || tab.mode === 'redis' || tab.mode === 'hbase'" class="h-3.5 w-3.5" />
|
||||
<DatabaseIcon v-else-if="tab.mode === 'mq'" :db-type="tabDatabaseIconType(tab)" class="h-3.5 w-3.5" />
|
||||
<TableProperties v-else-if="tab.mode === 'vector'" class="h-3.5 w-3.5" />
|
||||
<KeyRound v-else-if="tab.mode === 'etcd' || tab.mode === 'zookeeper'" class="h-3.5 w-3.5" />
|
||||
@@ -858,7 +861,8 @@ function onOverflowItemKeydown(event: KeyboardEvent, tabId: string, kind: "regul
|
||||
@mouseleave="tabDrag.clearTarget(tab.id)"
|
||||
>
|
||||
<span class="shrink-0" :class="tabIconClass(tab)">
|
||||
<Table2 v-if="tab.mode === 'data' || tab.mode === 'mongo' || tab.mode === 'redis' || tab.mode === 'hbase'" class="h-3.5 w-3.5" />
|
||||
<AlertTriangle v-if="tab.externalSqlFileMissing" class="h-3.5 w-3.5" />
|
||||
<Table2 v-else-if="tab.mode === 'data' || tab.mode === 'mongo' || tab.mode === 'redis' || tab.mode === 'hbase'" class="h-3.5 w-3.5" />
|
||||
<DatabaseIcon v-else-if="tab.mode === 'mq'" :db-type="tabDatabaseIconType(tab)" class="h-3.5 w-3.5" />
|
||||
<TableProperties v-else-if="tab.mode === 'vector'" class="h-3.5 w-3.5" />
|
||||
<KeyRound v-else-if="tab.mode === 'etcd' || tab.mode === 'zookeeper'" class="h-3.5 w-3.5" />
|
||||
|
||||
@@ -208,12 +208,12 @@ function collectDirPaths(entries: SqlFileEntry[], into: Set<string>) {
|
||||
async function openFile(path: string) {
|
||||
if (!isTauriRuntime()) return;
|
||||
try {
|
||||
const content = await api.readExternalSqlFile(path);
|
||||
const snapshot = await api.readExternalSqlFileSnapshot(path);
|
||||
const connectionId = connectionStore.activeConnectionId || connectionStore.connections[0]?.id || "";
|
||||
const connection = connectionId ? connectionStore.getConfig(connectionId) : undefined;
|
||||
const database = connection ? resolveDefaultDatabase(connection, []) : "";
|
||||
const target = resolveExternalSqlFileTarget(path, (savedConnectionId) => !!connectionStore.getConfig(savedConnectionId), { connectionId, database });
|
||||
queryStore.openExternalSqlFile(target.connectionId, target.database, path, content);
|
||||
queryStore.openExternalSqlFile(target.connectionId, target.database, path, snapshot.content, snapshot.version);
|
||||
} catch (e: any) {
|
||||
if (isExternalSqlFileTooLargeError(e)) {
|
||||
executeFile(path);
|
||||
|
||||
Reference in new issue
Block a user