From d906ba62be9edc5bf6fc57359f2f42d5ee0268dc Mon Sep 17 00:00:00 2001 From: Brian Cooper Date: Fri, 16 Nov 2018 18:19:54 -0500 Subject: [PATCH 1/8] proof of concept copy steps --- .../src/components/steplist/StepList.js | 5 +++- .../src/containers/ConnectedStepList.js | 1 + .../src/steplist/actions/thunks.js | 21 ++++++++++++++++ protocol-designer/src/steplist/reducers.js | 24 +++++++++++++++++++ 4 files changed, 50 insertions(+), 1 deletion(-) diff --git a/protocol-designer/src/components/steplist/StepList.js b/protocol-designer/src/components/steplist/StepList.js index 01d1d81b9dc..6c5f7ed9e1d 100644 --- a/protocol-designer/src/components/steplist/StepList.js +++ b/protocol-designer/src/components/steplist/StepList.js @@ -15,11 +15,12 @@ import {PortalRoot} from './TooltipPortal' type Props = { orderedSteps: Array, reorderSelectedStep: (delta: number) => mixed, + copySelectedStep: () => mixed, } export default class StepList extends React.Component { handleKeyDown = (e: SyntheticKeyboardEvent<*>) => { - const {reorderSelectedStep} = this.props + const {reorderSelectedStep, copySelectedStep} = this.props const key = e.key const altIsPressed = e.getModifierState('Alt') @@ -28,6 +29,8 @@ export default class StepList extends React.Component { reorderSelectedStep(-1) } else if (key === 'ArrowDown') { reorderSelectedStep(1) + } else if (key === 'ArrowRight') { + copySelectedStep() } } } diff --git a/protocol-designer/src/containers/ConnectedStepList.js b/protocol-designer/src/containers/ConnectedStepList.js index d12eccabdf0..228fc9d1639 100644 --- a/protocol-designer/src/containers/ConnectedStepList.js +++ b/protocol-designer/src/containers/ConnectedStepList.js @@ -25,6 +25,7 @@ function mapStateToProps (state: BaseState): SP { function mapDispatchToProps (dispatch: ThunkDispatch<*>): DP { return { + copySelectedStep: () => dispatch(steplistActions.copySelectedStep()), reorderSelectedStep: (delta: number) => dispatch(steplistActions.reorderSelectedStep(delta)), } diff --git a/protocol-designer/src/steplist/actions/thunks.js b/protocol-designer/src/steplist/actions/thunks.js index 3cc97f3be91..cbadc784678 100644 --- a/protocol-designer/src/steplist/actions/thunks.js +++ b/protocol-designer/src/steplist/actions/thunks.js @@ -108,3 +108,24 @@ export const reorderSelectedStep = (delta: number) => }) } } + +export type CopySelectedStepAction = { + type: 'COPY_SELECTED_STEP', + payload: { + selectedStepId: StepIdType, + nextStepId: StepIdType, + }, +} + +export const copySelectedStep = (delta: number) => + (dispatch: ThunkDispatch, getState: GetState) => { + const selectedStepId = selectors.getSelectedStepId(getState()) + const nextStepId = uuid() + + if (selectedStepId != null) { + dispatch({ + type: 'COPY_SELECTED_STEP', + payload: { selectedStepId, nextStepId }, + }) + } + } diff --git a/protocol-designer/src/steplist/reducers.js b/protocol-designer/src/steplist/reducers.js index 777a3a91316..5de78985c2a 100644 --- a/protocol-designer/src/steplist/reducers.js +++ b/protocol-designer/src/steplist/reducers.js @@ -127,6 +127,13 @@ const steps: Reducer = handleActions({ } }, {...initialStepState}) }, + COPY_SELECTED_STEP: (state: SavedStepFormState, action: CopySelectedStepAction): SavedStepFormState => ({ + ...state, + [action.payload.nextStepId]: { + ...(action.payload.selectedStepId ? state[action.payload.selectedStepId] : {}), + id: action.payload.nextStepId, + }, + }), }, initialStepState) type SavedStepFormState = { @@ -173,6 +180,13 @@ const savedStepForms: Reducer = handleActions({ ...action.payload.update, }, }), + COPY_SELECTED_STEP: (state: SavedStepFormState, action: CopySelectedStepAction): SavedStepFormState => ({ + ...state, + [action.payload.nextStepId]: { + ...(action.payload.selectedStepId ? state[action.payload.selectedStepId] : {}), + id: action.payload.nextStepId, + }, + }), }, {}) type CollapsedStepsState = { @@ -221,6 +235,16 @@ const orderedSteps: Reducer = handleActions({ ...stepsWithoutSelectedStep.slice(nextIndex), ] }, + COPY_SELECTED_STEP: (state: OrderedStepsState, action: CopySelectedStepAction): OrderedStepsState => { + const {selectedStepId, nextStepId} = action.payload + const selectedIndex = state.findIndex(s => s === selectedStepId) + + return [ + ...state.slice(0, selectedIndex + 1), + nextStepId, + ...state.slice(selectedIndex + 1, state.length), + ] + }, }, []) export type SelectableItem = { From e13e632612a014ce4052f2f220c73a0c83b7ad8d Mon Sep 17 00:00:00 2001 From: Brian Cooper Date: Mon, 26 Nov 2018 13:24:16 -0500 Subject: [PATCH 2/8] feat(protocol-designer): implement copy selected step extend steplist hotkey functionality to allow copying the selected step with the alt+ctl+arrow keys --- .../src/components/steplist/StepList.js | 12 +++++++----- .../src/containers/ConnectedStepList.js | 2 +- protocol-designer/src/steplist/actions/thunks.js | 5 +++-- protocol-designer/src/steplist/reducers.js | 11 +++++++---- 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/protocol-designer/src/components/steplist/StepList.js b/protocol-designer/src/components/steplist/StepList.js index 6c5f7ed9e1d..9623902aefb 100644 --- a/protocol-designer/src/components/steplist/StepList.js +++ b/protocol-designer/src/components/steplist/StepList.js @@ -15,7 +15,7 @@ import {PortalRoot} from './TooltipPortal' type Props = { orderedSteps: Array, reorderSelectedStep: (delta: number) => mixed, - copySelectedStep: () => mixed, + copySelectedStep: (delta: number) => mixed, } export default class StepList extends React.Component { @@ -23,15 +23,17 @@ export default class StepList extends React.Component { const {reorderSelectedStep, copySelectedStep} = this.props const key = e.key const altIsPressed = e.getModifierState('Alt') + const ctlIsPressed = e.getModifierState('Control') if (altIsPressed) { + let delta = 0 if (key === 'ArrowUp') { - reorderSelectedStep(-1) + delta = -1 } else if (key === 'ArrowDown') { - reorderSelectedStep(1) - } else if (key === 'ArrowRight') { - copySelectedStep() + delta = 1 } + if (!delta) return + ctlIsPressed ? copySelectedStep(delta) : reorderSelectedStep(delta) } } diff --git a/protocol-designer/src/containers/ConnectedStepList.js b/protocol-designer/src/containers/ConnectedStepList.js index 228fc9d1639..a104a453eeb 100644 --- a/protocol-designer/src/containers/ConnectedStepList.js +++ b/protocol-designer/src/containers/ConnectedStepList.js @@ -25,7 +25,7 @@ function mapStateToProps (state: BaseState): SP { function mapDispatchToProps (dispatch: ThunkDispatch<*>): DP { return { - copySelectedStep: () => dispatch(steplistActions.copySelectedStep()), + copySelectedStep: (delta: number) => dispatch(steplistActions.copySelectedStep(delta)), reorderSelectedStep: (delta: number) => dispatch(steplistActions.reorderSelectedStep(delta)), } diff --git a/protocol-designer/src/steplist/actions/thunks.js b/protocol-designer/src/steplist/actions/thunks.js index cbadc784678..acb34cea0d6 100644 --- a/protocol-designer/src/steplist/actions/thunks.js +++ b/protocol-designer/src/steplist/actions/thunks.js @@ -114,18 +114,19 @@ export type CopySelectedStepAction = { payload: { selectedStepId: StepIdType, nextStepId: StepIdType, + delta: number, }, } export const copySelectedStep = (delta: number) => - (dispatch: ThunkDispatch, getState: GetState) => { + (dispatch: ThunkDispatch, getState: GetState) => { const selectedStepId = selectors.getSelectedStepId(getState()) const nextStepId = uuid() if (selectedStepId != null) { dispatch({ type: 'COPY_SELECTED_STEP', - payload: { selectedStepId, nextStepId }, + payload: { selectedStepId, nextStepId, delta }, }) } } diff --git a/protocol-designer/src/steplist/reducers.js b/protocol-designer/src/steplist/reducers.js index 5de78985c2a..56c6631bd98 100644 --- a/protocol-designer/src/steplist/reducers.js +++ b/protocol-designer/src/steplist/reducers.js @@ -25,6 +25,7 @@ import type { ChangeFormInputAction, DeleteStepAction, ReorderSelectedStepAction, + CopySelectedStepAction, SaveStepFormAction, SelectStepAction, SelectTerminalItemAction, @@ -127,7 +128,7 @@ const steps: Reducer = handleActions({ } }, {...initialStepState}) }, - COPY_SELECTED_STEP: (state: SavedStepFormState, action: CopySelectedStepAction): SavedStepFormState => ({ + COPY_SELECTED_STEP: (state: StepsState, action: CopySelectedStepAction): StepsState => ({ ...state, [action.payload.nextStepId]: { ...(action.payload.selectedStepId ? state[action.payload.selectedStepId] : {}), @@ -236,13 +237,15 @@ const orderedSteps: Reducer = handleActions({ ] }, COPY_SELECTED_STEP: (state: OrderedStepsState, action: CopySelectedStepAction): OrderedStepsState => { - const {selectedStepId, nextStepId} = action.payload + const {selectedStepId, nextStepId, delta} = action.payload const selectedIndex = state.findIndex(s => s === selectedStepId) + if (delta <= 0 && selectedIndex === 0) return [nextStepId, ...state] + const boundary = delta <= 0 ? selectedIndex + 1 + delta : selectedIndex + delta return [ - ...state.slice(0, selectedIndex + 1), + ...state.slice(0, boundary), nextStepId, - ...state.slice(selectedIndex + 1, state.length), + ...state.slice(boundary, state.length), ] }, }, []) From 458ac8edeb74c2456044dea8aec34ad7d5db5823 Mon Sep 17 00:00:00 2001 From: Brian Cooper Date: Tue, 27 Nov 2018 15:54:00 -0500 Subject: [PATCH 3/8] confirm delete --- components/src/lists/TitledList.js | 6 +- .../src/components/steplist/ContextMenu.js | 130 ++++++++++++++++++ .../src/components/steplist/StepItem.css | 24 ++++ .../src/components/steplist/StepItem.js | 3 + .../src/components/steplist/StepList.js | 13 +- .../src/localization/en/context_menu.json | 6 + .../src/localization/en/index.js | 2 + .../src/localization/en/modal.json | 3 + .../src/steplist/actions/actions.js | 10 +- .../src/steplist/actions/thunks.js | 22 ++- .../stepFormToArgs/transferLikeFormToArgs.js | 1 - protocol-designer/src/steplist/reducers.js | 34 +++-- 12 files changed, 213 insertions(+), 41 deletions(-) create mode 100644 protocol-designer/src/components/steplist/ContextMenu.js create mode 100644 protocol-designer/src/localization/en/context_menu.json diff --git a/components/src/lists/TitledList.js b/components/src/lists/TitledList.js index 684eae73568..216c9233eb0 100644 --- a/components/src/lists/TitledList.js +++ b/components/src/lists/TitledList.js @@ -22,6 +22,8 @@ type ListProps = { description?: React.Node, /** optional click action (on title div, not children) */ onClick?: (event: SyntheticMouseEvent<>) => mixed, + /** optional right click action (on wrapping div) */ + onContextMenu?: (event: SyntheticMouseEvent<>) => mixed, /** optional mouseEnter action */ onMouseEnter?: (event: SyntheticMouseEvent<>) => mixed, /** optional mouseLeave action */ @@ -42,7 +44,7 @@ type ListProps = { * An ordered list with optional title, icon, and description. */ export default function TitledList (props: ListProps) { - const {iconName, disabled, onCollapseToggle, iconProps, onMouseEnter, onMouseLeave} = props + const {iconName, disabled, onCollapseToggle, iconProps, onMouseEnter, onMouseLeave, onContextMenu} = props const collapsible = onCollapseToggle != null const onClick = !disabled @@ -74,7 +76,7 @@ export default function TitledList (props: ListProps) { const iconClass = cx(styles.title_bar_icon, styles.icon_left_of_title, iconProps && iconProps.className) return ( -
+
{iconName && ( diff --git a/protocol-designer/src/components/steplist/ContextMenu.js b/protocol-designer/src/components/steplist/ContextMenu.js new file mode 100644 index 00000000000..7fd1bc1a3bb --- /dev/null +++ b/protocol-designer/src/components/steplist/ContextMenu.js @@ -0,0 +1,130 @@ +// @flow +import * as React from 'react' +import type {Dispatch} from 'redux' +import {connect} from 'react-redux' +import {ContinueModal} from '@opentrons/components' +import i18n from '../../localization' +import {actions as steplistActions} from '../../steplist' +import modalStyles from '../modals/modal.css' +import {Portal} from '../portals/TopPortal' +import type {StepIdType} from '../../form-types' +import styles from './StepItem.css' +import ConfirmDeleteModal from '../StepEditForm/ConfirmDeleteModal' + +const MENU_OFFSET_PX = 5 + +type DP = { + deleteStep: (StepIdType) => {}, + duplicateStep: (StepIdType) => {}, +} +type Props = {children: ({onContextMenu: (event: SyntheticMouseEvent<>) => mixed}) => React.Node} & DP +type State = { + visible: boolean, + left: ?number, + top: ?number, + stepId: ?StepIdType, + showConfirmDeleteModal: boolean, +} + +class ContextMenu extends React.Component { + state = { + visible: false, + left: null, + top: null, + stepId: null, + showConfirmDeleteModal: false, + } + menuRoot: HTMLElement + + componentDidMount () { + document.addEventListener('click', this.handleClick) + } + + componentWillUnmount () { + document.removeEventListener('click', this.handleClick) + } + + makeHandleContextMenu = (stepId: StepIdType) => (event) => { + event.preventDefault() + + const clickX = event.clientX + const clickY = event.clientY + + this.setState({visible: true, stepId}, () => { + const screenW = window.innerWidth + const screenH = window.innerHeight + const rootW = this.menuRoot.offsetWidth + const rootH = this.menuRoot.offsetHeight + + const left = (screenW - clickX) > rootW ? clickX + MENU_OFFSET_PX : clickX - rootW - MENU_OFFSET_PX + const top = (screenH - clickY) > rootH ? clickY + MENU_OFFSET_PX : clickY - rootH - MENU_OFFSET_PX + this.setState({left, top}) + }) + } + + handleClick = (event) => { + const { visible } = this.state + const wasOutside = !(event.target.contains === this.root) + + if (wasOutside && visible) this.setState({visible: false, stepId: null, left: null, top: null}) + } + + handleDuplicate = () => { + this.props.duplicateStep(this.state.stepId) + } + + toggleConfirmDeleteModal = () => { + this.setState({showConfirmDeleteModal: !this.state.showConfirmDeleteModal}) + } + + handleDelete = () => { + this.toggleConfirmDeleteModal() + this.props.deleteStep(this.state.stepId) + } + + render () { + return ( +
+ {this.props.children({makeStepOnContextMenu: this.makeHandleContextMenu})} + {this.state.visible && + + +
{ this.menuRoot = ref }} + style={{left: this.state.left, top: this.state.top}} + className={styles.context_menu}> +
+ {i18n.t('context_menu.step.duplicate')} +
+
+ {i18n.t('context_menu.step.delete')} +
+
+
+
+ } + {this.state.showConfirmDeleteModal && + + + {i18n.t('modal.delete_step.confirm')} + + + } +
+ ) + } +} + +const mapDispatchToProps = (dispatch: Dispatch<*>) => ({ + deleteStep: (stepId: StepIdType) => dispatch(steplistActions.deleteStep(stepId)), + duplicateStep: (stepId: StepIdType) => dispatch(steplistActions.duplicateStep(stepId)), +}) + +export default connect(null, mapDispatchToProps)(ContextMenu) diff --git a/protocol-designer/src/components/steplist/StepItem.css b/protocol-designer/src/components/steplist/StepItem.css index a28cee4992a..ecb541a34dd 100644 --- a/protocol-designer/src/components/steplist/StepItem.css +++ b/protocol-designer/src/components/steplist/StepItem.css @@ -151,3 +151,27 @@ .labware_display_name { cursor: default; } + +.context_menu { + position: absolute; + background: white; + box-shadow: 0 2px 10px #999; + z-index: 100000; +} + +.context_menu_item { + padding: 6px 50px 5px 10px; + min-width: 160px; + cursor: default; + font-size: 12px; +} + +.context_menu_item:hover { + background: linear-gradient(to top, #555, #333); + color: white; +} + +.context_menu_item:active { + color: #e9e9e9; + background: linear-gradient(to top, #555, #444); +} diff --git a/protocol-designer/src/components/steplist/StepItem.js b/protocol-designer/src/components/steplist/StepItem.js index 133bbfdc9ee..45c98883ac0 100644 --- a/protocol-designer/src/components/steplist/StepItem.js +++ b/protocol-designer/src/components/steplist/StepItem.js @@ -31,6 +31,7 @@ type StepItemProps = { getLabware: (labwareId: ?string) => ?Labware, handleSubstepHover: SubstepIdentifier => mixed, onStepClick?: (event?: SyntheticEvent<>) => mixed, + onStepContextMenu?: (event?: SyntheticEvent<>) => mixed, onStepItemCollapseToggle?: (event?: SyntheticEvent<>) => mixed, onStepHover?: (event?: SyntheticEvent<>) => mixed, onStepMouseLeave?: (event?: SyntheticEvent<>) => mixed, @@ -47,6 +48,7 @@ export default function StepItem (props: StepItemProps) { onStepMouseLeave, onStepClick, + onStepContextMenu, onStepItemCollapseToggle, onStepHover, } = props @@ -62,6 +64,7 @@ export default function StepItem (props: StepItemProps) { iconProps={{className: error ? styles.error_icon : ''}} title={title || ''} onClick={onStepClick} + onContextMenu={onStepContextMenu} onMouseEnter={onStepHover} onMouseLeave={onStepMouseLeave} onCollapseToggle={onStepItemCollapseToggle} diff --git a/protocol-designer/src/components/steplist/StepList.js b/protocol-designer/src/components/steplist/StepList.js index 9623902aefb..7ff9176fe48 100644 --- a/protocol-designer/src/components/steplist/StepList.js +++ b/protocol-designer/src/components/steplist/StepList.js @@ -11,6 +11,7 @@ import {END_TERMINAL_ITEM_ID} from '../../steplist' import type {StepIdType} from '../../form-types' import {PortalRoot} from './TooltipPortal' +import ContextMenu from './ContextMenu' type Props = { orderedSteps: Array, @@ -48,8 +49,16 @@ export default class StepList extends React.Component { render () { const {orderedSteps} = this.props - const stepItems = orderedSteps.map((stepId: StepIdType) => - ) + const stepItems = ( + + {({makeStepOnContextMenu}) => orderedSteps.map((stepId: StepIdType) => ( + + ))} + + ) return ( diff --git a/protocol-designer/src/localization/en/context_menu.json b/protocol-designer/src/localization/en/context_menu.json new file mode 100644 index 00000000000..e78b8340482 --- /dev/null +++ b/protocol-designer/src/localization/en/context_menu.json @@ -0,0 +1,6 @@ +{ + "step": { + "duplicate": "Duplicate Step", + "delete": "Delete Step" + } +} \ No newline at end of file diff --git a/protocol-designer/src/localization/en/index.js b/protocol-designer/src/localization/en/index.js index 22235ff6b04..f97ff3ad6d9 100644 --- a/protocol-designer/src/localization/en/index.js +++ b/protocol-designer/src/localization/en/index.js @@ -3,6 +3,7 @@ import alert from './alert.json' import button from './button.json' import card from './card.json' +import context_menu from './context_menu.json' import deck from './deck.json' import form from './form.json' import modal from './modal.json' @@ -15,6 +16,7 @@ export default { alert, button, card, + context_menu, deck, form, modal, diff --git a/protocol-designer/src/localization/en/modal.json b/protocol-designer/src/localization/en/modal.json index 240ca12d4f5..085bb38a965 100644 --- a/protocol-designer/src/localization/en/modal.json +++ b/protocol-designer/src/localization/en/modal.json @@ -42,5 +42,8 @@ "next_pipette_smaller": "Number of tips used may increase" } } + }, + "delete_step": { + "confirm": "Are you sure you want to delete this step?" } } diff --git a/protocol-designer/src/steplist/actions/actions.js b/protocol-designer/src/steplist/actions/actions.js index d101c24edf6..0b2768ba865 100644 --- a/protocol-designer/src/steplist/actions/actions.js +++ b/protocol-designer/src/steplist/actions/actions.js @@ -53,12 +53,10 @@ export type DeleteStepAction = { payload: StepIdType, } -export const deleteStep = () => (dispatch: Dispatch<*>, getState: GetState) => { - dispatch({ - type: 'DELETE_STEP', - payload: selectors.getSelectedStepId(getState()), - }) -} +export const deleteStep = (stepId: StepIdType) => ({ + type: 'DELETE_STEP', + payload: stepId, +}) type ExpandAddStepButtonAction = { type: 'EXPAND_ADD_STEP_BUTTON', diff --git a/protocol-designer/src/steplist/actions/thunks.js b/protocol-designer/src/steplist/actions/thunks.js index acb34cea0d6..c4c5461acd6 100644 --- a/protocol-designer/src/steplist/actions/thunks.js +++ b/protocol-designer/src/steplist/actions/thunks.js @@ -109,24 +109,22 @@ export const reorderSelectedStep = (delta: number) => } } -export type CopySelectedStepAction = { - type: 'COPY_SELECTED_STEP', +export type DuplicateStepAction = { + type: 'DUPLICATE_STEP', payload: { - selectedStepId: StepIdType, - nextStepId: StepIdType, - delta: number, + stepId: StepIdType, + duplicateStepId: StepIdType, }, } -export const copySelectedStep = (delta: number) => - (dispatch: ThunkDispatch, getState: GetState) => { - const selectedStepId = selectors.getSelectedStepId(getState()) - const nextStepId = uuid() +export const duplicateStep = (stepId: StepIdType) => + (dispatch: ThunkDispatch, getState: GetState) => { + const duplicateStepId = uuid() - if (selectedStepId != null) { + if (stepId != null) { dispatch({ - type: 'COPY_SELECTED_STEP', - payload: { selectedStepId, nextStepId, delta }, + type: 'DUPLICATE_STEP', + payload: {stepId, duplicateStepId}, }) } } diff --git a/protocol-designer/src/steplist/formLevel/stepFormToArgs/transferLikeFormToArgs.js b/protocol-designer/src/steplist/formLevel/stepFormToArgs/transferLikeFormToArgs.js index 735ca95a7c9..12f68189207 100644 --- a/protocol-designer/src/steplist/formLevel/stepFormToArgs/transferLikeFormToArgs.js +++ b/protocol-designer/src/steplist/formLevel/stepFormToArgs/transferLikeFormToArgs.js @@ -26,7 +26,6 @@ type TransferLikeStepArgs = ConsolidateFormData | DistributeFormData | TransferF // TODO: BC 2018-10-30 move getting labwareDef into hydration layer upstream const transferLikeFormToArgs = (hydratedFormData: FormData): TransferLikeStepArgs => { - console.log([hydratedFormData]) const stepType = hydratedFormData.stepType const pipette = hydratedFormData['pipette'] const volume = Number(hydratedFormData['volume']) diff --git a/protocol-designer/src/steplist/reducers.js b/protocol-designer/src/steplist/reducers.js index 56c6631bd98..d51609e14f5 100644 --- a/protocol-designer/src/steplist/reducers.js +++ b/protocol-designer/src/steplist/reducers.js @@ -25,7 +25,7 @@ import type { ChangeFormInputAction, DeleteStepAction, ReorderSelectedStepAction, - CopySelectedStepAction, + DuplicateStepAction, SaveStepFormAction, SelectStepAction, SelectTerminalItemAction, @@ -128,11 +128,11 @@ const steps: Reducer = handleActions({ } }, {...initialStepState}) }, - COPY_SELECTED_STEP: (state: StepsState, action: CopySelectedStepAction): StepsState => ({ + DUPLICATE_STEP: (state: StepsState, action: DuplicateStepAction): StepsState => ({ ...state, - [action.payload.nextStepId]: { - ...(action.payload.selectedStepId ? state[action.payload.selectedStepId] : {}), - id: action.payload.nextStepId, + [action.payload.duplicateStepId]: { + ...(action.payload.stepId !== null ? state[action.payload.stepId] : {}), + id: action.payload.duplicateStepId, }, }), }, initialStepState) @@ -177,15 +177,15 @@ const savedStepForms: Reducer = handleActions({ CHANGE_SAVED_STEP_FORM: (state: SavedStepFormState, action: ChangeSavedStepFormAction): SavedStepFormState => ({ ...state, [action.payload.stepId]: { - ...(action.payload.stepId ? state[action.payload.stepId] : {}), + ...(action.payload.stepId !== null ? state[action.payload.stepId] : {}), ...action.payload.update, }, }), - COPY_SELECTED_STEP: (state: SavedStepFormState, action: CopySelectedStepAction): SavedStepFormState => ({ + DUPLICATE_STEP: (state: SavedStepFormState, action: DuplicateStepAction): SavedStepFormState => ({ ...state, - [action.payload.nextStepId]: { - ...(action.payload.selectedStepId ? state[action.payload.selectedStepId] : {}), - id: action.payload.nextStepId, + [action.payload.duplicateStepId]: { + ...(action.payload.stepId !== null ? state[action.payload.stepId] : {}), + id: action.payload.duplicateStepId, }, }), }, {}) @@ -236,16 +236,14 @@ const orderedSteps: Reducer = handleActions({ ...stepsWithoutSelectedStep.slice(nextIndex), ] }, - COPY_SELECTED_STEP: (state: OrderedStepsState, action: CopySelectedStepAction): OrderedStepsState => { - const {selectedStepId, nextStepId, delta} = action.payload - const selectedIndex = state.findIndex(s => s === selectedStepId) + DUPLICATE_STEP: (state: OrderedStepsState, action: DuplicateStepAction): OrderedStepsState => { + const {stepId, duplicateStepId} = action.payload + const selectedIndex = state.findIndex(s => s === stepId) - if (delta <= 0 && selectedIndex === 0) return [nextStepId, ...state] - const boundary = delta <= 0 ? selectedIndex + 1 + delta : selectedIndex + delta return [ - ...state.slice(0, boundary), - nextStepId, - ...state.slice(boundary, state.length), + ...state.slice(0, selectedIndex + 1), + duplicateStepId, + ...state.slice(selectedIndex + 1, state.length), ] }, }, []) From d86cb78f4900c27b461a83887c93903e7157ec25 Mon Sep 17 00:00:00 2001 From: Brian Cooper Date: Tue, 27 Nov 2018 17:15:12 -0500 Subject: [PATCH 4/8] modal stylings --- .../src/components/steplist/ContextMenu.js | 29 ++++--------------- .../src/components/steplist/StepItem.css | 19 +++++++----- .../src/localization/en/alert.json | 1 + .../src/localization/en/modal.json | 3 -- 4 files changed, 19 insertions(+), 33 deletions(-) diff --git a/protocol-designer/src/components/steplist/ContextMenu.js b/protocol-designer/src/components/steplist/ContextMenu.js index 7fd1bc1a3bb..e9fdacb9d1e 100644 --- a/protocol-designer/src/components/steplist/ContextMenu.js +++ b/protocol-designer/src/components/steplist/ContextMenu.js @@ -2,14 +2,11 @@ import * as React from 'react' import type {Dispatch} from 'redux' import {connect} from 'react-redux' -import {ContinueModal} from '@opentrons/components' import i18n from '../../localization' import {actions as steplistActions} from '../../steplist' -import modalStyles from '../modals/modal.css' import {Portal} from '../portals/TopPortal' import type {StepIdType} from '../../form-types' import styles from './StepItem.css' -import ConfirmDeleteModal from '../StepEditForm/ConfirmDeleteModal' const MENU_OFFSET_PX = 5 @@ -23,7 +20,6 @@ type State = { left: ?number, top: ?number, stepId: ?StepIdType, - showConfirmDeleteModal: boolean, } class ContextMenu extends React.Component { @@ -32,7 +28,6 @@ class ContextMenu extends React.Component { left: null, top: null, stepId: null, - showConfirmDeleteModal: false, } menuRoot: HTMLElement @@ -66,20 +61,18 @@ class ContextMenu extends React.Component { const { visible } = this.state const wasOutside = !(event.target.contains === this.root) - if (wasOutside && visible) this.setState({visible: false, stepId: null, left: null, top: null}) + if (wasOutside && visible) this.setState({visible: false, left: null, top: null}) } handleDuplicate = () => { this.props.duplicateStep(this.state.stepId) } - toggleConfirmDeleteModal = () => { - this.setState({showConfirmDeleteModal: !this.state.showConfirmDeleteModal}) - } - handleDelete = () => { - this.toggleConfirmDeleteModal() - this.props.deleteStep(this.state.stepId) + if (confirm(i18n.t('alert.confirm_delete_step'))) { + this.props.deleteStep(this.state.stepId) + this.setState({stepId: null}) + } } render () { @@ -99,7 +92,7 @@ class ContextMenu extends React.Component { {i18n.t('context_menu.step.duplicate')}
{i18n.t('context_menu.step.delete')}
@@ -107,16 +100,6 @@ class ContextMenu extends React.Component { } - {this.state.showConfirmDeleteModal && - - - {i18n.t('modal.delete_step.confirm')} - - - }
) } diff --git a/protocol-designer/src/components/steplist/StepItem.css b/protocol-designer/src/components/steplist/StepItem.css index ecb541a34dd..9cc4cfc87e3 100644 --- a/protocol-designer/src/components/steplist/StepItem.css +++ b/protocol-designer/src/components/steplist/StepItem.css @@ -154,8 +154,9 @@ .context_menu { position: absolute; - background: white; - box-shadow: 0 2px 10px #999; + color: var(--c-font-light); + background-color: var(--c-bg-dark); + box-shadow: 0 2px 6px #999; z-index: 100000; } @@ -163,15 +164,19 @@ padding: 6px 50px 5px 10px; min-width: 160px; cursor: default; - font-size: 12px; + font-size: var(--fs-body-1); } .context_menu_item:hover { - background: linear-gradient(to top, #555, #333); - color: white; + background-color: color(var(--c-bg-dark) shade(30%)); + color: var(--c-font-light); } .context_menu_item:active { - color: #e9e9e9; - background: linear-gradient(to top, #555, #444); + color: white; + background-color: color(var(--c-bg-dark) shade(30%)); +} + +.confirm_delete_modal { + z-index: 1100; } diff --git a/protocol-designer/src/localization/en/alert.json b/protocol-designer/src/localization/en/alert.json index 824e7bcc7e6..d74a8af1d22 100644 --- a/protocol-designer/src/localization/en/alert.json +++ b/protocol-designer/src/localization/en/alert.json @@ -34,6 +34,7 @@ }, "window": { "confirm_create_new": "Are you sure you want to create a new file? Any unsaved changes will be discarded.", + "confirm_delete_step": "Are you sure you want to delete this step?", "confirm_import": "Are you sure you want to import this file? You will lose any current unsaved changes.", "confirm_leave": "Are you sure you want to leave? You will lose any unsaved changes." } diff --git a/protocol-designer/src/localization/en/modal.json b/protocol-designer/src/localization/en/modal.json index 085bb38a965..240ca12d4f5 100644 --- a/protocol-designer/src/localization/en/modal.json +++ b/protocol-designer/src/localization/en/modal.json @@ -42,8 +42,5 @@ "next_pipette_smaller": "Number of tips used may increase" } } - }, - "delete_step": { - "confirm": "Are you sure you want to delete this step?" } } From af902f0381bf0ceda9ec503f85767e972e851361 Mon Sep 17 00:00:00 2001 From: Brian Cooper Date: Tue, 27 Nov 2018 17:48:40 -0500 Subject: [PATCH 5/8] iron out flow --- .../__snapshots__/lists.test.js.snap | 6 ++++ .../src/components/StepEditForm/index.js | 8 +++--- .../src/components/steplist/ContextMenu.js | 28 +++++++++++-------- .../src/components/steplist/StepList.js | 6 ++-- .../src/containers/ConnectedStepList.js | 1 - protocol-designer/src/steplist/reducers.js | 6 ++-- 6 files changed, 31 insertions(+), 24 deletions(-) diff --git a/components/src/__tests__/__snapshots__/lists.test.js.snap b/components/src/__tests__/__snapshots__/lists.test.js.snap index 8e825c41569..9553b70e3d8 100644 --- a/components/src/__tests__/__snapshots__/lists.test.js.snap +++ b/components/src/__tests__/__snapshots__/lists.test.js.snap @@ -111,6 +111,7 @@ exports[`ListItem renders ListItem without icon correctly 1`] = ` exports[`TitledList renders TitledList with children correctly 1`] = `
@@ -137,6 +138,7 @@ exports[`TitledList renders TitledList with children correctly 1`] = ` exports[`TitledList renders TitledList with onMouseEnter & onMouseLeave correctly 1`] = `
@@ -156,6 +158,7 @@ exports[`TitledList renders TitledList with onMouseEnter & onMouseLeave correctl exports[`TitledList renders TitledList with optional icon correctly 1`] = `
@@ -175,6 +178,7 @@ exports[`TitledList renders TitledList with optional icon correctly 1`] = ` exports[`TitledList renders TitledList without icon correctly 1`] = `
@@ -194,6 +198,7 @@ exports[`TitledList renders TitledList without icon correctly 1`] = ` exports[`TitledList renders collapsed TitledList correctly 1`] = `
@@ -233,6 +238,7 @@ exports[`TitledList renders collapsed TitledList correctly 1`] = ` exports[`TitledList renders expanded TitledList correctly 1`] = `
diff --git a/protocol-designer/src/components/StepEditForm/index.js b/protocol-designer/src/components/StepEditForm/index.js index 340836b98c1..2df16d80667 100644 --- a/protocol-designer/src/components/StepEditForm/index.js +++ b/protocol-designer/src/components/StepEditForm/index.js @@ -6,7 +6,7 @@ import without from 'lodash/without' import cx from 'classnames' import {actions, selectors} from '../../steplist' -import type {FormData, StepType, StepFieldName} from '../../form-types' +import type {FormData, StepType, StepFieldName, StepIdType} from '../../form-types' import type {BaseState, ThunkDispatch} from '../../types' import formStyles from '../forms.css' import styles from './StepEditForm.css' @@ -37,7 +37,7 @@ type SP = { formData?: ?FormData, isNewStep?: boolean, } -type DP = { deleteStep: () => mixed } +type DP = { deleteStep: (StepIdType) => mixed } type StepEditFormState = { showConfirmDeleteModal: boolean, @@ -101,7 +101,7 @@ class StepEditForm extends React.Component { onCancelClick={this.toggleConfirmDeleteModal} onContinueClick={() => { this.toggleConfirmDeleteModal() - deleteStep() + this.props.formData && deleteStep(this.props.formData.id) }} />}
@@ -127,7 +127,7 @@ const mapStateToProps = (state: BaseState): SP => ({ }) const mapDispatchToProps = (dispatch: ThunkDispatch<*>): DP => ({ - deleteStep: () => dispatch(actions.deleteStep()), + deleteStep: (stepId: StepIdType) => dispatch(actions.deleteStep(stepId)), }) export default connect(mapStateToProps, mapDispatchToProps)(StepEditForm) diff --git a/protocol-designer/src/components/steplist/ContextMenu.js b/protocol-designer/src/components/steplist/ContextMenu.js index e9fdacb9d1e..85859532904 100644 --- a/protocol-designer/src/components/steplist/ContextMenu.js +++ b/protocol-designer/src/components/steplist/ContextMenu.js @@ -1,7 +1,7 @@ // @flow import * as React from 'react' -import type {Dispatch} from 'redux' import {connect} from 'react-redux' +import type {ThunkDispatch} from '../../types' import i18n from '../../localization' import {actions as steplistActions} from '../../steplist' import {Portal} from '../portals/TopPortal' @@ -14,7 +14,9 @@ type DP = { deleteStep: (StepIdType) => {}, duplicateStep: (StepIdType) => {}, } -type Props = {children: ({onContextMenu: (event: SyntheticMouseEvent<>) => mixed}) => React.Node} & DP +type Props = { + children: ({makeStepOnContextMenu: (StepIdType) => (event: SyntheticMouseEvent<>) => mixed}) => React.Node, +} & DP type State = { visible: boolean, left: ?number, @@ -29,14 +31,14 @@ class ContextMenu extends React.Component { top: null, stepId: null, } - menuRoot: HTMLElement + menuRoot: ?HTMLElement componentDidMount () { - document.addEventListener('click', this.handleClick) + global.addEventListener('click', this.handleClick) } componentWillUnmount () { - document.removeEventListener('click', this.handleClick) + global.removeEventListener('click', this.handleClick) } makeHandleContextMenu = (stepId: StepIdType) => (event) => { @@ -48,8 +50,8 @@ class ContextMenu extends React.Component { this.setState({visible: true, stepId}, () => { const screenW = window.innerWidth const screenH = window.innerHeight - const rootW = this.menuRoot.offsetWidth - const rootH = this.menuRoot.offsetHeight + const rootW = this.menuRoot ? this.menuRoot.offsetWidth : 0 + const rootH = this.menuRoot ? this.menuRoot.offsetHeight : 0 const left = (screenW - clickX) > rootW ? clickX + MENU_OFFSET_PX : clickX - rootW - MENU_OFFSET_PX const top = (screenH - clickY) > rootH ? clickY + MENU_OFFSET_PX : clickY - rootH - MENU_OFFSET_PX @@ -57,19 +59,21 @@ class ContextMenu extends React.Component { }) } - handleClick = (event) => { + handleClick = (event: SyntheticMouseEvent<*>) => { const { visible } = this.state - const wasOutside = !(event.target.contains === this.root) + const wasOutside = !(this.menuRoot && this.menuRoot.contains(event.currentTarget)) if (wasOutside && visible) this.setState({visible: false, left: null, top: null}) } handleDuplicate = () => { - this.props.duplicateStep(this.state.stepId) + if (this.state.stepId != null) { + this.props.duplicateStep(this.state.stepId) + } } handleDelete = () => { - if (confirm(i18n.t('alert.confirm_delete_step'))) { + if (this.state.stepId != null && confirm(i18n.t('alert.window.confirm_delete_step'))) { this.props.deleteStep(this.state.stepId) this.setState({stepId: null}) } @@ -105,7 +109,7 @@ class ContextMenu extends React.Component { } } -const mapDispatchToProps = (dispatch: Dispatch<*>) => ({ +const mapDispatchToProps = (dispatch: ThunkDispatch<*>) => ({ deleteStep: (stepId: StepIdType) => dispatch(steplistActions.deleteStep(stepId)), duplicateStep: (stepId: StepIdType) => dispatch(steplistActions.duplicateStep(stepId)), }) diff --git a/protocol-designer/src/components/steplist/StepList.js b/protocol-designer/src/components/steplist/StepList.js index 7ff9176fe48..3c0233605c2 100644 --- a/protocol-designer/src/components/steplist/StepList.js +++ b/protocol-designer/src/components/steplist/StepList.js @@ -16,15 +16,13 @@ import ContextMenu from './ContextMenu' type Props = { orderedSteps: Array, reorderSelectedStep: (delta: number) => mixed, - copySelectedStep: (delta: number) => mixed, } export default class StepList extends React.Component { handleKeyDown = (e: SyntheticKeyboardEvent<*>) => { - const {reorderSelectedStep, copySelectedStep} = this.props + const {reorderSelectedStep} = this.props const key = e.key const altIsPressed = e.getModifierState('Alt') - const ctlIsPressed = e.getModifierState('Control') if (altIsPressed) { let delta = 0 @@ -34,7 +32,7 @@ export default class StepList extends React.Component { delta = 1 } if (!delta) return - ctlIsPressed ? copySelectedStep(delta) : reorderSelectedStep(delta) + reorderSelectedStep(delta) } } diff --git a/protocol-designer/src/containers/ConnectedStepList.js b/protocol-designer/src/containers/ConnectedStepList.js index a104a453eeb..d12eccabdf0 100644 --- a/protocol-designer/src/containers/ConnectedStepList.js +++ b/protocol-designer/src/containers/ConnectedStepList.js @@ -25,7 +25,6 @@ function mapStateToProps (state: BaseState): SP { function mapDispatchToProps (dispatch: ThunkDispatch<*>): DP { return { - copySelectedStep: (delta: number) => dispatch(steplistActions.copySelectedStep(delta)), reorderSelectedStep: (delta: number) => dispatch(steplistActions.reorderSelectedStep(delta)), } diff --git a/protocol-designer/src/steplist/reducers.js b/protocol-designer/src/steplist/reducers.js index d51609e14f5..641f9a47b82 100644 --- a/protocol-designer/src/steplist/reducers.js +++ b/protocol-designer/src/steplist/reducers.js @@ -131,7 +131,7 @@ const steps: Reducer = handleActions({ DUPLICATE_STEP: (state: StepsState, action: DuplicateStepAction): StepsState => ({ ...state, [action.payload.duplicateStepId]: { - ...(action.payload.stepId !== null ? state[action.payload.stepId] : {}), + ...(action.payload.stepId != null ? state[action.payload.stepId] : {}), id: action.payload.duplicateStepId, }, }), @@ -177,14 +177,14 @@ const savedStepForms: Reducer = handleActions({ CHANGE_SAVED_STEP_FORM: (state: SavedStepFormState, action: ChangeSavedStepFormAction): SavedStepFormState => ({ ...state, [action.payload.stepId]: { - ...(action.payload.stepId !== null ? state[action.payload.stepId] : {}), + ...(action.payload.stepId != null ? state[action.payload.stepId] : {}), ...action.payload.update, }, }), DUPLICATE_STEP: (state: SavedStepFormState, action: DuplicateStepAction): SavedStepFormState => ({ ...state, [action.payload.duplicateStepId]: { - ...(action.payload.stepId !== null ? state[action.payload.stepId] : {}), + ...(action.payload.stepId != null ? state[action.payload.stepId] : {}), id: action.payload.duplicateStepId, }, }), From 39cbe200778239ac39f2785f10afe557556f275b Mon Sep 17 00:00:00 2001 From: Brian Cooper Date: Wed, 28 Nov 2018 10:30:58 -0500 Subject: [PATCH 6/8] fix close and make flow happy --- protocol-designer/src/components/steplist/ContextMenu.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/protocol-designer/src/components/steplist/ContextMenu.js b/protocol-designer/src/components/steplist/ContextMenu.js index 85859532904..63fa9abb075 100644 --- a/protocol-designer/src/components/steplist/ContextMenu.js +++ b/protocol-designer/src/components/steplist/ContextMenu.js @@ -61,7 +61,8 @@ class ContextMenu extends React.Component { handleClick = (event: SyntheticMouseEvent<*>) => { const { visible } = this.state - const wasOutside = !(this.menuRoot && this.menuRoot.contains(event.currentTarget)) + + const wasOutside = !(this.menuRoot && event.target instanceof Node && this.menuRoot.contains(event.target)) if (wasOutside && visible) this.setState({visible: false, left: null, top: null}) } From ce5e3d0e1afaf6076c367c54586ee4e73736911c Mon Sep 17 00:00:00 2001 From: Brian Cooper Date: Wed, 5 Dec 2018 11:04:22 -0500 Subject: [PATCH 7/8] fix persistent menu --- protocol-designer/src/components/steplist/ContextMenu.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/protocol-designer/src/components/steplist/ContextMenu.js b/protocol-designer/src/components/steplist/ContextMenu.js index 63fa9abb075..acdf028371b 100644 --- a/protocol-designer/src/components/steplist/ContextMenu.js +++ b/protocol-designer/src/components/steplist/ContextMenu.js @@ -70,13 +70,14 @@ class ContextMenu extends React.Component { handleDuplicate = () => { if (this.state.stepId != null) { this.props.duplicateStep(this.state.stepId) + this.setState({stepId: null, visible: false}) } } handleDelete = () => { if (this.state.stepId != null && confirm(i18n.t('alert.window.confirm_delete_step'))) { this.props.deleteStep(this.state.stepId) - this.setState({stepId: null}) + this.setState({stepId: null, visible: false}) } } From f695e684a641ad6d4e6ab78ff597fb26e6b33184 Mon Sep 17 00:00:00 2001 From: Brian Cooper Date: Wed, 5 Dec 2018 11:12:03 -0500 Subject: [PATCH 8/8] unused class --- protocol-designer/src/components/steplist/StepItem.css | 4 ---- 1 file changed, 4 deletions(-) diff --git a/protocol-designer/src/components/steplist/StepItem.css b/protocol-designer/src/components/steplist/StepItem.css index 9cc4cfc87e3..e6c34196353 100644 --- a/protocol-designer/src/components/steplist/StepItem.css +++ b/protocol-designer/src/components/steplist/StepItem.css @@ -176,7 +176,3 @@ color: white; background-color: color(var(--c-bg-dark) shade(30%)); } - -.confirm_delete_modal { - z-index: 1100; -}