diff --git a/frontend/api/crud.ts b/frontend/api/crud.ts index fbac12a37..a39991d7f 100644 --- a/frontend/api/crud.ts +++ b/frontend/api/crud.ts @@ -2,15 +2,12 @@ import { TaggedResource, SpecialStatus, ResourceName, - TaggedSequence + TaggedSequence, } from "farmbot"; import { isTaggedResource, } from "../resources/tagged_resources"; -import { - GetState, - ReduxAction -} from "../redux/interfaces"; +import { GetState, ReduxAction } from "../redux/interfaces"; import { API } from "./index"; import axios from "axios"; import { @@ -18,7 +15,7 @@ import { destroyOK, destroyNO, GeneralizedError, - saveOK + saveOK, } from "../resources/actions"; import { UnsafeError } from "../interfaces"; import { defensiveClone, unpackUUID } from "../util"; @@ -284,7 +281,7 @@ export function urlFor(tag: ResourceName) { User: API.current.usersPath, WebAppConfig: API.current.webAppConfigPath, WebcamFeed: API.current.webcamFeedPath, - Folder: API.current.foldersPath + Folder: API.current.foldersPath, }; const url = OPTIONS[tag]; if (url) { diff --git a/frontend/css/_blueprint_overrides.scss b/frontend/css/_blueprint_overrides.scss index 09b7e73f0..391d11aa9 100644 --- a/frontend/css/_blueprint_overrides.scss +++ b/frontend/css/_blueprint_overrides.scss @@ -15,9 +15,6 @@ } } -.float-right { float: right; } -.float-left { float: left; } - .hardware-widget { .bp3-popover-wrapper { float: right; diff --git a/frontend/css/sequences.scss b/frontend/css/sequences.scss index 26496ce65..c26fd4964 100644 --- a/frontend/css/sequences.scss +++ b/frontend/css/sequences.scss @@ -230,14 +230,16 @@ } .folders-panel { - height: calc(100vh - 15rem); - overflow-y: auto; - overflow-x: hidden; margin-left: -30px; margin-right: -20px; @media screen and (max-width: 767px) { margin-left: -15px; } + .non-empty-state { + height: calc(100vh - 15rem); + overflow-y: auto; + overflow-x: hidden; + } .panel-top { margin-left: 1rem !important; button { @@ -282,8 +284,8 @@ padding-left: 2rem; padding-right: 2rem; transition: height 0.5s ease-out, - padding-top 0.5s ease-out, - padding-bottom 0.5s ease-out; + padding-top 0.5s ease-out, + padding-bottom 0.5s ease-out; transition-delay: 0.4s; color: $gray; font-weight: bold; @@ -292,8 +294,8 @@ cursor: pointer; &.visible { transition: height 0.3s ease-in, - padding-top 0.3s ease-in, - padding-bottom 0.3s ease-in; + padding-top 0.3s ease-in, + padding-bottom 0.3s ease-in; transition-delay: 0.2s; height: 3rem; padding-top: 0.5rem; @@ -322,8 +324,8 @@ border-left: 4px solid $dark_gray; } .fa-chevron-down, .fa-chevron-right { - z-index: 2; position: absolute; + z-index: 2; width: 3rem; font-size: 1.1rem; } @@ -391,8 +393,8 @@ } } .input { - width: 90%; margin: 0.3rem; + width: 90%; } } } @@ -426,8 +428,8 @@ } padding-left: 3rem; .saucer, .icon-saucer { - top: 0.55rem; position: relative; + top: 0.55rem; margin: auto; margin-top: 0.6rem; } diff --git a/frontend/draggable/actions.ts b/frontend/draggable/actions.ts index a5056f62d..b360126b4 100644 --- a/frontend/draggable/actions.ts +++ b/frontend/draggable/actions.ts @@ -4,6 +4,7 @@ import { Everything } from "../interfaces"; import { ReduxAction } from "../redux/interfaces"; import * as React from "react"; import { Actions } from "../constants"; +import { UUID } from "../resources/interfaces"; export const STEP_DATATRANSFER_IDENTIFER = "farmbot/sequence-step"; /** SIDE EFFECT-Y!! Stores a step into store.draggable.dataTransfer and @@ -12,7 +13,8 @@ export const STEP_DATATRANSFER_IDENTIFER = "farmbot/sequence-step"; export function stepPut(value: Step, ev: React.DragEvent, intent: DataXferIntent, - draggerId: number): + draggerId: number, + resourceUuid?: UUID): ReduxAction { const uuid = id(); ev.dataTransfer.setData(STEP_DATATRANSFER_IDENTIFER, uuid); @@ -22,7 +24,8 @@ export function stepPut(value: Step, intent, uuid, value, - draggerId + draggerId, + resourceUuid, } }; } diff --git a/frontend/draggable/interfaces.ts b/frontend/draggable/interfaces.ts index e17a033ff..15b0cb109 100644 --- a/frontend/draggable/interfaces.ts +++ b/frontend/draggable/interfaces.ts @@ -1,4 +1,5 @@ import { SequenceBodyItem as Step } from "farmbot"; +import { UUID } from "../resources/interfaces"; /** An entry in the data transfer table. Used to transfer data from a "draggable" * to a "dropable". For type safety, this is a "tagged union". See Typescript @@ -17,6 +18,8 @@ export interface DataXferBase { /** "why" the drag/drop event took place (tagged union- See Typescript * documentation for more information). */ intent: DataXferIntent; + /** Optional resource UUID. */ + resourceUuid?: UUID; } /** Data transfer payload used when moving a *new* step into an existing step */ @@ -51,4 +54,5 @@ export interface StepDraggerProps { intent: DataXferIntent; children?: React.ReactNode; draggerId: number; + resourceUuid?: UUID; } diff --git a/frontend/draggable/step_dragger.tsx b/frontend/draggable/step_dragger.tsx index 6935142d7..9f3ac1764 100644 --- a/frontend/draggable/step_dragger.tsx +++ b/frontend/draggable/step_dragger.tsx @@ -2,6 +2,7 @@ import * as React from "react"; import { stepPut } from "./actions"; import { SequenceBodyItem as Step } from "farmbot"; import { DataXferIntent, StepDraggerProps } from "./interfaces"; +import { UUID } from "../resources/interfaces"; /** Magic number to indicate that the draggerId was not provided or can't be * known. */ @@ -21,22 +22,21 @@ export const NULL_DRAGGER_ID = 0xCAFEF00D; export const stepDragEventHandler = (dispatch: Function, step: Step, intent: DataXferIntent, - draggerId: number) => { + draggerId: number, + resourceUuid?: UUID) => { return (ev: React.DragEvent) => { - dispatch(stepPut(step, ev, intent, draggerId)); + dispatch(stepPut(step, ev, intent, draggerId, resourceUuid)); }; }; -export function StepDragger({ dispatch, - step, - children, - intent, - draggerId }: StepDraggerProps) { +export function StepDragger(props: StepDraggerProps) { + const { dispatch, step, children, intent, draggerId, resourceUuid } = props; return
+ draggerId, + resourceUuid)}> {children}
; } diff --git a/frontend/folders/__tests__/actions_test.ts b/frontend/folders/__tests__/actions_test.ts index fd4b92645..c1bde2ffc 100644 --- a/frontend/folders/__tests__/actions_test.ts +++ b/frontend/folders/__tests__/actions_test.ts @@ -1,3 +1,11 @@ +const mockStepGetResult = { + value: { kind: "execute", args: { sequence_id: 1 } }, + resourceUuid: "", +}; +jest.mock("../../draggable/actions", () => ({ + stepGet: jest.fn(() => () => mockStepGetResult), +})); + import { FolderNode } from "../constants"; import { ingest } from "../data_transfer"; import { @@ -11,7 +19,9 @@ import { toggleFolderOpenState, toggleFolderEditState, toggleAll, - moveSequence + moveSequence, + dropSequence, + sequenceEditMaybeSave, } from "../actions"; import { sample } from "lodash"; import { cloneAndClimb, climb } from "../climb"; @@ -24,6 +34,8 @@ import { save, edit, init, initSave, destroy } from "../../api/crud"; import { setActiveSequenceByName } from "../../sequences/set_active_sequence_by_name"; import { push } from "../../history"; import { fakeSequence } from "../../__test_support__/fake_state/resources"; +import { stepGet } from "../../draggable/actions"; +import { SpecialStatus } from "farmbot"; /** A set of fake Folder resources used exclusively for testing purposes. ``` @@ -76,7 +88,7 @@ const mockState: DeepPartial = jest.mock("../../redux/store", () => { return { store: { - dispatch: jest.fn(), + dispatch: jest.fn(x => typeof x === "function" && x()), getState: jest.fn(() => mockState) } }; @@ -146,10 +158,6 @@ export const TEST_GRAPH = ingest({ } }); -describe("deletion of folders", () => { - test.todo("can't delete populated folders"); -}); - describe("expand/collapse all", () => { const halfOpen = cloneAndClimb(TEST_GRAPH, (node) => { node.open = !sample([true, false]); @@ -209,6 +217,16 @@ describe("createFolder", () => { parent_id: 0 }); }); + + it("saves a new folder without inputs", () => { + createFolder(); + expect(store.dispatch).toHaveReturnedTimes(1); + expect(initSave).toHaveBeenCalledWith("Folder", { + color: "gray", + name: "New Folder", + parent_id: 0 + }); + }); }); describe("deleteFolder", () => { @@ -259,6 +277,24 @@ describe("toggleAll", () => { }); }); +describe("sequenceEditMaybeSave()", () => { + it("saves", () => { + const sequence = fakeSequence(); + sequence.specialStatus = SpecialStatus.SAVED; + sequenceEditMaybeSave(sequence, {}); + expect(edit).toHaveBeenCalled(); + expect(save).toHaveBeenCalledWith(sequence.uuid); + }); + + it("doesn't save", () => { + const sequence = fakeSequence(); + sequence.specialStatus = SpecialStatus.DIRTY; + sequenceEditMaybeSave(sequence, {}); + expect(edit).toHaveBeenCalled(); + expect(save).not.toHaveBeenCalled(); + }); +}); + describe("moveSequence", () => { it("silently fails when given bad UUIDs", () => { const uuid = "a.b.c"; @@ -276,3 +312,30 @@ describe("moveSequence", () => { expect(save).toHaveBeenCalledWith(uuid); }); }); + +describe("dropSequence()", () => { + const fakeDragEvent = ({ + dataTransfer: { getData: () => "fakeKey" } + } as unknown as React.DragEvent); + + it("updates folder_id", () => { + dropSequence(1)(fakeDragEvent); + expect(stepGet).toHaveBeenCalledWith("fakeKey"); + expect(edit).toHaveBeenCalledWith(mockSequence, { folder_id: 1 }); + }); + + it("handles missing sequence", () => { + mockStepGetResult.value.args.sequence_id = -1; + dropSequence(1)(fakeDragEvent); + expect(stepGet).toHaveBeenCalledWith("fakeKey"); + expect(edit).not.toHaveBeenCalled(); + }); + + it("gets sequence by UUID", () => { + mockStepGetResult.value.args.sequence_id = -1; + mockStepGetResult.resourceUuid = mockSequence.uuid; + dropSequence(1)(fakeDragEvent); + expect(stepGet).toHaveBeenCalledWith("fakeKey"); + expect(edit).toHaveBeenCalledWith(mockSequence, { folder_id: 1 }); + }); +}); diff --git a/frontend/folders/__tests__/component_test.tsx b/frontend/folders/__tests__/component_test.tsx index bf9e58ed0..0e23f9f65 100644 --- a/frontend/folders/__tests__/component_test.tsx +++ b/frontend/folders/__tests__/component_test.tsx @@ -1,7 +1,73 @@ +jest.mock("../actions", () => ({ + updateSearchTerm: jest.fn(), + toggleAll: jest.fn(), + moveSequence: jest.fn(), + dropSequence: jest.fn(() => jest.fn()), + sequenceEditMaybeSave: jest.fn(), + deleteFolder: jest.fn(), + toggleFolderEditState: jest.fn(), + createFolder: jest.fn(), + addNewSequenceToFolder: jest.fn(), + setFolderName: jest.fn(), + toggleFolderOpenState: jest.fn(), + setFolderColor: jest.fn(), +})); + +let mockPath = ""; +jest.mock("../../history", () => ({ + history: { getCurrentLocation: () => ({ pathname: mockPath }) } +})); + import * as React from "react"; -import { mount } from "enzyme"; -import { Folders } from "../component"; -import { FolderProps } from "../constants"; +import { mount, shallow } from "enzyme"; +import { + Folders, FolderPanelTop, SequenceDropArea, FolderNameEditor, + FolderButtonCluster, FolderListItem, FolderNameInput, +} from "../component"; +import { + FolderProps, FolderPanelTopProps, SequenceDropAreaProps, FolderNodeProps, + FolderNodeInitial, FolderButtonClusterProps, FolderItemProps, + FolderNameInputProps, + FolderNodeMedial, + FolderNodeTerminal, +} from "../constants"; +import { + updateSearchTerm, toggleAll, moveSequence, dropSequence, + sequenceEditMaybeSave, + deleteFolder, + toggleFolderEditState, + createFolder, + addNewSequenceToFolder, + setFolderName, + toggleFolderOpenState, + setFolderColor, +} from "../actions"; +import { fakeSequence } from "../../__test_support__/fake_state/resources"; +import { SpecialStatus, Color } from "farmbot"; + +const fakeRootFolder = (): FolderNodeInitial => ({ + kind: "initial", + children: [], + id: 1, + name: "my folder", + content: [], + color: "gray", + open: true, + editing: false, +}); + +const fakeFolderNode = (): FolderNodeMedial => { + const folder = fakeRootFolder() as unknown as FolderNodeMedial; + folder.kind = "medial"; + return folder; +}; + +const fakeTerminalFolder = (): FolderNodeTerminal => { + const folder = fakeRootFolder() as unknown as FolderNodeTerminal; + folder.children = undefined; + folder.kind = "terminal"; + return folder; +}; describe("", () => { const fakeProps = (): FolderProps => ({ @@ -21,4 +87,460 @@ describe("", () => { const wrapper = mount(); expect(wrapper.text()).toContain("No Sequences."); }); + + it("renders sequences outside of folders", () => { + const p = fakeProps(); + p.rootFolder.folders[0] = fakeRootFolder(); + const sequence = fakeSequence(); + p.sequences = { [sequence.uuid]: sequence }; + sequence.body.name = "my sequence"; + p.rootFolder.noFolder = [sequence.uuid]; + const wrapper = mount(); + expect(wrapper.text()).toContain("my sequence"); + }); + + it("renders empty folder", () => { + const p = fakeProps(); + p.rootFolder.folders[0] = fakeRootFolder(); + const wrapper = mount(); + expect(wrapper.text()).toContain("my folder"); + }); + + it("renders sequences in folder", () => { + const p = fakeProps(); + const sequence = fakeSequence(); + sequence.body.name = "my sequence"; + p.sequences = { [sequence.uuid]: sequence }; + const folder = fakeRootFolder(); + folder.content = [sequence.uuid]; + p.rootFolder.folders[0] = folder; + const wrapper = mount(); + expect(wrapper.text()).toContain("my sequence"); + }); + + it("renders folders in folder", () => { + const p = fakeProps(); + const folder = fakeRootFolder(); + const childFolder = fakeFolderNode(); + childFolder.name = "deeper folder"; + folder.children = [childFolder]; + p.rootFolder.folders[0] = folder; + const wrapper = mount(); + expect(wrapper.text()).toContain("deeper folder"); + }); + + it("renders terminal folder", () => { + const p = fakeProps(); + const folder = fakeRootFolder(); + folder.name = "folder"; + const childFolder = fakeFolderNode(); + childFolder.name = "deeper folder"; + const terminalFolder = fakeTerminalFolder(); + terminalFolder.name = "deepest folder"; + childFolder.children = [terminalFolder]; + folder.children = [childFolder]; + p.rootFolder.folders[0] = folder; + const wrapper = mount(); + ["folder", "deeper folder", "deepest folder"].map(string => + expect(wrapper.text()).toContain(string)); + }); + + it("toggles all folders", () => { + const wrapper = mount(); + expect(wrapper.state().toggleDirection).toEqual(false); + wrapper.instance().toggleAll(); + expect(toggleAll).toHaveBeenCalledWith(false); + expect(wrapper.state().toggleDirection).toEqual(true); + }); + + it("starts sequence move", () => { + const wrapper = mount(); + expect(wrapper.state().movedSequenceUuid).toEqual(undefined); + wrapper.instance().startSequenceMove("fakeUuid"); + expect(wrapper.state().movedSequenceUuid).toEqual("fakeUuid"); + expect(wrapper.state().stashedUuid).toEqual(undefined); + }); + + const toggleMoveTest = (p: { + prev: string | undefined, + current: string | undefined, + arg: string | undefined, + new: string | undefined + }) => { + const wrapper = mount(); + wrapper.setState({ movedSequenceUuid: p.current, stashedUuid: p.prev }); + wrapper.instance().toggleSequenceMove(p.arg); + expect(wrapper.state().movedSequenceUuid).toEqual(p.new); + }; + + it("toggle sequence move: on", () => { + toggleMoveTest({ + prev: undefined, current: undefined, arg: "fakeUuid", new: "fakeUuid" + }); + toggleMoveTest({ + prev: undefined, current: "oldFakeUuid", arg: "fakeUuid", new: "fakeUuid" + }); + }); + + it("toggle sequence move: off", () => { + toggleMoveTest({ + prev: undefined, current: undefined, arg: undefined, new: undefined + }); + toggleMoveTest({ + prev: "fakeUuid", current: "fakeUuid", arg: undefined, new: undefined + }); + toggleMoveTest({ + prev: "fakeUuid", current: undefined, arg: "fakeUuid", new: undefined + }); + toggleMoveTest({ + prev: "fakeUuid", current: "fakeUuid", arg: "fakeUuid", new: undefined + }); + }); + + it("ends sequence move", () => { + const wrapper = mount(); + wrapper.setState({ movedSequenceUuid: "fakeUuid" }); + wrapper.instance().endSequenceMove(1); + expect(moveSequence).toHaveBeenCalledWith("fakeUuid", 1); + expect(wrapper.state().movedSequenceUuid).toEqual(undefined); + }); + + it("ends sequence move: undefined", () => { + const wrapper = mount(); + wrapper.setState({ movedSequenceUuid: undefined }); + wrapper.instance().endSequenceMove(1); + expect(moveSequence).toHaveBeenCalledWith("", 1); + expect(wrapper.state().movedSequenceUuid).toEqual(undefined); + }); +}); + +describe("", () => { + const fakeProps = (): FolderItemProps => ({ + startSequenceMove: jest.fn(), + toggleSequenceMove: jest.fn(), + sequence: fakeSequence(), + movedSequenceUuid: undefined, + dispatch: jest.fn(), + variableData: undefined, + inUse: false, + }); + + it("renders", () => { + const p = fakeProps(); + p.sequence.body.name = "my sequence"; + const wrapper = mount(); + expect(wrapper.text()).toContain("my sequence"); + expect(wrapper.find("li").hasClass("move-source")).toBeFalsy(); + expect(wrapper.find("li").hasClass("active")).toBeFalsy(); + }); + + it("renders: move in progress", () => { + const p = fakeProps(); + p.movedSequenceUuid = p.sequence.uuid; + const wrapper = mount(); + expect(wrapper.find("li").hasClass("move-source")).toBeTruthy(); + }); + + it("renders: active", () => { + const p = fakeProps(); + p.sequence.body.name = "sequence"; + mockPath = "/app/sequences/sequence"; + const wrapper = mount(); + expect(wrapper.find("li").hasClass("active")).toBeTruthy(); + }); + + it("renders: unsaved", () => { + const p = fakeProps(); + p.sequence.body.name = "my sequence"; + p.sequence.specialStatus = SpecialStatus.DIRTY; + const wrapper = mount(); + expect(wrapper.text()).toContain("my sequence*"); + }); + + it("renders: in use", () => { + const p = fakeProps(); + p.inUse = true; + const wrapper = mount(); + expect(wrapper.find(".in-use").length).toEqual(1); + }); + + it("changes color", () => { + const p = fakeProps(); + p.sequence.body.id = undefined; + p.sequence.body.name = ""; + p.sequence.body.color = "" as Color; + const wrapper = shallow(); + wrapper.find("ColorPicker").simulate("change", "green"); + expect(sequenceEditMaybeSave).toHaveBeenCalledWith(p.sequence, { + color: "green" + }); + }); + + it("starts sequence move", () => { + const p = fakeProps(); + const wrapper = shallow(); + wrapper.find(".fa-bars").simulate("mouseDown"); + expect(p.startSequenceMove).toHaveBeenCalledWith(p.sequence.uuid); + }); + + it("toggles sequence move", () => { + const p = fakeProps(); + const wrapper = shallow(); + wrapper.find(".fa-bars").simulate("mouseUp"); + expect(p.toggleSequenceMove).toHaveBeenCalledWith(p.sequence.uuid); + }); +}); + +describe("", () => { + const fakeProps = (): FolderButtonClusterProps => ({ + node: fakeRootFolder(), + close: jest.fn(), + }); + + it("renders", () => { + const wrapper = mount(); + expect(wrapper.find("button").length).toEqual(4); + }); + + it("deletes folder", () => { + const p = fakeProps(); + p.node.id = 1; + const wrapper = mount(); + wrapper.find("button").at(0).simulate("click"); + expect(deleteFolder).toHaveBeenCalledWith(1); + }); + + it("edits folder", () => { + const p = fakeProps(); + p.node.id = 1; + const wrapper = mount(); + wrapper.find("button").at(1).simulate("click"); + expect(p.close).toHaveBeenCalled(); + expect(toggleFolderEditState).toHaveBeenCalledWith(1); + }); + + it("creates new folder", () => { + const p = fakeProps(); + p.node.id = 1; + const wrapper = mount(); + wrapper.find("button").at(2).simulate("click"); + expect(p.close).toHaveBeenCalled(); + expect(createFolder).toHaveBeenCalledWith({ parent_id: p.node.id }); + }); + + it("creates new sequence", () => { + const p = fakeProps(); + p.node.id = 1; + const wrapper = mount(); + wrapper.find("button").at(3).simulate("click"); + expect(p.close).toHaveBeenCalled(); + expect(addNewSequenceToFolder).toHaveBeenCalledWith(1); + }); +}); + +describe("", () => { + const fakeProps = (): FolderNameInputProps => ({ + node: fakeFolderNode(), + }); + + it("edits folder name", () => { + const p = fakeProps(); + p.node.editing = true; + const wrapper = shallow(); + wrapper.find("BlurableInput").simulate("commit", { + currentTarget: { value: "new name" } + }); + expect(setFolderName).toHaveBeenCalledWith(p.node.id, "new name"); + }); + + it("closes folder name input", () => { + const p = fakeProps(); + p.node.editing = true; + const wrapper = shallow(); + wrapper.find("button").simulate("click"); + expect(toggleFolderEditState).toHaveBeenCalledWith(p.node.id); + }); +}); + +describe("", () => { + const fakeProps = (): FolderNodeProps => ({ + node: fakeRootFolder(), + sequences: {}, + movedSequenceUuid: undefined, + startSequenceMove: jest.fn(), + toggleSequenceMove: jest.fn(), + onMoveEnd: jest.fn(), + dispatch: jest.fn(), + resourceUsage: {}, + sequenceMetas: {}, + }); + + it("renders", () => { + const p = fakeProps(); + const wrapper = mount(); + expect(wrapper.text()).toContain("my folder"); + expect(wrapper.find(".fa-ellipsis-v").hasClass("open")).toBeFalsy(); + expect(wrapper.find(".fa-chevron-down").length).toEqual(1); + expect(wrapper.find(".fa-chevron-right").length).toEqual(0); + expect(wrapper.find(".folder-name-input").length).toEqual(0); + }); + + it("renders: settings open", () => { + const p = fakeProps(); + const wrapper = mount(); + wrapper.setState({ settingsOpen: true }); + expect(wrapper.find(".fa-ellipsis-v").hasClass("open")).toBeTruthy(); + }); + + it("renders: folder closed", () => { + const p = fakeProps(); + p.node.open = false; + const wrapper = mount(); + expect(wrapper.find(".fa-chevron-down").length).toEqual(0); + expect(wrapper.find(".fa-chevron-right").length).toEqual(1); + }); + + it("renders: editing", () => { + const p = fakeProps(); + p.node.editing = true; + const wrapper = mount(); + expect(wrapper.find(".folder-name-input").length).toEqual(1); + }); + + it("closes folder", () => { + const p = fakeProps(); + p.node.open = true; + const wrapper = mount(); + wrapper.find("i").first().simulate("click"); + expect(toggleFolderOpenState).toHaveBeenCalledWith(p.node.id); + }); + + it("changes folder color", () => { + const p = fakeProps(); + const wrapper = shallow(); + wrapper.find("ColorPicker").simulate("change", "green"); + expect(setFolderColor).toHaveBeenCalledWith(p.node.id, "green"); + }); + + it("opens settings menu", () => { + const p = fakeProps(); + const wrapper = shallow(); + expect(wrapper.state().settingsOpen).toBeFalsy(); + wrapper.find("i").last().simulate("click"); + expect(wrapper.state().settingsOpen).toBeTruthy(); + }); + + it("closes settings menu", () => { + const p = fakeProps(); + const wrapper = mount(); + wrapper.setState({ settingsOpen: true }); + wrapper.instance().close(); + expect(wrapper.state().settingsOpen).toBeFalsy(); + }); +}); + +describe("", () => { + const fakeProps = (): SequenceDropAreaProps => ({ + dropAreaVisible: true, + onMoveEnd: jest.fn(), + toggleSequenceMove: jest.fn(), + folderId: 1, + folderName: "my folder", + }); + + it("shows drop area", () => { + const p = fakeProps(); + p.dropAreaVisible = true; + const wrapper = mount(); + expect(wrapper.find(".folder-drop-area").hasClass("visible")).toBeTruthy(); + expect(wrapper.text().toLowerCase()).toContain("move into my folder"); + }); + + it("hides drop area", () => { + const p = fakeProps(); + p.dropAreaVisible = false; + const wrapper = mount(); + expect(wrapper.find(".folder-drop-area").hasClass("visible")).toBeFalsy(); + }); + + it("has 'remove from folders' text", () => { + const p = fakeProps(); + p.dropAreaVisible = true; + p.folderId = 0; + const wrapper = mount(); + expect(wrapper.find(".folder-drop-area").hasClass("visible")).toBeTruthy(); + expect(wrapper.text()).not.toContain("my folder"); + expect(wrapper.text().toLowerCase()).toContain("move out of folders"); + }); + + it("handles click", () => { + const p = fakeProps(); + const wrapper = shallow(); + wrapper.find(".folder-drop-area").simulate("click"); + expect(p.onMoveEnd).toHaveBeenCalledWith(p.folderId); + }); + + it("handles drop", () => { + const p = fakeProps(); + const wrapper = shallow(); + wrapper.setState({ hovered: true }); + expect(wrapper.find(".folder-drop-area").hasClass("hovered")).toBeTruthy(); + wrapper.find(".folder-drop-area").simulate("drop"); + expect(wrapper.state().hovered).toBeFalsy(); + expect(dropSequence).toHaveBeenCalledWith(p.folderId); + expect(p.toggleSequenceMove).toHaveBeenCalled(); + }); + + it("handles drag over", () => { + const p = fakeProps(); + const wrapper = shallow(); + const e = { preventDefault: jest.fn() }; + wrapper.find(".folder-drop-area").simulate("dragOver", e); + expect(e.preventDefault).toHaveBeenCalled(); + }); + + it("handles drag enter", () => { + const p = fakeProps(); + const wrapper = shallow(); + wrapper.find(".folder-drop-area").simulate("dragEnter"); + expect(wrapper.state().hovered).toBeTruthy(); + }); + + it("handles drag leave", () => { + const p = fakeProps(); + const wrapper = shallow(); + wrapper.find(".folder-drop-area").simulate("dragLeave"); + expect(wrapper.state().hovered).toBeFalsy(); + }); +}); + +describe("", () => { + const fakeProps = (): FolderPanelTopProps => ({ + searchTerm: "", + toggleDirection: true, + toggleAll: jest.fn(), + }); + + it("changes search term", () => { + const p = fakeProps(); + const wrapper = shallow(); + wrapper.find("input").simulate("change", { + currentTarget: { value: "new" } + }); + expect(updateSearchTerm).toHaveBeenCalledWith("new"); + }); + + it("creates new folder", () => { + const p = fakeProps(); + const wrapper = mount(); + wrapper.find("button").at(1).simulate("click"); + expect(createFolder).toHaveBeenCalledWith(undefined); + }); + + it("creates new sequence", () => { + const p = fakeProps(); + const wrapper = mount(); + wrapper.find("button").at(2).simulate("click"); + expect(addNewSequenceToFolder).toHaveBeenCalledWith(undefined); + }); }); diff --git a/frontend/folders/actions.ts b/frontend/folders/actions.ts index cc829af81..369a5a0cd 100644 --- a/frontend/folders/actions.ts +++ b/frontend/folders/actions.ts @@ -118,7 +118,8 @@ export const dropSequence = (folder_id: number) => const dataXferObj = dispatch(stepGet(key)); const { sequence_id } = dataXferObj.value.args; const ri = store.getState().resources.index; - const seqUuid = ri.byKindAndId[joinKindAndId("Sequence", sequence_id)]; + const seqUuid = dataXferObj.resourceUuid || + ri.byKindAndId[joinKindAndId("Sequence", sequence_id)]; const sequence = maybeGetSequence(ri, seqUuid); if (sequence) { sequenceEditMaybeSave(sequence, { folder_id }); } }; diff --git a/frontend/folders/component.tsx b/frontend/folders/component.tsx index e1c988b01..82e5e5bfa 100644 --- a/frontend/folders/component.tsx +++ b/frontend/folders/component.tsx @@ -62,7 +62,8 @@ export const FolderListItem = (props: FolderItemProps) => { body: variableList(props.variableData) }} intent="step_splice" - draggerId={NULL_DRAGGER_ID}> + draggerId={NULL_DRAGGER_ID} + resourceUuid={sequence.uuid}>
  • { const AddFolderBtn = ({ folder, close }: AddFolderBtn) => { return