diff --git a/src/App.vue b/src/App.vue index c7c500707..e853f6a7d 100644 --- a/src/App.vue +++ b/src/App.vue @@ -201,10 +201,17 @@ async function openConnectionQuery(connectionId: string) { queryStore.createTab(connectionId, database); } -const DANGER_RE = /^\s*(DROP|DELETE|TRUNCATE|ALTER)\b/i; +const DANGER_RE = /\b(DROP|DELETE|TRUNCATE|ALTER|UPDATE|MERGE|REPLACE)\b/i; + +function stripSqlComments(sql: string): string { + return sql + .replace(/\/\*[\s\S]*?\*\//g, " ") + .replace(/--.*$/gm, " ") + .replace(/#.*$/gm, " "); +} function isDangerousSql(sql: string): boolean { - return DANGER_RE.test(sql); + return DANGER_RE.test(stripSqlComments(sql)); } function tryExecute() { diff --git a/src/components/editor/DangerConfirmDialog.vue b/src/components/editor/DangerConfirmDialog.vue index afc1cbb5e..24ab1f42a 100644 --- a/src/components/editor/DangerConfirmDialog.vue +++ b/src/components/editor/DangerConfirmDialog.vue @@ -10,9 +10,19 @@ const { t } = useI18n(); const open = defineModel("open", { default: false }); -defineProps<{ - sql: string; -}>(); +withDefaults(defineProps<{ + sql?: string; + title?: string; + message?: string; + details?: string; + confirmLabel?: string; +}>(), { + sql: "", + title: "", + message: "", + details: "", + confirmLabel: "", +}); const emit = defineEmits<{ confirm: []; @@ -30,18 +40,18 @@ function onConfirm() { - {{ t('dangerDialog.title') }} + {{ title || t('dangerDialog.title') }}
-

{{ t('dangerDialog.message') }}

-
{{ sql }}
+

{{ message || t('dangerDialog.message') }}

+
{{ details || sql }}
- + diff --git a/src/components/grid/DataGrid.vue b/src/components/grid/DataGrid.vue index 125d7968f..5cd6ac117 100644 --- a/src/components/grid/DataGrid.vue +++ b/src/components/grid/DataGrid.vue @@ -18,6 +18,7 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; +import DangerConfirmDialog from "@/components/editor/DangerConfirmDialog.vue"; import type { QueryResult, ColumnInfo, DatabaseType } from "@/types/database"; import { save as savePath } from "@tauri-apps/plugin-dialog"; import { writeTextFile } from "@tauri-apps/plugin-fs"; @@ -422,7 +423,7 @@ function addRow() { }); } -function deleteRow(rowId: number) { +function applyDeleteRow(rowId: number) { const item = getRowItem(rowId); if (!item) return; if (item.isNew && item.newIndex !== undefined) { @@ -434,6 +435,25 @@ function deleteRow(rowId: number) { if (editingCell.value?.rowId === rowId) editingCell.value = null; } +const showDeleteRowConfirm = ref(false); +const pendingDeleteRowId = ref(null); +const deleteRowDetails = computed(() => + props.tableMeta?.tableName + ? t("dangerDialog.deleteRowDetails", { table: props.tableMeta.tableName }) + : t("dangerDialog.deleteRowDetailsNoTable") +); + +function requestDeleteRow(rowId: number) { + pendingDeleteRowId.value = rowId; + showDeleteRowConfirm.value = true; +} + +function confirmDeleteRow() { + if (pendingDeleteRowId.value === null) return; + applyDeleteRow(pendingDeleteRowId.value); + pendingDeleteRowId.value = null; +} + function restoreRow(rowId: number) { const item = getRowItem(rowId); if (item?.sourceIndex !== undefined) { @@ -443,7 +463,7 @@ function restoreRow(rowId: number) { function deleteSelectedRow() { if (!contextCell.value) return; - deleteRow(contextCell.value.rowId); + requestDeleteRow(contextCell.value.rowId); } function escapeVal(v: CellValue): string { @@ -793,7 +813,7 @@ function escapeAndHighlightKeywords(s: string): string { size="icon" class="h-5 w-5 shrink-0 text-destructive" :title="t('grid.deleteRow')" - @click.stop="deleteRow(item.id)" + @click.stop="requestDeleteRow(item.id)" > @@ -955,6 +975,14 @@ function escapeAndHighlightKeywords(s: string): string { + + diff --git a/src/components/mongo/JsonEditNode.vue b/src/components/mongo/JsonEditNode.vue index 06a300e8b..6c00e97d2 100644 --- a/src/components/mongo/JsonEditNode.vue +++ b/src/components/mongo/JsonEditNode.vue @@ -1,8 +1,9 @@ diff --git a/src/components/mongo/MongoDocBrowser.vue b/src/components/mongo/MongoDocBrowser.vue index 53cb859f8..a67ed29c0 100644 --- a/src/components/mongo/MongoDocBrowser.vue +++ b/src/components/mongo/MongoDocBrowser.vue @@ -4,6 +4,7 @@ import { useI18n } from "vue-i18n"; import { RefreshCw, Trash2, Plus, Save, ChevronLeft, ChevronRight } from "lucide-vue-next"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; +import DangerConfirmDialog from "@/components/editor/DangerConfirmDialog.vue"; import * as api from "@/lib/tauri"; import JsonEditNode from "./JsonEditNode.vue"; import type { EditNode } from "@/types/editor"; @@ -31,6 +32,13 @@ const isEditing = ref(false); const isNew = ref(false); const error = ref(""); const editFields = ref([]); +const showDeleteConfirm = ref(false); + +type PendingDelete = + | { kind: "document"; index: number } + | { kind: "field"; index: number; name: string }; + +const pendingDelete = ref(null); const selectedDoc = computed(() => { if (selectedIdx.value === null) return null; @@ -44,6 +52,16 @@ const editKeyWidth = computed(() => { return `${Math.min(Math.max(longest + 4, 8), 36)}ch`; }); +const deleteDetails = computed(() => { + const pending = pendingDelete.value; + if (!pending) return ""; + if (pending.kind === "document") { + const id = documents.value[pending.index]?._id ?? ""; + return t("dangerDialog.mongoDocumentDetails", { collection: props.collection, id: String(id) }); + } + return t("dangerDialog.mongoFieldDetails", { field: pending.name || t("mongo.field") }); +}); + async function load() { loading.value = true; error.value = ""; @@ -150,11 +168,18 @@ function addField() { editFields.value.push(createEditNode("", "", false, false)); } -function removeField(idx: number) { +function applyRemoveField(idx: number) { if (editFields.value[idx]?.readonlyValue) return; editFields.value.splice(idx, 1); } +function requestRemoveField(idx: number) { + const field = editFields.value[idx]; + if (!field || field.readonlyValue) return; + pendingDelete.value = { kind: "field", index: idx, name: field.keyName }; + showDeleteConfirm.value = true; +} + function formatForEdit(value: unknown): string { if (value === undefined) return ""; if (value === null) return "null"; @@ -222,7 +247,7 @@ async function saveDoc() { } } -async function deleteDoc(idx: number) { +async function applyDeleteDoc(idx: number) { const doc = documents.value[idx]; const id = doc._id; if (!id) return; @@ -236,6 +261,22 @@ async function deleteDoc(idx: number) { } } +function requestDeleteDoc(idx: number) { + pendingDelete.value = { kind: "document", index: idx }; + showDeleteConfirm.value = true; +} + +async function confirmDelete() { + const pending = pendingDelete.value; + if (!pending) return; + if (pending.kind === "document") { + await applyDeleteDoc(pending.index); + } else { + applyRemoveField(pending.index); + } + pendingDelete.value = null; +} + function prevPage() { if (page.value <= 0) return; page.value--; @@ -297,7 +338,7 @@ onMounted(load); @click="selectDoc(idx)" > {{ docPreview(doc) }} - @@ -346,7 +387,7 @@ onMounted(load); :node="field" parent-kind="root" :removable="!field.readonlyValue" - @remove="removeField(idx)" + @remove="requestRemoveField(idx)" />