diff --git a/playwright-test/07_block_context_menu.spec.ts b/playwright-test/07_block_context_menu.spec.ts index c2c3b85a5..ea7a32f0b 100644 --- a/playwright-test/07_block_context_menu.spec.ts +++ b/playwright-test/07_block_context_menu.spec.ts @@ -35,8 +35,8 @@ test.describe("Block context menu", () => { timeout: 20000, }); - // Right click on `SINE` block - await window.locator("h2", { hasText: "SINE" }).click({ + // Right click on `RAND` block + await window.locator("h2", { hasText: "RAND" }).click({ button: "right", }); @@ -47,6 +47,15 @@ test.describe("Block context menu", () => { }); test("Should open block edit menu upon clicking Edit block", async () => { + // Take a screenshot + await window.screenshot({ + fullPage: true, + path: "test-results/before-right-click-block.jpeg", + }); + + // CI problem if not center due to multi-layered context menu + await window.locator("button[title='fit view']").click(); + // Click on Edit block button from context menu await window.getByTestId(Selectors.contextEditBlockBtn).click(); @@ -55,16 +64,16 @@ test.describe("Block context menu", () => { `[data-testid="${Selectors.blockEditParam}"]`, ); - // Expect 5 parameters for SINE block - expect(params).toHaveLength(5); + // Expect 5 parameters for RAND block + expect(params).toHaveLength(4); // Close the block edit menu await window.getByTestId(Selectors.blockEditMenuCloseBtn).click(); }); test("Should open block info modal", async () => { - // Right click on `SINE` block - await window.locator("h2", { hasText: "SINE" }).click({ + // Right click on `RAND` block + await window.locator("h2", { hasText: "RAND" }).click({ button: "right", }); @@ -87,16 +96,16 @@ test.describe("Block context menu", () => { }); test("Should delete a block", async () => { - // Right click on `SINE` block - await window.locator("h2", { hasText: "SINE" }).click({ + // Right click on `RAND` block + await window.locator("h2", { hasText: "RAND" }).click({ button: "right", }); // Click on Delete block button from context menu await window.getByTestId(Selectors.contextDeleteBlockBtn).click(); - // Expect SINE block to disappear from DOM - await expect(window.locator("h2", { hasText: "SINE" })).toBeHidden(); + // Expect RAND block to disappear from DOM + await expect(window.locator("h2", { hasText: "RAND" })).toBeHidden(); }); test("Should duplicate a block", async () => { diff --git a/src/renderer/hooks/useTestSequencerProject.ts b/src/renderer/hooks/useTestSequencerProject.ts index 3ce2a3bcc..e33605743 100644 --- a/src/renderer/hooks/useTestSequencerProject.ts +++ b/src/renderer/hooks/useTestSequencerProject.ts @@ -17,8 +17,16 @@ import { toastResultPromise } from "../utils/report-error"; function usePrepareStateManager(): StateManager { const { elems, project } = useDisplayedSequenceState(); - const { addNewSequence, removeSequence, sequences } = useSequencerState(); - return { elems, project, addNewSequence, removeSequence, sequences }; + const { addNewSequence, removeSequence, sequences, setSequences } = + useSequencerState(); + return { + elems, + project, + addNewSequence, + removeSequence, + sequences, + setSequences, + }; } export function useSaveSequence() { diff --git a/src/renderer/routes/test_sequencer_panel/components/data-table/SequenceTable.tsx b/src/renderer/routes/test_sequencer_panel/components/data-table/SequenceTable.tsx index 7ddd1466c..a07e7abf9 100644 --- a/src/renderer/routes/test_sequencer_panel/components/data-table/SequenceTable.tsx +++ b/src/renderer/routes/test_sequencer_panel/components/data-table/SequenceTable.tsx @@ -2,6 +2,7 @@ import { ContextMenu, ContextMenuContent, ContextMenuItem, + ContextMenuSeparator, ContextMenuTrigger, } from "@/renderer/components/ui/context-menu"; import { @@ -40,7 +41,7 @@ import { import { parseInt, map } from "lodash"; import { ChevronDownIcon, ChevronUpIcon, TrashIcon } from "lucide-react"; import LockableButton from "@/renderer/routes/test_sequencer_panel/components/lockable/LockedButtons"; -import { useState } from "react"; +import { useRef, useState } from "react"; import { DraggableRowSequence } from "@/renderer/routes/test_sequencer_panel/components/dnd/DraggableRowSequence"; import { getCompletionTime, getSuccessRate, mapStatusToDisplay } from "./utils"; import useWithPermission from "@/renderer/hooks/useWithPermission"; @@ -48,6 +49,9 @@ import { useImportSequences } from "@/renderer/hooks/useTestSequencerProject"; import { useSequencerModalStore } from "@/renderer/stores/modal"; import { useSequencerStore } from "@/renderer/stores/sequencer"; import { useShallow } from "zustand/react/shallow"; +import { RenameModal } from "../modals/RenameModal"; +import { toast } from "sonner"; +import { produce } from "immer"; export function SequenceTable() { const { project, isLocked } = useDisplayedSequenceState(); @@ -257,8 +261,78 @@ export function SequenceTable() { ); }; + const onRenameSequence = (idx: number) => { + const sequence = sequences[idx]; + renameForIdx.current = idx; + setRenameTarget(sequence.project.name); + setInitialName(sequence.project.name); + setIsRenameNameModalOpen(true); + }; + + const handleRenameSequence = (newName: string) => { + // make sure the new name is unique + if (sequences.some((seq) => seq.project.name === newName)) { + toast.error("Sequence name must be unique"); + return; + } + if (newName === "") { + toast.error("Sequence name cannot be empty"); + return; + } + setSequences( + produce(sequences, (draft) => { + const seq = draft[renameForIdx.current]; + seq.project.name = newName; + seq.testSequenceUnsaved = true; + }), + ); + setIsRenameDescModalOpen(false); + + setIsRenameNameModalOpen(false); + }; + const [isRenameNameModalOpen, setIsRenameNameModalOpen] = useState(false); + const [isRenameDescModalOpen, setIsRenameDescModalOpen] = useState(false); + const [renameTarget, setRenameTarget] = useState(""); + const [initialName, setInitialName] = useState(""); + const renameForIdx = useRef(-1); + + const onRenameDescription = (idx: number) => { + const sequence = sequences[idx]; + renameForIdx.current = idx; + setRenameTarget(sequence.project.name); + setInitialName(sequence.project.description); + setIsRenameDescModalOpen(true); + }; + + const handleRenameDescription = (newDescription: string) => { + setSequences( + produce(sequences, (draft) => { + const seq = draft[renameForIdx.current]; + seq.project.description = newDescription; + seq.testSequenceUnsaved = true; + }), + ); + setIsRenameDescModalOpen(false); + }; + return (