From c342ece2ee7985c022d941cebbc219a37055a871 Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Mon, 18 Aug 2025 10:34:12 +0200 Subject: [PATCH] Prepare field option dropdown for RecordField usage (#13935) This PR prepares and simplify option dropdown for using record fields. I had a problem while trying to use currentRecordFields directly with ordering so I stayed with the actual column definition system. --------- Co-authored-by: Charles Bochet --- .../ISSUE_TEMPLATE/request-technical-work.md | 2 +- .../ObjectOptionsDropdownFieldsContent.tsx | 52 +----- ...jectOptionsDropdownHiddenFieldsContent.tsx | 43 +---- .../hooks/useObjectOptionsForBoard.ts | 4 +- .../record-table/hooks/useTableColumns.ts | 5 +- .../ViewFieldsHiddenDropdownSection.tsx | 112 +++++++++++++ .../ViewFieldsVisibilityDropdownSection.tsx | 153 ------------------ .../ViewFieldsVisibleDropdownSection.tsx | 136 ++++++++++++++++ 8 files changed, 262 insertions(+), 245 deletions(-) create mode 100644 packages/twenty-front/src/modules/views/components/ViewFieldsHiddenDropdownSection.tsx delete mode 100644 packages/twenty-front/src/modules/views/components/ViewFieldsVisibilityDropdownSection.tsx create mode 100644 packages/twenty-front/src/modules/views/components/ViewFieldsVisibleDropdownSection.tsx diff --git a/.github/ISSUE_TEMPLATE/request-technical-work.md b/.github/ISSUE_TEMPLATE/request-technical-work.md index 4e61687305..5c858ab2c2 100644 --- a/.github/ISSUE_TEMPLATE/request-technical-work.md +++ b/.github/ISSUE_TEMPLATE/request-technical-work.md @@ -27,7 +27,7 @@ Having a list that is draggable will be useful, not only in dropdown. Create a folder @/ui/draggable-list with a DraggableList component This component should take as prop: itemsComponents, onDragEnd((previousIndex, nextIndex) => {}) -Use this component in ViewFieldsVisibilityDropdownSection (move the logic from ViewFieldsVisibilityDropdownSection to DraggableList) by passing a list of DraggableMenuItems +Use this component in ObjectOptionsDropdownHiddenFieldsContent (move the logic from ObjectOptionsDropdownHiddenFieldsContent to DraggableList) by passing a list of DraggableMenuItems Add a storybook test on this list (we don't know how to actually test the draggable behavior, but we can at least make sure the component renders correctly a list of items) ``` diff --git a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownFieldsContent.tsx b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownFieldsContent.tsx index e6e89a9bf0..49e13303fa 100644 --- a/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownFieldsContent.tsx +++ b/packages/twenty-front/src/modules/object-record/object-options-dropdown/components/ObjectOptionsDropdownFieldsContent.tsx @@ -1,55 +1,17 @@ import { useObjectOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsDropdown'; -import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard'; -import { useObjectOptionsForTable } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForTable'; import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader'; import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; -import { ViewFieldsVisibilityDropdownSection } from '@/views/components/ViewFieldsVisibilityDropdownSection'; -import { ViewType } from '@/views/types/ViewType'; +import { ViewFieldsVisibleDropdownSection } from '@/views/components/ViewFieldsVisibleDropdownSection'; import { useLingui } from '@lingui/react/macro'; import { IconChevronLeft, IconEyeOff } from 'twenty-ui/display'; import { MenuItemNavigate } from 'twenty-ui/navigation'; export const ObjectOptionsDropdownFieldsContent = () => { const { t } = useLingui(); - const { - viewType, - recordIndexId, - objectMetadataItem, - onContentChange, - resetContent, - } = useObjectOptionsDropdown(); - - const { - handleColumnVisibilityChange, - handleReorderColumns, - visibleTableColumns, - } = useObjectOptionsForTable(recordIndexId, objectMetadataItem.id); - - const { - visibleBoardFields, - handleReorderBoardFields, - handleBoardFieldVisibilityChange, - } = useObjectOptionsForBoard({ - objectNameSingular: objectMetadataItem.nameSingular, - recordBoardId: recordIndexId, - viewBarId: recordIndexId, - }); - - const visibleRecordFields = - viewType === ViewType.Kanban ? visibleBoardFields : visibleTableColumns; - - const handleReorderFields = - viewType === ViewType.Kanban - ? handleReorderBoardFields - : handleReorderColumns; - - const handleChangeFieldVisibility = - viewType === ViewType.Kanban - ? handleBoardFieldVisibilityChange - : handleColumnVisibilityChange; + const { onContentChange, resetContent } = useObjectOptionsDropdown(); return ( @@ -63,15 +25,7 @@ export const ObjectOptionsDropdownFieldsContent = () => { > {t`Fields`} - + { const { t } = useLingui(); - const { - viewType, - recordIndexId, - objectMetadataItem, - onContentChange, - closeDropdown, - } = useObjectOptionsDropdown(); + const { objectMetadataItem, onContentChange, closeDropdown } = + useObjectOptionsDropdown(); const { objectNamePlural } = useObjectNamePluralFromSingular({ objectNameSingular: objectMetadataItem.nameSingular, @@ -38,24 +30,6 @@ export const ObjectOptionsDropdownHiddenFieldsContent = () => { objectNamePlural, }); - const { handleColumnVisibilityChange, hiddenTableColumns } = - useObjectOptionsForTable(recordIndexId, objectMetadataItem.id); - - const { hiddenBoardFields, handleBoardFieldVisibilityChange } = - useObjectOptionsForBoard({ - objectNameSingular: objectMetadataItem.nameSingular, - recordBoardId: recordIndexId, - viewBarId: recordIndexId, - }); - - const hiddenRecordFields = - viewType === ViewType.Kanban ? hiddenBoardFields : hiddenTableColumns; - - const handleChangeFieldVisibility = - viewType === ViewType.Kanban - ? handleBoardFieldVisibilityChange - : handleColumnVisibilityChange; - const location = useLocation(); const setNavigationMemorizedUrl = useSetRecoilState( navigationMemorizedUrlState, @@ -73,16 +47,7 @@ export const ObjectOptionsDropdownHiddenFieldsContent = () => { > {t`Hidden Fields`} - {hiddenRecordFields.length > 0 && ( - - )} + , - 'size' | 'position' + 'fieldMetadataId' | 'isVisible' >, ) => { const isNewViewField = !( diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/useTableColumns.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/useTableColumns.ts index 4d10fd617e..755c2c8821 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/useTableColumns.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/useTableColumns.ts @@ -49,7 +49,10 @@ export const useTableColumns = ({ const handleColumnVisibilityChange = useCallback( async ( - viewField: Omit, 'size' | 'position'>, + viewField: Pick< + ColumnDefinition, + 'fieldMetadataId' | 'isVisible' + >, ) => { const shouldShowColumn = !visibleTableColumns.some( (visibleColumn) => diff --git a/packages/twenty-front/src/modules/views/components/ViewFieldsHiddenDropdownSection.tsx b/packages/twenty-front/src/modules/views/components/ViewFieldsHiddenDropdownSection.tsx new file mode 100644 index 0000000000..937455cf05 --- /dev/null +++ b/packages/twenty-front/src/modules/views/components/ViewFieldsHiddenDropdownSection.tsx @@ -0,0 +1,112 @@ +import { type DropResult, type ResponderProvided } from '@hello-pangea/dnd'; + +import { useActiveFieldMetadataItems } from '@/object-metadata/hooks/useActiveFieldMetadataItems'; +import { useObjectOptionsForBoard } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForBoard'; +import { useObjectOptionsForTable } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsForTable'; +import { ObjectOptionsDropdownContext } from '@/object-record/object-options-dropdown/states/contexts/ObjectOptionsDropdownContext'; +import { currentRecordFieldsComponentState } from '@/object-record/record-field/states/currentRecordFieldsComponentState'; +import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem'; +import { DraggableList } from '@/ui/layout/draggable-list/components/DraggableList'; +import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; +import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { ViewType } from '@/views/types/ViewType'; +import { useContext } from 'react'; +import { IconEye, useIcons } from 'twenty-ui/display'; +import { MenuItemDraggable } from 'twenty-ui/navigation'; + +export const ViewFieldsHiddenDropdownSection = () => { + const { viewType, objectMetadataItem, recordIndexId } = useContext( + ObjectOptionsDropdownContext, + ); + + const { handleReorderColumns, handleColumnVisibilityChange } = + useObjectOptionsForTable(recordIndexId, objectMetadataItem.id); + + const { handleReorderBoardFields, handleBoardFieldVisibilityChange } = + useObjectOptionsForBoard({ + objectNameSingular: objectMetadataItem.nameSingular, + recordBoardId: recordIndexId, + viewBarId: recordIndexId, + }); + + const handleReorderFields = + viewType === ViewType.Kanban + ? handleReorderBoardFields + : handleReorderColumns; + + const handleChangeFieldVisibility = + viewType === ViewType.Kanban + ? handleBoardFieldVisibilityChange + : handleColumnVisibilityChange; + + const handleDragEnd = (result: DropResult, provided: ResponderProvided) => { + handleReorderFields(result, provided); + }; + + const currentRecordFields = useRecoilComponentValue( + currentRecordFieldsComponentState, + ); + + const visibleRecordFields = currentRecordFields.filter( + (recordFieldToFilter) => recordFieldToFilter.isVisible === true, + ); + + const { activeFieldMetadataItems } = useActiveFieldMetadataItems({ + objectMetadataItem, + }); + + const availableFieldMetadataItemsToShow = activeFieldMetadataItems.filter( + (fieldMetadataItemToFilter) => + !visibleRecordFields + .map((recordField) => recordField.fieldMetadataItemId) + .includes(fieldMetadataItemToFilter.id), + ); + + const { getIcon } = useIcons(); + + return ( + <> + + {availableFieldMetadataItemsToShow.length > 0 && ( + + {availableFieldMetadataItemsToShow.map( + (fieldMetadataItem, index) => { + const fieldIndex = index; + + return ( + + handleChangeFieldVisibility({ + fieldMetadataId: fieldMetadataItem.id, + isVisible: false, + }), + }, + ]} + text={fieldMetadataItem.label} + /> + } + /> + ); + }, + )} + + } + /> + )} + + + ); +}; diff --git a/packages/twenty-front/src/modules/views/components/ViewFieldsVisibilityDropdownSection.tsx b/packages/twenty-front/src/modules/views/components/ViewFieldsVisibilityDropdownSection.tsx deleted file mode 100644 index 254a622edc..0000000000 --- a/packages/twenty-front/src/modules/views/components/ViewFieldsVisibilityDropdownSection.tsx +++ /dev/null @@ -1,153 +0,0 @@ -import { - type DropResult, - type OnDragEndResponder, - type ResponderProvided, -} from '@hello-pangea/dnd'; -import { useState } from 'react'; -import { createPortal } from 'react-dom'; - -import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldMetadata'; -import { type ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; -import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem'; -import { DraggableList } from '@/ui/layout/draggable-list/components/DraggableList'; -import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; -import { StyledDropdownMenuSubheader } from '@/ui/layout/dropdown/components/StyledDropdownMenuSubheader'; -import { isDefined } from 'twenty-shared/utils'; -import { - AppTooltip, - IconEye, - IconEyeOff, - IconInfoCircle, - useIcons, -} from 'twenty-ui/display'; -import { MenuItemDraggable } from 'twenty-ui/navigation'; -import { groupArrayItemsBy } from '~/utils/array/groupArrayItemsBy'; - -type ViewFieldsVisibilityDropdownSectionProps = { - fields: Omit, 'size'>[]; - isDraggable: boolean; - onDragEnd?: OnDragEndResponder; - onVisibilityChange: ( - field: Omit, 'size' | 'position'>, - ) => void; - title: string; - showSubheader: boolean; - showDragGrip: boolean; -}; - -export const ViewFieldsVisibilityDropdownSection = ({ - fields, - isDraggable, - onDragEnd, - onVisibilityChange, - title, - showSubheader = true, - showDragGrip, -}: ViewFieldsVisibilityDropdownSectionProps) => { - const handleOnDrag = (result: DropResult, provided: ResponderProvided) => { - onDragEnd?.(result, provided); - }; - - const [openToolTipIndex, setOpenToolTipIndex] = useState(); - - const handleInfoButtonClick = (index: number) => { - setOpenToolTipIndex(index === openToolTipIndex ? undefined : index); - }; - - const { getIcon } = useIcons(); - - const getIconButtons = ( - index: number, - field: Omit, 'size' | 'position'>, - ) => { - const iconButtons = [ - field.infoTooltipContent - ? { - Icon: IconInfoCircle, - onClick: () => handleInfoButtonClick(index), - isActive: openToolTipIndex === index, - } - : null, - field.isLabelIdentifier - ? null - : { - Icon: field.isVisible ? IconEyeOff : IconEye, - onClick: () => onVisibilityChange(field), - }, - ].filter(isDefined); - - return iconButtons.length ? iconButtons : undefined; - }; - - const { nonDraggableItems = [], draggableItems = [] } = isDraggable - ? groupArrayItemsBy(fields, ({ isLabelIdentifier }) => - isLabelIdentifier ? 'nonDraggableItems' : 'draggableItems', - ) - : { nonDraggableItems: fields, draggableItems: [] }; - - return ( - <> - {showSubheader && ( - {title} - )} - - {nonDraggableItems.map((field, fieldIndex) => ( - - ))} - {!!draggableItems.length && ( - - {draggableItems.map((field, index) => { - const fieldIndex = index + nonDraggableItems.length; - - return ( - - } - /> - ); - })} - - } - /> - )} - - {isDefined(openToolTipIndex) && - createPortal( -