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 (
+ +
{isAdmin() ? ( + { + onRenameSequence(row.index); + }} + > + Rename sequence + + { + onRenameDescription(row.index); + }} + > + Edit description + + { onRemoveSequence([row.index]); diff --git a/src/renderer/routes/test_sequencer_panel/components/data-table/TestTable.tsx b/src/renderer/routes/test_sequencer_panel/components/data-table/TestTable.tsx index 6ffec7d78..8b7055b22 100644 --- a/src/renderer/routes/test_sequencer_panel/components/data-table/TestTable.tsx +++ b/src/renderer/routes/test_sequencer_panel/components/data-table/TestTable.tsx @@ -2,6 +2,7 @@ import { ContextMenu, ContextMenuContent, ContextMenuItem, + ContextMenuSeparator, ContextMenuTrigger, } from "@/renderer/components/ui/context-menu"; import { @@ -549,48 +550,51 @@ export function TestTable() { > Add Conditional - onRemoveTest([parseInt(row.id)])} - > - {row.original.type === "test" - ? "Remove Test" - : "Remove Conditional"} - {row.original.type === "test" && ( <> + { - openRenameTestModal(row.original.id); + setOpenPyTestFileModal(true); + setTestToDisplay(row.original as Test); }} > - Rename Test + Consult Code + { - onClickWriteMinMax(parseInt(row.id)); + openRenameTestModal(row.original.id); }} > - Edit Expected Value + Rename Test { - setOpenPyTestFileModal(true); - setTestToDisplay(row.original as Test); + toggleExportToCloud(row.original.id); }} > - Consult Code + {row.original.exportToCloud + ? "Disable export to Cloud" + : "Enable export to Cloud"} { - toggleExportToCloud(row.original.id); + onClickWriteMinMax(parseInt(row.id)); }} > - {row.original.exportToCloud - ? "Disable export to Cloud" - : "Enable export to Cloud"} + Edit Expected Value )} + + onRemoveTest([parseInt(row.id)])} + > + {row.original.type === "test" + ? "Remove Test" + : "Remove Conditional"} + )) diff --git a/src/renderer/routes/test_sequencer_panel/components/dnd/DraggableRowSequence.tsx b/src/renderer/routes/test_sequencer_panel/components/dnd/DraggableRowSequence.tsx index 875ff230c..3691de93d 100644 --- a/src/renderer/routes/test_sequencer_panel/components/dnd/DraggableRowSequence.tsx +++ b/src/renderer/routes/test_sequencer_panel/components/dnd/DraggableRowSequence.tsx @@ -13,6 +13,7 @@ import { TestSequenceContainer } from "@/renderer/types/test-sequencer"; export const DraggableRowSequence = ({ isSelected, row, + ...props }: { isSelected: boolean; row: Row; @@ -80,6 +81,7 @@ export const DraggableRowSequence = ({ className={"relative" + (isSelected ? " bg-primary-foreground" : "")} onClick={() => handleDisplaySequence(row.index)} ref={drag} + {...props} > {/* capture drag on above */}
diff --git a/src/renderer/routes/test_sequencer_panel/components/modals/RenameModal.tsx b/src/renderer/routes/test_sequencer_panel/components/modals/RenameModal.tsx new file mode 100644 index 000000000..27aff0320 --- /dev/null +++ b/src/renderer/routes/test_sequencer_panel/components/modals/RenameModal.tsx @@ -0,0 +1,42 @@ +import { Button } from "@/renderer/components/ui/button"; +import { Dialog, DialogContent } from "@/renderer/components/ui/dialog"; +import { Input } from "@/renderer/components/ui/input"; +import { useEffect, useState } from "react"; + +export const RenameModal = ({ + title, + isModalOpen, + setModalOpen, + initialName, + handleSubmit, + target, +}: { + title: string; + isModalOpen: boolean; + setModalOpen: (value: boolean) => void; + initialName: string; + handleSubmit: (newName: string) => void; + target: string; +}) => { + const [newName, setNewName] = useState(initialName); + useEffect(() => { + setNewName(initialName); + }, [initialName]); + + return ( + + +

{title}

+
+

{target}

+
+ setNewName(e.target.value)} + /> + +
+
+ ); +}; diff --git a/src/renderer/routes/test_sequencer_panel/utils/SequenceHandler.ts b/src/renderer/routes/test_sequencer_panel/utils/SequenceHandler.ts index 8f827b39f..bb539dabe 100644 --- a/src/renderer/routes/test_sequencer_panel/utils/SequenceHandler.ts +++ b/src/renderer/routes/test_sequencer_panel/utils/SequenceHandler.ts @@ -28,6 +28,7 @@ export type StateManager = { removeSequence: (name: string) => void; project: TestSequencerProject | null; sequences: TestSequenceContainer[]; + setSequences: (sequences: TestSequenceContainer[]) => void; }; export async function createSequence( @@ -82,6 +83,15 @@ export async function saveSequence( if (isSync.isErr()) { return isSync; } + // Set the sequence as saved + stateManager.setSequences( + stateManager.sequences.map((seq) => { + if (seq.project.name === sequence.name) { + return { ...seq, testSequenceUnsaved: false }; + } + return seq; + }), + ); return ok(undefined); } @@ -110,6 +120,12 @@ export async function saveSequences( if (res.isErr()) { return err(res.error); } + stateManager.setSequences( + stateManager.sequences.map((seq) => ({ + ...seq, + testSequenceUnsaved: false, + })), + ); }); return ok(undefined); }