7338 refactor actionbar and contextmenu to use the context store (#7462)
Closes #7338
This commit is contained in:
@ -1,212 +0,0 @@
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useRecoilCallback, useSetRecoilState } from 'recoil';
|
||||
import { IconFileExport, IconHeart, IconHeartOff, IconTrash } from 'twenty-ui';
|
||||
|
||||
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { DELETE_MAX_COUNT } from '@/object-record/constants/DeleteMaxCount';
|
||||
import { useDeleteTableData } from '@/object-record/record-index/options/hooks/useDeleteTableData';
|
||||
import {
|
||||
displayedExportProgress,
|
||||
useExportTableData,
|
||||
} from '@/object-record/record-index/options/hooks/useExportTableData';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
|
||||
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 { isDefined } from '~/utils/isDefined';
|
||||
|
||||
type useRecordActionBarProps = {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
selectedRecordIds: string[];
|
||||
callback?: () => void;
|
||||
totalNumberOfRecordsSelected?: number;
|
||||
};
|
||||
|
||||
export const useRecordActionBar = ({
|
||||
objectMetadataItem,
|
||||
selectedRecordIds,
|
||||
callback,
|
||||
totalNumberOfRecordsSelected,
|
||||
}: useRecordActionBarProps) => {
|
||||
const setContextMenuEntries = useSetRecoilState(contextMenuEntriesState);
|
||||
const setActionBarEntriesState = useSetRecoilState(actionBarEntriesState);
|
||||
const [isDeleteRecordsModalOpen, setIsDeleteRecordsModalOpen] =
|
||||
useState(false);
|
||||
|
||||
const { createFavorite, favorites, deleteFavorite } = useFavorites();
|
||||
|
||||
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 (isDefined(selectedRecord)) {
|
||||
createFavorite(selectedRecord, objectMetadataItem.nameSingular);
|
||||
}
|
||||
callback?.();
|
||||
},
|
||||
[
|
||||
callback,
|
||||
createFavorite,
|
||||
deleteFavorite,
|
||||
favorites,
|
||||
objectMetadataItem.nameSingular,
|
||||
selectedRecordIds,
|
||||
],
|
||||
);
|
||||
|
||||
const baseTableDataParams = {
|
||||
delayMs: 100,
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
recordIndexId: objectMetadataItem.namePlural,
|
||||
};
|
||||
|
||||
const { deleteTableData } = useDeleteTableData(baseTableDataParams);
|
||||
|
||||
const handleDeleteClick = useCallback(() => {
|
||||
deleteTableData(selectedRecordIds);
|
||||
}, [deleteTableData, selectedRecordIds]);
|
||||
|
||||
const { progress, download } = useExportTableData({
|
||||
...baseTableDataParams,
|
||||
filename: `${objectMetadataItem.nameSingular}.csv`,
|
||||
});
|
||||
|
||||
const isRemoteObject = objectMetadataItem.isRemote;
|
||||
|
||||
const numberOfSelectedRecords =
|
||||
totalNumberOfRecordsSelected ?? selectedRecordIds.length;
|
||||
const canDelete =
|
||||
!isRemoteObject && numberOfSelectedRecords < DELETE_MAX_COUNT;
|
||||
|
||||
const menuActions: ContextMenuEntry[] = useMemo(
|
||||
() =>
|
||||
[
|
||||
{
|
||||
label: displayedExportProgress(progress),
|
||||
Icon: IconFileExport,
|
||||
accent: 'default',
|
||||
onClick: () => download(),
|
||||
} satisfies ContextMenuEntry,
|
||||
canDelete
|
||||
? ({
|
||||
label: 'Delete',
|
||||
Icon: IconTrash,
|
||||
accent: 'danger',
|
||||
onClick: () => {
|
||||
setIsDeleteRecordsModalOpen(true);
|
||||
},
|
||||
ConfirmationModal: (
|
||||
<ConfirmationModal
|
||||
isOpen={isDeleteRecordsModalOpen}
|
||||
setIsOpen={setIsDeleteRecordsModalOpen}
|
||||
title={`Delete ${numberOfSelectedRecords} ${
|
||||
numberOfSelectedRecords === 1 ? `record` : 'records'
|
||||
}`}
|
||||
subtitle={`Are you sure you want to delete ${
|
||||
numberOfSelectedRecords === 1
|
||||
? 'this record'
|
||||
: 'these records'
|
||||
}? ${
|
||||
numberOfSelectedRecords === 1 ? 'It' : 'They'
|
||||
} can be recovered from the Options menu.`}
|
||||
onConfirmClick={() => handleDeleteClick()}
|
||||
deleteButtonText={`Delete ${
|
||||
numberOfSelectedRecords > 1 ? 'Records' : 'Record'
|
||||
}`}
|
||||
/>
|
||||
),
|
||||
} satisfies ContextMenuEntry)
|
||||
: undefined,
|
||||
].filter(isDefined),
|
||||
[
|
||||
download,
|
||||
progress,
|
||||
canDelete,
|
||||
handleDeleteClick,
|
||||
isDeleteRecordsModalOpen,
|
||||
numberOfSelectedRecords,
|
||||
],
|
||||
);
|
||||
|
||||
const hasOnlyOneRecordSelected = selectedRecordIds.length === 1;
|
||||
|
||||
const isFavorite =
|
||||
isNonEmptyString(selectedRecordIds[0]) &&
|
||||
!!favorites?.find((favorite) => favorite.recordId === selectedRecordIds[0]);
|
||||
|
||||
return {
|
||||
setContextMenuEntries: useCallback(() => {
|
||||
setContextMenuEntries([
|
||||
...menuActions,
|
||||
...(!isRemoteObject && isFavorite && hasOnlyOneRecordSelected
|
||||
? [
|
||||
{
|
||||
label: 'Remove from favorites',
|
||||
Icon: IconHeartOff,
|
||||
onClick: handleFavoriteButtonClick,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(!isRemoteObject && !isFavorite && hasOnlyOneRecordSelected
|
||||
? [
|
||||
{
|
||||
label: 'Add to favorites',
|
||||
Icon: IconHeart,
|
||||
onClick: handleFavoriteButtonClick,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]);
|
||||
}, [
|
||||
menuActions,
|
||||
handleFavoriteButtonClick,
|
||||
hasOnlyOneRecordSelected,
|
||||
isFavorite,
|
||||
isRemoteObject,
|
||||
setContextMenuEntries,
|
||||
]),
|
||||
|
||||
setActionBarEntries: useCallback(() => {
|
||||
setActionBarEntriesState([
|
||||
/*
|
||||
{
|
||||
label: 'Actions',
|
||||
Icon: IconClick,
|
||||
subActions:
|
||||
|
||||
/* [
|
||||
{
|
||||
label: 'Enrich',
|
||||
Icon: IconPuzzle,
|
||||
onClick: handleExecuteQuickActionOnClick,
|
||||
},
|
||||
{
|
||||
label: 'Send to mailjet',
|
||||
Icon: IconMail,
|
||||
},
|
||||
],
|
||||
*/
|
||||
...menuActions,
|
||||
]);
|
||||
}, [menuActions, setActionBarEntriesState]),
|
||||
};
|
||||
};
|
||||
@ -1,22 +0,0 @@
|
||||
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 { selectedRecordIdsSelector } = useRecordBoardStates(recordBoardId);
|
||||
|
||||
const selectedRecordIds = useRecoilValue(selectedRecordIdsSelector());
|
||||
|
||||
if (!selectedRecordIds.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <ActionBar selectedIds={selectedRecordIds} />;
|
||||
};
|
||||
@ -69,7 +69,7 @@ export const RecordBoard = ({ recordBoardId }: RecordBoardProps) => {
|
||||
|
||||
useListenClickOutsideByClassName({
|
||||
classNames: ['record-board-card'],
|
||||
excludeClassNames: ['action-bar', 'context-menu'],
|
||||
excludeClassNames: ['bottom-bar', 'context-menu'],
|
||||
callback: resetRecordSelection,
|
||||
});
|
||||
|
||||
|
||||
@ -1,22 +0,0 @@
|
||||
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 { selectedRecordIdsSelector } = useRecordBoardStates(recordBoardId);
|
||||
|
||||
const selectedRecordIds = useRecoilValue(selectedRecordIdsSelector());
|
||||
|
||||
if (!selectedRecordIds.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <ContextMenu />;
|
||||
};
|
||||
@ -1,17 +1,23 @@
|
||||
import { useRecoilCallback, useSetRecoilState } from 'recoil';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates';
|
||||
import { contextMenuIsOpenState } from '@/ui/navigation/context-menu/states/contextMenuIsOpenState';
|
||||
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
|
||||
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
|
||||
|
||||
export const useRecordBoardSelection = (recordBoardId?: string) => {
|
||||
const setContextMenuOpenState = useSetRecoilState(contextMenuIsOpenState);
|
||||
export const useRecordBoardSelection = (recordBoardId: string) => {
|
||||
const { selectedRecordIdsSelector, isRecordBoardCardSelectedFamilyState } =
|
||||
useRecordBoardStates(recordBoardId);
|
||||
|
||||
const resetRecordSelection = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
() => {
|
||||
setContextMenuOpenState(false);
|
||||
const isActionMenuDropdownOpenState = extractComponentState(
|
||||
isDropdownOpenComponentState,
|
||||
`action-menu-dropdown-${recordBoardId}`,
|
||||
);
|
||||
|
||||
set(isActionMenuDropdownOpenState, false);
|
||||
|
||||
const recordIds = snapshot
|
||||
.getLoadable(selectedRecordIdsSelector())
|
||||
.getValue();
|
||||
@ -21,9 +27,9 @@ export const useRecordBoardSelection = (recordBoardId?: string) => {
|
||||
}
|
||||
},
|
||||
[
|
||||
recordBoardId,
|
||||
selectedRecordIdsSelector,
|
||||
isRecordBoardCardSelectedFamilyState,
|
||||
setContextMenuOpenState,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import { useActionMenu } from '@/action-menu/hooks/useActionMenu';
|
||||
import { actionMenuDropdownPositionComponentState } from '@/action-menu/states/actionMenuDropdownPositionComponentState';
|
||||
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
|
||||
import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates';
|
||||
import { RecordBoardCardContext } from '@/object-record/record-board/record-board-card/contexts/RecordBoardCardContext';
|
||||
import { RecordBoardScopeInternalContext } from '@/object-record/record-board/scopes/scope-internal-context/RecordBoardScopeInternalContext';
|
||||
import {
|
||||
FieldContext,
|
||||
RecordUpdateHook,
|
||||
@ -17,10 +20,10 @@ import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
|
||||
import { Checkbox, CheckboxVariant } from '@/ui/input/components/Checkbox';
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
import { contextMenuIsOpenState } from '@/ui/navigation/context-menu/states/contextMenuIsOpenState';
|
||||
import { contextMenuPositionState } from '@/ui/navigation/context-menu/states/contextMenuPositionState';
|
||||
import { AnimatedEaseInOut } from '@/ui/utilities/animation/components/AnimatedEaseInOut';
|
||||
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
|
||||
import { RecordBoardScrollWrapperContext } from '@/ui/utilities/scroll/contexts/ScrollWrapperContexts';
|
||||
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
|
||||
import styled from '@emotion/styled';
|
||||
import { ReactNode, useContext, useState } from 'react';
|
||||
import { useInView } from 'react-intersection-observer';
|
||||
@ -175,17 +178,27 @@ export const RecordBoardCard = ({
|
||||
|
||||
const record = useRecoilValue(recordStoreFamilyState(recordId));
|
||||
|
||||
const setContextMenuPosition = useSetRecoilState(contextMenuPositionState);
|
||||
const setContextMenuOpenState = useSetRecoilState(contextMenuIsOpenState);
|
||||
const recordBoardId = useAvailableScopeIdOrThrow(
|
||||
RecordBoardScopeInternalContext,
|
||||
);
|
||||
|
||||
const handleContextMenu = (event: React.MouseEvent) => {
|
||||
const setActionMenuDropdownPosition = useSetRecoilState(
|
||||
extractComponentState(
|
||||
actionMenuDropdownPositionComponentState,
|
||||
`action-menu-dropdown-${recordBoardId}`,
|
||||
),
|
||||
);
|
||||
|
||||
const { openActionMenuDropdown } = useActionMenu(recordBoardId);
|
||||
|
||||
const handleActionMenuDropdown = (event: React.MouseEvent) => {
|
||||
event.preventDefault();
|
||||
setIsCurrentCardSelected(true);
|
||||
setContextMenuPosition({
|
||||
setActionMenuDropdownPosition({
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
});
|
||||
setContextMenuOpenState(true);
|
||||
openActionMenuDropdown();
|
||||
};
|
||||
|
||||
const PreventSelectOnClickContainer = ({
|
||||
@ -235,7 +248,7 @@ export const RecordBoardCard = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledBoardCardWrapper onContextMenu={handleContextMenu}>
|
||||
<StyledBoardCardWrapper onContextMenu={handleActionMenuDropdown}>
|
||||
{!isCreating && <RecordValueSetterEffect recordId={recordId} />}
|
||||
<StyledBoardCard
|
||||
ref={cardRef}
|
||||
|
||||
@ -4,9 +4,7 @@ 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';
|
||||
import { recordIndexKanbanFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexKanbanFieldMetadataIdState';
|
||||
|
||||
@ -51,8 +49,6 @@ export const RecordIndexBoardContainer = ({
|
||||
}}
|
||||
>
|
||||
<RecordBoard recordBoardId={recordBoardId} />
|
||||
<RecordBoardActionBar recordBoardId={recordBoardId} />
|
||||
<RecordBoardContextMenu recordBoardId={recordBoardId} />
|
||||
</RecordBoardContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
@ -6,9 +6,7 @@ import { contextStoreCurrentObjectMetadataIdState } from '@/context-store/states
|
||||
import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug';
|
||||
import { useRecordActionBar } from '@/object-record/record-action-bar/hooks/useRecordActionBar';
|
||||
import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard';
|
||||
import { useRecordBoardSelection } from '@/object-record/record-board/hooks/useRecordBoardSelection';
|
||||
import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/states/recordIndexFieldDefinitionsState';
|
||||
import { recordIndexIsCompactModeActiveState } from '@/object-record/record-index/states/recordIndexIsCompactModeActiveState';
|
||||
import { recordIndexKanbanFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexKanbanFieldMetadataIdState';
|
||||
@ -79,8 +77,6 @@ export const RecordIndexBoardDataLoaderEffect = ({
|
||||
setNavigationMemorizedUrl,
|
||||
]);
|
||||
|
||||
const { resetRecordSelection } = useRecordBoardSelection(recordBoardId);
|
||||
|
||||
useEffect(() => {
|
||||
setObjectSingularName(objectNameSingular);
|
||||
}, [objectNameSingular, setObjectSingularName]);
|
||||
@ -125,12 +121,6 @@ export const RecordIndexBoardDataLoaderEffect = ({
|
||||
|
||||
const selectedRecordIds = useRecoilValue(selectedRecordIdsSelector());
|
||||
|
||||
const { setActionBarEntries, setContextMenuEntries } = useRecordActionBar({
|
||||
objectMetadataItem,
|
||||
selectedRecordIds,
|
||||
callback: resetRecordSelection,
|
||||
});
|
||||
|
||||
const setContextStoreTargetedRecordIds = useSetRecoilState(
|
||||
contextStoreTargetedRecordIdsState,
|
||||
);
|
||||
@ -140,9 +130,8 @@ export const RecordIndexBoardDataLoaderEffect = ({
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setActionBarEntries?.();
|
||||
setContextMenuEntries?.();
|
||||
}, [setActionBarEntries, setContextMenuEntries]);
|
||||
setContextStoreTargetedRecordIds(selectedRecordIds);
|
||||
}, [selectedRecordIds, setContextStoreTargetedRecordIds]);
|
||||
|
||||
useEffect(() => {
|
||||
setContextStoreTargetedRecordIds(selectedRecordIds);
|
||||
|
||||
@ -23,6 +23,13 @@ import { RecordIndexRootPropsContext } from '@/object-record/record-index/contex
|
||||
import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
|
||||
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
||||
import { SpreadsheetImportProvider } from '@/spreadsheet-import/provider/components/SpreadsheetImportProvider';
|
||||
|
||||
import { ActionMenuBar } from '@/action-menu/components/ActionMenuBar';
|
||||
import { ActionMenuConfirmationModals } from '@/action-menu/components/ActionMenuConfirmationModals';
|
||||
import { ActionMenuDropdown } from '@/action-menu/components/ActionMenuDropdown';
|
||||
import { ActionMenuEffect } from '@/action-menu/components/ActionMenuEffect';
|
||||
import { ActionMenuEntriesProvider } from '@/action-menu/components/ActionMenuEntriesProvider';
|
||||
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
|
||||
import { ViewBar } from '@/views/components/ViewBar';
|
||||
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
|
||||
import { ViewField } from '@/views/types/ViewField';
|
||||
@ -191,6 +198,15 @@ export const RecordIndexContainer = () => {
|
||||
/>
|
||||
</StyledContainerWithPadding>
|
||||
)}
|
||||
<ActionMenuComponentInstanceContext.Provider
|
||||
value={{ instanceId: recordIndexId }}
|
||||
>
|
||||
<ActionMenuEffect />
|
||||
<ActionMenuEntriesProvider />
|
||||
<ActionMenuBar />
|
||||
<ActionMenuDropdown />
|
||||
<ActionMenuConfirmationModals />
|
||||
</ActionMenuComponentInstanceContext.Provider>
|
||||
</RecordFieldValueSelectorContextProvider>
|
||||
</ViewComponentInstanceContext.Provider>
|
||||
</StyledContainer>
|
||||
|
||||
@ -2,9 +2,7 @@ import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
import { RecordUpdateHookParams } from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { RecordIndexRemoveSortingModal } from '@/object-record/record-index/components/RecordIndexRemoveSortingModal';
|
||||
import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
|
||||
import { RecordTableActionBar } from '@/object-record/record-table/action-bar/components/RecordTableActionBar';
|
||||
import { RecordTableWithWrappers } from '@/object-record/record-table/components/RecordTableWithWrappers';
|
||||
import { RecordTableContextMenu } from '@/object-record/record-table/context-menu/components/RecordTableContextMenu';
|
||||
import { useContext } from 'react';
|
||||
|
||||
type RecordIndexTableContainerProps = {
|
||||
@ -37,9 +35,7 @@ export const RecordIndexTableContainer = ({
|
||||
viewBarId={viewBarId}
|
||||
updateRecordMutation={updateEntity}
|
||||
/>
|
||||
<RecordTableActionBar recordTableId={recordTableId} />
|
||||
<RecordIndexRemoveSortingModal recordTableId={recordTableId} />
|
||||
<RecordTableContextMenu recordTableId={recordTableId} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -5,14 +5,10 @@ import { contextStoreCurrentObjectMetadataIdState } from '@/context-store/states
|
||||
import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState';
|
||||
import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useRecordActionBar } from '@/object-record/record-action-bar/hooks/useRecordActionBar';
|
||||
import { useHandleToggleColumnFilter } from '@/object-record/record-index/hooks/useHandleToggleColumnFilter';
|
||||
import { useHandleToggleColumnSort } from '@/object-record/record-index/hooks/useHandleToggleColumnSort';
|
||||
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
|
||||
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useSetRecordCountInCurrentView } from '@/views/hooks/useSetRecordCountInCurrentView';
|
||||
import { entityCountInCurrentViewComponentState } from '@/views/states/entityCountInCurrentViewComponentState';
|
||||
|
||||
type RecordIndexTableContainerEffectProps = {
|
||||
objectNameSingular: string;
|
||||
@ -28,7 +24,6 @@ export const RecordIndexTableContainerEffect = ({
|
||||
const {
|
||||
setAvailableTableColumns,
|
||||
setOnEntityCountChange,
|
||||
resetTableRowSelection,
|
||||
selectedRowIdsSelector,
|
||||
setOnToggleColumnFilter,
|
||||
setOnToggleColumnSort,
|
||||
@ -58,34 +53,8 @@ export const RecordIndexTableContainerEffect = ({
|
||||
setAvailableTableColumns(columnDefinitions);
|
||||
}, [columnDefinitions, setAvailableTableColumns]);
|
||||
|
||||
const { tableRowIdsState, hasUserSelectedAllRowsState } =
|
||||
useRecordTableStates(recordTableId);
|
||||
|
||||
// TODO: verify this instance id works
|
||||
const entityCountInCurrentView = useRecoilComponentValueV2(
|
||||
entityCountInCurrentViewComponentState,
|
||||
recordTableId,
|
||||
);
|
||||
const hasUserSelectedAllRows = useRecoilValue(hasUserSelectedAllRowsState);
|
||||
const tableRowIds = useRecoilValue(tableRowIdsState);
|
||||
|
||||
const selectedRowIds = useRecoilValue(selectedRowIdsSelector());
|
||||
|
||||
const numSelected =
|
||||
hasUserSelectedAllRows && entityCountInCurrentView
|
||||
? selectedRowIds.length === tableRowIds.length
|
||||
? entityCountInCurrentView
|
||||
: entityCountInCurrentView -
|
||||
(tableRowIds.length - selectedRowIds.length) // unselected row Ids
|
||||
: selectedRowIds.length;
|
||||
|
||||
const { setActionBarEntries, setContextMenuEntries } = useRecordActionBar({
|
||||
objectMetadataItem,
|
||||
selectedRecordIds: selectedRowIds,
|
||||
callback: resetTableRowSelection,
|
||||
totalNumberOfRecordsSelected: numSelected,
|
||||
});
|
||||
|
||||
const handleToggleColumnFilter = useHandleToggleColumnFilter({
|
||||
objectNameSingular,
|
||||
viewBarId,
|
||||
@ -110,11 +79,6 @@ export const RecordIndexTableContainerEffect = ({
|
||||
);
|
||||
}, [setOnToggleColumnSort, handleToggleColumnSort]);
|
||||
|
||||
useEffect(() => {
|
||||
setActionBarEntries?.();
|
||||
setContextMenuEntries?.();
|
||||
}, [setActionBarEntries, setContextMenuEntries]);
|
||||
|
||||
useEffect(() => {
|
||||
setOnEntityCountChange(
|
||||
() => (entityCount: number) => setRecordCountInCurrentView(entityCount),
|
||||
|
||||
@ -1,47 +0,0 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
|
||||
import { ActionBar } from '@/ui/navigation/action-bar/components/ActionBar';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { entityCountInCurrentViewComponentState } from '@/views/states/entityCountInCurrentViewComponentState';
|
||||
|
||||
export const RecordTableActionBar = ({
|
||||
recordTableId,
|
||||
}: {
|
||||
recordTableId: string;
|
||||
}) => {
|
||||
const {
|
||||
selectedRowIdsSelector,
|
||||
tableRowIdsState,
|
||||
hasUserSelectedAllRowsState,
|
||||
} = useRecordTableStates(recordTableId);
|
||||
|
||||
// TODO: verify this instance id works
|
||||
const entityCountInCurrentView = useRecoilComponentValueV2(
|
||||
entityCountInCurrentViewComponentState,
|
||||
recordTableId,
|
||||
);
|
||||
|
||||
const hasUserSelectedAllRows = useRecoilValue(hasUserSelectedAllRowsState);
|
||||
const tableRowIds = useRecoilValue(tableRowIdsState);
|
||||
const selectedRowIds = useRecoilValue(selectedRowIdsSelector());
|
||||
|
||||
const totalNumberOfSelectedRecords =
|
||||
hasUserSelectedAllRows && entityCountInCurrentView
|
||||
? selectedRowIds.length === tableRowIds.length
|
||||
? entityCountInCurrentView
|
||||
: entityCountInCurrentView -
|
||||
(tableRowIds.length - selectedRowIds.length) // unselected row Ids
|
||||
: selectedRowIds.length;
|
||||
|
||||
if (!selectedRowIds.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ActionBar
|
||||
selectedIds={selectedRowIds}
|
||||
totalNumberOfSelectedRecords={totalNumberOfSelectedRecords}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -12,7 +12,7 @@ import {
|
||||
OpenTableCellArgs,
|
||||
useOpenRecordTableCellV2,
|
||||
} from '@/object-record/record-table/record-table-cell/hooks/useOpenRecordTableCellV2';
|
||||
import { useTriggerContextMenu } from '@/object-record/record-table/record-table-cell/hooks/useTriggerContextMenu';
|
||||
import { useTriggerActionMenuDropdown } from '@/object-record/record-table/record-table-cell/hooks/useTriggerActionMenuDropdown';
|
||||
import { useUpsertRecord } from '@/object-record/record-table/record-table-cell/hooks/useUpsertRecord';
|
||||
import { MoveFocusDirection } from '@/object-record/record-table/types/MoveFocusDirection';
|
||||
import { TableCellPosition } from '@/object-record/record-table/types/TableCellPosition';
|
||||
@ -75,12 +75,15 @@ export const RecordTableContextProvider = ({
|
||||
moveSoftFocusToCell(cellPosition);
|
||||
};
|
||||
|
||||
const { triggerContextMenu } = useTriggerContextMenu({
|
||||
const { triggerActionMenuDropdown } = useTriggerActionMenuDropdown({
|
||||
recordTableId,
|
||||
});
|
||||
|
||||
const handleContextMenu = (event: React.MouseEvent, recordId: string) => {
|
||||
triggerContextMenu(event, recordId);
|
||||
const handleActionMenuDropdown = (
|
||||
event: React.MouseEvent,
|
||||
recordId: string,
|
||||
) => {
|
||||
triggerActionMenuDropdown(event, recordId);
|
||||
};
|
||||
|
||||
const { handleContainerMouseEnter } = useHandleContainerMouseEnter({
|
||||
@ -99,7 +102,7 @@ export const RecordTableContextProvider = ({
|
||||
onMoveFocus: handleMoveFocus,
|
||||
onCloseTableCell: handleCloseTableCell,
|
||||
onMoveSoftFocusToCell: handleMoveSoftFocusToCell,
|
||||
onContextMenu: handleContextMenu,
|
||||
onActionMenuDropdownOpened: handleActionMenuDropdown,
|
||||
onCellMouseEnter: handleContainerMouseEnter,
|
||||
visibleTableColumns,
|
||||
recordTableId,
|
||||
|
||||
@ -46,7 +46,7 @@ export const RecordTableInternalEffect = ({
|
||||
|
||||
useListenClickOutsideByClassName({
|
||||
classNames: ['entity-table-cell'],
|
||||
excludeClassNames: ['action-bar', 'context-menu'],
|
||||
excludeClassNames: ['bottom-bar', 'context-menu'],
|
||||
callback: () => {
|
||||
resetTableRowSelection();
|
||||
},
|
||||
|
||||
@ -81,7 +81,9 @@ export const RecordTableWithWrappers = ({
|
||||
/>
|
||||
<DragSelect
|
||||
dragSelectable={tableBodyRef}
|
||||
onDragSelectionStart={resetTableRowSelection}
|
||||
onDragSelectionStart={() => {
|
||||
resetTableRowSelection();
|
||||
}}
|
||||
onDragSelectionChange={setRowSelected}
|
||||
/>
|
||||
</StyledTableInternalContainer>
|
||||
|
||||
@ -70,7 +70,7 @@ const meta: Meta = {
|
||||
onMoveFocus: () => {},
|
||||
onCloseTableCell: () => {},
|
||||
onMoveSoftFocusToCell: () => {},
|
||||
onContextMenu: () => {},
|
||||
onActionMenuDropdownOpened: () => {},
|
||||
onCellMouseEnter: () => {},
|
||||
visibleTableColumns: mockPerformance.visibleTableColumns as any,
|
||||
objectNameSingular:
|
||||
|
||||
@ -1,20 +0,0 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
|
||||
import { ContextMenu } from '@/ui/navigation/context-menu/components/ContextMenu';
|
||||
|
||||
export const RecordTableContextMenu = ({
|
||||
recordTableId,
|
||||
}: {
|
||||
recordTableId: string;
|
||||
}) => {
|
||||
const { selectedRowIdsSelector } = useRecordTableStates(recordTableId);
|
||||
|
||||
const selectedRowIds = useRecoilValue(selectedRowIdsSelector());
|
||||
|
||||
if (!selectedRowIds.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <ContextMenu />;
|
||||
};
|
||||
@ -24,7 +24,10 @@ export type RecordTableContextProps = {
|
||||
onMoveFocus: (direction: MoveFocusDirection) => void;
|
||||
onCloseTableCell: () => void;
|
||||
onMoveSoftFocusToCell: (cellPosition: TableCellPosition) => void;
|
||||
onContextMenu: (event: React.MouseEvent, recordId: string) => void;
|
||||
onActionMenuDropdownOpened: (
|
||||
event: React.MouseEvent,
|
||||
recordId: string,
|
||||
) => void;
|
||||
onCellMouseEnter: (args: HandleContainerMouseEnterArgs) => void;
|
||||
visibleTableColumns: ColumnDefinition<FieldMetadata>[];
|
||||
recordTableId: string;
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
|
||||
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
|
||||
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
|
||||
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
|
||||
|
||||
export const useResetTableRowSelection = (recordTableId?: string) => {
|
||||
const {
|
||||
@ -20,7 +22,19 @@ export const useResetTableRowSelection = (recordTableId?: string) => {
|
||||
}
|
||||
|
||||
set(hasUserSelectedAllRowsState, false);
|
||||
|
||||
const isActionMenuDropdownOpenState = extractComponentState(
|
||||
isDropdownOpenComponentState,
|
||||
`action-menu-dropdown-${recordTableId}`,
|
||||
);
|
||||
|
||||
set(isActionMenuDropdownOpenState, false);
|
||||
},
|
||||
[tableRowIdsState, isRowSelectedFamilyState, hasUserSelectedAllRowsState],
|
||||
[
|
||||
tableRowIdsState,
|
||||
hasUserSelectedAllRowsState,
|
||||
recordTableId,
|
||||
isRowSelectedFamilyState,
|
||||
],
|
||||
);
|
||||
};
|
||||
|
||||
@ -71,10 +71,10 @@ export const RecordTableCellBaseContainer = ({
|
||||
}
|
||||
};
|
||||
|
||||
const { onContextMenu } = useContext(RecordTableContext);
|
||||
const { onActionMenuDropdownOpened } = useContext(RecordTableContext);
|
||||
|
||||
const handleContextMenu = (event: React.MouseEvent) => {
|
||||
onContextMenu(event, recordId);
|
||||
const handleActionMenuDropdown = (event: React.MouseEvent) => {
|
||||
onActionMenuDropdownOpened(event, recordId);
|
||||
};
|
||||
|
||||
const { hotkeyScope } = useContext(FieldContext);
|
||||
@ -87,7 +87,7 @@ export const RecordTableCellBaseContainer = ({
|
||||
onMouseLeave={handleContainerMouseLeave}
|
||||
onMouseMove={handleContainerMouseMove}
|
||||
onClick={handleContainerClick}
|
||||
onContextMenu={handleContextMenu}
|
||||
onContextMenu={handleActionMenuDropdown}
|
||||
backgroundColorTransparentSecondary={
|
||||
theme.background.transparent.secondary
|
||||
}
|
||||
|
||||
@ -1,13 +1,12 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { useCallback, useContext } from 'react';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext';
|
||||
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
|
||||
import { RecordTableTd } from '@/object-record/record-table/record-table-cell/components/RecordTableTd';
|
||||
import { useSetCurrentRowSelected } from '@/object-record/record-table/record-table-row/hooks/useSetCurrentRowSelected';
|
||||
import { Checkbox } from '@/ui/input/components/Checkbox';
|
||||
import { actionBarOpenState } from '@/ui/navigation/action-bar/states/actionBarIsOpenState';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
align-items: center;
|
||||
@ -24,14 +23,12 @@ export const RecordTableCellCheckbox = () => {
|
||||
|
||||
const { recordId } = useContext(RecordTableRowContext);
|
||||
const { isRowSelectedFamilyState } = useRecordTableStates();
|
||||
const setActionBarOpenState = useSetRecoilState(actionBarOpenState);
|
||||
const { setCurrentRowSelected } = useSetCurrentRowSelected();
|
||||
const currentRowSelected = useRecoilValue(isRowSelectedFamilyState(recordId));
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
setCurrentRowSelected(!currentRowSelected);
|
||||
setActionBarOpenState(true);
|
||||
}, [currentRowSelected, setActionBarOpenState, setCurrentRowSelected]);
|
||||
}, [currentRowSelected, setCurrentRowSelected]);
|
||||
|
||||
return (
|
||||
<RecordTableTd isSelected={isSelected} hasRightBorder={false}>
|
||||
|
||||
@ -0,0 +1,67 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { actionMenuDropdownPositionComponentState } from '@/action-menu/states/actionMenuDropdownPositionComponentState';
|
||||
import { isRowSelectedComponentFamilyState } from '@/object-record/record-table/record-table-row/states/isRowSelectedComponentFamilyState';
|
||||
import { isBottomBarOpenedComponentState } from '@/ui/layout/bottom-bar/states/isBottomBarOpenedComponentState';
|
||||
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
|
||||
import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
|
||||
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
|
||||
import { extractComponentFamilyState } from '@/ui/utilities/state/component-state/utils/extractComponentFamilyState';
|
||||
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
|
||||
|
||||
export const useTriggerActionMenuDropdown = ({
|
||||
recordTableId,
|
||||
}: {
|
||||
recordTableId: string;
|
||||
}) => {
|
||||
const triggerActionMenuDropdown = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
(event: React.MouseEvent, recordId: string) => {
|
||||
event.preventDefault();
|
||||
|
||||
const tableScopeId = getScopeIdFromComponentId(recordTableId);
|
||||
|
||||
set(
|
||||
extractComponentState(
|
||||
actionMenuDropdownPositionComponentState,
|
||||
`action-menu-dropdown-${recordTableId}`,
|
||||
),
|
||||
{
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
},
|
||||
);
|
||||
|
||||
const isRowSelectedFamilyState = extractComponentFamilyState(
|
||||
isRowSelectedComponentFamilyState,
|
||||
tableScopeId,
|
||||
);
|
||||
|
||||
const isRowSelected = getSnapshotValue(
|
||||
snapshot,
|
||||
isRowSelectedFamilyState(recordId),
|
||||
);
|
||||
|
||||
if (isRowSelected !== true) {
|
||||
set(isRowSelectedFamilyState(recordId), true);
|
||||
}
|
||||
|
||||
const isActionMenuDropdownOpenState = extractComponentState(
|
||||
isDropdownOpenComponentState,
|
||||
`action-menu-dropdown-${recordTableId}`,
|
||||
);
|
||||
|
||||
const isActionBarOpenState = isBottomBarOpenedComponentState.atomFamily(
|
||||
{
|
||||
instanceId: `action-bar-${recordTableId}`,
|
||||
},
|
||||
);
|
||||
|
||||
set(isActionBarOpenState, false);
|
||||
set(isActionMenuDropdownOpenState, true);
|
||||
},
|
||||
[recordTableId],
|
||||
);
|
||||
|
||||
return { triggerActionMenuDropdown };
|
||||
};
|
||||
@ -1,46 +0,0 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { isRowSelectedComponentFamilyState } from '@/object-record/record-table/record-table-row/states/isRowSelectedComponentFamilyState';
|
||||
import { contextMenuIsOpenState } from '@/ui/navigation/context-menu/states/contextMenuIsOpenState';
|
||||
import { contextMenuPositionState } from '@/ui/navigation/context-menu/states/contextMenuPositionState';
|
||||
import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
|
||||
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
|
||||
import { extractComponentFamilyState } from '@/ui/utilities/state/component-state/utils/extractComponentFamilyState';
|
||||
|
||||
export const useTriggerContextMenu = ({
|
||||
recordTableId,
|
||||
}: {
|
||||
recordTableId: string;
|
||||
}) => {
|
||||
const triggerContextMenu = useRecoilCallback(
|
||||
({ set, snapshot }) =>
|
||||
(event: React.MouseEvent, recordId: string) => {
|
||||
event.preventDefault();
|
||||
|
||||
const tableScopeId = getScopeIdFromComponentId(recordTableId);
|
||||
|
||||
set(contextMenuPositionState, {
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
});
|
||||
set(contextMenuIsOpenState, true);
|
||||
|
||||
const isRowSelectedFamilyState = extractComponentFamilyState(
|
||||
isRowSelectedComponentFamilyState,
|
||||
tableScopeId,
|
||||
);
|
||||
|
||||
const isRowSelected = getSnapshotValue(
|
||||
snapshot,
|
||||
isRowSelectedFamilyState(recordId),
|
||||
);
|
||||
|
||||
if (isRowSelected !== true) {
|
||||
set(isRowSelectedFamilyState(recordId), true);
|
||||
}
|
||||
},
|
||||
[recordTableId],
|
||||
);
|
||||
|
||||
return { triggerContextMenu };
|
||||
};
|
||||
Reference in New Issue
Block a user