Skip to content

Commit

Permalink
feat(ui): handle bulk delete in one transaction - WF-148
Browse files Browse the repository at this point in the history
  • Loading branch information
madeindjs committed Dec 19, 2024
1 parent fec6320 commit 21ecc92
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 27 deletions.
9 changes: 9 additions & 0 deletions src/ui/src/builder/builderManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,14 @@ export function generateBuilderManager() {
return state.value.selection.some((s) => s.componentId === componentId);
};

const removeSelectedComponentId = (componentId: string) => {
const newSelection = state.value.selection.filter(
(c) => c.componentId === componentId,
);
if (newSelection.length === state.value.selection.length) return;
state.value.selection = newSelection;
};

/** @deprecated use `selectionStatus` instead */
const isSelectionActive = () => {
return state.value.selection.length > 0;
Expand Down Expand Up @@ -358,6 +366,7 @@ export function generateBuilderManager() {
firstSelectedId,
firstSelectedItem,
isSelectionActive,
removeSelectedComponentId,
setSelection,
appendSelection,
getSelection,
Expand Down
13 changes: 4 additions & 9 deletions src/ui/src/builder/settings/BuilderSettingsActions.vue
Original file line number Diff line number Diff line change
Expand Up @@ -193,18 +193,17 @@ const {
isPasteAllowed,
isDeleteAllowed,
getEnabledMoves,
removeComponentSubtree,
removeComponentsSubtree,
goToParent,
} = useComponentActions(wf, ssbm);
function deleteSelectedComponents() {
if (!shortcutsInfo.value.isDeleteEnabled) return;
// TODO: do one transaction
for (const { componentId } of ssbm.getSelection()) {
removeComponentSubtree(componentId);
}
ssbm.setSelection(null);
const componentIds = ssbm.getSelection().map((c) => c.componentId);
if (componentIds.length === 0) return;
removeComponentsSubtree(...componentIds);
}
const selectedId = ssbm.firstSelectedId;
Expand Down Expand Up @@ -239,10 +238,6 @@ const validChildrenTypes = computed(() => {
return result;
});
function clearSelection() {
ssbm.setSelection(null);
}
function addComponent(event: Event) {
const definitionName = (event.target as HTMLInputElement).value;
const matchingTypes = Object.entries(validChildrenTypes.value).filter(
Expand Down
63 changes: 63 additions & 0 deletions src/ui/src/builder/useComponentAction.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { useComponentActions } from "./useComponentActions";
import { Core } from "@/writerTypes.js";
import { generateBuilderManager } from "./builderManager";

describe(useComponentActions.name, () => {
let core: Core;
let ssbm: ReturnType<typeof generateBuilderManager>;

beforeEach(() => {
ssbm = generateBuilderManager();

// @ts-expect-error we mock only necessary functions
core = {
getComponentById: vi
.fn()
.mockImplementation((id: string) => ({ id })),
getComponents: vi.fn().mockReturnValue([]),
deleteComponent: vi.fn(),
sendComponentUpdate: vi.fn(),
};
});

describe("removeComponentSubtree", () => {
it("should delete the component in a transaction", () => {
const { removeComponentSubtree } = useComponentActions(core, ssbm);

const openMutationTransaction = vi.spyOn(
ssbm,
"openMutationTransaction",
);

removeComponentSubtree("1");

expect(core.sendComponentUpdate).toHaveBeenCalledOnce();
expect(openMutationTransaction).toHaveBeenNthCalledWith(
1,
"delete-1",
"Delete",
);
});
});

describe("removeComponentsSubtree", () => {
it("should delete the component in a transaction", () => {
const { removeComponentsSubtree } = useComponentActions(core, ssbm);

const openMutationTransaction = vi.spyOn(
ssbm,
"openMutationTransaction",
);

removeComponentsSubtree("1", "2", "3");

expect(core.sendComponentUpdate).toHaveBeenCalledOnce();
expect(openMutationTransaction).toHaveBeenNthCalledWith(
1,
"delete-1,2,3",
"Delete",
);
});
});
});
54 changes: 36 additions & 18 deletions src/ui/src/builder/useComponentActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,34 +251,51 @@ export function useComponentActions(wf: Core, ssbm: BuilderManager) {
}

/**
* Removes a component and all its descendents
* Removes multiples components at the same time (including their descendents) inside an unique transaction.
*
* @param componentId Id of the target component
*/
function removeComponentSubtree(componentId: Component["id"]): void {
const component = wf.getComponentById(componentId);
if (!component) return;
const parentId = wf.getComponentById(componentId).parentId;
function removeComponentsSubtree(...componentIds: Component["id"][]): void {
const components = componentIds
.map((i) => wf.getComponentById(i))
.filter(Boolean);
if (components.length === 0) return;

const transactionId = `delete-${componentId}`;
const transactionId = `delete-${components.map((c) => c.id).join(",")}`;
ssbm.openMutationTransaction(transactionId, `Delete`);
if (parentId) {
repositionHigherSiblings(component.id, -1);

for (const component of components) {
if (wf.getComponentById(component.id).parentId) {
repositionHigherSiblings(component.id, -1);
}
const dependencies = getNodeDependencies(component.id);
for (const c of dependencies) {
ssbm.registerPreMutation(c);
c.outs = [
...c.outs.filter((out) => out.toNodeId !== component.id),
];
}
const subtree = getFlatComponentSubtree(component.id);
for (const c of subtree) {
ssbm.registerPreMutation(c);
wf.deleteComponent(c.id);
ssbm.removeSelectedComponentId(c.id);
}
}
const dependencies = getNodeDependencies(componentId);
dependencies.map((c) => {
ssbm.registerPreMutation(c);
c.outs = [...c.outs.filter((out) => out.toNodeId !== componentId)];
});
const subtree = getFlatComponentSubtree(componentId);
subtree.map((c) => {
ssbm.registerPreMutation(c);
wf.deleteComponent(c.id);
});

ssbm.closeMutationTransaction(transactionId);
wf.sendComponentUpdate();
}

/**
* Removes a component and all its descendents
*
* @param componentId Id of the target component
*/
function removeComponentSubtree(componentId: Component["id"]): void {
return removeComponentsSubtree(componentId);
}

/**
* Whether a target component is the root
*/
Expand Down Expand Up @@ -941,6 +958,7 @@ export function useComponentActions(wf: Core, ssbm: BuilderManager) {
pasteComponent,
createAndInsertComponent,
removeComponentSubtree,
removeComponentsSubtree,
isPasteAllowed,
undo,
redo,
Expand Down

0 comments on commit 21ecc92

Please sign in to comment.