fix(mongodb): move sidebar index actions to indexes group
This commit is contained in:
parent
e82c1aedf0
commit
a2e1128670
|
|
@ -3577,7 +3577,7 @@ routeDangerDialog(showDropMongoIndexConfirm, () =>
|
|||
routeDangerDialog(showDropAllMongoIndexesConfirm, () =>
|
||||
dangerRequest({
|
||||
title: t("contextMenu.dropAllIndexes"),
|
||||
message: t("contextMenu.confirmDropMongoAllIndexesMessage", { name: activeNode.value.label }),
|
||||
message: t("contextMenu.confirmDropMongoAllIndexesMessage", { name: activeNode.value.tableName || activeNode.value.label }),
|
||||
detailsText: t("contextMenu.confirmDropMongoAllIndexesDetails"),
|
||||
sql: mongoDropAllIndexesPreview(activeNode.value),
|
||||
confirmLabel: t("contextMenu.dropAllIndexes"),
|
||||
|
|
@ -4248,17 +4248,13 @@ function buildSpecialSidebarMenu(context: SidebarMenuFactoryContext): boolean {
|
|||
}
|
||||
if (canDropMongoDatabase.value) {
|
||||
items.push({ label: "", separator: true });
|
||||
items.push(
|
||||
moreActionsSubmenu([
|
||||
{
|
||||
label: t("contextMenu.dropDatabase"),
|
||||
action: dropDatabase,
|
||||
icon: Trash2,
|
||||
shortcut: shortcutDelete,
|
||||
variant: "destructive" as const,
|
||||
},
|
||||
]),
|
||||
);
|
||||
items.push({
|
||||
label: t("contextMenu.dropDatabase"),
|
||||
action: dropDatabase,
|
||||
icon: Trash2,
|
||||
shortcut: shortcutDelete,
|
||||
variant: "destructive" as const,
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
@ -4292,17 +4288,9 @@ function buildSpecialSidebarMenu(context: SidebarMenuFactoryContext): boolean {
|
|||
shortcut: shortcutRename,
|
||||
});
|
||||
}
|
||||
if (canCreateMongoIndex.value || canDropAllMongoIndexes.value || canDropMongoCollection.value) {
|
||||
if (canDropMongoCollection.value) {
|
||||
items.push({ label: "", separator: true });
|
||||
if (canCreateMongoIndex.value) {
|
||||
items.push({ label: t("contextMenu.createMongoIndex"), action: openCreateMongoIndexDialog, icon: Plus });
|
||||
}
|
||||
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 });
|
||||
}
|
||||
items.push({ label: t("contextMenu.dropCollection"), action: dropMongoCollection, icon: Trash2, shortcut: shortcutDelete, variant: "destructive" as const });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
@ -4569,7 +4557,8 @@ function buildObjectGroupSidebarMenu(context: SidebarMenuFactoryContext): boolea
|
|||
if (isGroupLabel(node)) {
|
||||
const mysqlObjectTemplate = node.connectionId ? mysqlObjectTemplateForGroup(connectionStore.getConfig(node.connectionId), node) : null;
|
||||
const hasMongoCreateIndexAction = node.type === "group-indexes" && canCreateMongoIndex.value;
|
||||
const hasGroupCreateAction = (node.type === "group-tables" && canCreateTable.value) || (node.type === "group-views" && !!node.connectionId && !!node.database) || !!mysqlObjectTemplate || hasMongoCreateIndexAction;
|
||||
const hasMongoDropAllIndexesAction = node.type === "group-indexes" && canDropAllMongoIndexes.value;
|
||||
const hasGroupAction = (node.type === "group-tables" && canCreateTable.value) || (node.type === "group-views" && !!node.connectionId && !!node.database) || !!mysqlObjectTemplate || hasMongoCreateIndexAction || hasMongoDropAllIndexesAction;
|
||||
const canLoadAllObjectGroup = node.type === "group-tables" || node.type === "group-views" || node.type === "group-materialized-views";
|
||||
if (node.type === "group-tables" && canCreateTable.value) {
|
||||
items.push({ label: t("contextMenu.createTable"), action: createTable, icon: Plus });
|
||||
|
|
@ -4589,7 +4578,11 @@ function buildObjectGroupSidebarMenu(context: SidebarMenuFactoryContext): boolea
|
|||
if (hasMongoCreateIndexAction) {
|
||||
items.push({ label: t("contextMenu.createMongoIndex"), action: openCreateMongoIndexDialog, icon: Plus });
|
||||
}
|
||||
if (hasGroupCreateAction) {
|
||||
if (hasMongoDropAllIndexesAction) {
|
||||
if (hasMongoCreateIndexAction) items.push({ label: "", separator: true });
|
||||
items.push({ label: t("contextMenu.dropAllIndexes"), action: dropAllMongoIndexes, icon: Trash2, variant: "destructive" as const });
|
||||
}
|
||||
if (hasGroupAction) {
|
||||
items.push({ label: "", separator: true });
|
||||
}
|
||||
if (node.type === "group-extensions") {
|
||||
|
|
|
|||
|
|
@ -153,7 +153,7 @@ describe("MongoDB sidebar mutation runtime", () => {
|
|||
resetMongoCreateIndexForm();
|
||||
});
|
||||
|
||||
it("allows Legacy connections to create and delete MongoDB tree objects while keeping rename native-only", () => {
|
||||
it("keeps Legacy MongoDB mutations available while limiting index actions to the Indexes group", () => {
|
||||
mocks.getConfig.mockReturnValue(mongoConfig("mongodb-legacy"));
|
||||
const activeNode = shallowRef(mongoDatabaseNode());
|
||||
const feature = useSidebarDatabaseSpecificMutationRuntime({
|
||||
|
|
@ -172,10 +172,14 @@ describe("MongoDB sidebar mutation runtime", () => {
|
|||
expect(feature.canDropMongoDatabase.value).toBe(true);
|
||||
activeNode.value = mongoCollectionNode();
|
||||
expect(feature.canDropMongoCollection.value).toBe(true);
|
||||
expect(feature.canDropAllMongoIndexes.value).toBe(true);
|
||||
expect(feature.canDropAllMongoIndexes.value).toBe(false);
|
||||
expect(feature.canRenameMongoCollection.value).toBe(false);
|
||||
expect(feature.canCreateMongoIndex.value).toBe(true);
|
||||
expect(feature.canCreateMongoIndex.value).toBe(false);
|
||||
activeNode.value = mongoIndexesGroupNode();
|
||||
expect(feature.canDropAllMongoIndexes.value).toBe(true);
|
||||
expect(feature.canCreateMongoIndex.value).toBe(true);
|
||||
activeNode.value = mongoIndexesGroupNode("timeseries");
|
||||
expect(feature.canDropAllMongoIndexes.value).toBe(true);
|
||||
expect(feature.canCreateMongoIndex.value).toBe(true);
|
||||
activeNode.value = mongoIndexNode("email_1");
|
||||
expect(feature.canDropMongoIndex.value).toBe(true);
|
||||
|
|
@ -201,11 +205,20 @@ describe("MongoDB sidebar mutation runtime", () => {
|
|||
expect(feature.canDropMongoCollection.value).toBe(true);
|
||||
expect(feature.canDropAllMongoIndexes.value).toBe(false);
|
||||
activeNode.value = mongoIndexesGroupNode("view");
|
||||
expect(feature.canDropAllMongoIndexes.value).toBe(false);
|
||||
expect(feature.canCreateMongoIndex.value).toBe(false);
|
||||
activeNode.value = mongoIndexNode("email_1", "view");
|
||||
expect(feature.canDropMongoIndex.value).toBe(false);
|
||||
});
|
||||
|
||||
it("does not expose Indexes group mutations for read-only MongoDB connections", () => {
|
||||
mocks.getConfig.mockReturnValue({ ...mongoConfig(), read_only: true });
|
||||
const feature = runtime(mongoIndexesGroupNode());
|
||||
|
||||
expect(feature.canCreateMongoIndex.value).toBe(false);
|
||||
expect(feature.canDropAllMongoIndexes.value).toBe(false);
|
||||
});
|
||||
|
||||
it("creates an index from the shared sidebar dialog state", async () => {
|
||||
mocks.getConfig.mockReturnValue(mongoConfig("mongodb-legacy"));
|
||||
const node = mongoIndexesGroupNode();
|
||||
|
|
@ -253,16 +266,26 @@ describe("MongoDB sidebar mutation runtime", () => {
|
|||
expect(showCreateMongoIndexDialog.value).toBe(true);
|
||||
});
|
||||
|
||||
it("allows a collection menu to create an index before its Indexes group is expanded", async () => {
|
||||
it("does not expose index creation through a collection node", async () => {
|
||||
const node = mongoCollectionNode();
|
||||
const feature = runtime(node);
|
||||
sidebarFormTarget.value = node;
|
||||
|
||||
feature.prepareCreateMongoIndexDialog();
|
||||
mongoCreateIndexForm.value.fields[0]!.path = "email";
|
||||
expect(showCreateMongoIndexDialog.value).toBe(false);
|
||||
await feature.confirmCreateMongoIndex();
|
||||
|
||||
expect(mocks.mongoCreateIndex).toHaveBeenCalledWith("conn-1", "app", "users", '{"email":1}', undefined);
|
||||
expect(mocks.mongoCreateIndex).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not clear indexes through a collection node", async () => {
|
||||
const node = mongoCollectionNode();
|
||||
const feature = runtime(node);
|
||||
sidebarDangerTarget.value = node;
|
||||
|
||||
await feature.confirmDropAllMongoIndexes();
|
||||
|
||||
expect(mocks.mongoDropIndexes).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps the sidebar form target when the active node changes", async () => {
|
||||
|
|
@ -296,12 +319,13 @@ describe("MongoDB sidebar mutation runtime", () => {
|
|||
});
|
||||
|
||||
it("drops every removable index through the shared mutation and refreshes metadata", async () => {
|
||||
const node = mongoCollectionNode();
|
||||
const node = mongoIndexesGroupNode();
|
||||
const feature = runtime(node);
|
||||
sidebarDangerTarget.value = node;
|
||||
showDropAllMongoIndexesConfirm.value = true;
|
||||
mocks.mongoDropIndexes.mockResolvedValueOnce({ dropped_names: ["email_1", "created_at_-1"], affected_rows: 2 });
|
||||
|
||||
expect(feature.mongoDropAllIndexesPreview(node)).toBe('db.getSiblingDB("app").getCollection("users").dropIndexes()');
|
||||
await feature.confirmDropAllMongoIndexes();
|
||||
|
||||
expect(mocks.mongoDropIndexes).toHaveBeenCalledWith("conn-1", "app", "users", undefined, false);
|
||||
|
|
@ -323,7 +347,7 @@ describe("MongoDB sidebar mutation runtime", () => {
|
|||
});
|
||||
|
||||
it("reports partial index deletion after forcing a metadata refresh", async () => {
|
||||
const node = mongoCollectionNode();
|
||||
const node = mongoIndexesGroupNode();
|
||||
const feature = runtime(node);
|
||||
sidebarDangerTarget.value = node;
|
||||
mocks.mongoDropIndexes.mockResolvedValueOnce({
|
||||
|
|
@ -432,4 +456,22 @@ describe("MongoDB sidebar mutation runtime", () => {
|
|||
expect(mocks.mongoCreateIndex).not.toHaveBeenCalled();
|
||||
expect(showCreateMongoIndexDialog.value).toBe(true);
|
||||
});
|
||||
|
||||
it("does not clear indexes when production confirmation is cancelled", async () => {
|
||||
mocks.getConfig.mockReturnValue(mongoConfig(undefined, true));
|
||||
const node = mongoIndexesGroupNode();
|
||||
const feature = runtime(node);
|
||||
sidebarDangerTarget.value = node;
|
||||
showDropAllMongoIndexesConfirm.value = true;
|
||||
const pending = feature.confirmDropAllMongoIndexes();
|
||||
await Promise.resolve();
|
||||
|
||||
const { useProductionSafetyStore } = await import("@/stores/productionSafetyStore");
|
||||
useProductionSafetyStore().cancel();
|
||||
await pending;
|
||||
|
||||
expect(mocks.ensureConnected).not.toHaveBeenCalled();
|
||||
expect(mocks.mongoDropIndexes).not.toHaveBeenCalled();
|
||||
expect(showDropAllMongoIndexesConfirm.value).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -175,19 +175,26 @@ export function useSidebarDatabaseSpecificMutationRuntime(options: SidebarDataba
|
|||
|
||||
const canDropMongoIndex = computed(() => canDropMongoIndexNode(activeNode.value));
|
||||
|
||||
const canDropAllMongoIndexes = computed(() => activeNode.value.type === "mongo-collection" && canMutateMongoIndexes(activeNode.value));
|
||||
function mongoIndexCollectionName(node: TreeNode): string {
|
||||
return node.type === "group-indexes" ? node.tableName || "" : "";
|
||||
}
|
||||
|
||||
function canManageMongoIndexesNode(node: TreeNode): boolean {
|
||||
return !!mongoIndexCollectionName(node) && !!node.database && canMutateMongoIndexes(node);
|
||||
}
|
||||
|
||||
const canDropAllMongoIndexes = computed(() => canManageMongoIndexesNode(activeNode.value));
|
||||
|
||||
function mongoIndexDropPreview(node: Pick<TreeNode, "database" | "tableName">, indexName: string): string {
|
||||
return mongoDropIndexPreview(node.database || "", node.tableName || "", indexName);
|
||||
}
|
||||
|
||||
function mongoDropAllIndexesPreviewForNode(node: Pick<TreeNode, "database" | "label">): string {
|
||||
return mongoDropAllIndexesPreview(node.database || "", node.label);
|
||||
function mongoDropAllIndexesPreviewForNode(node: TreeNode): string {
|
||||
return mongoDropAllIndexesPreview(node.database || "", mongoIndexCollectionName(node));
|
||||
}
|
||||
|
||||
function canCreateMongoIndexNode(node: TreeNode): boolean {
|
||||
const collectionName = mongoIndexCollectionName(node);
|
||||
return !!collectionName && !!node.database && canMutateMongoIndexes(node);
|
||||
return canManageMongoIndexesNode(node);
|
||||
}
|
||||
|
||||
const canCreateMongoIndex = computed(() => canCreateMongoIndexNode(activeNode.value));
|
||||
|
|
@ -202,11 +209,6 @@ export function useSidebarDatabaseSpecificMutationRuntime(options: SidebarDataba
|
|||
{ deep: true },
|
||||
);
|
||||
|
||||
function mongoIndexCollectionName(node: TreeNode): string {
|
||||
if (node.type === "mongo-collection") return node.label;
|
||||
return node.type === "group-indexes" ? node.tableName || "" : "";
|
||||
}
|
||||
|
||||
function prepareCreateMongoIndexDialog() {
|
||||
const node = activeNode.value;
|
||||
if (!canCreateMongoIndexNode(node) || !node.connectionId || !node.database) return;
|
||||
|
|
@ -505,8 +507,8 @@ export function useSidebarDatabaseSpecificMutationRuntime(options: SidebarDataba
|
|||
const node = sidebarDangerTarget.value ?? activeNode.value;
|
||||
const connectionId = node.connectionId;
|
||||
const database = node.database;
|
||||
if (node.type !== "mongo-collection" || !canMutateMongoIndexes(node) || !connectionId || !database) return;
|
||||
const collectionName = node.label;
|
||||
if (!canManageMongoIndexesNode(node) || !connectionId || !database) return;
|
||||
const collectionName = mongoIndexCollectionName(node);
|
||||
await runMongoSidebarMutation({
|
||||
connection: connectionStore.getConfig(connectionId),
|
||||
database,
|
||||
|
|
|
|||
|
|
@ -127,8 +127,14 @@ test("mongo sidebar mutations share the production-gated runMongoSidebarMutation
|
|||
const dropDatabaseBody = functionBody(hostSource, "confirmDropDatabase");
|
||||
assert.match(dropDatabaseBody, /confirmDropMongoDatabase/, "host drop-database confirm should delegate mongo to the mutation runtime");
|
||||
assert.ok(!dropDatabaseBody.includes("api.mongoDropDatabase"), "host drop-database confirm should not call mongo APIs directly");
|
||||
const mongoCollectionMenuBody = functionBody(hostSource, "buildSpecialSidebarMenu");
|
||||
assert.match(mongoCollectionMenuBody, /action:\s*dropAllMongoIndexes/, "collection context menu must keep the drop-all-indexes entrypoint");
|
||||
const mongoSpecialMenuBody = functionBody(hostSource, "buildSpecialSidebarMenu");
|
||||
const mongoIndexGroupMenuBody = functionBody(hostSource, "buildObjectGroupSidebarMenu");
|
||||
assert.match(mongoSpecialMenuBody, /items\.push\(\{\s*label: t\("contextMenu\.dropDatabase"\),\s*action: dropDatabase/, "MongoDB database deletion must be a top-level menu action");
|
||||
assert.doesNotMatch(mongoSpecialMenuBody, /moreActionsSubmenu\(\[\s*\{\s*label: t\("contextMenu\.dropDatabase"\)/, "MongoDB database deletion must not be nested under More");
|
||||
assert.doesNotMatch(mongoSpecialMenuBody, /action:\s*openCreateMongoIndexDialog/, "collection context menu must not expose index creation");
|
||||
assert.doesNotMatch(mongoSpecialMenuBody, /action:\s*dropAllMongoIndexes/, "collection context menu must not expose the drop-all-indexes entrypoint");
|
||||
assert.match(mongoIndexGroupMenuBody, /action:\s*openCreateMongoIndexDialog/, "Indexes group context menu must expose index creation");
|
||||
assert.match(mongoIndexGroupMenuBody, /action:\s*dropAllMongoIndexes/, "Indexes group context menu must expose the drop-all-indexes entrypoint");
|
||||
const batchDropBody = functionBody(hostSource, "confirmBatchDrop");
|
||||
assert.match(batchDropBody, /catch\s*\([^)]*\)\s*\{[\s\S]*?failedCount \+= groupTargets\.length/, "cross-collection index deletion must retain earlier successes after a group failure");
|
||||
assert.match(batchDropBody, /droppedCount === 0[\s\S]*?throw firstGroupError/, "an entirely failed cross-collection request must preserve its original error");
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import { test } from "vitest";
|
||||
import { afterAll, beforeAll, test, vi } from "vitest";
|
||||
import assert from "node:assert/strict";
|
||||
import { copyNameForTreeNode, isDocumentBrowserTreeNode, objectSourceKindForTreeNode, shouldRunTreeNodeRowAction, sidebarSelectionCopyAction, treeNodeRowAction, treeNodeRowDoubleClickAction } from "../../apps/desktop/src/lib/sidebar/treeNodeClick.ts";
|
||||
|
||||
beforeAll(() => vi.stubGlobal("navigator", { platform: "Linux x86_64" }));
|
||||
afterAll(() => vi.unstubAllGlobals());
|
||||
|
||||
test("table and view rows open data without toggling structure groups", () => {
|
||||
assert.equal(treeNodeRowAction("table", true), "open-data");
|
||||
assert.equal(treeNodeRowAction("view", true), "open-data");
|
||||
|
|
|
|||
Loading…
Reference in New Issue