From bded46444d5c32c56f347bc4b90e0334a5d44935 Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Wed, 13 Dec 2023 15:24:06 +0100 Subject: [PATCH] Added ability to edit filter and sort chip directly (#2968) * - Added EditableSortChip - Fixed EditableFilterChip onRemove not closing * Added missing script in dependencies * Linted files * Finished fixing lint --- packages/eslint-plugin-twenty/package.json | 1 + .../object-record/hooks/useFindManyRecords.ts | 7 +- .../MultipleFiltersDropdownContent.tsx | 10 ++- .../RecordBoardScopeInternalContext.ts | 4 +- .../utils/turnFiltersIntoWhereClause.ts | 3 +- .../components/__stories__/Card.stories.tsx | 2 +- .../layout/dropdown/components/Dropdown.tsx | 1 + .../views/components/EditableFilterChip.tsx | 28 +++++++ .../EditableFilterDropdownButton.tsx | 79 +++++++++++++++++++ .../views/components/EditableSortChip.tsx | 35 ++++++++ .../views/components/SortOrFilterChip.tsx | 40 ++++++---- .../src/modules/views/components/ViewBar.tsx | 1 + .../views/components/ViewBarDetails.tsx | 62 ++++++++------- yarn.lock | 14 +++- 14 files changed, 232 insertions(+), 55 deletions(-) create mode 100644 packages/twenty-front/src/modules/views/components/EditableFilterChip.tsx create mode 100644 packages/twenty-front/src/modules/views/components/EditableFilterDropdownButton.tsx create mode 100644 packages/twenty-front/src/modules/views/components/EditableSortChip.tsx diff --git a/packages/eslint-plugin-twenty/package.json b/packages/eslint-plugin-twenty/package.json index a5cb7462e4..2347d04895 100644 --- a/packages/eslint-plugin-twenty/package.json +++ b/packages/eslint-plugin-twenty/package.json @@ -32,6 +32,7 @@ "jest": "^28.1.3", "postcss": "^8.4.29", "prettier": "^3.0.3", + "rimraf": "^5.0.5", "ts-jest": "^29.1.1", "ts-node": "^10.9.1", "typescript": "^5.2.2" diff --git a/packages/twenty-front/src/modules/object-record/hooks/useFindManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useFindManyRecords.ts index 37af0b652f..54f7e3d6e5 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useFindManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useFindManyRecords.ts @@ -140,10 +140,9 @@ export const useFindManyRecords = < if (isNonEmptyArray(previousEdges) && isNonEmptyArray(nextEdges)) { newEdges = filterUniqueRecordEdgesByCursor([ - // eslint-disable-next-line no-unsafe-optional-chaining - ...prev?.[objectMetadataItem.namePlural]?.edges, - // eslint-disable-next-line no-unsafe-optional-chaining - ...fetchMoreResult?.[objectMetadataItem.namePlural]?.edges, + ...(prev?.[objectMetadataItem.namePlural]?.edges ?? []), + ...(fetchMoreResult?.[objectMetadataItem.namePlural]?.edges ?? + []), ]); } diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersDropdownContent.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersDropdownContent.tsx index 2f68d19c55..67dc3c3d4d 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersDropdownContent.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersDropdownContent.tsx @@ -11,12 +11,18 @@ import { ObjectFilterDropdownOperandSelect } from './ObjectFilterDropdownOperand import { ObjectFilterDropdownRecordSelect } from './ObjectFilterDropdownRecordSelect'; import { ObjectFilterDropdownTextSearchInput } from './ObjectFilterDropdownTextSearchInput'; -export const MultipleFiltersDropdownContent = () => { +type MultipleFiltersDropdownContentProps = { + filterDropdownId?: string; +}; + +export const MultipleFiltersDropdownContent = ({ + filterDropdownId, +}: MultipleFiltersDropdownContentProps) => { const { isObjectFilterDropdownOperandSelectUnfolded, filterDefinitionUsedInDropdown, selectedOperandInDropdown, - } = useFilterDropdown(); + } = useFilterDropdown({ filterDropdownId }); return ( <> diff --git a/packages/twenty-front/src/modules/object-record/record-board/scopes/scope-internal-context/RecordBoardScopeInternalContext.ts b/packages/twenty-front/src/modules/object-record/record-board/scopes/scope-internal-context/RecordBoardScopeInternalContext.ts index be1ff1a067..a20426426f 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/scopes/scope-internal-context/RecordBoardScopeInternalContext.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/scopes/scope-internal-context/RecordBoardScopeInternalContext.ts @@ -1,7 +1,7 @@ import { ScopedStateKey } from '@/ui/utilities/recoil-scope/scopes-internal/types/ScopedStateKey'; import { createScopeInternalContext } from '@/ui/utilities/recoil-scope/scopes-internal/utils/createScopeInternalContext'; -type RecordBoardScopeInternalContextType = ScopedStateKey; +type RecordBoardScopeInternalContextProps = ScopedStateKey; export const RecordBoardScopeInternalContext = - createScopeInternalContext(); + createScopeInternalContext(); diff --git a/packages/twenty-front/src/modules/object-record/utils/turnFiltersIntoWhereClause.ts b/packages/twenty-front/src/modules/object-record/utils/turnFiltersIntoWhereClause.ts index eefebb1c1b..2d0495b7ba 100644 --- a/packages/twenty-front/src/modules/object-record/utils/turnFiltersIntoWhereClause.ts +++ b/packages/twenty-front/src/modules/object-record/utils/turnFiltersIntoWhereClause.ts @@ -106,7 +106,7 @@ export const turnFiltersIntoObjectRecordFilters = ( ); } break; - case 'RELATION': + case 'RELATION': { try { JSON.parse(rawUIFilter.value); } catch (e) { @@ -143,6 +143,7 @@ export const turnFiltersIntoObjectRecordFilters = ( } } break; + } case 'CURRENCY': switch (rawUIFilter.operand) { case ViewFilterOperand.GreaterThan: diff --git a/packages/twenty-front/src/modules/ui/layout/card/components/__stories__/Card.stories.tsx b/packages/twenty-front/src/modules/ui/layout/card/components/__stories__/Card.stories.tsx index c3309e37cf..b30740ea46 100644 --- a/packages/twenty-front/src/modules/ui/layout/card/components/__stories__/Card.stories.tsx +++ b/packages/twenty-front/src/modules/ui/layout/card/components/__stories__/Card.stories.tsx @@ -1,4 +1,4 @@ -import { Meta, StoryObj } from '@storybook/react'; +import { Meta, Story, StoryObj } from '@storybook/react'; import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator'; diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/components/Dropdown.tsx b/packages/twenty-front/src/modules/ui/layout/dropdown/components/Dropdown.tsx index 970e64de5c..a242ee5749 100644 --- a/packages/twenty-front/src/modules/ui/layout/dropdown/components/Dropdown.tsx +++ b/packages/twenty-front/src/modules/ui/layout/dropdown/components/Dropdown.tsx @@ -56,6 +56,7 @@ export const Dropdown = ({ useDropdown(); const offsetMiddlewares = []; + if (dropdownOffset.x) { offsetMiddlewares.push(offset({ crossAxis: dropdownOffset.x })); } diff --git a/packages/twenty-front/src/modules/views/components/EditableFilterChip.tsx b/packages/twenty-front/src/modules/views/components/EditableFilterChip.tsx new file mode 100644 index 0000000000..06ab6a0d7e --- /dev/null +++ b/packages/twenty-front/src/modules/views/components/EditableFilterChip.tsx @@ -0,0 +1,28 @@ +import { getOperandLabelShort } from '@/object-record/object-filter-dropdown/utils/getOperandLabel'; +import { useLazyLoadIcons } from '@/ui/input/hooks/useLazyLoadIcons'; +import { SortOrFilterChip } from '@/views/components/SortOrFilterChip'; +import { ViewFilter } from '@/views/types/ViewFilter'; + +type EditableFilterChipProps = { + viewFilter: ViewFilter; + onRemove: () => void; +}; + +export const EditableFilterChip = ({ + viewFilter, + onRemove, +}: EditableFilterChipProps) => { + const { icons } = useLazyLoadIcons(); + return ( + + ); +}; diff --git a/packages/twenty-front/src/modules/views/components/EditableFilterDropdownButton.tsx b/packages/twenty-front/src/modules/views/components/EditableFilterDropdownButton.tsx new file mode 100644 index 0000000000..ac41b7baf2 --- /dev/null +++ b/packages/twenty-front/src/modules/views/components/EditableFilterDropdownButton.tsx @@ -0,0 +1,79 @@ +import { useEffect } from 'react'; + +import { MultipleFiltersDropdownContent } from '@/object-record/object-filter-dropdown/components/MultipleFiltersDropdownContent'; +import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; +import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; +import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; +import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; +import { EditableFilterChip } from '@/views/components/EditableFilterChip'; +import { useViewBar } from '@/views/hooks/useViewBar'; +import { ViewFilter } from '@/views/types/ViewFilter'; + +type EditableFilterDropdownButtonProps = { + viewFilterDropdownId: string; + viewFilter: ViewFilter; + hotkeyScope: HotkeyScope; +}; + +export const EditableFilterDropdownButton = ({ + viewFilterDropdownId, + viewFilter, + hotkeyScope, +}: EditableFilterDropdownButtonProps) => { + const { + availableFilterDefinitions, + setFilterDefinitionUsedInDropdown, + setSelectedOperandInDropdown, + setSelectedFilter, + } = useFilterDropdown({ + filterDropdownId: viewFilterDropdownId, + }); + + const { closeDropdown } = useDropdown({ + dropdownScopeId: viewFilterDropdownId, + }); + + const { removeViewFilter } = useViewBar(); + + useEffect(() => { + const filterDefinition = availableFilterDefinitions.find( + (filterDefinition) => + filterDefinition.fieldMetadataId === viewFilter.fieldMetadataId, + ); + + if (filterDefinition) { + setFilterDefinitionUsedInDropdown(filterDefinition); + setSelectedOperandInDropdown(viewFilter.operand); + setSelectedFilter(viewFilter); + } + }, [ + availableFilterDefinitions, + setFilterDefinitionUsedInDropdown, + viewFilter, + setSelectedOperandInDropdown, + setSelectedFilter, + viewFilterDropdownId, + ]); + + const handleRemove = () => { + closeDropdown(); + + removeViewFilter(viewFilter.fieldMetadataId); + }; + + return ( + + } + dropdownComponents={ + + } + dropdownHotkeyScope={hotkeyScope} + dropdownOffset={{ y: 8, x: 0 }} + dropdownPlacement="bottom-start" + /> + ); +}; diff --git a/packages/twenty-front/src/modules/views/components/EditableSortChip.tsx b/packages/twenty-front/src/modules/views/components/EditableSortChip.tsx new file mode 100644 index 0000000000..4dbd7f6e93 --- /dev/null +++ b/packages/twenty-front/src/modules/views/components/EditableSortChip.tsx @@ -0,0 +1,35 @@ +import { IconArrowDown, IconArrowUp } from '@/ui/display/icon/index'; +import { SortOrFilterChip } from '@/views/components/SortOrFilterChip'; +import { useViewBar } from '@/views/hooks/useViewBar'; +import { ViewSort } from '@/views/types/ViewSort'; + +type EditableSortChipProps = { + viewSort: ViewSort; +}; + +export const EditableSortChip = ({ viewSort }: EditableSortChipProps) => { + const { removeViewSort, upsertViewSort } = useViewBar(); + + const handleRemoveClick = () => { + removeViewSort(viewSort.fieldMetadataId); + }; + + const handleClick = () => { + upsertViewSort({ + ...viewSort, + direction: viewSort.direction === 'asc' ? 'desc' : 'asc', + }); + }; + + return ( + + ); +}; diff --git a/packages/twenty-front/src/modules/views/components/SortOrFilterChip.tsx b/packages/twenty-front/src/modules/views/components/SortOrFilterChip.tsx index 070d0c4cae..b7fb3717c1 100644 --- a/packages/twenty-front/src/modules/views/components/SortOrFilterChip.tsx +++ b/packages/twenty-front/src/modules/views/components/SortOrFilterChip.tsx @@ -4,15 +4,6 @@ import styled from '@emotion/styled'; import { IconX } from '@/ui/display/icon/index'; import { IconComponent } from '@/ui/display/icon/types/IconComponent'; -type SortOrFilterChipProps = { - labelKey?: string; - labelValue: string; - Icon?: IconComponent; - onRemove: () => void; - isSort?: boolean; - testId?: string; -}; - type StyledChipProps = { isSort?: boolean; }; @@ -23,13 +14,16 @@ const StyledChip = styled.div` border: 1px solid ${({ theme }) => theme.accent.tertiary}; border-radius: 4px; color: ${({ theme }) => theme.color.blue}; + cursor: pointer; display: flex; flex-direction: row; flex-shrink: 0; font-size: ${({ theme }) => theme.font.size.sm}; font-weight: ${({ isSort }) => (isSort ? 'bold' : 'normal')}; padding: ${({ theme }) => theme.spacing(1) + ' ' + theme.spacing(2)}; + user-select: none; `; + const StyledIcon = styled.div` align-items: center; display: flex; @@ -54,17 +48,34 @@ const StyledLabelKey = styled.div` font-weight: ${({ theme }) => theme.font.weight.medium}; `; -const SortOrFilterChip = ({ +type SortOrFilterChipProps = { + labelKey?: string; + labelValue: string; + Icon?: IconComponent; + onRemove: () => void; + onClick?: () => void; + isSort?: boolean; + testId?: string; +}; + +export const SortOrFilterChip = ({ labelKey, labelValue, Icon, onRemove, isSort, testId, + onClick, }: SortOrFilterChipProps) => { const theme = useTheme(); + + const handleDeleteClick = (e: React.MouseEvent) => { + e.stopPropagation(); + onRemove(); + }; + return ( - + {Icon && ( @@ -72,11 +83,12 @@ const SortOrFilterChip = ({ )} {labelKey && {labelKey}} {labelValue} - + ); }; - -export default SortOrFilterChip; diff --git a/packages/twenty-front/src/modules/views/components/ViewBar.tsx b/packages/twenty-front/src/modules/views/components/ViewBar.tsx index ae58a8a0fb..53af0c8eaa 100644 --- a/packages/twenty-front/src/modules/views/components/ViewBar.tsx +++ b/packages/twenty-front/src/modules/views/components/ViewBar.tsx @@ -97,6 +97,7 @@ export const ViewBar = ({ { const { currentViewSortsState, @@ -98,7 +101,6 @@ export const ViewBarDetails = ({ canPersistSortsSelector, isViewBarExpandedState, } = useViewScopedStates(); - const { icons } = useLazyLoadIcons(); const currentViewSorts = useRecoilValue(currentViewSortsState); const currentViewFilters = useRecoilValue(currentViewFiltersState); @@ -106,7 +108,7 @@ export const ViewBarDetails = ({ const canPersistSorts = useRecoilValue(canPersistSortsSelector); const isViewBarExpanded = useRecoilValue(isViewBarExpandedState); - const { resetViewBar, removeViewSort, removeViewFilter } = useViewBar(); + const { resetViewBar } = useViewBar(); const canPersistView = canPersistFilters || canPersistSorts; @@ -114,6 +116,10 @@ export const ViewBarDetails = ({ resetViewBar(); }; + const { upsertViewFilter } = useViewBar({ + viewBarId: viewBarId, + }); + const shouldExpandViewBar = canPersistView || ((currentViewSorts?.length || currentViewFilters?.length) && @@ -128,36 +134,32 @@ export const ViewBarDetails = ({ {currentViewSorts?.map((sort) => { - return ( - removeViewSort(sort.fieldMetadataId)} - /> - ); + return ; })} {!!currentViewSorts?.length && !!currentViewFilters?.length && ( )} - {currentViewFilters?.map((filter) => { + {currentViewFilters?.map((viewFilter) => { return ( - { - removeViewFilter(filter.fieldMetadataId); - }} - /> + + + + + + ); })} diff --git a/yarn.lock b/yarn.lock index cc8674e7ed..8df7d08c82 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18991,6 +18991,7 @@ __metadata: jest: "npm:^28.1.3" postcss: "npm:^8.4.29" prettier: "npm:^3.0.3" + rimraf: "npm:^5.0.5" ts-jest: "npm:^29.1.1" ts-node: "npm:^10.9.1" typescript: "npm:^5.2.2" @@ -20527,7 +20528,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:^10.0.0, glob@npm:^10.2.2, glob@npm:^10.3.10": +"glob@npm:^10.0.0, glob@npm:^10.2.2, glob@npm:^10.3.10, glob@npm:^10.3.7": version: 10.3.10 resolution: "glob@npm:10.3.10" dependencies: @@ -31483,6 +31484,17 @@ __metadata: languageName: node linkType: hard +"rimraf@npm:^5.0.5": + version: 5.0.5 + resolution: "rimraf@npm:5.0.5" + dependencies: + glob: "npm:^10.3.7" + bin: + rimraf: dist/esm/bin.mjs + checksum: d50dbe724f33835decd88395b25ed35995077c60a50ae78ded06e0185418914e555817aad1b4243edbff2254548c2f6ad6f70cc850040bebb4da9e8cc016f586 + languageName: node + linkType: hard + "rimraf@npm:~2.6.2": version: 2.6.3 resolution: "rimraf@npm:2.6.3"