diff --git a/packages/twenty-front/src/modules/object-record/hooks/useRecordTableContextMenuEntries.tsx b/packages/twenty-front/src/modules/object-record/hooks/useRecordTableContextMenuEntries.tsx deleted file mode 100644 index dd5e6417f..000000000 --- a/packages/twenty-front/src/modules/object-record/hooks/useRecordTableContextMenuEntries.tsx +++ /dev/null @@ -1,226 +0,0 @@ -import { useCallback } from 'react'; -import { isNonEmptyString } from '@sniptt/guards'; -import { useRecoilCallback, useRecoilValue, useSetRecoilState } from 'recoil'; - -import { useOpenCreateActivityDrawerForSelectedRowIds } from '@/activities/hooks/useOpenCreateActivityDrawerForSelectedRowIds'; -import { useFavorites } from '@/favorites/hooks/useFavorites'; -import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural'; -import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords'; -import { useExecuteQuickActionOnOneRecord } from '@/object-record/hooks/useExecuteQuickActionOnOneRecord'; -import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; -import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates'; -import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; -import { - IconCheckbox, - IconClick, - IconHeart, - IconHeartOff, - IconMail, - IconNotes, - IconPuzzle, - IconTrash, -} from '@/ui/display/icon'; -import { actionBarEntriesState } from '@/ui/navigation/action-bar/states/actionBarEntriesState'; -import { contextMenuEntriesState } from '@/ui/navigation/context-menu/states/contextMenuEntriesState'; -import { ContextMenuEntry } from '@/ui/navigation/context-menu/types/ContextMenuEntry'; -import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue'; -import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; - -type useRecordTableContextMenuEntriesProps = { - objectNamePlural: string; - recordTableId: string; -}; - -// TODO: refactor this -export const useRecordTableContextMenuEntries = ( - props: useRecordTableContextMenuEntriesProps, -) => { - const setContextMenuEntries = useSetRecoilState(contextMenuEntriesState); - const setActionBarEntriesState = useSetRecoilState(actionBarEntriesState); - - const { getSelectedRowIdsSelector } = useRecordTableStates( - props?.recordTableId, - ); - - const selectedRowIds = useRecoilValue(getSelectedRowIdsSelector()); - - const { resetTableRowSelection } = useRecordTable({ - recordTableId: props?.recordTableId, - }); - - const { objectNameSingular } = useObjectNameSingularFromPlural({ - objectNamePlural: props.objectNamePlural, - }); - - const { createFavorite, favorites, deleteFavorite } = useFavorites(); - - const handleFavoriteButtonClick = useRecoilCallback(({ snapshot }) => () => { - const selectedRowIds = getSnapshotValue( - snapshot, - getSelectedRowIdsSelector(), - ); - - const selectedRowId = selectedRowIds.length === 1 ? selectedRowIds[0] : ''; - - const selectedRecord = snapshot - .getLoadable(recordStoreFamilyState(selectedRowId)) - .getValue(); - - const foundFavorite = favorites?.find( - (favorite) => favorite.recordId === selectedRowId, - ); - - const isFavorite = !!selectedRowId && !!foundFavorite; - - resetTableRowSelection(); - - if (isFavorite) { - deleteFavorite(foundFavorite.id); - } else if (selectedRecord) { - createFavorite(selectedRecord, objectNameSingular); - } - }); - - const { deleteManyRecords } = useDeleteManyRecords({ - objectNameSingular, - }); - - const { executeQuickActionOnOneRecord } = useExecuteQuickActionOnOneRecord({ - objectNameSingular, - }); - - const handleDeleteClick = useRecoilCallback( - ({ snapshot }) => - async () => { - const rowIdsToDelete = getSnapshotValue( - snapshot, - getSelectedRowIdsSelector(), - ); - - resetTableRowSelection(); - await deleteManyRecords(rowIdsToDelete); - }, - [deleteManyRecords, resetTableRowSelection, getSelectedRowIdsSelector], - ); - - const handleExecuteQuickActionOnClick = useRecoilCallback( - ({ snapshot }) => - async () => { - const rowIdsToExecuteQuickActionOn = getSnapshotValue( - snapshot, - getSelectedRowIdsSelector(), - ); - - resetTableRowSelection(); - await Promise.all( - rowIdsToExecuteQuickActionOn.map(async (rowId) => { - await executeQuickActionOnOneRecord(rowId); - }), - ); - }, - [ - executeQuickActionOnOneRecord, - resetTableRowSelection, - getSelectedRowIdsSelector, - ], - ); - - const dataExecuteQuickActionOnmentEnabled = useIsFeatureEnabled( - 'IS_QUICK_ACTIONS_ENABLED', - ); - - const openCreateActivityDrawer = useOpenCreateActivityDrawerForSelectedRowIds( - props.recordTableId, - ); - - return { - setContextMenuEntries: useCallback(() => { - const selectedRowId = - selectedRowIds.length === 1 ? selectedRowIds[0] : ''; - - const isFavorite = - isNonEmptyString(selectedRowId) && - !!favorites?.find((favorite) => favorite.recordId === selectedRowId); - - const contextMenuEntries = [ - // { - // label: 'New task', - // Icon: IconCheckbox, - // onClick: () => {}, - // }, - // { - // label: 'New note', - // Icon: IconNotes, - // onClick: () => {}, - // }, - - { - label: 'Delete', - Icon: IconTrash, - accent: 'danger', - onClick: () => handleDeleteClick(), - }, - ] as ContextMenuEntry[]; - - if (selectedRowIds.length === 1) { - contextMenuEntries.unshift({ - label: isFavorite ? 'Remove from favorites' : 'Add to favorites', - Icon: isFavorite ? IconHeartOff : IconHeart, - onClick: () => handleFavoriteButtonClick(), - }); - } - - setContextMenuEntries(contextMenuEntries); - }, [ - selectedRowIds, - favorites, - handleDeleteClick, - handleFavoriteButtonClick, - setContextMenuEntries, - ]), - - setActionBarEntries: useRecoilCallback(() => () => { - setActionBarEntriesState([ - { - label: 'Task', - Icon: IconCheckbox, - onClick: () => { - openCreateActivityDrawer('Task', objectNameSingular); - }, - }, - { - label: 'Note', - Icon: IconNotes, - onClick: () => { - openCreateActivityDrawer('Note', objectNameSingular); - }, - }, - ...(dataExecuteQuickActionOnmentEnabled - ? [ - { - label: 'Actions', - Icon: IconClick, - subActions: [ - { - label: 'Enrich', - Icon: IconPuzzle, - onClick: () => handleExecuteQuickActionOnClick(), - }, - { - label: 'Send to mailjet', - Icon: IconMail, - }, - ], - }, - ] - : []), - { - label: 'Delete', - Icon: IconTrash, - accent: 'danger', - onClick: () => handleDeleteClick(), - }, - ]); - }), - }; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-action-bar/hooks/useRecordActionBar.tsx b/packages/twenty-front/src/modules/object-record/record-action-bar/hooks/useRecordActionBar.tsx new file mode 100644 index 000000000..a60920374 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-action-bar/hooks/useRecordActionBar.tsx @@ -0,0 +1,168 @@ +import { useCallback, useMemo } from 'react'; +import { isNonEmptyString } from '@sniptt/guards'; +import { useRecoilCallback, useSetRecoilState } from 'recoil'; + +import { useFavorites } from '@/favorites/hooks/useFavorites'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords'; +import { useExecuteQuickActionOnOneRecord } from '@/object-record/hooks/useExecuteQuickActionOnOneRecord'; +import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; +import { + IconClick, + IconHeart, + IconHeartOff, + IconMail, + IconPuzzle, + IconTrash, +} from '@/ui/display/icon'; +import { actionBarEntriesState } from '@/ui/navigation/action-bar/states/actionBarEntriesState'; +import { contextMenuEntriesState } from '@/ui/navigation/context-menu/states/contextMenuEntriesState'; +import { ContextMenuEntry } from '@/ui/navigation/context-menu/types/ContextMenuEntry'; +import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; + +type useRecordActionBarProps = { + objectMetadataItem: ObjectMetadataItem; + selectedRecordIds: string[]; + callback?: () => void; +}; + +export const useRecordActionBar = ({ + objectMetadataItem, + selectedRecordIds, + callback, +}: useRecordActionBarProps) => { + const setContextMenuEntries = useSetRecoilState(contextMenuEntriesState); + const setActionBarEntriesState = useSetRecoilState(actionBarEntriesState); + + const { createFavorite, favorites, deleteFavorite } = useFavorites(); + + const { deleteManyRecords } = useDeleteManyRecords({ + objectNameSingular: objectMetadataItem.nameSingular, + }); + + const { executeQuickActionOnOneRecord } = useExecuteQuickActionOnOneRecord({ + objectNameSingular: objectMetadataItem.nameSingular, + }); + + const handleFavoriteButtonClick = useRecoilCallback(({ snapshot }) => () => { + if (selectedRecordIds.length > 1) { + return; + } + + const selectedRecordId = selectedRecordIds[0]; + const selectedRecord = snapshot + .getLoadable(recordStoreFamilyState(selectedRecordId)) + .getValue(); + + const foundFavorite = favorites?.find( + (favorite) => favorite.recordId === selectedRecordId, + ); + + const isFavorite = !!selectedRecordId && !!foundFavorite; + + if (isFavorite) { + deleteFavorite(foundFavorite.id); + } else if (selectedRecord) { + createFavorite(selectedRecord, objectMetadataItem.nameSingular); + } + callback?.(); + }); + + const handleDeleteClick = useCallback(async () => { + callback?.(); + await deleteManyRecords(selectedRecordIds); + }, [callback, deleteManyRecords, selectedRecordIds]); + + const handleExecuteQuickActionOnClick = useCallback(async () => { + callback?.(); + await Promise.all( + selectedRecordIds.map(async (recordId) => { + await executeQuickActionOnOneRecord(recordId); + }), + ); + }, [callback, executeQuickActionOnOneRecord, selectedRecordIds]); + + const baseActions: ContextMenuEntry[] = useMemo( + () => [ + { + label: 'Delete', + Icon: IconTrash, + accent: 'danger', + onClick: () => handleDeleteClick(), + }, + ], + [handleDeleteClick], + ); + + const dataExecuteQuickActionOnmentEnabled = useIsFeatureEnabled( + 'IS_QUICK_ACTIONS_ENABLED', + ); + + const hasOnlyOneRecordSelected = selectedRecordIds.length === 1; + + const isFavorite = + isNonEmptyString(selectedRecordIds[0]) && + !!favorites?.find((favorite) => favorite.recordId === selectedRecordIds[0]); + + return { + setContextMenuEntries: useCallback(() => { + setContextMenuEntries([ + ...baseActions, + ...(isFavorite && hasOnlyOneRecordSelected + ? [ + { + label: 'Remove from favorites', + Icon: IconHeartOff, + onClick: handleFavoriteButtonClick, + }, + ] + : []), + ...(!isFavorite && hasOnlyOneRecordSelected + ? [ + { + label: 'Add to favorites', + Icon: IconHeart, + onClick: handleFavoriteButtonClick, + }, + ] + : []), + ]); + }, [ + baseActions, + handleFavoriteButtonClick, + hasOnlyOneRecordSelected, + isFavorite, + setContextMenuEntries, + ]), + + setActionBarEntries: useCallback(() => { + setActionBarEntriesState([ + ...(dataExecuteQuickActionOnmentEnabled + ? [ + { + label: 'Actions', + Icon: IconClick, + subActions: [ + { + label: 'Enrich', + Icon: IconPuzzle, + onClick: handleExecuteQuickActionOnClick, + }, + { + label: 'Send to mailjet', + Icon: IconMail, + }, + ], + }, + ] + : []), + ...baseActions, + ]); + }, [ + baseActions, + dataExecuteQuickActionOnmentEnabled, + handleExecuteQuickActionOnClick, + setActionBarEntriesState, + ]), + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-board-deprecated/action-bar/components/RecordBoardDeprecatedActionBar.tsx b/packages/twenty-front/src/modules/object-record/record-board-deprecated/action-bar/components/RecordBoardDeprecatedActionBar.tsx index 95c50a5cb..b2e01c273 100644 --- a/packages/twenty-front/src/modules/object-record/record-board-deprecated/action-bar/components/RecordBoardDeprecatedActionBar.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board-deprecated/action-bar/components/RecordBoardDeprecatedActionBar.tsx @@ -7,5 +7,10 @@ import { ActionBar } from '@/ui/navigation/action-bar/components/ActionBar'; export const RecordBoardDeprecatedActionBar = () => { const { selectedCardIdsSelector } = useRecordBoardDeprecatedScopedStates(); const selectedCardIds = useRecoilValue(selectedCardIdsSelector); - return ; + + if (!selectedCardIds.length) { + return null; + } + + return ; }; diff --git a/packages/twenty-front/src/modules/object-record/record-board-deprecated/context-menu/components/RecordBoardDeprecatedContextMenu.tsx b/packages/twenty-front/src/modules/object-record/record-board-deprecated/context-menu/components/RecordBoardDeprecatedContextMenu.tsx index 38078a6b5..900d3d199 100644 --- a/packages/twenty-front/src/modules/object-record/record-board-deprecated/context-menu/components/RecordBoardDeprecatedContextMenu.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board-deprecated/context-menu/components/RecordBoardDeprecatedContextMenu.tsx @@ -6,5 +6,9 @@ import { ContextMenu } from '@/ui/navigation/context-menu/components/ContextMenu export const RecordBoardDeprecatedContextMenu = () => { const { selectedCardIdsSelector } = useRecordBoardDeprecatedScopedStates(); const selectedCardIds = useRecoilValue(selectedCardIdsSelector); - return ; + + if (!selectedCardIds.length) { + return null; + } + return ; }; diff --git a/packages/twenty-front/src/modules/object-record/record-board/action-bar/components/RecordBoardActionBar.tsx b/packages/twenty-front/src/modules/object-record/record-board/action-bar/components/RecordBoardActionBar.tsx new file mode 100644 index 000000000..d0d810222 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-board/action-bar/components/RecordBoardActionBar.tsx @@ -0,0 +1,22 @@ +import { useRecoilValue } from 'recoil'; + +import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; +import { ActionBar } from '@/ui/navigation/action-bar/components/ActionBar'; + +type RecordBoardActionBarProps = { + recordBoardId: string; +}; + +export const RecordBoardActionBar = ({ + recordBoardId, +}: RecordBoardActionBarProps) => { + const { getSelectedRecordIdsSelector } = useRecordBoardStates(recordBoardId); + + const selectedRecordIds = useRecoilValue(getSelectedRecordIdsSelector()); + + if (!selectedRecordIds.length) { + return null; + } + + return ; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-board/context-menu/components/RecordBoardContextMenu.tsx b/packages/twenty-front/src/modules/object-record/record-board/context-menu/components/RecordBoardContextMenu.tsx new file mode 100644 index 000000000..b98245d90 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-board/context-menu/components/RecordBoardContextMenu.tsx @@ -0,0 +1,22 @@ +import { useRecoilValue } from 'recoil'; + +import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; +import { ContextMenu } from '@/ui/navigation/context-menu/components/ContextMenu'; + +type RecordBoardContextMenuProps = { + recordBoardId: string; +}; + +export const RecordBoardContextMenu = ({ + recordBoardId, +}: RecordBoardContextMenuProps) => { + const { getSelectedRecordIdsSelector } = useRecordBoardStates(recordBoardId); + + const selectedRecordIds = useRecoilValue(getSelectedRecordIdsSelector()); + + if (!selectedRecordIds.length) { + return null; + } + + return ; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/useRecordBoardStates.ts b/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/useRecordBoardStates.ts index 18c63b269..63b98c804 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/useRecordBoardStates.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/useRecordBoardStates.ts @@ -10,6 +10,7 @@ import { recordBoardObjectSingularNameStateScopeMap } from '@/object-record/reco import { recordBoardRecordIdsByColumnIdFamilyStateScopeMap } from '@/object-record/record-board/states/recordBoardRecordIdsByColumnIdFamilyStateScopeMap'; import { recordBoardSortsStateScopeMap } from '@/object-record/record-board/states/recordBoardSortsStateScopeMap'; import { recordBoardColumnsFamilySelectorScopeMap } from '@/object-record/record-board/states/selectors/recordBoardColumnsFamilySelectorScopeMap'; +import { recordBoardSelectedRecordIdsSelectorScopeMap } from '@/object-record/record-board/states/selectors/recordBoardSelectedRecordIdsSelectorScopeMap'; import { recordBoardVisibleFieldDefinitionsScopedSelector } from '@/object-record/record-board/states/selectors/recordBoardVisibleFieldDefinitionsScopedSelector'; import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId'; import { getFamilyState } from '@/ui/utilities/recoil-scope/utils/getFamilyState'; @@ -54,7 +55,7 @@ export const useRecordBoardStates = (recordBoardId?: string) => { scopeId, ), - recordBoardRecordIdsByColumnIdFamilyState: getFamilyState( + recordIdsByColumnIdFamilyState: getFamilyState( recordBoardRecordIdsByColumnIdFamilyStateScopeMap, scopeId, ), @@ -62,6 +63,10 @@ export const useRecordBoardStates = (recordBoardId?: string) => { isRecordBoardCardSelectedFamilyStateScopeMap, scopeId, ), + getSelectedRecordIdsSelector: getSelectorReadOnly( + recordBoardSelectedRecordIdsSelectorScopeMap, + scopeId, + ), getIsCompactModeActiveState: getState( isRecordBoardCompactModeActiveStateScopeMap, diff --git a/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/useSetRecordBoardRecordIds.ts b/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/useSetRecordBoardRecordIds.ts index aa40b3377..db2811af0 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/useSetRecordBoardRecordIds.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/useSetRecordBoardRecordIds.ts @@ -7,7 +7,7 @@ import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; export const useSetRecordBoardRecordIds = (recordBoardId?: string) => { const { scopeId, - recordBoardRecordIdsByColumnIdFamilyState, + recordIdsByColumnIdFamilyState, columnsFamilySelector, getColumnIdsState, } = useRecordBoardStates(recordBoardId); @@ -23,7 +23,7 @@ export const useSetRecordBoardRecordIds = (recordBoardId?: string) => { .getValue(); const existingColumnRecordIds = snapshot - .getLoadable(recordBoardRecordIdsByColumnIdFamilyState(columnId)) + .getLoadable(recordIdsByColumnIdFamilyState(columnId)) .getValue(); const columnRecordIds = records @@ -31,18 +31,11 @@ export const useSetRecordBoardRecordIds = (recordBoardId?: string) => { .map((record) => record.id); if (!isDeeplyEqual(existingColumnRecordIds, columnRecordIds)) { - set( - recordBoardRecordIdsByColumnIdFamilyState(columnId), - columnRecordIds, - ); + set(recordIdsByColumnIdFamilyState(columnId), columnRecordIds); } }); }, - [ - columnsFamilySelector, - getColumnIdsState, - recordBoardRecordIdsByColumnIdFamilyState, - ], + [columnsFamilySelector, getColumnIdsState, recordIdsByColumnIdFamilyState], ); return { diff --git a/packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoard.ts b/packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoard.ts index 59f11807c..486bce6e1 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoard.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoard.ts @@ -5,8 +5,12 @@ import { useSetRecordBoardColumns } from '@/object-record/record-board/hooks/int import { useSetRecordBoardRecordIds } from '@/object-record/record-board/hooks/internal/useSetRecordBoardRecordIds'; export const useRecordBoard = (recordBoardId?: string) => { - const { scopeId, getFieldDefinitionsState, getObjectSingularNameState } = - useRecordBoardStates(recordBoardId); + const { + scopeId, + getFieldDefinitionsState, + getObjectSingularNameState, + getSelectedRecordIdsSelector, + } = useRecordBoardStates(recordBoardId); const { setColumns } = useSetRecordBoardColumns(recordBoardId); const { setRecordIds } = useSetRecordBoardRecordIds(recordBoardId); @@ -19,5 +23,6 @@ export const useRecordBoard = (recordBoardId?: string) => { setRecordIds, setFieldDefinitions, setObjectSingularName, + getSelectedRecordIdsSelector, }; }; diff --git a/packages/twenty-front/src/modules/object-record/record-board/hooks/useResetBoardRecordSelection.ts b/packages/twenty-front/src/modules/object-record/record-board/hooks/useResetBoardRecordSelection.ts new file mode 100644 index 000000000..6266eb536 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-board/hooks/useResetBoardRecordSelection.ts @@ -0,0 +1,24 @@ +import { useRecoilCallback } from 'recoil'; + +import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; + +export const useResetBoardRecordSelection = (recordBoardId?: string) => { + const { getSelectedRecordIdsSelector, isRecordBoardCardSelectedFamilyState } = + useRecordBoardStates(recordBoardId); + + const resetRecordSelection = useRecoilCallback( + ({ snapshot, set }) => + () => { + const recordIds = snapshot + .getLoadable(getSelectedRecordIdsSelector()) + .getValue(); + + for (const recordId of recordIds) { + set(isRecordBoardCardSelectedFamilyState(recordId), false); + } + }, + [getSelectedRecordIdsSelector, isRecordBoardCardSelectedFamilyState], + ); + + return { resetRecordSelection }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumn.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumn.tsx index 0f65fdf3b..5a048c78f 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumn.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumn.tsx @@ -32,7 +32,7 @@ export const RecordBoardColumn = ({ isFirstColumnFamilyState, isLastColumnFamilyState, columnsFamilySelector, - recordBoardRecordIdsByColumnIdFamilyState, + recordIdsByColumnIdFamilyState, } = useRecordBoardStates(); const columnDefinition = useRecoilValue( columnsFamilySelector(recordBoardColumnId), @@ -47,7 +47,7 @@ export const RecordBoardColumn = ({ ); const recordIds = useRecoilValue( - recordBoardRecordIdsByColumnIdFamilyState(recordBoardColumnId), + recordIdsByColumnIdFamilyState(recordBoardColumnId), ); if (!columnDefinition) { diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeader.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeader.tsx index 76a6d0e10..31d47a39a 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeader.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeader.tsx @@ -14,7 +14,6 @@ const StyledHeader = styled.div` cursor: pointer; display: flex; flex-direction: row; - height: 24px; justify-content: left; margin-bottom: ${({ theme }) => theme.spacing(2)}; width: 100%; @@ -31,7 +30,7 @@ const StyledNumChildren = styled.div` border-radius: ${({ theme }) => theme.border.radius.rounded}; color: ${({ theme }) => theme.font.color.tertiary}; display: flex; - height: 20px; + height: 24px; justify-content: center; line-height: ${({ theme }) => theme.text.lineHeight.lg}; margin-left: auto; diff --git a/packages/twenty-front/src/modules/object-record/record-board/states/selectors/recordBoardSelectedRecordIdsSelectorScopeMap.ts b/packages/twenty-front/src/modules/object-record/record-board/states/selectors/recordBoardSelectedRecordIdsSelectorScopeMap.ts new file mode 100644 index 000000000..437fcfa56 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-board/states/selectors/recordBoardSelectedRecordIdsSelectorScopeMap.ts @@ -0,0 +1,35 @@ +import { isRecordBoardCardSelectedFamilyStateScopeMap } from '@/object-record/record-board/states/isRecordBoardCardSelectedFamilyStateScopeMap'; +import { recordBoardColumnIdsStateScopeMap } from '@/object-record/record-board/states/recordBoardColumnIdsStateScopeMap'; +import { recordBoardRecordIdsByColumnIdFamilyStateScopeMap } from '@/object-record/record-board/states/recordBoardRecordIdsByColumnIdFamilyStateScopeMap'; +import { createSelectorReadOnlyScopeMap } from '@/ui/utilities/recoil-scope/utils/createSelectorReadOnlyScopeMap'; + +export const recordBoardSelectedRecordIdsSelectorScopeMap = + createSelectorReadOnlyScopeMap({ + key: 'recordBoardSelectedRecordIdsSelectorScopeMap', + get: + ({ scopeId }) => + ({ get }) => { + const columnIds = get(recordBoardColumnIdsStateScopeMap({ scopeId })); + + const recordIdsByColumn = columnIds.map((columnId) => + get( + recordBoardRecordIdsByColumnIdFamilyStateScopeMap({ + scopeId, + familyKey: columnId, + }), + ), + ); + + const recordIds = recordIdsByColumn.flat(); + + return recordIds.filter( + (recordId) => + get( + isRecordBoardCardSelectedFamilyStateScopeMap({ + scopeId, + familyKey: recordId, + }), + ) === true, + ); + }, + }); diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardContainer.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardContainer.tsx index 39e1fba80..80f8ad7c6 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardContainer.tsx @@ -2,7 +2,9 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord'; import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord'; import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; +import { RecordBoardActionBar } from '@/object-record/record-board/action-bar/components/RecordBoardActionBar'; import { RecordBoard } from '@/object-record/record-board/components/RecordBoard'; +import { RecordBoardContextMenu } from '@/object-record/record-board/context-menu/components/RecordBoardContextMenu'; import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext'; type RecordIndexBoardContainerProps = { @@ -32,6 +34,8 @@ export const RecordIndexBoardContainer = ({ }} > + + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardContainerEffect.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardContainerEffect.tsx index ce57e0a23..338ab9408 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardContainerEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardContainerEffect.tsx @@ -1,8 +1,11 @@ import { useCallback, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; +import { useRecoilValue } from 'recoil'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { useRecordActionBar } from '@/object-record/record-action-bar/hooks/useRecordActionBar'; import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard'; +import { useResetBoardRecordSelection } from '@/object-record/record-board/hooks/useResetBoardRecordSelection'; import { useLoadRecordIndexBoard } from '@/object-record/record-index/hooks/useLoadRecordIndexBoard'; import { computeRecordBoardColumnDefinitionsFromObjectMetadata } from '@/object-record/utils/computeRecordBoardColumnDefinitionsFromObjectMetadata'; @@ -28,7 +31,9 @@ export const RecordIndexBoardContainerEffect = ({ navigate(`/settings/objects/${objectMetadataItem.namePlural}`); }, [navigate, objectMetadataItem.namePlural]); - const { setColumns, setObjectSingularName } = useRecordBoard(recordBoardId); + const { setColumns, setObjectSingularName, getSelectedRecordIdsSelector } = + useRecordBoard(recordBoardId); + const { resetRecordSelection } = useResetBoardRecordSelection(recordBoardId); useEffect(() => { setObjectSingularName(objectNameSingular); @@ -48,5 +53,18 @@ export const RecordIndexBoardContainerEffect = ({ setColumns, ]); + const selectedRecordIds = useRecoilValue(getSelectedRecordIdsSelector()); + + const { setActionBarEntries, setContextMenuEntries } = useRecordActionBar({ + objectMetadataItem, + selectedRecordIds, + callback: resetRecordSelection, + }); + + useEffect(() => { + setActionBarEntries?.(); + setContextMenuEntries?.(); + }, [setActionBarEntries, setContextMenuEntries]); + return <>; }; diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainerEffect.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainerEffect.tsx index 6c0ea1a95..eab492600 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainerEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainerEffect.tsx @@ -1,8 +1,9 @@ import { useEffect } from 'react'; +import { useRecoilValue } from 'recoil'; import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; -import { useRecordTableContextMenuEntries } from '@/object-record/hooks/useRecordTableContextMenuEntries'; +import { useRecordActionBar } from '@/object-record/record-action-bar/hooks/useRecordActionBar'; import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; import { filterAvailableTableColumns } from '@/object-record/utils/filterAvailableTableColumns'; import { useViewBar } from '@/views/hooks/useViewBar'; @@ -18,7 +19,12 @@ export const RecordIndexTableContainerEffect = ({ recordTableId, viewBarId, }: RecordIndexTableContainerEffectProps) => { - const { setAvailableTableColumns, setOnEntityCountChange } = useRecordTable({ + const { + setAvailableTableColumns, + setOnEntityCountChange, + resetTableRowSelection, + getSelectedRowIdsSelector, + } = useRecordTable({ recordTableId, }); @@ -47,11 +53,13 @@ export const RecordIndexTableContainerEffect = ({ setAvailableTableColumns, ]); - const { setActionBarEntries, setContextMenuEntries } = - useRecordTableContextMenuEntries({ - objectNamePlural: objectMetadataItem.namePlural, - recordTableId, - }); + const selectedRowIds = useRecoilValue(getSelectedRowIdsSelector()); + + const { setActionBarEntries, setContextMenuEntries } = useRecordActionBar({ + objectMetadataItem, + selectedRecordIds: selectedRowIds, + callback: resetTableRowSelection, + }); useEffect(() => { setActionBarEntries?.(); diff --git a/packages/twenty-front/src/modules/object-record/record-table/action-bar/components/RecordTableActionBar.tsx b/packages/twenty-front/src/modules/object-record/record-table/action-bar/components/RecordTableActionBar.tsx index a7f87bb6f..86b5e3534 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/action-bar/components/RecordTableActionBar.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/action-bar/components/RecordTableActionBar.tsx @@ -12,5 +12,9 @@ export const RecordTableActionBar = ({ const selectedRowIds = useRecoilValue(getSelectedRowIdsSelector()); - return ; + if (!selectedRowIds.length) { + return null; + } + + return ; }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/context-menu/components/RecordTableContextMenu.tsx b/packages/twenty-front/src/modules/object-record/record-table/context-menu/components/RecordTableContextMenu.tsx index 9ab570b8e..2ccbae769 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/context-menu/components/RecordTableContextMenu.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/context-menu/components/RecordTableContextMenu.tsx @@ -12,5 +12,9 @@ export const RecordTableContextMenu = ({ const selectedRowIds = useRecoilValue(getSelectedRowIdsSelector()); - return ; + if (!selectedRowIds.length) { + return null; + } + + return ; }; diff --git a/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockContainerEffect.tsx b/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockContainerEffect.tsx index 1bdcb0f16..8f851546c 100644 --- a/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockContainerEffect.tsx +++ b/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockContainerEffect.tsx @@ -2,7 +2,7 @@ import { useEffect } from 'react'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural'; -import { useRecordTableContextMenuEntries } from '@/object-record/hooks/useRecordTableContextMenuEntries'; +import { useRecordActionBar } from '@/object-record/record-action-bar/hooks/useRecordActionBar'; import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; import { filterAvailableTableColumns } from '@/object-record/utils/filterAvailableTableColumns'; import { @@ -30,6 +30,7 @@ export const SignInBackgroundMockContainerEffect = ({ setOnEntityCountChange, setRecordTableData, setTableColumns, + resetTableRowSelection, } = useRecordTable({ recordTableId, }); @@ -80,11 +81,11 @@ export const SignInBackgroundMockContainerEffect = ({ setTableColumns, ]); - const { setActionBarEntries, setContextMenuEntries } = - useRecordTableContextMenuEntries({ - objectNamePlural, - recordTableId, - }); + const { setActionBarEntries, setContextMenuEntries } = useRecordActionBar({ + objectMetadataItem, + selectedRecordIds: [], + callback: resetTableRowSelection, + }); useEffect(() => { setActionBarEntries?.(); diff --git a/packages/twenty-front/src/modules/ui/navigation/action-bar/components/ActionBar.tsx b/packages/twenty-front/src/modules/ui/navigation/action-bar/components/ActionBar.tsx index c966c89ad..d5d1996b3 100644 --- a/packages/twenty-front/src/modules/ui/navigation/action-bar/components/ActionBar.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/action-bar/components/ActionBar.tsx @@ -9,10 +9,6 @@ import { actionBarOpenState } from '../states/actionBarIsOpenState'; import { ActionBarItem } from './ActionBarItem'; -type ActionBarProps = { - selectedIds: string[]; -}; - const StyledContainerActionBar = styled.div` align-items: center; background: ${({ theme }) => theme.background.secondary}; @@ -33,30 +29,24 @@ const StyledContainerActionBar = styled.div` z-index: 1; `; -export const ActionBar = ({ selectedIds }: ActionBarProps) => { +export const ActionBar = () => { const actionBarOpen = useRecoilValue(actionBarOpenState); const contextMenuIsOpen = useRecoilValue(contextMenuIsOpenState); const actionBarEntries = useRecoilValue(actionBarEntriesState); const wrapperRef = useRef(null); - if (selectedIds.length === 0 || !actionBarOpen || contextMenuIsOpen) { + if (!actionBarOpen || contextMenuIsOpen) { return null; } + return ( - {actionBarEntries.map((item) => ( - + {actionBarEntries.map((item, index) => ( + ))} ); diff --git a/packages/twenty-front/src/modules/ui/navigation/action-bar/components/ActionBarItem.tsx b/packages/twenty-front/src/modules/ui/navigation/action-bar/components/ActionBarItem.tsx index a94034e4e..e6061b797 100644 --- a/packages/twenty-front/src/modules/ui/navigation/action-bar/components/ActionBarItem.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/action-bar/components/ActionBarItem.tsx @@ -3,22 +3,17 @@ import styled from '@emotion/styled'; import { MenuItem } from 'tsup.ui.index'; import { IconChevronDown } from '@/ui/display/icon'; -import { IconComponent } from '@/ui/display/icon/types/IconComponent'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; - -import { ActionBarItemAccent } from '../types/ActionBarItemAccent'; +import { ActionBarEntry } from '@/ui/navigation/action-bar/types/ActionBarEntry'; +import { MenuItemAccent } from '@/ui/navigation/menu-item/types/MenuItemAccent'; type ActionBarItemProps = { - Icon: IconComponent; - label: string; - accent?: ActionBarItemAccent; - onClick?: () => void; - subActions?: ActionBarItemProps[]; + item: ActionBarEntry; }; -const StyledButton = styled.div<{ accent: ActionBarItemAccent }>` +const StyledButton = styled.div<{ accent: MenuItemAccent }>` border-radius: ${({ theme }) => theme.border.radius.sm}; color: ${(props) => props.accent === 'danger' @@ -45,19 +40,13 @@ const StyledButtonLabel = styled.div` margin-left: ${({ theme }) => theme.spacing(1)}; `; -export const ActionBarItem = ({ - label, - Icon, - accent = 'standard', - onClick, - subActions, -}: ActionBarItemProps) => { +export const ActionBarItem = ({ item }: ActionBarItemProps) => { const theme = useTheme(); - const dropdownId = `action-bar-item-${label}`; + const dropdownId = `action-bar-item-${item.label}`; const { toggleDropdown, closeDropdown } = useDropdown(dropdownId); return ( <> - {Array.isArray(subActions) ? ( + {Array.isArray(item.subActions) ? ( - {Icon && } - {label} + + {item.Icon && } + {item.label} } dropdownComponents={ - {subActions.map((subAction) => ( + {item.subActions.map((subAction) => ( ) : ( - - {Icon && } - {label} + item.onClick?.()} + > + {item.Icon && } + {item.label} )} diff --git a/packages/twenty-front/src/modules/ui/navigation/action-bar/components/__stories__/ActionBar.stories.tsx b/packages/twenty-front/src/modules/ui/navigation/action-bar/components/__stories__/ActionBar.stories.tsx index 7e75d381a..825122c53 100644 --- a/packages/twenty-front/src/modules/ui/navigation/action-bar/components/__stories__/ActionBar.stories.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/action-bar/components/__stories__/ActionBar.stories.tsx @@ -8,10 +8,10 @@ import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator'; import { actionBarOpenState } from '../../states/actionBarIsOpenState'; import { ActionBar } from '../ActionBar'; -const FilledActionBar = (props: { selectedIds: string[] }) => { +const FilledActionBar = () => { const setActionBarOpenState = useSetRecoilState(actionBarOpenState); setActionBarOpenState(true); - return ; + return ; }; const meta: Meta = { diff --git a/packages/twenty-front/src/modules/ui/navigation/action-bar/types/ActionBarEntry.ts b/packages/twenty-front/src/modules/ui/navigation/action-bar/types/ActionBarEntry.ts index f5324e03f..ddd947a16 100644 --- a/packages/twenty-front/src/modules/ui/navigation/action-bar/types/ActionBarEntry.ts +++ b/packages/twenty-front/src/modules/ui/navigation/action-bar/types/ActionBarEntry.ts @@ -1,11 +1,10 @@ import { IconComponent } from '@/ui/display/icon/types/IconComponent'; - -import { ActionBarItemAccent } from './ActionBarItemAccent'; +import { MenuItemAccent } from '@/ui/navigation/menu-item/types/MenuItemAccent'; export type ActionBarEntry = { label: string; Icon: IconComponent; - accent?: ActionBarItemAccent; + accent?: MenuItemAccent; onClick?: () => void; subActions?: ActionBarEntry[]; }; diff --git a/packages/twenty-front/src/modules/ui/navigation/context-menu/components/ContextMenu.tsx b/packages/twenty-front/src/modules/ui/navigation/context-menu/components/ContextMenu.tsx index 2cd44f49b..0c1ee250b 100644 --- a/packages/twenty-front/src/modules/ui/navigation/context-menu/components/ContextMenu.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/context-menu/components/ContextMenu.tsx @@ -14,10 +14,6 @@ import { PositionType } from '../types/PositionType'; import { ContextMenuItem } from './ContextMenuItem'; -type ContextMenuProps = { - selectedIds: string[]; -}; - type StyledContainerProps = { position: PositionType; }; @@ -41,7 +37,7 @@ const StyledContainerContextMenu = styled.div` z-index: 2; `; -export const ContextMenu = ({ selectedIds }: ContextMenuProps) => { +export const ContextMenu = () => { const contextMenuPosition = useRecoilValue(contextMenuPositionState); const contextMenuIsOpen = useRecoilValue(contextMenuIsOpenState); const contextMenuEntries = useRecoilValue(contextMenuEntriesState); @@ -57,7 +53,7 @@ export const ContextMenu = ({ selectedIds }: ContextMenuProps) => { }, }); - if (selectedIds.length === 0 || !contextMenuIsOpen) { + if (!contextMenuIsOpen) { return null; } @@ -75,15 +71,9 @@ export const ContextMenu = ({ selectedIds }: ContextMenuProps) => { > - {contextMenuEntries.map((item) => ( - - ))} + {contextMenuEntries.map((item, index) => { + return ; + })} diff --git a/packages/twenty-front/src/modules/ui/navigation/context-menu/components/ContextMenuItem.tsx b/packages/twenty-front/src/modules/ui/navigation/context-menu/components/ContextMenuItem.tsx index c5af324b2..4ec9822a6 100644 --- a/packages/twenty-front/src/modules/ui/navigation/context-menu/components/ContextMenuItem.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/context-menu/components/ContextMenuItem.tsx @@ -1,20 +1,15 @@ -import { IconComponent } from '@/ui/display/icon/types/IconComponent'; +import { ContextMenuEntry } from '@/ui/navigation/context-menu/types/ContextMenuEntry'; import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; -import { ContextMenuItemAccent } from '../types/ContextMenuItemAccent'; - type ContextMenuItemProps = { - Icon: IconComponent; - label: string; - accent?: ContextMenuItemAccent; - onClick: () => void; + item: ContextMenuEntry; }; -export const ContextMenuItem = ({ - label, - Icon, - accent = 'default', - onClick, -}: ContextMenuItemProps) => ( - +export const ContextMenuItem = ({ item }: ContextMenuItemProps) => ( + ); diff --git a/packages/twenty-front/src/modules/ui/navigation/context-menu/components/__stories__/ContextMenu.stories.tsx b/packages/twenty-front/src/modules/ui/navigation/context-menu/components/__stories__/ContextMenu.stories.tsx index a1cd3b02f..ea3f62ca4 100644 --- a/packages/twenty-front/src/modules/ui/navigation/context-menu/components/__stories__/ContextMenu.stories.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/context-menu/components/__stories__/ContextMenu.stories.tsx @@ -9,7 +9,7 @@ import { contextMenuIsOpenState } from '../../states/contextMenuIsOpenState'; import { contextMenuPositionState } from '../../states/contextMenuPositionState'; import { ContextMenu } from '../ContextMenu'; -const FilledContextMenu = (props: { selectedIds: string[] }) => { +const FilledContextMenu = () => { const setContextMenuPosition = useSetRecoilState(contextMenuPositionState); setContextMenuPosition({ x: 100, @@ -17,7 +17,7 @@ const FilledContextMenu = (props: { selectedIds: string[] }) => { }); const setContextMenuOpenState = useSetRecoilState(contextMenuIsOpenState); setContextMenuOpenState(true); - return ; + return ; }; const meta: Meta = { diff --git a/packages/twenty-front/src/modules/ui/navigation/context-menu/types/ContextMenuEntry.ts b/packages/twenty-front/src/modules/ui/navigation/context-menu/types/ContextMenuEntry.ts index 96c85dde1..d858b6462 100644 --- a/packages/twenty-front/src/modules/ui/navigation/context-menu/types/ContextMenuEntry.ts +++ b/packages/twenty-front/src/modules/ui/navigation/context-menu/types/ContextMenuEntry.ts @@ -1,10 +1,9 @@ import { IconComponent } from '@/ui/display/icon/types/IconComponent'; - -import { ContextMenuItemAccent } from './ContextMenuItemAccent'; +import { MenuItemAccent } from '@/ui/navigation/menu-item/types/MenuItemAccent'; export type ContextMenuEntry = { label: string; Icon: IconComponent; - accent?: ContextMenuItemAccent; + accent?: MenuItemAccent; onClick: () => void; };