diff --git a/frontend/__test_support__/fake_designer_state.ts b/frontend/__test_support__/fake_designer_state.ts index b82b5e69ec..b05c304aec 100644 --- a/frontend/__test_support__/fake_designer_state.ts +++ b/frontend/__test_support__/fake_designer_state.ts @@ -1,5 +1,6 @@ import { DesignerState } from "../farm_designer/interfaces"; import { HelpState } from "../help/reducer"; +import { RunButtonMenuOpen } from "../sequences/interfaces"; export const fakeDesignerState = (): DesignerState => ({ selectedPoints: undefined, @@ -56,3 +57,8 @@ export const fakeHelpState = (): HelpState => ({ currentTour: undefined, currentTourStep: undefined, }); + +export const fakeMenuOpenState = (): RunButtonMenuOpen => ({ + component: undefined, + uuid: undefined, +}); diff --git a/frontend/__tests__/app_test.tsx b/frontend/__tests__/app_test.tsx index a57271d01c..10cafdeb56 100644 --- a/frontend/__tests__/app_test.tsx +++ b/frontend/__tests__/app_test.tsx @@ -29,7 +29,9 @@ import { fakeTimeSettings } from "../__test_support__/fake_time_settings"; import { error, warning } from "../toast/toast"; import { fakePings } from "../__test_support__/fake_state/pings"; import { auth } from "../__test_support__/fake_state/token"; -import { fakeHelpState } from "../__test_support__/fake_designer_state"; +import { + fakeHelpState, fakeMenuOpenState, +} from "../__test_support__/fake_designer_state"; import { Path } from "../internal_urls"; import { push } from "../history"; import { app } from "../__test_support__/fake_state/app"; @@ -65,7 +67,7 @@ const fakeProps = (): AppProps => ({ feeds: [], peripherals: [], sequences: [], - menuOpen: undefined, + menuOpen: fakeMenuOpenState(), }); describe(": Loading", () => { diff --git a/frontend/app.tsx b/frontend/app.tsx index a402016ee8..f503b7ed09 100644 --- a/frontend/app.tsx +++ b/frontend/app.tsx @@ -37,7 +37,7 @@ import { FirmwareConfig } from "farmbot/dist/resources/configs/firmware"; import { getFirmwareConfig, getFbosConfig } from "./resources/getters"; import { intersection, isString, uniq } from "lodash"; import { t } from "./i18next_wrapper"; -import { ResourceIndex, UUID } from "./resources/interfaces"; +import { ResourceIndex } from "./resources/interfaces"; import { getAllAlerts } from "./messages/state_to_props"; import { PingDictionary } from "./devices/connectivity/qos"; import { getEnv } from "./farmware/state_to_props"; @@ -55,6 +55,7 @@ import { AppState } from "./reducer"; import { sourceFbosConfigValue, sourceFwConfigValue, } from "./settings/source_config_value"; +import { RunButtonMenuOpen } from "./sequences/interfaces"; export interface AppProps { dispatch: Function; @@ -83,7 +84,7 @@ export interface AppProps { feeds: TaggedWebcamFeed[]; peripherals: TaggedPeripheral[]; sequences: TaggedSequence[]; - menuOpen: UUID | undefined; + menuOpen: RunButtonMenuOpen; appState: AppState; children?: React.ReactNode; } diff --git a/frontend/controls/__tests__/controls_test.tsx b/frontend/controls/__tests__/controls_test.tsx index 4c5b985957..a53cbde1cb 100644 --- a/frontend/controls/__tests__/controls_test.tsx +++ b/frontend/controls/__tests__/controls_test.tsx @@ -11,6 +11,7 @@ import { DesignerControlsProps } from "../interfaces"; import { fakeMovementState } from "../../__test_support__/fake_bot_data"; import { app } from "../../__test_support__/fake_state/app"; import { Actions } from "../../constants"; +import { fakeMenuOpenState } from "../../__test_support__/fake_designer_state"; describe("", () => { const fakeProps = (): DesignerControlsProps => ({ @@ -20,7 +21,7 @@ describe("", () => { peripherals: [], sequences: [], resources: buildResourceIndex([]).index, - menuOpen: undefined, + menuOpen: fakeMenuOpenState(), firmwareSettings: bot.hardware.mcu_params, getConfigValue: jest.fn(), sourceFwConfig: () => ({ value: 0, consistent: true }), @@ -54,7 +55,7 @@ describe("", () => { peripherals: [], sequences: [], resources: buildResourceIndex([]).index, - menuOpen: undefined, + menuOpen: fakeMenuOpenState(), firmwareSettings: bot.hardware.mcu_params, }); diff --git a/frontend/controls/__tests__/pinned_sequence_list_test.tsx b/frontend/controls/__tests__/pinned_sequence_list_test.tsx index 2a4ecfb352..1d19020511 100644 --- a/frontend/controls/__tests__/pinned_sequence_list_test.tsx +++ b/frontend/controls/__tests__/pinned_sequence_list_test.tsx @@ -4,13 +4,14 @@ import { PinnedSequences } from "../pinned_sequence_list"; import { buildResourceIndex } from "../../__test_support__/resource_index_builder"; import { PinnedSequencesProps } from "../interfaces"; import { fakeSequence } from "../../__test_support__/fake_state/resources"; +import { fakeMenuOpenState } from "../../__test_support__/fake_designer_state"; describe("", () => { const fakeProps = (): PinnedSequencesProps => ({ syncStatus: undefined, sequences: [], resources: buildResourceIndex([]).index, - menuOpen: undefined, + menuOpen: fakeMenuOpenState(), dispatch: jest.fn(), }); diff --git a/frontend/controls/controls.tsx b/frontend/controls/controls.tsx index 09d3225cd2..3a2e1b5585 100644 --- a/frontend/controls/controls.tsx +++ b/frontend/controls/controls.tsx @@ -18,10 +18,11 @@ import { FirmwareHardware, McuParams, TaggedLog, TaggedPeripheral, TaggedSequence, TaggedWebcamFeed, } from "farmbot"; -import { ResourceIndex, UUID } from "../resources/interfaces"; +import { ResourceIndex } from "../resources/interfaces"; import { t } from "../i18next_wrapper"; import { push } from "../history"; import { Path } from "../internal_urls"; +import { RunButtonMenuOpen } from "../sequences/interfaces"; export class RawDesignerControls extends React.Component { @@ -52,7 +53,7 @@ export interface ControlsPanelProps { peripherals: TaggedPeripheral[]; sequences: TaggedSequence[]; resources: ResourceIndex; - menuOpen: UUID | undefined; + menuOpen: RunButtonMenuOpen; firmwareSettings: McuParams; } diff --git a/frontend/controls/interfaces.ts b/frontend/controls/interfaces.ts index 2c0237fb84..0601d7b558 100644 --- a/frontend/controls/interfaces.ts +++ b/frontend/controls/interfaces.ts @@ -5,10 +5,11 @@ import { Vector3, McuParams, Xyz, AxisState, SyncStatus, TaggedSequence, FirmwareHardware, TaggedPeripheral, TaggedWebcamFeed, TaggedLog, } from "farmbot"; -import { ResourceIndex, UUID } from "../resources/interfaces"; +import { ResourceIndex } from "../resources/interfaces"; import { GetWebAppConfigValue } from "../config_storage/actions"; import { MovementState } from "../interfaces"; import { PinBindingListItems } from "../settings/pin_bindings/interfaces"; +import { RunButtonMenuOpen } from "../sequences/interfaces"; export interface AxisDisplayGroupProps { position: BotPosition; @@ -58,7 +59,7 @@ export interface PinnedSequencesProps { syncStatus: SyncStatus | undefined; sequences: TaggedSequence[]; resources: ResourceIndex; - menuOpen: UUID | undefined; + menuOpen: RunButtonMenuOpen; dispatch: Function; } @@ -69,7 +70,7 @@ export interface DesignerControlsProps { peripherals: TaggedPeripheral[]; sequences: TaggedSequence[]; resources: ResourceIndex; - menuOpen: UUID | undefined; + menuOpen: RunButtonMenuOpen; firmwareSettings: McuParams; getConfigValue: GetWebAppConfigValue; sourceFwConfig: SourceFwConfig; diff --git a/frontend/controls/pinned_sequence_list.tsx b/frontend/controls/pinned_sequence_list.tsx index 3ad69bf479..3d9f6ba109 100644 --- a/frontend/controls/pinned_sequence_list.tsx +++ b/frontend/controls/pinned_sequence_list.tsx @@ -22,7 +22,7 @@ export const PinnedSequences = (props: PinnedSequencesProps) => { - ({ kind: "initial", @@ -111,7 +112,7 @@ describe("", () => { sequenceMetas: {}, getWebAppConfigValue: jest.fn(), resources: buildResourceIndex([]).index, - menuOpen: undefined, + menuOpen: fakeMenuOpenState(), syncStatus: undefined, }); @@ -258,7 +259,7 @@ describe("", () => { inUse: false, getWebAppConfigValue: jest.fn(), resources: buildResourceIndex([]).index, - menuOpen: undefined, + menuOpen: fakeMenuOpenState(), syncStatus: undefined, searchTerm: undefined, }); @@ -523,7 +524,7 @@ describe("", () => { sequenceMetas: {}, getWebAppConfigValue: jest.fn(), resources: buildResourceIndex([]).index, - menuOpen: undefined, + menuOpen: fakeMenuOpenState(), syncStatus: undefined, searchTerm: undefined, dragging: false, diff --git a/frontend/folders/component.tsx b/frontend/folders/component.tsx index 1b0414d617..d01fe00bc9 100644 --- a/frontend/folders/component.tsx +++ b/frontend/folders/component.tsx @@ -51,7 +51,7 @@ import { } from "../sequences/sequence_editor_middle_active"; import { Path } from "../internal_urls"; import { copySequence } from "../sequences/actions"; -import { TestButton } from "../sequences/test_button"; +import { TestButton, isMenuOpen } from "../sequences/test_button"; import { TaggedSequence } from "farmbot"; export const FolderListItem = (props: FolderItemProps) => { @@ -63,7 +63,9 @@ export const FolderListItem = (props: FolderItemProps) => { const active = Path.lastChunkEquals(urlFriendly(seqName)) ? "active" : ""; const [settingsOpen, setSettingsOpen] = React.useState(false); const [descriptionOpen, setDescriptionOpen] = React.useState(false); - const hovered = props.menuOpen == sequence.uuid || settingsOpen || descriptionOpen + const menuOpen = isMenuOpen(props.menuOpen, + { component: "list", uuid: sequence.uuid }); + const hovered = menuOpen || settingsOpen || descriptionOpen ? "hovered" : ""; const matched = (props.searchTerm && @@ -93,7 +95,7 @@ export const FolderListItem = (props: FolderItemProps) => { draggable={false}>

{nameWithSaveIndicator}

- ; getWebAppConfigValue: GetWebAppConfigValue; resources: ResourceIndex; - menuOpen: UUID | undefined; + menuOpen: RunButtonMenuOpen; syncStatus: SyncStatus | undefined; } @@ -95,7 +96,7 @@ export interface FolderNodeProps { sequenceMetas: Record; getWebAppConfigValue: GetWebAppConfigValue; resources: ResourceIndex; - menuOpen: UUID | undefined; + menuOpen: RunButtonMenuOpen; syncStatus: SyncStatus | undefined; searchTerm: string | undefined; } @@ -127,7 +128,7 @@ export interface FolderItemProps { inUse: boolean; getWebAppConfigValue: GetWebAppConfigValue; resources: ResourceIndex; - menuOpen: UUID | undefined; + menuOpen: RunButtonMenuOpen; syncStatus: SyncStatus | undefined; searchTerm: string | undefined; } diff --git a/frontend/nav/__tests__/index_test.tsx b/frontend/nav/__tests__/index_test.tsx index 911a76021c..ad42d17f84 100644 --- a/frontend/nav/__tests__/index_test.tsx +++ b/frontend/nav/__tests__/index_test.tsx @@ -23,7 +23,9 @@ import { fakePings } from "../../__test_support__/fake_state/pings"; import { Link } from "../../link"; import { refresh } from "../../api/crud"; import { push } from "../../history"; -import { fakeHelpState } from "../../__test_support__/fake_designer_state"; +import { + fakeHelpState, fakeMenuOpenState, +} from "../../__test_support__/fake_designer_state"; import { Path } from "../../internal_urls"; import { fakePercentJob } from "../../__test_support__/fake_bot_data"; import { @@ -55,7 +57,7 @@ describe("", () => { sourceFbosConfig: jest.fn(), firmwareConfig: fakeFirmwareConfig().body, resources: buildResourceIndex([]).index, - menuOpen: undefined, + menuOpen: fakeMenuOpenState(), env: {}, feeds: [], peripherals: [], diff --git a/frontend/nav/interfaces.ts b/frontend/nav/interfaces.ts index af91b48a35..3d8620d68b 100644 --- a/frontend/nav/interfaces.ts +++ b/frontend/nav/interfaces.ts @@ -14,8 +14,9 @@ import { TimeSettings } from "../interfaces"; import { PingDictionary } from "../devices/connectivity/qos"; import { HelpState } from "../help/reducer"; import { AppState } from "../reducer"; -import { ResourceIndex, UUID } from "../resources/interfaces"; +import { ResourceIndex } from "../resources/interfaces"; import { FirmwareConfig } from "farmbot/dist/resources/configs/firmware"; +import { RunButtonMenuOpen } from "../sequences/interfaces"; export interface NavBarProps { logs: TaggedLog[]; @@ -38,7 +39,7 @@ export interface NavBarProps { helpState: HelpState; telemetry: TaggedTelemetry[]; appState: AppState; - menuOpen: UUID | undefined; + menuOpen: RunButtonMenuOpen; env: UserEnv; feeds: TaggedWebcamFeed[]; peripherals: TaggedPeripheral[]; diff --git a/frontend/sequences/__tests__/test_button_test.tsx b/frontend/sequences/__tests__/test_button_test.tsx index 571139fc96..0faed09cb6 100644 --- a/frontend/sequences/__tests__/test_button_test.tsx +++ b/frontend/sequences/__tests__/test_button_test.tsx @@ -27,15 +27,17 @@ import { fakeVariableNameSet } from "../../__test_support__/fake_variables"; import { SequenceMeta } from "../../resources/sequence_meta"; import { clickButton } from "../../__test_support__/helpers"; import { fakeSequence } from "../../__test_support__/fake_state/resources"; +import { fakeMenuOpenState } from "../../__test_support__/fake_designer_state"; -describe("", () => { +describe("", () => { const fakeProps = (): TestBtnProps => ({ sequence: fakeSequence(), syncStatus: "synced", resources: buildResourceIndex().index, - menuOpen: undefined, + menuOpen: fakeMenuOpenState(), dispatch: jest.fn(), + component: "list", }); it("doesn't fire if unsaved", () => { @@ -88,13 +90,16 @@ describe("", () => { expect(btn.hasClass("orange")).toBeTruthy(); expect(warning).not.toHaveBeenCalled(); expect(mockDevice.execSequence).not.toHaveBeenCalled(); - expect(props.dispatch).toHaveBeenCalledWith(setMenuOpen(props.sequence.uuid)); + expect(props.dispatch).toHaveBeenCalledWith(setMenuOpen({ + component: "list", uuid: props.sequence.uuid, + })); }); it("has open parameter assignment menu", () => { const props = fakeProps(); mockHasParameters = true; - props.menuOpen = props.sequence.uuid; + props.menuOpen.component = "list"; + props.menuOpen.uuid = props.sequence.uuid; const result = mount(); const btn = result.find("button").first(); expect(btn.hasClass("gray")).toBeTruthy(); @@ -104,7 +109,8 @@ describe("", () => { it("closes parameter assignment menu", () => { const p = fakeProps(); - p.menuOpen = p.sequence.uuid; + p.menuOpen.component = "list"; + p.menuOpen.uuid = p.sequence.uuid; p.syncStatus = "synced"; p.sequence.specialStatus = SpecialStatus.SAVED; p.sequence.body.id = 1; @@ -115,7 +121,7 @@ describe("", () => { expect(btn.hasClass("gray")).toBeTruthy(); expect(warning).not.toHaveBeenCalled(); expect(mockDevice.execSequence).not.toHaveBeenCalled(); - expect(p.dispatch).toHaveBeenCalledWith(setMenuOpen(undefined)); + expect(p.dispatch).toHaveBeenCalledWith(setMenuOpen(fakeMenuOpenState())); }); it("edits body variables", () => { @@ -185,6 +191,6 @@ describe("", () => { const props = fakeProps(); const wrapper = mount(); wrapper.unmount(); - expect(props.dispatch).toHaveBeenCalledWith(setMenuOpen(undefined)); + expect(props.dispatch).toHaveBeenCalledWith(setMenuOpen(fakeMenuOpenState())); }); }); diff --git a/frontend/sequences/interfaces.ts b/frontend/sequences/interfaces.ts index cfb23341e1..6d65176efc 100644 --- a/frontend/sequences/interfaces.ts +++ b/frontend/sequences/interfaces.ts @@ -139,9 +139,14 @@ export const MESSAGE_TYPES = Object.keys(MessageType); export const isMessageType = (x: any): x is ALLOWED_MESSAGE_TYPES => MESSAGE_TYPES.includes(x as string); +export interface RunButtonMenuOpen { + component: "list" | "editor" | "pinned" | undefined; + uuid: UUID | undefined; +} + export interface SequenceReducerState { current: string | undefined; - menuOpen: UUID | undefined; + menuOpen: RunButtonMenuOpen; stepIndex: number | undefined; } diff --git a/frontend/sequences/reducer.ts b/frontend/sequences/reducer.ts index ad8ce476b8..b4ee40e2d9 100644 --- a/frontend/sequences/reducer.ts +++ b/frontend/sequences/reducer.ts @@ -1,12 +1,11 @@ -import { SequenceReducerState } from "./interfaces"; +import { RunButtonMenuOpen, SequenceReducerState } from "./interfaces"; import { generateReducer } from "../redux/generate_reducer"; import { TaggedResource } from "farmbot"; import { Actions } from "../constants"; -import { UUID } from "../resources/interfaces"; export const initialState: SequenceReducerState = { current: undefined, - menuOpen: undefined, + menuOpen: { component: undefined, uuid: undefined }, stepIndex: undefined, }; @@ -23,7 +22,7 @@ export const sequenceReducer = generateReducer(initialStat s.current = payload; return s; }) - .add(Actions.SET_SEQUENCE_POPUP_STATE, (s, { payload }) => { + .add(Actions.SET_SEQUENCE_POPUP_STATE, (s, { payload }) => { s.menuOpen = payload; return s; }) diff --git a/frontend/sequences/sequence_editor_middle_active.tsx b/frontend/sequences/sequence_editor_middle_active.tsx index 94643f33a7..1d7f13f1f0 100644 --- a/frontend/sequences/sequence_editor_middle_active.tsx +++ b/frontend/sequences/sequence_editor_middle_active.tsx @@ -275,7 +275,7 @@ export const SequenceBtnGroup = ({ dispatch(save(sequence.uuid)).then(() => push(Path.sequences(urlFriendly(sequence.body.name))))} /> - store.dispatch(selectSequence(uuid)); export function setActiveSequenceByName() { const chunk = Path.getLastChunk(); - store.dispatch(setMenuOpen(undefined)); + store.dispatch(setMenuOpen({ component: undefined, uuid: undefined })); if (!chunk || chunk == "sequences") { return; } diff --git a/frontend/sequences/test_button.tsx b/frontend/sequences/test_button.tsx index 119465ae92..ea6f6fa050 100644 --- a/frontend/sequences/test_button.tsx +++ b/frontend/sequences/test_button.tsx @@ -13,13 +13,23 @@ import { t } from "../i18next_wrapper"; import { warning } from "../toast/toast"; import { forceOnline } from "../devices/must_be_online"; import { Popover } from "../ui"; +import { RunButtonMenuOpen } from "./interfaces"; + +export const isMenuOpen = ( + state: RunButtonMenuOpen, + current: RunButtonMenuOpen, +): boolean => (state.component == current.component) && (state.uuid == current.uuid); + +const closedMenu = (): RunButtonMenuOpen => ({ + component: undefined, uuid: undefined, +}); /** Can't test without saving and syncing sequence. */ const saveAndSyncWarning = () => warning(t("Save sequence and sync device before running.")); /** Open or close the sequence test parameter assignment menu. */ -export const setMenuOpen = (payload: UUID | undefined) => ({ +export const setMenuOpen = (payload: RunButtonMenuOpen) => ({ type: Actions.SET_SEQUENCE_POPUP_STATE, payload }); @@ -43,12 +53,12 @@ class ParameterAssignmentMenu extends React.Component { componentWillUnmount() { - this.props.dispatch(setMenuOpen(undefined)); + this.props.dispatch(setMenuOpen(closedMenu())); } /** Click actions for test button inside parameter assignment menu. */ onClick = () => { - this.props.dispatch(setMenuOpen(undefined)); + this.props.dispatch(setMenuOpen(closedMenu())); this.props.canTest ? execSequence(this.props.sequence.body.id, this.props.bodyVariables) : saveAndSyncWarning(); @@ -98,7 +108,8 @@ export interface TestBtnProps { sequence: TaggedSequence; resources: ResourceIndex; /** Parameter assignment menu open? */ - menuOpen: UUID | undefined; + menuOpen: RunButtonMenuOpen; + component: RunButtonMenuOpen["component"]; dispatch: Function; } @@ -131,14 +142,16 @@ export class TestButton extends React.Component { /** Click actions for test button. */ onClick = () => { - const { dispatch, menuOpen, sequence } = this.props; + const { dispatch, menuOpen, sequence, component } = this.props; const sequenceBody = sequence.body; const bodyVariables = mergeParameterApplications(this.varData, this.state.bodyVariables); this.setState({ bodyVariables }); /** Open the variable menu if the sequence has parameter declarations. */ isParameterized(sequenceBody) && dispatch(setMenuOpen( - menuOpen == sequence.uuid ? undefined : sequence.uuid)); + isMenuOpen(menuOpen, { component, uuid: sequence.uuid }) + ? closedMenu() + : { component, uuid: sequence.uuid })); this.canTest /** Execute if sequence is synced, saved, and doesn't use parameters. */ ? !isParameterized(sequenceBody) && execSequence(sequenceBody.id) @@ -146,9 +159,9 @@ export class TestButton extends React.Component { }; render() { - const { menuOpen, sequence } = this.props; + const { menuOpen, sequence, component } = this.props; const hasMenu = isParameterized(this.props.sequence.body); - const isOpen = menuOpen == sequence.uuid; + const isOpen = isMenuOpen(menuOpen, { component, uuid: sequence.uuid }); return