From f5d6d529a58465204f24b2e8abdc84e35b991cb3 Mon Sep 17 00:00:00 2001 From: Filipe Freire Date: Tue, 9 May 2023 15:13:57 +0100 Subject: [PATCH] Add drag and drop to 'Manage Environments' (#5940) * wip: environments drag and drop with react-aria Co-authored-by: James Gatz * wip * wip * 1st working version * wip * working! * wip * rm outline * type issues * add redux hack * set active * refactor and rename * add aria label to fix warning --------- Co-authored-by: James Gatz Co-authored-by: jackkav --- .../workspace-environments-edit-modal.tsx | 314 +++++++++++++----- .../ui/css/components/environment-modal.less | 61 +--- 2 files changed, 242 insertions(+), 133 deletions(-) diff --git a/packages/insomnia/src/ui/components/modals/workspace-environments-edit-modal.tsx b/packages/insomnia/src/ui/components/modals/workspace-environments-edit-modal.tsx index cc371164b..76de983c0 100644 --- a/packages/insomnia/src/ui/components/modals/workspace-environments-edit-modal.tsx +++ b/packages/insomnia/src/ui/components/modals/workspace-environments-edit-modal.tsx @@ -1,6 +1,8 @@ import classnames from 'classnames'; import React, { FC, forwardRef, Fragment, useImperativeHandle, useRef, useState } from 'react'; +import { ListDropTargetDelegate, ListKeyboardDelegate, mergeProps, useDraggableCollection, useDraggableItem, useDropIndicator, useDroppableCollection, useDroppableItem, useFocusRing, useListBox, useOption } from 'react-aria'; import { useSelector } from 'react-redux'; +import { DraggableCollectionState, DroppableCollectionState, Item, ListState, useDraggableCollectionState, useDroppableCollectionState, useListState } from 'react-stately'; import { docsTemplateTags } from '../../../common/documentation'; import * as models from '../../../models'; @@ -17,38 +19,24 @@ import { PromptButton } from '../base/prompt-button'; import { EnvironmentEditor, EnvironmentEditorHandle } from '../editors/environment-editor'; import { HelpTooltip } from '../help-tooltip'; import { Tooltip } from '../tooltip'; + const ROOT_ENVIRONMENT_NAME = 'Base Environment'; -interface SidebarListProps { - environments: Environment[]; - changeEnvironmentName: (environment: Environment, name?: string) => void; - selectedEnvironmentId: string | null; - showEnvironment: (id: string) => void; -} interface SidebarListItemProps { environment: Environment; - changeEnvironmentName: (environment: Environment, name?: string) => void; - selectedEnvironmentId: string | null; - showEnvironment: (id: string) => void; } + const SidebarListItem: FC = ({ - changeEnvironmentName, environment, - selectedEnvironmentId, - showEnvironment, }: SidebarListItemProps) => { const workspaceMeta = useSelector(selectActiveWorkspaceMeta); - - return (
  • - 'theme--dialog': true, - })} - > - -
    - {environment._id === workspaceMeta?.activeEnvironmentId ? ( - - ) : ( - - )} -
    -
  • ); + <>{environment.name} + ); }; -const SidebarList: FC = - ({ - changeEnvironmentName, - environments, - selectedEnvironmentId, - showEnvironment, - }: SidebarListProps) => { - return ( -
      - {environments.map(environment => - ( - ) +// @ts-expect-error props any +const DropIndicator = props => { + const ref = React.useRef(null); + const { dropIndicatorProps, isHidden, isDropTarget } = + useDropIndicator(props, props.dropState, ref); + if (isHidden) { + return null; + } + + return ( +
    • + ); +}; + +// @ts-expect-error Node not generic? +const ReorderableOption = ({ item, state, dragState, dropState }: { item: Node; state: ListState>; dragState: DraggableCollectionState; dropState: DroppableCollectionState }): JSX.Element => { + const ref = React.useRef(null); + const { optionProps } = useOption({ key: item.key }, state, ref); + const { focusProps } = useFocusRing(); + + // Register the item as a drop target. + const { dropProps } = useDroppableItem( + { + target: { type: 'item', key: item.key, dropPosition: 'on' }, + }, + dropState, + ref + ); + // Register the item as a drag source. + const { dragProps } = useDraggableItem({ + key: item.key, + }, dragState); + + const environment = item.value as unknown as Environment; + + return ( + <> + +
    • ); - }; + ref={ref} + className={classnames({ + 'env-modal__sidebar-item': true, + })} + > + +
    • + {state.collection.getKeyAfter(item.key) == null && + ( + + )} + + ); +}; + +// @ts-expect-error props any +const ReorderableListBox = props => { + // See useListBox docs for more details. + const state = useListState(props); + const ref = React.useRef(null); + const { listBoxProps } = useListBox( + { + ...props, + shouldSelectOnPressUp: true, + }, + state, + ref + ); + + const dropState = useDroppableCollectionState({ + ...props, + collection: state.collection, + selectionManager: state.selectionManager, + }); + + const { collectionProps } = useDroppableCollection( + { + ...props, + keyboardDelegate: new ListKeyboardDelegate( + state.collection, + state.disabledKeys, + ref + ), + dropTargetDelegate: new ListDropTargetDelegate( + state.collection, + ref + ), + }, + dropState, + ref + ); + + // Setup drag state for the collection. + const dragState = useDraggableCollectionState({ + ...props, + // Collection and selection manager come from list state. + collection: state.collection, + selectionManager: state.selectionManager, + // Provide data for each dragged item. This function could + // also be provided by the user of the component. + getItems: props.getItems || (keys => { + return [...keys].map(key => { + const item = state.collection.getItem(key); + + return { + 'text/plain': item.textValue, + }; + }); + }), + }); + + useDraggableCollection(props, dragState, ref); + + return ( +
        + {[...state.collection].map(item => ( + + ))} +
      + ); +}; interface State { baseEnvironment: Environment | null; selectedEnvironmentId: string | null; @@ -152,13 +264,17 @@ export const WorkspaceEnvironmentsEditModal = forwardRef evt._id === e.anchorKey)[0]; setState(state => ({ ...state, - selectedEnvironmentId: environmentId || null, + selectedEnvironmentId: environment._id || null, })); + if (workspaceMeta?.activeEnvironmentId !== environment._id && workspaceMeta) { + models.workspaceMeta.update(workspaceMeta, { activeEnvironmentId: environment._id }); + } } } @@ -202,27 +318,57 @@ export const WorkspaceEnvironmentsEditModal = forwardRef e.parentId === baseEnvironment?._id).find(subEnvironment => subEnvironment._id === selectedEnvironmentId) || null; const selectedEnvironmentName = selectedEnvironment?.name || ''; const selectedEnvironmentColor = selectedEnvironment?.color || null; + const subEnvironments = environments + .filter(environment => environment.parentId === (baseEnvironment && baseEnvironment._id)) + .sort((e1, e2) => e1.metaSortKey - e2.metaSortKey); if (inputRef.current && selectedEnvironmentColor) { inputRef.current.value = selectedEnvironmentColor; } + + function onReorder(e: any) { + const source = [...e.keys][0]; + const sourceEnv = subEnvironments.find(evt => evt._id === source); + const targetEnv = subEnvironments.find(evt => evt._id === e.target.key); + if (!sourceEnv || !targetEnv) { + return; + } + const dropPosition = e.target.dropPosition; + if (dropPosition === 'before') { + sourceEnv.metaSortKey = targetEnv.metaSortKey - 1; + } + if (dropPosition === 'after') { + sourceEnv.metaSortKey = targetEnv.metaSortKey + 1; + } + updateEnvironment(sourceEnv._id, { metaSortKey: sourceEnv.metaSortKey }); + } + return ( Manage Environments
      -
    • - -
    • +

      Sub Environments

      - e.parentId === baseEnvironment?._id)} - selectedEnvironmentId={selectedEnvironmentId} - showEnvironment={handleShowEnvironment} - changeEnvironmentName={(environment, name) => updateEnvironment(environment._id, { name })} - /> + + {(environment: any) => + + {environment.name} + + } +
      diff --git a/packages/insomnia/src/ui/css/components/environment-modal.less b/packages/insomnia/src/ui/css/components/environment-modal.less index c200ec2d0..e16f013cc 100644 --- a/packages/insomnia/src/ui/css/components/environment-modal.less +++ b/packages/insomnia/src/ui/css/components/environment-modal.less @@ -18,12 +18,12 @@ align-items: center; padding: var(--padding-sm) var(--padding-sm) 0; - & > * { + &>* { height: 100%; padding: 0; } - & > *:first-child { + &>*:first-child { width: 100%; padding: var(--padding-sm) 0 var(--padding-sm) var(--padding-sm); } @@ -64,7 +64,7 @@ } h1, - h1 > * { + h1>* { padding: 0; font-size: var(--font-size-xl); width: 100%; @@ -75,66 +75,21 @@ // These are at the top-level because sorting pulls the row out into .env-modal__sidebar-root-item, .env-modal__sidebar-item { - z-index: 100000; - display: grid; - grid-template-columns: minmax(0, 1fr) auto; - grid-template-rows: var(--line-height-xs); + display: flex; color: var(--hl); - & > button { - padding: 0 var(--padding-md) 0 var(--padding-md); - width: 100%; - &:first-child { - padding-right: 0; - } + padding: var(--padding-sm) 0; - position: relative; - .editable { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - display: block; - padding-right: var(--padding-xs); - box-sizing: border-box; - width: 100%; - line-height: var(--line-height-xs); - } - } - - .env-status { - padding: 0 var(--padding-sm) 0 var(--padding-sm); - align-self: center; - - .active { - color: var(--hl-xl); - } - - .inactive { - color: var(--hl-xl); - opacity: 0; - } - } &:hover { background-color: var(--hl-xs); } - &:hover .inactive { - opacity: 1; - } - - &:hover .inactive:hover { - color: var(--color-font); - } } -.env-modal__sidebar-item { - & > button { - display: flex; - flex-direction: row; - align-items: center; - padding-left: var(--padding-md); - } +.env-modal__sidebar-item--active { + color: var(--color-font); + }