From 955deaf878803b601da8ef740c4c0d03e84dd953 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tha=C3=AFs?= Date: Fri, 15 Sep 2023 17:26:00 +0200 Subject: [PATCH] feat: improve table options dropdown view name input (#1604) - Always show view name input in dropdown - Edit current view name by default - Add focus style - Reset view edit mode on dropdown close Closes #1540 --- .../ui/dropdown/components/DropdownButton.tsx | 6 +- .../components/DropdownMenuHeader.tsx | 3 +- .../dropdown/components/DropdownMenuInput.tsx | 2 +- .../components/TableOptionsDropdown.tsx | 6 ++ .../TableOptionsDropdownContent.tsx | 55 ++++++++++++++---- .../TableOptionsDropdown.stories.tsx | 37 ++++++++++++ .../ui/view-bar/hooks/useUpsertView.ts | 57 +++++++++++-------- .../ui/view-bar/states/viewEditModeState.ts | 4 +- 8 files changed, 130 insertions(+), 40 deletions(-) create mode 100644 front/src/modules/ui/table/options/components/__stories__/TableOptionsDropdown.stories.tsx diff --git a/front/src/modules/ui/dropdown/components/DropdownButton.tsx b/front/src/modules/ui/dropdown/components/DropdownButton.tsx index edfe28fd5..d2acfeb6f 100644 --- a/front/src/modules/ui/dropdown/components/DropdownButton.tsx +++ b/front/src/modules/ui/dropdown/components/DropdownButton.tsx @@ -2,10 +2,10 @@ import { useRef } from 'react'; import { Keys } from 'react-hotkeys-hook'; import { flip, offset, Placement, useFloating } from '@floating-ui/react'; +import { HotkeyEffect } from '@/ui/utilities/hotkey/components/HotkeyEffect'; import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; -import { HotkeyEffect } from '../../utilities/hotkey/components/HotkeyEffect'; import { useDropdownButton } from '../hooks/useDropdownButton'; import { useInternalHotkeyScopeManagement } from '../hooks/useInternalHotkeyScopeManagement'; @@ -19,6 +19,7 @@ type OwnProps = { }; dropdownHotkeyScope?: HotkeyScope; dropdownPlacement?: Placement; + onClickOutside?: () => void; }; export function DropdownButton({ @@ -28,6 +29,7 @@ export function DropdownButton({ hotkey, dropdownHotkeyScope, dropdownPlacement = 'bottom-end', + onClickOutside, }: OwnProps) { const containerRef = useRef(null); @@ -48,6 +50,8 @@ export function DropdownButton({ useListenClickOutside({ refs: [containerRef], callback: () => { + onClickOutside?.(); + if (isDropdownButtonOpen) { closeDropdownButton(); } diff --git a/front/src/modules/ui/dropdown/components/DropdownMenuHeader.tsx b/front/src/modules/ui/dropdown/components/DropdownMenuHeader.tsx index d72c7f2a4..96725a501 100644 --- a/front/src/modules/ui/dropdown/components/DropdownMenuHeader.tsx +++ b/front/src/modules/ui/dropdown/components/DropdownMenuHeader.tsx @@ -11,8 +11,7 @@ const StyledHeader = styled.li` font-size: ${({ theme }) => theme.font.size.sm}; font-weight: ${({ theme }) => theme.font.weight.medium}; - padding: calc(${({ theme }) => theme.spacing(2)}) - calc(${({ theme }) => theme.spacing(2)}); + padding: ${({ theme }) => theme.spacing(2)}; user-select: none; diff --git a/front/src/modules/ui/dropdown/components/DropdownMenuInput.tsx b/front/src/modules/ui/dropdown/components/DropdownMenuInput.tsx index d17807987..4003a3a35 100644 --- a/front/src/modules/ui/dropdown/components/DropdownMenuInput.tsx +++ b/front/src/modules/ui/dropdown/components/DropdownMenuInput.tsx @@ -13,7 +13,7 @@ const StyledDropdownMenuInputContainer = styled.div` height: calc(36px - 2 * var(--vertical-padding)); padding: var(--vertical-padding) 0; - width: calc(100%); + width: 100%; `; const StyledInput = styled.input` diff --git a/front/src/modules/ui/table/options/components/TableOptionsDropdown.tsx b/front/src/modules/ui/table/options/components/TableOptionsDropdown.tsx index 2f1bead26..efcbbf23b 100644 --- a/front/src/modules/ui/table/options/components/TableOptionsDropdown.tsx +++ b/front/src/modules/ui/table/options/components/TableOptionsDropdown.tsx @@ -1,5 +1,8 @@ +import { useResetRecoilState } from 'recoil'; + import { DropdownButton } from '@/ui/dropdown/components/DropdownButton'; import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; +import { viewEditModeState } from '@/ui/view-bar/states/viewEditModeState'; import { TableOptionsDropdownId } from '../../constants/TableOptionsDropdownId'; @@ -15,12 +18,15 @@ export function TableOptionsDropdown({ onImport, customHotkeyScope, }: TableOptionsDropdownProps) { + const resetViewEditMode = useResetRecoilState(viewEditModeState); + return ( } dropdownHotkeyScope={customHotkeyScope} dropdownId={TableOptionsDropdownId} dropdownComponents={} + onClickOutside={resetViewEditMode} /> ); } diff --git a/front/src/modules/ui/table/options/components/TableOptionsDropdownContent.tsx b/front/src/modules/ui/table/options/components/TableOptionsDropdownContent.tsx index 031676a00..0214d164b 100644 --- a/front/src/modules/ui/table/options/components/TableOptionsDropdownContent.tsx +++ b/front/src/modules/ui/table/options/components/TableOptionsDropdownContent.tsx @@ -1,20 +1,23 @@ import { useRef, useState } from 'react'; -import { useRecoilCallback, useRecoilValue } from 'recoil'; +import styled from '@emotion/styled'; +import { useRecoilCallback, useRecoilValue, useResetRecoilState } from 'recoil'; import { Key } from 'ts-key-enum'; import { DropdownMenuHeader } from '@/ui/dropdown/components/DropdownMenuHeader'; -import { DropdownMenuInput } from '@/ui/dropdown/components/DropdownMenuInput'; import { StyledDropdownMenu } from '@/ui/dropdown/components/StyledDropdownMenu'; import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer'; import { StyledDropdownMenuSeparator } from '@/ui/dropdown/components/StyledDropdownMenuSeparator'; import { useDropdownButton } from '@/ui/dropdown/hooks/useDropdownButton'; import { IconChevronLeft, IconFileImport, IconTag } from '@/ui/icon'; import { MenuItem } from '@/ui/menu-item/components/MenuItem'; +import { rgba } from '@/ui/theme/constants/colors'; +import { textInputStyle } from '@/ui/theme/constants/effects'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useContextScopeId } from '@/ui/utilities/recoil-scope/hooks/useContextScopeId'; import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue'; import { ViewFieldsVisibilityDropdownSection } from '@/ui/view-bar/components/ViewFieldsVisibilityDropdownSection'; import { useUpsertView } from '@/ui/view-bar/hooks/useUpsertView'; +import { currentViewScopedSelector } from '@/ui/view-bar/states/selectors/currentViewScopedSelector'; import { viewsByIdScopedSelector } from '@/ui/view-bar/states/selectors/viewsByIdScopedSelector'; import { viewEditModeState } from '@/ui/view-bar/states/viewEditModeState'; @@ -33,6 +36,29 @@ type TableOptionsDropdownButtonProps = { type TableOptionsMenu = 'fields'; +const StyledInputContainer = styled.div` + box-sizing: border-box; + padding: ${({ theme }) => theme.spacing(1)}; + width: 100%; +`; + +const StyledViewNameInput = styled.input` + ${textInputStyle} + + border: 1px solid ${({ theme }) => theme.border.color.medium}; + border-radius: ${({ theme }) => theme.border.radius.sm}; + box-sizing: border-box; + font-weight: ${({ theme }) => theme.font.weight.medium}; + height: 32px; + position: relative; + width: 100%; + + &:focus { + border-color: ${({ theme }) => theme.color.blue}; + box-shadow: 0px 0px 0px 3px ${({ theme }) => rgba(theme.color.blue, 0.1)}; + } +`; + export function TableOptionsDropdownContent({ onImport, }: TableOptionsDropdownButtonProps) { @@ -48,7 +74,12 @@ export function TableOptionsDropdownContent({ const viewEditInputRef = useRef(null); + const currentView = useRecoilScopedValue( + currentViewScopedSelector, + TableRecoilScopeContext, + ); const viewEditMode = useRecoilValue(viewEditModeState); + const resetViewEditMode = useResetRecoilState(viewEditModeState); const visibleTableColumns = useRecoilScopedValue( visibleTableColumnsScopedSelector, TableRecoilScopeContext, @@ -95,6 +126,7 @@ export function TableOptionsDropdownContent({ useScopedHotkeys( Key.Escape, () => { + resetViewEditMode(); closeDropdownButton(); }, TableOptionsHotkeyScope.Dropdown, @@ -105,6 +137,7 @@ export function TableOptionsDropdownContent({ () => { handleViewNameSubmit(); resetMenu(); + resetViewEditMode(); closeDropdownButton(); }, TableOptionsHotkeyScope.Dropdown, @@ -114,22 +147,24 @@ export function TableOptionsDropdownContent({ {!currentMenu && ( <> - {!!viewEditMode.mode ? ( - + - ) : ( - View settings - )} + = { + title: 'UI/Table/Options/TableOptionsDropdown', + component: TableOptionsDropdown, + decorators: [ + (Story) => ( + + + + + + ), + ComponentDecorator, + ], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const dropdownButton = canvas.getByText('Options'); + + await userEvent.click(dropdownButton); + }, +}; diff --git a/front/src/modules/ui/view-bar/hooks/useUpsertView.ts b/front/src/modules/ui/view-bar/hooks/useUpsertView.ts index 2dd0b8fd4..d7eed35c4 100644 --- a/front/src/modules/ui/view-bar/hooks/useUpsertView.ts +++ b/front/src/modules/ui/view-bar/hooks/useUpsertView.ts @@ -1,5 +1,5 @@ -import { type Context, useCallback, useContext } from 'react'; -import { useRecoilCallback, useRecoilState } from 'recoil'; +import { type Context, useContext } from 'react'; +import { useRecoilCallback, useRecoilValue, useResetRecoilState } from 'recoil'; import { v4 } from 'uuid'; import { useContextScopeId } from '@/ui/utilities/recoil-scope/hooks/useContextScopeId'; @@ -10,6 +10,7 @@ import { currentViewIdScopedState } from '../states/currentViewIdScopedState'; import { filtersScopedState } from '../states/filtersScopedState'; import { savedFiltersFamilyState } from '../states/savedFiltersFamilyState'; import { savedSortsFamilyState } from '../states/savedSortsFamilyState'; +import { currentViewScopedSelector } from '../states/selectors/currentViewScopedSelector'; import { viewsByIdScopedSelector } from '../states/selectors/viewsByIdScopedSelector'; import { sortsScopedState } from '../states/sortsScopedState'; import { viewEditModeState } from '../states/viewEditModeState'; @@ -25,12 +26,8 @@ export const useUpsertView = ({ const filters = useRecoilScopedValue(filtersScopedState, scopeContext); const sorts = useRecoilScopedValue(sortsScopedState, scopeContext); - const [viewEditMode, setViewEditMode] = useRecoilState(viewEditModeState); - - const resetViewEditMode = useCallback( - () => setViewEditMode({ mode: undefined, viewId: undefined }), - [setViewEditMode], - ); + const viewEditMode = useRecoilValue(viewEditModeState); + const resetViewEditMode = useResetRecoilState(viewEditModeState); const upsertView = useRecoilCallback( ({ set, snapshot }) => @@ -60,26 +57,38 @@ export const useUpsertView = ({ return createdView; } - if (viewEditMode.mode === 'edit' && viewEditMode.viewId) { - const viewsById = await snapshot.getPromise( - viewsByIdScopedSelector(recoilScopeId), - ); - const editedView = { ...viewsById[viewEditMode.viewId], name }; + const viewsById = await snapshot.getPromise( + viewsByIdScopedSelector(recoilScopeId), + ); + const currentView = await snapshot.getPromise( + currentViewScopedSelector(recoilScopeId), + ); - set(viewsScopedState(recoilScopeId), (previousViews) => - previousViews.map((previousView) => - previousView.id === viewEditMode.viewId - ? editedView - : previousView, - ), - ); - - await onViewEdit?.(editedView); + const viewToEdit = viewEditMode.viewId + ? viewsById[viewEditMode.viewId] + : currentView; + if (!viewToEdit) { resetViewEditMode(); - - return editedView; + return; } + + const editedView = { + ...viewToEdit, + name, + }; + + set(viewsScopedState(recoilScopeId), (previousViews) => + previousViews.map((previousView) => + previousView.id === editedView.id ? editedView : previousView, + ), + ); + + await onViewEdit?.(editedView); + + resetViewEditMode(); + + return editedView; }, [ filters, diff --git a/front/src/modules/ui/view-bar/states/viewEditModeState.ts b/front/src/modules/ui/view-bar/states/viewEditModeState.ts index 0660a6830..4579a8f36 100644 --- a/front/src/modules/ui/view-bar/states/viewEditModeState.ts +++ b/front/src/modules/ui/view-bar/states/viewEditModeState.ts @@ -1,9 +1,9 @@ import { atom } from 'recoil'; export const viewEditModeState = atom<{ - mode: 'create' | 'edit' | undefined; + mode: 'create' | 'edit'; viewId: string | undefined; }>({ key: 'viewEditModeState', - default: { mode: undefined, viewId: undefined }, + default: { mode: 'edit', viewId: undefined }, });