feat(mysql): manage and edit MySQL events
This commit is contained in:
1 parent
701f188e45
commit
d8bc5153d2
35 files changed
+981
-27
No files matched your search
@@ -0,0 +1,190 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import type { CalendarDateTime } from "@internationalized/date";
|
||||
import { Check, Clock, Eye, Loader2, X } from "@lucide/vue";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import DateTimePicker from "@/components/ui/date-time-picker/DateTimePicker.vue";
|
||||
import { formatLocalDateTime, parseLocalDateTime } from "@/components/ui/date-time-picker/dateTimePicker";
|
||||
import * as api from "@/lib/backend/api";
|
||||
import { executeWithProductionSqlGuard } from "@/lib/database/productionExecutionGuard";
|
||||
import { buildMysqlEventSql } from "@/lib/table/mysqlEventSql";
|
||||
import type { ConnectionConfig, MysqlEventInfo } from "@/types/database";
|
||||
|
||||
const props = defineProps<{ connection: ConnectionConfig; database: string; schema: string; name?: string; readOnly?: boolean }>();
|
||||
const emit = defineEmits<{ saved: [name: string]; close: [] }>();
|
||||
const { t, locale } = useI18n();
|
||||
const loading = ref(false),
|
||||
saving = ref(false),
|
||||
error = ref(""),
|
||||
preview = ref("");
|
||||
const activeTab = ref<"definition" | "schedule" | "comment" | "preview">("definition");
|
||||
const draft = ref<any>({ name: props.name || "", schema: props.schema, schedule: { mode: "every", intervalValue: "1", intervalUnit: "DAY" }, preserve: true, enabled: true, body: "" });
|
||||
const existing = computed(() => !!props.name);
|
||||
const units = ["SECOND", "MINUTE", "HOUR", "DAY", "WEEK", "MONTH", "QUARTER", "YEAR"];
|
||||
const tabs = computed(
|
||||
() =>
|
||||
[
|
||||
{ id: "definition", label: t("contextMenu.eventDefinition") },
|
||||
{ id: "schedule", label: t("contextMenu.eventSchedule") },
|
||||
{ id: "comment", label: t("contextMenu.eventComment") },
|
||||
{ id: "preview", label: t("contextMenu.eventSqlPreview") },
|
||||
] as const,
|
||||
);
|
||||
const executeAtDate = computed<CalendarDateTime | null>({
|
||||
get: () => parseLocalDateTime(draft.value.schedule.executeAt || ""),
|
||||
set: (value) => {
|
||||
draft.value.schedule.executeAt = value ? formatLocalDateTime(value) : "";
|
||||
refreshPreview();
|
||||
},
|
||||
});
|
||||
const startsDate = computed<CalendarDateTime | null>({
|
||||
get: () => parseLocalDateTime(draft.value.starts || ""),
|
||||
set: (value) => {
|
||||
draft.value.starts = value ? formatLocalDateTime(value) : "";
|
||||
refreshPreview();
|
||||
},
|
||||
});
|
||||
const endsDate = computed<CalendarDateTime | null>({
|
||||
get: () => parseLocalDateTime(draft.value.ends || ""),
|
||||
set: (value) => {
|
||||
draft.value.ends = value ? formatLocalDateTime(value) : "";
|
||||
refreshPreview();
|
||||
},
|
||||
});
|
||||
function refreshPreview() {
|
||||
try {
|
||||
preview.value = buildMysqlEventSql(draft.value, existing.value ? "ALTER" : "CREATE");
|
||||
error.value = "";
|
||||
} catch (e) {
|
||||
preview.value = "";
|
||||
error.value = e instanceof Error ? e.message : String(e);
|
||||
}
|
||||
}
|
||||
async function load() {
|
||||
if (!props.name) {
|
||||
refreshPreview();
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
try {
|
||||
const info: MysqlEventInfo = await api.getEventInfo(props.connection.id, props.database, props.schema, props.name);
|
||||
draft.value = {
|
||||
name: info.name,
|
||||
schema: info.schema,
|
||||
schedule: info.execute_at ? { mode: "at", executeAt: info.execute_at } : { mode: "every", intervalValue: info.interval_value || "1", intervalUnit: info.interval_field || "DAY" },
|
||||
starts: info.starts || "",
|
||||
ends: info.ends || "",
|
||||
preserve: info.on_completion?.toUpperCase() === "PRESERVE",
|
||||
enabled: info.status?.toUpperCase() === "ENABLED",
|
||||
comment: info.comment || "",
|
||||
body: info.event_definition || info.event_body || "",
|
||||
};
|
||||
refreshPreview();
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : String(e);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
async function save() {
|
||||
if (props.readOnly) return;
|
||||
refreshPreview();
|
||||
if (!preview.value) return;
|
||||
saving.value = true;
|
||||
try {
|
||||
await executeWithProductionSqlGuard({
|
||||
connection: props.connection,
|
||||
database: props.database,
|
||||
sql: preview.value,
|
||||
source: t("contextMenu.eventEditorTitle"),
|
||||
execute: async () => {
|
||||
await api.executeQuery(props.connection.id, props.database, preview.value, props.schema);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
emit("saved", draft.value.name.trim());
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : String(e);
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
onMounted(load);
|
||||
</script>
|
||||
<template>
|
||||
<div class="flex min-h-0 flex-1 flex-col gap-3 overflow-auto p-4" data-mysql-event-editor>
|
||||
<div class="flex items-center justify-between border-b pb-2">
|
||||
<div class="flex items-center gap-2 text-sm font-semibold"><Clock class="h-4 w-4 text-orange-300" /> {{ t("contextMenu.eventEditorTitle") }}</div>
|
||||
<Button variant="ghost" size="icon" class="h-6 w-6" :aria-label="t('contextMenu.eventCancel')" @click="emit('close')"><X class="h-4 w-4" /></Button>
|
||||
</div>
|
||||
<div v-if="loading" class="flex items-center gap-2 text-sm text-muted-foreground"><Loader2 class="h-4 w-4 animate-spin" /> {{ t("contextMenu.eventLoading") }}</div>
|
||||
<template v-else>
|
||||
<div class="flex gap-1 border-b" role="tablist">
|
||||
<button v-for="tab in tabs" :key="tab.id" type="button" class="border-b-2 px-3 py-1.5 text-xs" :class="activeTab === tab.id ? 'border-primary text-foreground' : 'border-transparent text-muted-foreground'" @click="activeTab = tab.id">{{ tab.label }}</button>
|
||||
</div>
|
||||
<div v-if="activeTab === 'definition'" class="flex flex-col gap-3">
|
||||
<label class="text-xs">{{ t("contextMenu.eventName") }}<Input v-model="draft.name" :disabled="props.readOnly || existing" class="mt-1 h-8" @input="refreshPreview" /></label
|
||||
><textarea v-model="draft.body" :disabled="props.readOnly" class="min-h-40 rounded border bg-background p-2 font-mono text-xs" :placeholder="t('contextMenu.eventBodyPlaceholder')" @input="refreshPreview" />
|
||||
</div>
|
||||
<div v-else-if="activeTab === 'schedule'" class="flex flex-col gap-3">
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<label class="text-xs"
|
||||
>{{ t("contextMenu.eventSchedule")
|
||||
}}<select v-model="draft.schedule.mode" :disabled="props.readOnly" class="mt-1 h-8 w-full rounded border bg-background px-2 text-xs" @change="refreshPreview">
|
||||
<option value="every">EVERY</option>
|
||||
<option value="at">AT</option>
|
||||
</select></label
|
||||
><label v-if="draft.schedule.mode === 'every'" class="text-xs"
|
||||
>{{ t("contextMenu.eventIntervalUnit")
|
||||
}}<select v-model="draft.schedule.intervalUnit" :disabled="props.readOnly" class="mt-1 h-8 w-full rounded border bg-background px-2 text-xs" @change="refreshPreview">
|
||||
<option v-for="unit in units" :key="unit" :value="unit">{{ unit }}</option>
|
||||
</select></label
|
||||
>
|
||||
</div>
|
||||
<label class="text-xs"
|
||||
>{{ draft.schedule.mode === "at" ? t("contextMenu.eventExecuteAt") : t("contextMenu.eventIntervalValue")
|
||||
}}<DateTimePicker v-if="draft.schedule.mode === 'at'" v-model="executeAtDate" class="mt-1" :locale="locale" :placeholder="t('dateTimePicker.inputPlaceholder')" :disabled="props.readOnly" full-width /><Input
|
||||
v-else
|
||||
v-model="draft.schedule.intervalValue"
|
||||
:disabled="props.readOnly"
|
||||
class="mt-1 h-8"
|
||||
placeholder="1"
|
||||
@input="refreshPreview"
|
||||
/></label>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<label class="text-xs">{{ t("contextMenu.eventStarts") }}<DateTimePicker v-model="startsDate" class="mt-1" :locale="locale" :placeholder="t('dateTimePicker.inputPlaceholder')" :disabled="props.readOnly" full-width /></label
|
||||
><label class="text-xs">{{ t("contextMenu.eventEnds") }}<DateTimePicker v-model="endsDate" class="mt-1" :locale="locale" :placeholder="t('dateTimePicker.inputPlaceholder')" :disabled="props.readOnly" full-width /></label>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<label class="text-xs"
|
||||
>{{ t("contextMenu.eventCompletion")
|
||||
}}<select v-model="draft.preserve" :disabled="props.readOnly" class="mt-1 h-8 w-full rounded border bg-background px-2 text-xs" @change="refreshPreview">
|
||||
<option :value="true">PRESERVE</option>
|
||||
<option :value="false">NOT PRESERVE</option>
|
||||
</select></label
|
||||
><label class="text-xs"
|
||||
>{{ t("contextMenu.eventStatus")
|
||||
}}<select v-model="draft.enabled" :disabled="props.readOnly" class="mt-1 h-8 w-full rounded border bg-background px-2 text-xs" @change="refreshPreview">
|
||||
<option :value="true">ENABLE</option>
|
||||
<option :value="false">DISABLE</option>
|
||||
</select></label
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="activeTab === 'comment'">
|
||||
<label class="text-xs">{{ t("contextMenu.eventComment") }}<Input v-model="draft.comment" :disabled="props.readOnly" class="mt-1 h-8" :placeholder="t('contextMenu.eventCommentPlaceholder')" @input="refreshPreview" /></label>
|
||||
</div>
|
||||
<div v-else class="flex min-h-0 flex-1 flex-col gap-2">
|
||||
<div class="flex items-center gap-2 text-xs font-medium"><Eye class="h-3.5 w-3.5" /> {{ t("contextMenu.eventSqlPreview") }}</div>
|
||||
<pre class="min-h-48 flex-1 overflow-auto rounded border bg-muted/30 p-2 text-xs whitespace-pre-wrap">{{ preview }}</pre>
|
||||
</div>
|
||||
<div v-if="error" class="whitespace-pre-wrap text-xs text-destructive">{{ error }}</div>
|
||||
<div class="flex justify-end gap-2">
|
||||
<Button variant="outline" size="sm" @click="emit('close')">{{ t("contextMenu.eventCancel") }}</Button
|
||||
><Button v-if="!props.readOnly" size="sm" :disabled="saving || !preview" @click="save"><Loader2 v-if="saving" class="mr-1 h-3.5 w-3.5 animate-spin" /><Check v-else class="mr-1 h-3.5 w-3.5" />{{ t("contextMenu.eventSave") }}</Button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
ArrowUp,
|
||||
Braces,
|
||||
CheckSquare,
|
||||
Clock,
|
||||
Clipboard,
|
||||
Code2,
|
||||
Copy,
|
||||
@@ -95,6 +96,7 @@ import { useExportTracker, type ExportTask } from "@/composables/useExportTracke
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
import { useQueryStore } from "@/stores/queryStore";
|
||||
import QueryEditor from "@/components/editor/QueryEditor.vue";
|
||||
import MySqlEventEditor from "@/components/objects/MySqlEventEditor.vue";
|
||||
import { sqlFormatDialectForDbType, type SqlFormatDialect } from "@/lib/sql/sqlFormatter";
|
||||
import { isCancelSearchShortcut } from "@/lib/editor/keyboardShortcuts";
|
||||
import { executeWithProductionSqlGuard } from "@/lib/database/productionExecutionGuard";
|
||||
@@ -134,6 +136,9 @@ import { cacheObjectBrowserRows, createObjectBrowserRowsCacheWriteToken, getCach
|
||||
import { createObjectBrowserRowsLoadGuard, type ObjectBrowserRowsLoadHandle } from "@/lib/table/objectBrowserRowsLoadGuard";
|
||||
import { loadObjectDdl, type ObjectDdlRequest } from "@/lib/metadata/objectDdlCache";
|
||||
import { loadObjectMetadataFacet } from "@/lib/metadata/objectMetadataCache";
|
||||
import { invalidateObjectMetadataCache } from "@/lib/metadata/objectMetadataCache";
|
||||
import { invalidateObjectDdl } from "@/lib/metadata/objectDdlCache";
|
||||
import { invalidateObjectBrowserRowsCache } from "@/lib/table/objectBrowserRowsCache";
|
||||
|
||||
type ObjectFilter = ObjectBrowserFilter;
|
||||
type ObjectBrowserColumnKey = "select" | "name" | "type" | "estimatedRows" | "totalBytes" | "created_at" | "updated_at" | "comment";
|
||||
@@ -143,6 +148,10 @@ const props = defineProps<{
|
||||
database: string;
|
||||
catalog?: string;
|
||||
schema?: string;
|
||||
initialEventName?: string;
|
||||
initialEventReadOnly?: boolean;
|
||||
initialEventOpenRequestId?: number;
|
||||
initialObjectFilter?: "tables" | "events";
|
||||
viewport?: ObjectBrowserViewport;
|
||||
}>();
|
||||
|
||||
@@ -183,7 +192,9 @@ const sourceCanEdit = ref(true);
|
||||
// --- Right-side panel state ---
|
||||
// Unified panel: either "table-info" (for tables) or "source" (for views/procedures/etc.)
|
||||
const sidePanelRow = ref<ObjectBrowserRow | null>(null);
|
||||
const sidePanelMode = ref<"table-info" | "source" | "type-info">("source");
|
||||
const openedInitialEvent = ref("");
|
||||
const isEventEditor = computed(() => sidePanelMode.value === "event-editor");
|
||||
const sidePanelMode = ref<"table-info" | "source" | "type-info" | "event-editor">("source");
|
||||
// Table info panel state
|
||||
const tableInfoTab = ref<TableInfoTab>("ddl");
|
||||
const tableColumns = ref<ColumnInfo[]>([]);
|
||||
@@ -322,6 +333,7 @@ const objectFilters = computed<ObjectFilter[]>(() =>
|
||||
["procedures", objectCounts.value.procedures],
|
||||
["functions", objectCounts.value.functions],
|
||||
["triggers", objectCounts.value.triggers],
|
||||
["events", objectCounts.value.events],
|
||||
["sequences", objectCounts.value.sequences],
|
||||
["packages", objectCounts.value.packages],
|
||||
["types", objectCounts.value.types],
|
||||
@@ -459,6 +471,7 @@ watch(objectFilter, () => {
|
||||
}
|
||||
scrollObjectsToTop();
|
||||
});
|
||||
watch([() => props.initialEventName, () => props.initialEventOpenRequestId, rows, loadingObjects], openInitialEventIfNeeded, { flush: "post" });
|
||||
watch(
|
||||
() => props.connection.show_system_schemas,
|
||||
(value, oldValue) => {
|
||||
@@ -606,6 +619,7 @@ function iconFor(row: ObjectBrowserRow) {
|
||||
if (row.type === "PROCEDURE") return ScrollText;
|
||||
if (row.type === "FUNCTION") return Braces;
|
||||
if (row.type === "TRIGGER") return RotateCcw;
|
||||
if (row.type === "EVENT") return Clock;
|
||||
if (row.type === "SEQUENCE") return ListTree;
|
||||
if (row.type === "PACKAGE" || row.type === "PACKAGE_BODY") return Package;
|
||||
if (row.type === "TYPE" || row.type === "TYPE_BODY") return Braces;
|
||||
@@ -618,6 +632,7 @@ function typeLabel(type: ObjectBrowserRow["type"]) {
|
||||
if (type === "PROCEDURE") return t("objects.procedure");
|
||||
if (type === "FUNCTION") return t("objects.function");
|
||||
if (type === "TRIGGER") return t("objects.trigger");
|
||||
if (type === "EVENT") return t("tree.events");
|
||||
if (type === "SEQUENCE") return t("objects.sequence");
|
||||
if (type === "PACKAGE") return t("objects.package");
|
||||
if (type === "PACKAGE_BODY") return t("objects.packageBody");
|
||||
@@ -721,6 +736,7 @@ function rowMatchesFilter(row: ObjectBrowserRow, filter: ObjectFilter) {
|
||||
if (filter === "procedures") return row.type === "PROCEDURE";
|
||||
if (filter === "functions") return row.type === "FUNCTION";
|
||||
if (filter === "triggers") return row.type === "TRIGGER";
|
||||
if (filter === "events") return row.type === "EVENT";
|
||||
if (filter === "sequences") return row.type === "SEQUENCE";
|
||||
if (filter === "packages") return row.type === "PACKAGE" || row.type === "PACKAGE_BODY";
|
||||
if (filter === "types") return row.type === "TYPE" || row.type === "TYPE_BODY";
|
||||
@@ -814,6 +830,7 @@ function iconClass(type: ObjectBrowserRow["type"]) {
|
||||
if (type === "PROCEDURE") return "text-blue-500";
|
||||
if (type === "FUNCTION") return "text-amber-500";
|
||||
if (type === "TRIGGER") return "text-rose-500";
|
||||
if (type === "EVENT") return "text-orange-500";
|
||||
if (type === "SEQUENCE") return "text-emerald-500";
|
||||
if (type === "PACKAGE" || type === "PACKAGE_BODY") return "text-cyan-500";
|
||||
if (type === "TYPE" || type === "TYPE_BODY") return "text-violet-500";
|
||||
@@ -825,6 +842,7 @@ function iconBgClass(type: ObjectBrowserRow["type"]) {
|
||||
if (type === "PROCEDURE") return "object-browser-icon-bg object-browser-icon-bg-procedure";
|
||||
if (type === "FUNCTION") return "object-browser-icon-bg object-browser-icon-bg-function";
|
||||
if (type === "TRIGGER") return "object-browser-icon-bg object-browser-icon-bg-procedure";
|
||||
if (type === "EVENT") return "object-browser-icon-bg object-browser-icon-bg-procedure";
|
||||
if (type === "SEQUENCE") return "object-browser-icon-bg object-browser-icon-bg-sequence";
|
||||
if (type === "PACKAGE" || type === "PACKAGE_BODY") return "object-browser-icon-bg object-browser-icon-bg-package";
|
||||
if (type === "TYPE" || type === "TYPE_BODY") return "object-browser-icon-bg object-browser-icon-bg-function";
|
||||
@@ -867,7 +885,7 @@ function executeRowAction(row: ObjectBrowserRow, action: ObjectBrowserRowAction)
|
||||
emit("openTable", { tableName: row.name, schema: row.schema, catalog: props.catalog });
|
||||
break;
|
||||
case "open-source":
|
||||
void openSource(row);
|
||||
void (row.type === "EVENT" ? openEventEditor(row) : openSource(row));
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1301,6 +1319,29 @@ async function openSource(row: ObjectBrowserRow) {
|
||||
}
|
||||
}
|
||||
|
||||
function openEventEditor(row: ObjectBrowserRow) {
|
||||
sidePanelGuard.start();
|
||||
sidePanelRow.value = row;
|
||||
sourceRow.value = null;
|
||||
sidePanelMode.value = "event-editor";
|
||||
}
|
||||
|
||||
async function onEventSaved(savedName: string) {
|
||||
const row = sidePanelRow.value;
|
||||
const name = savedName.trim() || row?.name || "";
|
||||
if (name) {
|
||||
const cacheSchema = row?.schema || selectedSchema.value || props.database;
|
||||
const cacheScope = { connectionId: props.connection.id, database: props.database, schema: cacheSchema, tableName: name };
|
||||
await Promise.all([invalidateObjectMetadataCache(cacheScope), invalidateObjectDdl(cacheScope)]);
|
||||
invalidateObjectBrowserRowsCache({ connectionId: props.connection.id, database: props.database, schema: cacheSchema });
|
||||
if (row && row.name !== name) {
|
||||
sidePanelRow.value = { ...row, id: `event:${cacheSchema}:${name}`, name, displayName: name };
|
||||
}
|
||||
}
|
||||
await loadObjects({ allowCached: false });
|
||||
toast(t("objects.sourceSaved"));
|
||||
}
|
||||
|
||||
async function openNewQuery(row: ObjectBrowserRow) {
|
||||
const schema = row.schema || selectedSchema.value;
|
||||
const tabId = queryStore.createTab(props.connection.id, props.database, row.name, "query", schema, undefined, props.catalog);
|
||||
@@ -1466,8 +1507,11 @@ async function confirmDrop() {
|
||||
const sql = dropPreviewSql.value || (await buildDropSqlForRow(row, { cascade: canDropTargetCascade.value && dropTableCascade.value }));
|
||||
const executed = await executeObjectBrowserSqlWithProductionGuard(sql, () => api.executeQuery(props.connection.id, props.database, sql));
|
||||
if (!executed) return;
|
||||
const successKey = row.type === "VIEW" ? "contextMenu.dropViewSuccess" : row.type === "PROCEDURE" ? "contextMenu.dropProcedureSuccess" : row.type === "FUNCTION" ? "contextMenu.dropFunctionSuccess" : "contextMenu.dropTableSuccess";
|
||||
const successKey = row.type === "VIEW" ? "contextMenu.dropViewSuccess" : row.type === "PROCEDURE" ? "contextMenu.dropProcedureSuccess" : row.type === "FUNCTION" ? "contextMenu.dropFunctionSuccess" : row.type === "EVENT" ? "contextMenu.dropEventSuccess" : "contextMenu.dropTableSuccess";
|
||||
toast(t(successKey, { name: row.name }));
|
||||
const cacheSchema = row.schema || selectedSchema.value || props.database;
|
||||
await Promise.all([invalidateObjectMetadataCache({ connectionId: props.connection.id, database: props.database, schema: cacheSchema, tableName: row.name }), invalidateObjectDdl({ connectionId: props.connection.id, database: props.database, schema: cacheSchema, tableName: row.name })]);
|
||||
invalidateObjectBrowserRowsCache({ connectionId: props.connection.id, database: props.database, schema: cacheSchema });
|
||||
closeDroppedTableObjectTabsForRow(row);
|
||||
removePinnedObjectBrowserRows([row]);
|
||||
await reload();
|
||||
@@ -1513,6 +1557,7 @@ function dropConfirmTitle(): string {
|
||||
if (type === "VIEW" || type === "MATERIALIZED_VIEW") return t("contextMenu.confirmDropViewTitle");
|
||||
if (type === "PROCEDURE") return t("contextMenu.confirmDropProcedureTitle");
|
||||
if (type === "FUNCTION") return t("contextMenu.confirmDropFunctionTitle");
|
||||
if (type === "EVENT") return t("contextMenu.confirmDropEventTitle");
|
||||
return t("contextMenu.confirmDropTableTitle");
|
||||
}
|
||||
|
||||
@@ -1523,6 +1568,7 @@ function dropConfirmMessage(): string {
|
||||
if (type === "VIEW" || type === "MATERIALIZED_VIEW") return t("contextMenu.confirmDropViewMessage", { name });
|
||||
if (type === "PROCEDURE") return t("contextMenu.confirmDropProcedureMessage", { name });
|
||||
if (type === "FUNCTION") return t("contextMenu.confirmDropFunctionMessage", { name });
|
||||
if (type === "EVENT") return t("contextMenu.confirmDropEventMessage", { name });
|
||||
return t("contextMenu.confirmDropTableMessage", { name });
|
||||
}
|
||||
|
||||
@@ -2584,17 +2630,34 @@ function applyObjectBrowserRows(nextRows: ObjectBrowserRow[]) {
|
||||
expandedPartitionParentIds.value = new Set([...expandedPartitionParentIds.value].filter((id) => rows.value.some((row) => row.id === id && row.partitionCount)));
|
||||
}
|
||||
|
||||
function openInitialEventIfNeeded() {
|
||||
const name = props.initialEventName?.trim();
|
||||
const requestKey = `${props.initialEventOpenRequestId ?? 0}:${name}`;
|
||||
if (!name || openedInitialEvent.value === requestKey || loadingObjects.value) return;
|
||||
const row = rows.value.find((candidate) => candidate.type === "EVENT" && candidate.name === name);
|
||||
if (!row) return;
|
||||
openedInitialEvent.value = requestKey;
|
||||
openEventEditor(row);
|
||||
}
|
||||
|
||||
function finishObjectBrowserRowsLoad() {
|
||||
loadingObjects.value = false;
|
||||
if (!userHasSelectedFilter.value && objectCounts.value.tables > 0) {
|
||||
const preferredFilter = props.initialObjectFilter ?? (props.initialEventName ? "events" : "tables");
|
||||
if (!userHasSelectedFilter.value && objectCounts.value[preferredFilter] > 0) {
|
||||
// The default table filter is a presentation choice, not a user query
|
||||
// change, so preserve the tab's saved scroll offset across remounts.
|
||||
preserveObjectFilterScrollOnce = objectFilter.value !== "tables";
|
||||
objectFilter.value = "tables";
|
||||
objectFilter.value = preferredFilter;
|
||||
}
|
||||
openInitialEventIfNeeded();
|
||||
restoreObjectBrowserViewport();
|
||||
}
|
||||
|
||||
watch([() => props.initialEventName, () => props.initialEventOpenRequestId], ([name, requestId], [previousName, previousRequestId]) => {
|
||||
if (name !== previousName || requestId !== previousRequestId) openedInitialEvent.value = "";
|
||||
openInitialEventIfNeeded();
|
||||
});
|
||||
|
||||
async function loadObjects(options?: { allowCached?: boolean }) {
|
||||
error.value = "";
|
||||
const schema = needsSchema.value ? selectedSchema.value || "" : props.database;
|
||||
@@ -2704,13 +2767,15 @@ function filterLabel(filter: ObjectFilter) {
|
||||
? "objects.functions"
|
||||
: filter === "triggers"
|
||||
? "tree.triggers"
|
||||
: filter === "sequences"
|
||||
? "objects.sequences"
|
||||
: filter === "packages"
|
||||
? "objects.packages"
|
||||
: filter === "types"
|
||||
? "tree.types"
|
||||
: "objects.all";
|
||||
: filter === "events"
|
||||
? "tree.events"
|
||||
: filter === "sequences"
|
||||
? "objects.sequences"
|
||||
: filter === "packages"
|
||||
? "objects.packages"
|
||||
: filter === "types"
|
||||
? "tree.types"
|
||||
: "objects.all";
|
||||
return `${t(key)} ${filterCount(filter)}`;
|
||||
}
|
||||
|
||||
@@ -2930,6 +2995,17 @@ function getProcFuncMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
|
||||
];
|
||||
}
|
||||
|
||||
function getEventMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
|
||||
return [
|
||||
{ label: t("contextMenu.editObject"), action: () => openEventEditor(item), icon: PencilLine },
|
||||
{ label: t("contextMenu.viewSource"), action: () => openSource(item), icon: Code2 },
|
||||
{ label: "", separator: true },
|
||||
{ label: t("contextMenu.dropObject"), action: () => requestDrop(item), icon: Trash2, variant: "destructive" as const },
|
||||
{ label: "", separator: true },
|
||||
{ label: t("contextMenu.copyName"), action: () => copyName(item), icon: Copy },
|
||||
];
|
||||
}
|
||||
|
||||
function getPackageMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
|
||||
return [
|
||||
...(effectiveDatabaseType.value === "xugu" && buildXuguCompileSql({ objectType: item.type, schema: item.schema || selectedSchema.value, name: item.name }) ? [{ label: t("contextMenu.compileObject"), action: () => compileXuguObject(item), icon: Wrench }] : []),
|
||||
@@ -2964,6 +3040,7 @@ function getTypeMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
|
||||
function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
|
||||
if (item.type === "TABLE") return getTableMenuItems(item);
|
||||
if (item.type === "VIEW" || item.type === "MATERIALIZED_VIEW") return getViewMenuItems(item);
|
||||
if (item.type === "EVENT") return getEventMenuItems(item);
|
||||
if (item.type === "TYPE" || item.type === "TYPE_BODY") return getTypeMenuItems(item);
|
||||
if (isSourceOnlyObjectBrowserRow(item)) return getPackageMenuItems(item);
|
||||
return getProcFuncMenuItems(item);
|
||||
@@ -2972,7 +3049,7 @@ function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
|
||||
|
||||
<template>
|
||||
<div ref="rootRef" data-object-browser-root class="flex h-full min-h-0 min-w-0 flex-col bg-background outline-none" tabindex="0" @keydown="onObjectBrowserKeydown">
|
||||
<div class="flex h-10 shrink-0 items-center gap-2 border-b px-3">
|
||||
<div v-if="!isEventEditor" class="flex h-10 shrink-0 items-center gap-2 border-b px-3">
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
<span class="inline-flex max-w-[14rem] min-w-0 items-center rounded border border-border bg-muted/50 px-2 py-0.5 text-xs font-medium truncate" :title="selectedSchema || props.database">
|
||||
{{ selectedSchema || props.database }}
|
||||
@@ -3097,7 +3174,7 @@ function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
|
||||
<div v-else-if="filteredRows.length === 0" class="flex flex-1 items-center justify-center text-sm text-muted-foreground">
|
||||
{{ t("objects.empty") }}
|
||||
</div>
|
||||
<div v-else class="flex min-h-0 min-w-0 flex-1">
|
||||
<div v-else class="flex min-h-0 min-w-0 flex-1" :class="{ 'event-editor-layout': isEventEditor }">
|
||||
<div class="flex min-h-0 min-w-0 flex-1 flex-col">
|
||||
<div v-if="isListView" class="object-browser-table flex min-h-0 min-w-0 flex-1 flex-col overflow-x-auto overflow-y-hidden">
|
||||
<div class="grid h-7 shrink-0 items-center gap-3 border-b bg-muted/40 px-3 text-xs font-medium text-muted-foreground" :style="{ gridTemplateColumns, minWidth: `${objectGridMinWidth}px` }">
|
||||
@@ -3463,6 +3540,9 @@ function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
|
||||
<template v-else-if="sidePanelMode === 'type-info'">
|
||||
<CustomTypeInfoPanel ref="sidePanelRef" :connection="props.connection" :database="props.database" :schema="sidePanelRow?.schema || selectedSchema || props.database" :name="sidePanelRow?.name || ''" :catalog="props.catalog" @close="closeSidePanel" />
|
||||
</template>
|
||||
<template v-else-if="sidePanelMode === 'event-editor'">
|
||||
<MySqlEventEditor :connection="props.connection" :database="props.database" :schema="sidePanelRow?.schema || selectedSchema || props.database" :name="sidePanelRow?.name" :read-only="props.initialEventReadOnly" @saved="onEventSaved" @close="closeSidePanel" />
|
||||
</template>
|
||||
<!-- Source mode (views, procedures, functions, sequences) -->
|
||||
<template v-else>
|
||||
<div class="flex h-8 shrink-0 items-center gap-2 border-b bg-muted/20 px-3">
|
||||
@@ -3820,6 +3900,15 @@ function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
|
||||
container-type: inline-size;
|
||||
}
|
||||
|
||||
.event-editor-layout > :first-child {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.event-editor-layout > .object-browser-side-panel {
|
||||
width: 100% !important;
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
.table-info-action-button {
|
||||
gap: 0.25rem;
|
||||
max-width: 8rem;
|
||||
|
||||
Reference in new issue
Block a user