diff --git a/apps/desktop/src/components/editor/DangerConfirmDialog.vue b/apps/desktop/src/components/editor/DangerConfirmDialog.vue
index 094199ef8..cab88ae3c 100644
--- a/apps/desktop/src/components/editor/DangerConfirmDialog.vue
+++ b/apps/desktop/src/components/editor/DangerConfirmDialog.vue
@@ -21,6 +21,7 @@ const props = withDefaults(
title?: string;
message?: string;
details?: string;
+ detailsText?: string;
confirmLabel?: string;
showSuppressToggle?: boolean;
suppressToggleLabel?: string;
@@ -32,6 +33,7 @@ const props = withDefaults(
title: "",
message: "",
details: "",
+ detailsText: "",
confirmLabel: "",
showSuppressToggle: false,
suppressToggleLabel: "",
@@ -73,6 +75,7 @@ function onConfirm() {
{{ message || t("dangerDialog.message") }}
+
{{ detailsText }}
+
+
+
-
{{ index.name }}
-
- PK
- UNIQUE
- {{ index.index_type }}
-
-
- {{ index.columns.join(", ") }}
+
+
+
{{ index.name }}
+
+ PK
+ UNIQUE
+ {{ index.index_type }}
+
+
+ {{ index.columns.join(", ") }}
+
+
+
@@ -10143,6 +10225,27 @@ const gridContextMenuItems = computed
(() => {
:confirm-label="pendingDeleteRowIds.length > 1 ? t('grid.deleteRows', { count: pendingDeleteRowIds.length }) : t('grid.deleteRow')"
@confirm="confirmDeleteRow"
/>
+
+
diff --git a/apps/desktop/src/components/sidebar/TreeItem.vue b/apps/desktop/src/components/sidebar/TreeItem.vue
index d62d63055..5cfa10fba 100644
--- a/apps/desktop/src/components/sidebar/TreeItem.vue
+++ b/apps/desktop/src/components/sidebar/TreeItem.vue
@@ -1528,6 +1528,10 @@ const showDropDatabaseConfirm = ref(false);
const dropDatabaseLoading = ref(false);
const showDropMongoCollectionConfirm = ref(false);
const dropMongoCollectionLoading = ref(false);
+const showDropMongoIndexConfirm = ref(false);
+const dropMongoIndexLoading = ref(false);
+const showDropAllMongoIndexesConfirm = ref(false);
+const dropAllMongoIndexesLoading = ref(false);
const showFlushRedisDbConfirm = ref(false);
const showCreateSchemaDialog = ref(false);
const createSchemaName = ref("");
@@ -1756,6 +1760,7 @@ function canDropTreeNode(node: TreeNode): boolean {
if (node.type === "view" || node.type === "materialized_view" || node.type === "procedure" || node.type === "function") {
return !!node.connectionId && !!node.database && !!dropObjectSqlOptionsForNode(node);
}
+ if (canDropMongoIndexNode(node)) return true;
return canDropTableChildObjectNode(node);
}
@@ -1770,16 +1775,41 @@ function selectedBatchDropTargets(): TreeNode[] {
return selected;
}
+function selectedBatchMongoIndexTargets(): TreeNode[] {
+ const targets = selectedBatchDropTargets();
+ return targets.length > 1 && targets.every((node) => canDropMongoIndexNode(node)) ? targets : [];
+}
+
+function selectedBatchIndexTableName(targets: TreeNode[]): string | null {
+ const first = targets[0];
+ if (!first) return null;
+ const table = first.tableName || first.label;
+ return table && targets.every((node) => (node.tableName || node.label) === table) ? table : null;
+}
+
function batchDropMenuLabel(): string {
- return t("contextMenu.batchDrop", { count: selectedBatchDropTargets().length });
+ const targets = selectedBatchDropTargets();
+ if (targets.length > 1 && targets.every((node) => node.type === "index")) {
+ return t("contextMenu.batchDropIndexes", { count: targets.length });
+ }
+ return t("contextMenu.batchDrop", { count: targets.length });
}
function batchDropConfirmTitle(): string {
- return t("contextMenu.confirmBatchDropTitle", { count: selectedBatchDropTargets().length });
+ const targets = selectedBatchDropTargets();
+ if (targets.length > 1 && targets.every((node) => node.type === "index")) {
+ return t("contextMenu.confirmDropIndexTitle");
+ }
+ return t("contextMenu.confirmBatchDropTitle", { count: targets.length });
}
function batchDropConfirmMessage(): string {
- return t("contextMenu.confirmBatchDropMessage", { count: selectedBatchDropTargets().length });
+ const targets = selectedBatchDropTargets();
+ const table = selectedBatchIndexTableName(targets);
+ if (targets.length > 1 && targets.every((node) => node.type === "index") && table) {
+ return t("contextMenu.confirmDropBatchIndexesMessage", { count: targets.length, table });
+ }
+ return t("contextMenu.confirmBatchDropMessage", { count: targets.length });
}
async function dropSqlForTreeNode(node: TreeNode): Promise {
@@ -1792,6 +1822,9 @@ async function dropSqlForTreeNode(node: TreeNode): Promise {
}
const objectOptions = dropObjectSqlOptionsForNode(node);
if (objectOptions) return buildDropObjectSql(objectOptions);
+ if (canDropMongoIndexNode(node)) {
+ return `db.getCollection("${(node.tableName || "").replace(/\\/g, "\\\\").replace(/"/g, '\\"')}").dropIndex(${JSON.stringify(mongoIndexNameForNode(node))})`;
+ }
const childOptions = dropTableChildObjectSqlOptionsForNode(node);
if (childOptions && canDropTableChildObjectNode(node)) return buildDropTableChildObjectSql(childOptions);
return null;
@@ -1799,6 +1832,11 @@ async function dropSqlForTreeNode(node: TreeNode): Promise {
async function refreshBatchDropPreviewSql() {
const targets = selectedBatchDropTargets();
+ const mongoIndexTargets = selectedBatchMongoIndexTargets();
+ if (mongoIndexTargets.length) {
+ batchDropPreviewSql.value = mongoIndexTargets.map((target) => mongoIndexDropPreview(target, mongoIndexNameForNode(target))).join("\n");
+ return;
+ }
const statements: string[] = [];
for (const target of targets) {
const sql = await dropSqlForTreeNode(target);
@@ -1832,6 +1870,10 @@ function requestDropSelectedNode(): boolean {
requestDropObject();
return true;
}
+ if (canDropMongoIndex.value) {
+ dropMongoIndex();
+ return true;
+ }
if (canDropTableChildObject.value) {
requestDropTableChildObject();
return true;
@@ -1974,6 +2016,32 @@ async function confirmBatchDrop() {
const targets = selectedBatchDropTargets();
if (!targets.length) return;
try {
+ const mongoIndexTargets = selectedBatchMongoIndexTargets();
+ if (mongoIndexTargets.length) {
+ const grouped = new Map();
+ for (const target of mongoIndexTargets) {
+ const key = `${target.connectionId}:${target.database}:${target.tableName || ""}`;
+ const list = grouped.get(key) ?? [];
+ list.push(target);
+ grouped.set(key, list);
+ }
+ let droppedCount = 0;
+ for (const groupTargets of grouped.values()) {
+ const first = groupTargets[0];
+ if (!first?.connectionId || !first.database || !first.tableName) continue;
+ await connectionStore.ensureConnected(first.connectionId);
+ const names = groupTargets.map((target) => mongoIndexNameForNode(target));
+ const result = await api.mongoDropIndexes(first.connectionId, first.database, first.tableName, JSON.stringify(names.length === 1 ? names[0] : names), false);
+ const dropped = new Set(result.dropped_names);
+ droppedCount += result.dropped_names.length;
+ for (const target of groupTargets) {
+ if (dropped.has(mongoIndexNameForNode(target))) connectionStore.removeTreeNode(target.id);
+ }
+ }
+ toast(t("contextMenu.batchDropSuccess", { count: droppedCount }), 3000);
+ showBatchDropConfirm.value = false;
+ return;
+ }
for (const target of targets) {
if (!target.connectionId || !target.database) continue;
await connectionStore.ensureConnected(target.connectionId);
@@ -2041,6 +2109,32 @@ const canDropMongoCollection = computed(() => {
return props.node.type === "mongo-collection" && !!props.node.database && config?.driver_profile !== "mongodb-legacy";
});
+function mongoIndexNameForNode(node: TreeNode): string {
+ if (node.type !== "index") return "";
+ return node.meta && "name" in node.meta ? node.meta.name : node.label.replace(/\s+\(.+\)$/, "");
+}
+
+function canDropMongoIndexNode(node: TreeNode): boolean {
+ if (node.type !== "index" || !node.connectionId || !node.database || !node.tableName) return false;
+ const config = connectionStore.getConfig(node.connectionId);
+ return config?.db_type === "mongodb" && config.driver_profile !== "mongodb-legacy" && mongoIndexNameForNode(node) !== "_id_";
+}
+
+const canDropMongoIndex = computed(() => canDropMongoIndexNode(props.node));
+
+function mongoIndexDropPreview(node: Pick, indexName: string): string {
+ return `db.getCollection(${JSON.stringify(node.tableName || "")}).dropIndex(${JSON.stringify(indexName)})`;
+}
+
+const canDropAllMongoIndexes = computed(() => {
+ const config = props.node.connectionId ? connectionStore.getConfig(props.node.connectionId) : undefined;
+ return props.node.type === "mongo-collection" && !!props.node.database && config?.db_type === "mongodb" && config.driver_profile !== "mongodb-legacy";
+});
+
+function mongoDropAllIndexesPreview(node: Pick): string {
+ return `db.getCollection(${JSON.stringify(node.label)}).dropIndexes()`;
+}
+
const canCreateSchema = computed(() => {
const config = props.node.connectionId ? connectionStore.getConfig(props.node.connectionId) : undefined;
return props.node.type === "database" && usesTreeSchemaMode(effectiveDatabaseTypeForConnection(config)) && !connectionUsesDatabaseObjectTreeMode(config);
@@ -2417,6 +2511,16 @@ function dropMongoCollection() {
showDropMongoCollectionConfirm.value = true;
}
+function dropMongoIndex() {
+ dropMongoIndexLoading.value = false;
+ showDropMongoIndexConfirm.value = true;
+}
+
+function dropAllMongoIndexes() {
+ dropAllMongoIndexesLoading.value = false;
+ showDropAllMongoIndexesConfirm.value = true;
+}
+
function flushRedisDb() {
showFlushRedisDbConfirm.value = true;
}
@@ -2486,6 +2590,53 @@ async function confirmDropMongoCollection() {
}
}
+function mongoIndexesGroupNodeId(node: Pick): string | null {
+ if (!node.connectionId || !node.database) return null;
+ const tableName = node.tableName || node.label;
+ return node.schema ? `${node.connectionId}:${node.database}:${node.schema}:${tableName}:__indexes` : `${node.connectionId}:${node.database}:${tableName}:__indexes`;
+}
+
+async function refreshMongoIndexTree(node: Pick) {
+ const nodeId = mongoIndexesGroupNodeId(node);
+ if (!node.connectionId || !node.database || !nodeId) return;
+ await connectionStore.loadIndexes(node.connectionId, node.database, node.tableName || node.label, node.schema, nodeId);
+}
+
+async function confirmDropMongoIndex() {
+ const node = props.node;
+ if (!canDropMongoIndexNode(node) || !node.connectionId || !node.database || !node.tableName || dropMongoIndexLoading.value) return;
+ dropMongoIndexLoading.value = true;
+ try {
+ await connectionStore.ensureConnected(node.connectionId);
+ const indexName = mongoIndexNameForNode(node);
+ await api.mongoDropIndexes(node.connectionId, node.database, node.tableName, JSON.stringify(indexName), true);
+ toast(t("contextMenu.dropTableChildObjectSuccess", { name: indexName }), 3000);
+ showDropMongoIndexConfirm.value = false;
+ await refreshMongoIndexTree(node);
+ } catch (e: any) {
+ toast(t("contextMenu.tableOperationFailed", { message: e?.message || String(e) }), 5000);
+ } finally {
+ dropMongoIndexLoading.value = false;
+ }
+}
+
+async function confirmDropAllMongoIndexes() {
+ const node = props.node;
+ if (node.type !== "mongo-collection" || !node.connectionId || !node.database || dropAllMongoIndexesLoading.value) return;
+ dropAllMongoIndexesLoading.value = true;
+ try {
+ await connectionStore.ensureConnected(node.connectionId);
+ const result = await api.mongoDropIndexes(node.connectionId, node.database, node.label, undefined, false);
+ toast(t("contextMenu.dropAllIndexesSuccess", { count: result.dropped_names.length, name: node.label }), 3000);
+ showDropAllMongoIndexesConfirm.value = false;
+ await refreshMongoIndexTree(node);
+ } catch (e: any) {
+ toast(t("contextMenu.tableOperationFailed", { message: e?.message || String(e) }), 5000);
+ } finally {
+ dropAllMongoIndexesLoading.value = false;
+ }
+}
+
function openCreateSchemaDialog() {
createSchemaName.value = "";
showCreateSchemaDialog.value = true;
@@ -3958,9 +4109,14 @@ function treeItemMenuItems(): ContextMenuItem[] {
items.push({ label: "", separator: true });
items.push({ label: t("contextMenu.viewData"), action: toggle, icon: TableProperties });
items.push({ label: t("contextMenu.newQuery"), action: newQuery, icon: TerminalSquare });
- if (canDropMongoCollection.value) {
+ if (canDropAllMongoIndexes.value || canDropMongoCollection.value) {
items.push({ label: "", separator: true });
- items.push({ label: t("contextMenu.dropCollection"), action: dropMongoCollection, icon: Trash2, shortcut: shortcutDelete, variant: "destructive" as const });
+ if (canDropAllMongoIndexes.value) {
+ items.push({ label: t("contextMenu.dropAllIndexes"), action: dropAllMongoIndexes, icon: Trash2, variant: "destructive" as const });
+ }
+ if (canDropMongoCollection.value) {
+ items.push({ label: t("contextMenu.dropCollection"), action: dropMongoCollection, icon: Trash2, shortcut: shortcutDelete, variant: "destructive" as const });
+ }
}
return items;
}
@@ -4108,7 +4264,16 @@ function treeItemMenuItems(): ContextMenuItem[] {
if (node.type === "index" || node.type === "fkey" || node.type === "trigger") {
items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value });
- if (canDropTableChildObject.value) {
+ if (node.type === "index" && canDropMongoIndex.value) {
+ items.push({ label: "", separator: true });
+ items.push({
+ label: deleteMenuLabel(t("contextMenu.dropIndex")),
+ action: deleteMenuAction(dropMongoIndex),
+ icon: Trash2,
+ shortcut: shortcutDelete,
+ variant: "destructive" as const,
+ });
+ } else if (canDropTableChildObject.value) {
items.push({ label: "", separator: true });
items.push({
label: deleteMenuLabel(dropTableChildObjectMenuLabel()),
@@ -4625,6 +4790,29 @@ function treeItemMenuItems(): ContextMenuItem[] {
@confirm="confirmDropMongoCollection"
/>
+
+
+
+