feat(export): add SQL INSERT batch or single-row mode
This commit is contained in:
1 parent
5d65be03a3
commit
ab35a5b1da
29 files changed
+563
-23
No files matched your search
@@ -0,0 +1,65 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import type { SqlInsertMode } from "@/lib/export/sqlInsertMode";
|
||||
|
||||
const { t } = useI18n();
|
||||
const open = defineModel<boolean>("open", { default: false });
|
||||
const selected = ref<SqlInsertMode>("batch");
|
||||
let outcomeEmitted = false;
|
||||
|
||||
const emit = defineEmits<{
|
||||
confirm: [mode: SqlInsertMode];
|
||||
cancel: [];
|
||||
}>();
|
||||
|
||||
function onConfirm() {
|
||||
outcomeEmitted = true;
|
||||
open.value = false;
|
||||
emit("confirm", selected.value);
|
||||
}
|
||||
|
||||
function onCancel() {
|
||||
if (outcomeEmitted) return;
|
||||
outcomeEmitted = true;
|
||||
open.value = false;
|
||||
emit("cancel");
|
||||
}
|
||||
|
||||
function onOpenChange(value: boolean) {
|
||||
if (!value) onCancel();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog v-model:open="open" @update:open="onOpenChange">
|
||||
<DialogContent class="sm:max-w-md" @interact-outside.prevent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ t("grid.sqlInsertModeTitle") }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div class="space-y-3 py-2">
|
||||
<p class="text-sm text-muted-foreground">{{ t("grid.sqlInsertModePrompt") }}</p>
|
||||
<label class="flex cursor-pointer items-start gap-3 rounded-md border p-3 transition-colors hover:bg-accent/50">
|
||||
<input v-model="selected" type="radio" value="batch" class="mt-0.5 h-4 w-4 shrink-0" data-sql-insert-mode="batch" />
|
||||
<span class="min-w-0">
|
||||
<span class="block text-sm font-medium">{{ t("grid.sqlInsertModeBatch") }}</span>
|
||||
<span class="mt-1 block text-xs text-muted-foreground">{{ t("grid.sqlInsertModeBatchDescription") }}</span>
|
||||
</span>
|
||||
</label>
|
||||
<label class="flex cursor-pointer items-start gap-3 rounded-md border p-3 transition-colors hover:bg-accent/50">
|
||||
<input v-model="selected" type="radio" value="single" class="mt-0.5 h-4 w-4 shrink-0" data-sql-insert-mode="single" />
|
||||
<span class="min-w-0">
|
||||
<span class="block text-sm font-medium">{{ t("grid.sqlInsertModeSingle") }}</span>
|
||||
<span class="mt-1 block text-xs text-muted-foreground">{{ t("grid.sqlInsertModeSingleDescription") }}</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="onCancel">{{ t("common.cancel") }}</Button>
|
||||
<Button data-sql-insert-mode-confirm @click="onConfirm">{{ t("common.confirm") }}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
@@ -0,0 +1,94 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { createApp, h, nextTick, type App, defineComponent } from "vue";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import i18n from "@/i18n";
|
||||
|
||||
vi.mock("@/components/ui/dialog", async () => {
|
||||
const { defineComponent, h } = await import("vue");
|
||||
const passthrough = defineComponent({
|
||||
inheritAttrs: false,
|
||||
setup(_props, { attrs, slots }) {
|
||||
return () => h("div", attrs, slots.default?.());
|
||||
},
|
||||
});
|
||||
return { Dialog: passthrough, DialogContent: passthrough, DialogHeader: passthrough, DialogTitle: passthrough, DialogFooter: passthrough };
|
||||
});
|
||||
|
||||
vi.mock("@/components/ui/button", async () => {
|
||||
const { defineComponent, h } = await import("vue");
|
||||
return {
|
||||
Button: defineComponent({
|
||||
inheritAttrs: false,
|
||||
setup(_props, { attrs, slots }) {
|
||||
return () => h("button", attrs, slots.default?.());
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
import SqlInsertModeDialog from "@/components/export/SqlInsertModeDialog.vue";
|
||||
|
||||
const mountedApps: App[] = [];
|
||||
|
||||
afterEach(() => {
|
||||
for (const app of mountedApps.splice(0)) app.unmount();
|
||||
document.body.innerHTML = "";
|
||||
i18n.global.locale.value = "en";
|
||||
});
|
||||
|
||||
async function mountDialog(onConfirm = () => {}, onCancel = () => {}) {
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
const app = createApp(
|
||||
defineComponent({
|
||||
setup() {
|
||||
return () => h(SqlInsertModeDialog, { open: true, onConfirm, onCancel });
|
||||
},
|
||||
}),
|
||||
);
|
||||
mountedApps.push(app);
|
||||
app.use(i18n);
|
||||
app.mount(container);
|
||||
await nextTick();
|
||||
}
|
||||
|
||||
describe("SqlInsertModeDialog", () => {
|
||||
it("defaults to batch mode and emits the selected single-row mode", async () => {
|
||||
const onConfirm = vi.fn();
|
||||
await mountDialog(onConfirm);
|
||||
|
||||
const batch = document.querySelector<HTMLInputElement>('input[data-sql-insert-mode="batch"]');
|
||||
const single = document.querySelector<HTMLInputElement>('input[data-sql-insert-mode="single"]');
|
||||
expect(batch?.checked).toBe(true);
|
||||
expect(single?.checked).toBe(false);
|
||||
|
||||
single?.click();
|
||||
await nextTick();
|
||||
document.querySelector<HTMLButtonElement>("[data-sql-insert-mode-confirm]")?.click();
|
||||
|
||||
expect(onConfirm).toHaveBeenCalledOnce();
|
||||
expect(onConfirm).toHaveBeenCalledWith("single");
|
||||
});
|
||||
|
||||
it("renders the batch and single-row explanations", async () => {
|
||||
await mountDialog();
|
||||
|
||||
expect(document.body.textContent).toContain("Batch append");
|
||||
expect(document.body.textContent).toContain("One row per statement");
|
||||
expect(document.body.textContent).toContain("Combine multiple rows into each INSERT statement");
|
||||
expect(document.body.textContent).toContain("Write one complete INSERT statement per row");
|
||||
});
|
||||
|
||||
it("emits cancel without selecting a mode", async () => {
|
||||
const onConfirm = vi.fn();
|
||||
const onCancel = vi.fn();
|
||||
await mountDialog(onConfirm, onCancel);
|
||||
|
||||
const cancel = [...document.querySelectorAll("button")].find((button) => button.textContent?.includes("Cancel"));
|
||||
cancel?.click();
|
||||
|
||||
expect(onCancel).toHaveBeenCalledOnce();
|
||||
expect(onConfirm).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user