From 772d54d29fafdabecf93b839757c88e015854e7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tha=C3=AFs?= Date: Wed, 20 Sep 2023 21:53:35 +0200 Subject: [PATCH] feat: add DropdownMenuInput and use as view name input in board (#1680) Closes #1510 --- .../components/CompanyProgressPicker.tsx | 4 +- .../components/MatchColumnSelect.tsx | 4 +- .../board/components/BoardOptionsDropdown.tsx | 6 + .../BoardOptionsDropdownContent.tsx | 50 ++-- .../BoardOptionsDropdown.stories.tsx | 57 +++++ .../dropdown/components/DropdownMenuInput.tsx | 44 +--- .../components/DropdownMenuInputContainer.tsx | 9 + .../components/DropdownMenuSearchInput.tsx | 43 ++++ .../__stories__/DropdownMenu.stories.tsx | 237 +++++++----------- .../__stories__/DropdownMenuInput.stories.tsx | 25 ++ .../components/MultipleEntitySelect.tsx | 4 +- .../components/SingleEntitySelect.tsx | 4 +- .../TableOptionsDropdownContent.tsx | 34 +-- .../FilterDropdownEntitySearchInput.tsx | 4 +- .../FilterDropdownNumberSearchInput.tsx | 4 +- .../FilterDropdownTextSearchInput.tsx | 4 +- 16 files changed, 284 insertions(+), 249 deletions(-) create mode 100644 front/src/modules/ui/board/components/__stories__/BoardOptionsDropdown.stories.tsx create mode 100644 front/src/modules/ui/dropdown/components/DropdownMenuInputContainer.tsx create mode 100644 front/src/modules/ui/dropdown/components/DropdownMenuSearchInput.tsx create mode 100644 front/src/modules/ui/dropdown/components/__stories__/DropdownMenuInput.stories.tsx diff --git a/front/src/modules/companies/components/CompanyProgressPicker.tsx b/front/src/modules/companies/components/CompanyProgressPicker.tsx index f384dab36..ec5367d22 100644 --- a/front/src/modules/companies/components/CompanyProgressPicker.tsx +++ b/front/src/modules/companies/components/CompanyProgressPicker.tsx @@ -3,7 +3,7 @@ import { useRecoilState } from 'recoil'; import { currentPipelineState } from '@/pipeline/states/currentPipelineState'; import { DropdownMenuHeader } from '@/ui/dropdown/components/DropdownMenuHeader'; -import { DropdownMenuInput } from '@/ui/dropdown/components/DropdownMenuInput'; +import { DropdownMenuSearchInput } from '@/ui/dropdown/components/DropdownMenuSearchInput'; import { StyledDropdownMenu } from '@/ui/dropdown/components/StyledDropdownMenu'; import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer'; import { StyledDropdownMenuSeparator } from '@/ui/dropdown/components/StyledDropdownMenuSeparator'; @@ -106,7 +106,7 @@ export const CompanyProgressPicker = ({ {selectedPipelineStage?.name} - - { + const resetViewEditMode = useResetRecoilState(viewEditModeState); + return ( } @@ -28,6 +33,7 @@ export const BoardOptionsDropdown = ({ } dropdownHotkeyScope={customHotkeyScope} dropdownId={BoardOptionsDropdownKey} + onClickOutside={resetViewEditMode} /> ); }; diff --git a/front/src/modules/ui/board/components/BoardOptionsDropdownContent.tsx b/front/src/modules/ui/board/components/BoardOptionsDropdownContent.tsx index 5c8fc800d..3580c0783 100644 --- a/front/src/modules/ui/board/components/BoardOptionsDropdownContent.tsx +++ b/front/src/modules/ui/board/components/BoardOptionsDropdownContent.tsx @@ -1,13 +1,18 @@ import { useContext, useRef, useState } from 'react'; -import { useTheme } from '@emotion/react'; -import styled from '@emotion/styled'; -import { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil'; +import { + useRecoilCallback, + useRecoilState, + useRecoilValue, + useResetRecoilState, +} from 'recoil'; import { Key } from 'ts-key-enum'; import { v4 } from 'uuid'; import { BoardContext } from '@/companies/states/contexts/BoardContext'; import { DropdownMenuHeader } from '@/ui/dropdown/components/DropdownMenuHeader'; import { DropdownMenuInput } from '@/ui/dropdown/components/DropdownMenuInput'; +import { DropdownMenuInputContainer } from '@/ui/dropdown/components/DropdownMenuInputContainer'; +import { DropdownMenuSearchInput } from '@/ui/dropdown/components/DropdownMenuSearchInput'; import { StyledDropdownMenu } from '@/ui/dropdown/components/StyledDropdownMenu'; import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer'; import { StyledDropdownMenuSeparator } from '@/ui/dropdown/components/StyledDropdownMenuSeparator'; @@ -16,7 +21,6 @@ import { IconChevronLeft, IconLayoutKanban, IconPlus, - IconSettings, IconTag, } from '@/ui/icon'; import { MenuItem } from '@/ui/menu-item/components/MenuItem'; @@ -28,6 +32,7 @@ import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoi import { useRecoilScopeId } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopeId'; 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'; @@ -45,10 +50,6 @@ export type BoardOptionsDropdownContentProps = { onStageAdd?: (boardColumn: BoardColumnDefinition) => void; }; -const StyledIconSettings = styled(IconSettings)` - margin-right: ${({ theme }) => theme.spacing(1)}; -`; - type BoardOptionsMenu = 'fields' | 'stage-creation' | 'stages'; type ColumnForCreate = { @@ -62,10 +63,7 @@ export const BoardOptionsDropdownContent = ({ customHotkeyScope, onStageAdd, }: BoardOptionsDropdownContentProps) => { - const theme = useTheme(); - - const BoardRecoilScopeContext = - useContext(BoardContext).BoardRecoilScopeContext; + const { BoardRecoilScopeContext } = useContext(BoardContext); const boardRecoilScopeId = useRecoilScopeId(BoardRecoilScopeContext); @@ -93,7 +91,12 @@ export const BoardOptionsDropdownContent = ({ viewsByIdScopedSelector, BoardRecoilScopeContext, // TODO: replace with ViewBarRecoilScopeContext ); + const currentView = useRecoilScopedValue( + currentViewScopedSelector, + BoardRecoilScopeContext, + ); const viewEditMode = useRecoilValue(viewEditModeState); + const resetViewEditMode = useResetRecoilState(viewEditModeState); const handleStageSubmit = () => { if (currentMenu !== 'stage-creation' || !stageInputRef?.current?.value) @@ -148,6 +151,7 @@ export const BoardOptionsDropdownContent = ({ useScopedHotkeys( Key.Escape, () => { + resetViewEditMode(); closeDropdownButton(); }, customHotkeyScope.scope, @@ -158,6 +162,7 @@ export const BoardOptionsDropdownContent = ({ () => { handleStageSubmit(); handleViewNameSubmit(); + resetViewEditMode(); closeDropdownButton(); }, customHotkeyScope.scope, @@ -167,25 +172,24 @@ export const BoardOptionsDropdownContent = ({ {!currentMenu && ( <> - {!!viewEditMode.mode ? ( + - ) : ( - - - Settings - - )} + )} {currentMenu === 'stage-creation' && ( - = { + title: 'UI/Board/Options/BoardOptionsDropdown', + component: BoardOptionsDropdown, + decorators: [ + (Story, { parameters }) => ( + + + + + + + + ), + ComponentWithRecoilScopeDecorator, + ComponentDecorator, + ], + parameters: { + customRecoilScopeContext: CompanyBoardRecoilScopeContext, + }, + args: { + customHotkeyScope: { scope: 'scope' }, + }, +}; + +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/dropdown/components/DropdownMenuInput.tsx b/front/src/modules/ui/dropdown/components/DropdownMenuInput.tsx index 4003a3a35..aa564104d 100644 --- a/front/src/modules/ui/dropdown/components/DropdownMenuInput.tsx +++ b/front/src/modules/ui/dropdown/components/DropdownMenuInput.tsx @@ -1,43 +1,23 @@ -import { forwardRef, InputHTMLAttributes } from 'react'; import styled from '@emotion/styled'; +import { rgba } from '@/ui/theme/constants/colors'; import { textInputStyle } from '@/ui/theme/constants/effects'; -const StyledDropdownMenuInputContainer = styled.div` - --vertical-padding: ${({ theme }) => theme.spacing(1)}; - - align-items: center; - - display: flex; - flex-direction: row; - height: calc(36px - 2 * var(--vertical-padding)); - padding: var(--vertical-padding) 0; - - width: 100%; -`; - -const StyledInput = styled.input` +const StyledViewNameInput = styled.input` ${textInputStyle} - font-size: ${({ theme }) => theme.font.size.sm}; + 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%; - &[type='number']::-webkit-outer-spin-button, - &[type='number']::-webkit-inner-spin-button { - -webkit-appearance: none; - margin: 0; - } - - &[type='number'] { - -moz-appearance: textfield; + &:focus { + border-color: ${({ theme }) => theme.color.blue}; + box-shadow: 0px 0px 0px 3px ${({ theme }) => rgba(theme.color.blue, 0.1)}; } `; -export const DropdownMenuInput = forwardRef< - HTMLInputElement, - InputHTMLAttributes ->((props, ref) => ( - - - -)); +export { StyledViewNameInput as DropdownMenuInput }; diff --git a/front/src/modules/ui/dropdown/components/DropdownMenuInputContainer.tsx b/front/src/modules/ui/dropdown/components/DropdownMenuInputContainer.tsx new file mode 100644 index 000000000..6bc0c8e27 --- /dev/null +++ b/front/src/modules/ui/dropdown/components/DropdownMenuInputContainer.tsx @@ -0,0 +1,9 @@ +import styled from '@emotion/styled'; + +const StyledInputContainer = styled.div` + box-sizing: border-box; + padding: ${({ theme }) => theme.spacing(1)}; + width: 100%; +`; + +export { StyledInputContainer as DropdownMenuInputContainer }; diff --git a/front/src/modules/ui/dropdown/components/DropdownMenuSearchInput.tsx b/front/src/modules/ui/dropdown/components/DropdownMenuSearchInput.tsx new file mode 100644 index 000000000..6e6b9abb3 --- /dev/null +++ b/front/src/modules/ui/dropdown/components/DropdownMenuSearchInput.tsx @@ -0,0 +1,43 @@ +import { forwardRef, InputHTMLAttributes } from 'react'; +import styled from '@emotion/styled'; + +import { textInputStyle } from '@/ui/theme/constants/effects'; + +const StyledDropdownMenuSearchInputContainer = styled.div` + --vertical-padding: ${({ theme }) => theme.spacing(1)}; + + align-items: center; + + display: flex; + flex-direction: row; + height: calc(36px - 2 * var(--vertical-padding)); + padding: var(--vertical-padding) 0; + + width: 100%; +`; + +const StyledInput = styled.input` + ${textInputStyle} + + font-size: ${({ theme }) => theme.font.size.sm}; + width: 100%; + + &[type='number']::-webkit-outer-spin-button, + &[type='number']::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; + } + + &[type='number'] { + -moz-appearance: textfield; + } +`; + +export const DropdownMenuSearchInput = forwardRef< + HTMLInputElement, + InputHTMLAttributes +>((props, ref) => ( + + + +)); diff --git a/front/src/modules/ui/dropdown/components/__stories__/DropdownMenu.stories.tsx b/front/src/modules/ui/dropdown/components/__stories__/DropdownMenu.stories.tsx index 4d7c805c7..b9c24d68c 100644 --- a/front/src/modules/ui/dropdown/components/__stories__/DropdownMenu.stories.tsx +++ b/front/src/modules/ui/dropdown/components/__stories__/DropdownMenu.stories.tsx @@ -1,8 +1,7 @@ import { useState } from 'react'; import styled from '@emotion/styled'; -import type { Meta, StoryObj } from '@storybook/react'; +import type { Decorator, Meta, StoryObj } from '@storybook/react'; -import { IconPlus, IconUser } from '@/ui/icon'; import { DropdownMenuSkeletonItem } from '@/ui/input/relation-picker/components/skeletons/DropdownMenuSkeletonItem'; import { MenuItem } from '@/ui/menu-item/components/MenuItem'; import { MenuItemMultiSelectAvatar } from '@/ui/menu-item/components/MenuItemMultiSelectAvatar'; @@ -12,6 +11,8 @@ import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator'; import { DropdownMenuHeader } from '../DropdownMenuHeader'; import { DropdownMenuInput } from '../DropdownMenuInput'; +import { DropdownMenuInputContainer } from '../DropdownMenuInputContainer'; +import { DropdownMenuSearchInput } from '../DropdownMenuSearchInput'; import { StyledDropdownMenu } from '../StyledDropdownMenu'; import { StyledDropdownMenuItemsContainer } from '../StyledDropdownMenuItemsContainer'; import { StyledDropdownMenuSeparator } from '../StyledDropdownMenuSeparator'; @@ -23,7 +24,9 @@ const meta: Meta = { decorators: [ComponentDecorator], argTypes: { as: { table: { disable: true } }, + children: { control: false }, theme: { table: { disable: true } }, + width: { type: 'number', defaultValue: undefined }, }, }; @@ -122,21 +125,22 @@ const FakeSelectableMenuItemList = ({ hasAvatar }: { hasAvatar?: boolean }) => { }; const FakeCheckableMenuItemList = ({ hasAvatar }: { hasAvatar?: boolean }) => { - const [selectedItems, setSelectedItems] = useState([]); + const [selectedItemsById, setSelectedItemsById] = useState< + Record + >({}); return ( <> {mockSelectArray.map((item) => ( { - if (checked) { - setSelectedItems([...selectedItems, item.id]); - } else { - setSelectedItems(selectedItems.filter((id) => id !== item.id)); - } - }} + selected={selectedItemsById[item.id]} + onSelectChange={(checked) => + setSelectedItemsById((previous) => ({ + ...previous, + [item.id]: checked, + })) + } avatar={ hasAvatar ? ( { ); }; +const WithContentBelowDecorator: Decorator = (Story) => ( + + + + + + +); + export const Empty: Story = { - render: (args) => ( - - - - ), -}; - -export const WithContentBelow: Story = { - ...Empty, - decorators: [ - (Story) => ( - - - - - - - ), - ], -}; - -export const SimpleMenuItem: Story = { - ...WithContentBelow, - render: (args) => ( - - - {mockSelectArray.map(({ name }) => ( - - ))} - - - ), + args: { children: }, }; export const WithHeaders: Story = { - ...WithContentBelow, - render: (args) => ( - - Header - - Subheader 1 - - {mockSelectArray.slice(0, 3).map(({ name }) => ( - - ))} - - - Subheader 2 - - {mockSelectArray.slice(3).map(({ name }) => ( - - ))} - - - ), -}; - -export const WithIcons: Story = { - ...WithContentBelow, - render: (args) => ( - - - {mockSelectArray.map(({ name }) => ( - - ))} - - - ), -}; - -export const WithActions: Story = { - ...WithContentBelow, - render: (args) => ( - - - {mockSelectArray.map(({ name }, index) => ( - - ))} - - - ), - parameters: { - pseudo: { hover: ['.hover'] }, + decorators: [WithContentBelowDecorator], + args: { + children: ( + <> + Header + + Subheader 1 + + {mockSelectArray.slice(0, 3).map(({ name }) => ( + + ))} + + + Subheader 2 + + {mockSelectArray.slice(3).map(({ name }) => ( + + ))} + + + ), }, }; -export const LoadingMenu: Story = { - ...WithContentBelow, - render: () => ( - - - - - - - - ), +export const SearchWithLoadingMenu: Story = { + decorators: [WithContentBelowDecorator], + args: { + children: ( + <> + + + + + + + ), + }, }; -export const Search: Story = { - ...WithContentBelow, - render: (args) => ( - - - - - {mockSelectArray.map(({ name }) => ( - - ))} - - - ), -}; - -export const SelectableMenuItem: Story = { - ...WithContentBelow, - render: (args) => ( - - - - - - ), +export const WithInput: Story = { + decorators: [WithContentBelowDecorator], + args: { + children: ( + <> + + + + + + {mockSelectArray.map(({ name }) => ( + + ))} + + + ), + }, }; export const SelectableMenuItemWithAvatar: Story = { - ...WithContentBelow, - render: (args) => ( - + decorators: [WithContentBelowDecorator], + args: { + children: ( - - ), -}; - -export const CheckableMenuItem: Story = { - ...WithContentBelow, - render: (args) => ( - - - - - - ), + ), + }, }; export const CheckableMenuItemWithAvatar: Story = { - ...WithContentBelow, - render: (args) => ( - + decorators: [WithContentBelowDecorator], + args: { + children: ( - - ), + ), + }, }; diff --git a/front/src/modules/ui/dropdown/components/__stories__/DropdownMenuInput.stories.tsx b/front/src/modules/ui/dropdown/components/__stories__/DropdownMenuInput.stories.tsx new file mode 100644 index 000000000..9e3478b01 --- /dev/null +++ b/front/src/modules/ui/dropdown/components/__stories__/DropdownMenuInput.stories.tsx @@ -0,0 +1,25 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator'; + +import { DropdownMenuInput } from '../DropdownMenuInput'; + +const meta: Meta = { + title: 'UI/Dropdown/DropdownMenuInput', + component: DropdownMenuInput, + decorators: [ComponentDecorator], + args: { defaultValue: 'Lorem ipsum' }, + argTypes: { + as: { table: { disable: true } }, + theme: { table: { disable: true } }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; + +export const Focused: Story = { + args: { autoFocus: true }, +}; diff --git a/front/src/modules/ui/input/relation-picker/components/MultipleEntitySelect.tsx b/front/src/modules/ui/input/relation-picker/components/MultipleEntitySelect.tsx index f4308bfb5..00fddc91b 100644 --- a/front/src/modules/ui/input/relation-picker/components/MultipleEntitySelect.tsx +++ b/front/src/modules/ui/input/relation-picker/components/MultipleEntitySelect.tsx @@ -1,7 +1,7 @@ import { useRef } from 'react'; import debounce from 'lodash.debounce'; -import { DropdownMenuInput } from '@/ui/dropdown/components/DropdownMenuInput'; +import { DropdownMenuSearchInput } from '@/ui/dropdown/components/DropdownMenuSearchInput'; import { StyledDropdownMenu } from '@/ui/dropdown/components/StyledDropdownMenu'; import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer'; import { StyledDropdownMenuSeparator } from '@/ui/dropdown/components/StyledDropdownMenuSeparator'; @@ -73,7 +73,7 @@ export const MultipleEntitySelect = < return ( - - 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 const TableOptionsDropdownContent = () => { const scopeId = useRecoilScopeId(TableRecoilScopeContext); @@ -158,8 +134,8 @@ export const TableOptionsDropdownContent = () => { {!currentMenu && ( <> - - + { : currentView?.name } /> - + { return ( filterDefinitionUsedInDropdown && selectedOperandInDropdown && ( - { return ( filterDefinitionUsedInDropdown && selectedOperandInDropdown && ( - ) => { diff --git a/front/src/modules/ui/view-bar/components/FilterDropdownTextSearchInput.tsx b/front/src/modules/ui/view-bar/components/FilterDropdownTextSearchInput.tsx index c1f5343ff..d9ce41ffb 100644 --- a/front/src/modules/ui/view-bar/components/FilterDropdownTextSearchInput.tsx +++ b/front/src/modules/ui/view-bar/components/FilterDropdownTextSearchInput.tsx @@ -1,6 +1,6 @@ import { ChangeEvent } from 'react'; -import { DropdownMenuInput } from '@/ui/dropdown/components/DropdownMenuInput'; +import { DropdownMenuSearchInput } from '@/ui/dropdown/components/DropdownMenuSearchInput'; import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; import { useFilterCurrentlyEdited } from '../hooks/useFilterCurrentlyEdited'; @@ -38,7 +38,7 @@ export const FilterDropdownTextSearchInput = () => { return ( filterDefinitionUsedInDropdown && selectedOperandInDropdown && ( -