7665 handle the select all case inside the action menu (#7742)

Closes #7665 
- Handle select all
- Handle Filters

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Raphaël Bosi
2024-10-21 14:22:03 +02:00
committed by GitHub
parent eaab2d0dd2
commit 40152d3b92
52 changed files with 788 additions and 427 deletions

View File

@ -1,51 +1,91 @@
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries'; import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
import { contextStoreCurrentObjectMetadataIdState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdState'; import { contextStoreNumberOfSelectedRecordsState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsState';
import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState'; import { contextStoreTargetedRecordsRuleState } from '@/context-store/states/contextStoreTargetedRecordsRuleState';
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById'; import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters';
import { useFavorites } from '@/favorites/hooks/useFavorites';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { DELETE_MAX_COUNT } from '@/object-record/constants/DeleteMaxCount'; import { DELETE_MAX_COUNT } from '@/object-record/constants/DeleteMaxCount';
import { useDeleteTableData } from '@/object-record/record-index/options/hooks/useDeleteTableData'; import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords';
import { useFetchAllRecordIds } from '@/object-record/hooks/useFetchAllRecordIds';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal'; import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { IconTrash } from 'twenty-ui'; import { IconTrash, isDefined } from 'twenty-ui';
export const DeleteRecordsActionEffect = ({ export const DeleteRecordsActionEffect = ({
position, position,
objectMetadataItem,
}: { }: {
position: number; position: number;
objectMetadataItem: ObjectMetadataItem;
}) => { }) => {
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries(); const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
const contextStoreTargetedRecordIds = useRecoilValue(
contextStoreTargetedRecordIdsState,
);
const contextStoreCurrentObjectMetadataId = useRecoilValue(
contextStoreCurrentObjectMetadataIdState,
);
const { objectMetadataItem } = useObjectMetadataItemById({
objectId: contextStoreCurrentObjectMetadataId,
});
const [isDeleteRecordsModalOpen, setIsDeleteRecordsModalOpen] = const [isDeleteRecordsModalOpen, setIsDeleteRecordsModalOpen] =
useState(false); useState(false);
const { deleteTableData } = useDeleteTableData({ const { resetTableRowSelection } = useRecordTable({
objectNameSingular: objectMetadataItem?.nameSingular ?? '', recordTableId: objectMetadataItem.namePlural,
recordIndexId: objectMetadataItem?.namePlural ?? '',
}); });
const handleDeleteClick = useCallback(() => { const { deleteManyRecords } = useDeleteManyRecords({
deleteTableData(contextStoreTargetedRecordIds); objectNameSingular: objectMetadataItem.nameSingular,
}, [deleteTableData, contextStoreTargetedRecordIds]); });
const isRemoteObject = objectMetadataItem?.isRemote ?? false; const { favorites, deleteFavorite } = useFavorites();
const numberOfSelectedRecords = contextStoreTargetedRecordIds.length; const contextStoreNumberOfSelectedRecords = useRecoilValue(
contextStoreNumberOfSelectedRecordsState,
);
const contextStoreTargetedRecordsRule = useRecoilValue(
contextStoreTargetedRecordsRuleState,
);
const graphqlFilter = computeContextStoreFilters(
contextStoreTargetedRecordsRule,
objectMetadataItem,
);
const { fetchAllRecordIds } = useFetchAllRecordIds({
objectNameSingular: objectMetadataItem.nameSingular,
filter: graphqlFilter,
});
const handleDeleteClick = useCallback(async () => {
const recordIdsToDelete = await fetchAllRecordIds();
resetTableRowSelection();
for (const recordIdToDelete of recordIdsToDelete) {
const foundFavorite = favorites?.find(
(favorite) => favorite.recordId === recordIdToDelete,
);
if (foundFavorite !== undefined) {
deleteFavorite(foundFavorite.id);
}
}
await deleteManyRecords(recordIdsToDelete, {
delayInMsBetweenRequests: 50,
});
}, [
deleteFavorite,
deleteManyRecords,
favorites,
fetchAllRecordIds,
resetTableRowSelection,
]);
const isRemoteObject = objectMetadataItem.isRemote;
const canDelete = const canDelete =
!isRemoteObject && numberOfSelectedRecords < DELETE_MAX_COUNT; !isRemoteObject &&
isDefined(contextStoreNumberOfSelectedRecords) &&
contextStoreNumberOfSelectedRecords < DELETE_MAX_COUNT &&
contextStoreNumberOfSelectedRecords > 0;
useEffect(() => { useEffect(() => {
if (canDelete) { if (canDelete) {
@ -62,17 +102,19 @@ export const DeleteRecordsActionEffect = ({
<ConfirmationModal <ConfirmationModal
isOpen={isDeleteRecordsModalOpen} isOpen={isDeleteRecordsModalOpen}
setIsOpen={setIsDeleteRecordsModalOpen} setIsOpen={setIsDeleteRecordsModalOpen}
title={`Delete ${numberOfSelectedRecords} ${ title={`Delete ${contextStoreNumberOfSelectedRecords} ${
numberOfSelectedRecords === 1 ? `record` : 'records' contextStoreNumberOfSelectedRecords === 1 ? `record` : 'records'
}`} }`}
subtitle={`Are you sure you want to delete ${ subtitle={`Are you sure you want to delete ${
numberOfSelectedRecords === 1 ? 'this record' : 'these records' contextStoreNumberOfSelectedRecords === 1
? 'this record'
: 'these records'
}? ${ }? ${
numberOfSelectedRecords === 1 ? 'It' : 'They' contextStoreNumberOfSelectedRecords === 1 ? 'It' : 'They'
} can be recovered from the Options menu.`} } can be recovered from the Options menu.`}
onConfirmClick={() => handleDeleteClick()} onConfirmClick={() => handleDeleteClick()}
deleteButtonText={`Delete ${ deleteButtonText={`Delete ${
numberOfSelectedRecords > 1 ? 'Records' : 'Record' contextStoreNumberOfSelectedRecords > 1 ? 'Records' : 'Record'
}`} }`}
/> />
), ),
@ -80,14 +122,18 @@ export const DeleteRecordsActionEffect = ({
} else { } else {
removeActionMenuEntry('delete'); removeActionMenuEntry('delete');
} }
return () => {
removeActionMenuEntry('delete');
};
}, [ }, [
canDelete,
addActionMenuEntry, addActionMenuEntry,
removeActionMenuEntry, canDelete,
isDeleteRecordsModalOpen, contextStoreNumberOfSelectedRecords,
numberOfSelectedRecords,
handleDeleteClick, handleDeleteClick,
isDeleteRecordsModalOpen,
position, position,
removeActionMenuEntry,
]); ]);
return null; return null;

View File

@ -1,38 +1,27 @@
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries'; import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
import { contextStoreCurrentObjectMetadataIdState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdState';
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { import {
displayedExportProgress, displayedExportProgress,
useExportTableData, useExportRecordData,
} from '@/object-record/record-index/options/hooks/useExportTableData'; } from '@/action-menu/hooks/useExportRecordData';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useRecoilValue } from 'recoil';
import { IconFileExport } from 'twenty-ui'; import { IconFileExport } from 'twenty-ui';
export const ExportRecordsActionEffect = ({ export const ExportRecordsActionEffect = ({
position, position,
objectMetadataItem,
}: { }: {
position: number; position: number;
objectMetadataItem: ObjectMetadataItem;
}) => { }) => {
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries(); const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
const contextStoreCurrentObjectMetadataId = useRecoilValue( const { progress, download } = useExportRecordData({
contextStoreCurrentObjectMetadataIdState,
);
const { objectMetadataItem } = useObjectMetadataItemById({
objectId: contextStoreCurrentObjectMetadataId,
});
const baseTableDataParams = {
delayMs: 100, delayMs: 100,
objectNameSingular: objectMetadataItem?.nameSingular ?? '', objectMetadataItem,
recordIndexId: objectMetadataItem?.namePlural ?? '', recordIndexId: objectMetadataItem.namePlural,
}; filename: `${objectMetadataItem.nameSingular}.csv`,
const { progress, download } = useExportTableData({
...baseTableDataParams,
filename: `${objectMetadataItem?.nameSingular}.csv`,
}); });
useEffect(() => { useEffect(() => {

View File

@ -1,8 +1,7 @@
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries'; import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
import { contextStoreCurrentObjectMetadataIdState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdState'; import { contextStoreTargetedRecordsRuleState } from '@/context-store/states/contextStoreTargetedRecordsRuleState';
import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState';
import { useFavorites } from '@/favorites/hooks/useFavorites'; import { useFavorites } from '@/favorites/hooks/useFavorites';
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
@ -10,30 +9,28 @@ import { IconHeart, IconHeartOff, isDefined } from 'twenty-ui';
export const ManageFavoritesActionEffect = ({ export const ManageFavoritesActionEffect = ({
position, position,
objectMetadataItem,
}: { }: {
position: number; position: number;
objectMetadataItem: ObjectMetadataItem;
}) => { }) => {
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries(); const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
const contextStoreTargetedRecordIds = useRecoilValue( const contextStoreTargetedRecordsRule = useRecoilValue(
contextStoreTargetedRecordIdsState, contextStoreTargetedRecordsRuleState,
);
const contextStoreCurrentObjectMetadataId = useRecoilValue(
contextStoreCurrentObjectMetadataIdState,
); );
const { favorites, createFavorite, deleteFavorite } = useFavorites(); const { favorites, createFavorite, deleteFavorite } = useFavorites();
const selectedRecordId = contextStoreTargetedRecordIds[0]; const selectedRecordId =
contextStoreTargetedRecordsRule.mode === 'selection'
? contextStoreTargetedRecordsRule.selectedRecordIds[0]
: undefined;
const selectedRecord = useRecoilValue( const selectedRecord = useRecoilValue(
recordStoreFamilyState(selectedRecordId), recordStoreFamilyState(selectedRecordId ?? ''),
); );
const { objectMetadataItem } = useObjectMetadataItemById({
objectId: contextStoreCurrentObjectMetadataId,
});
const foundFavorite = favorites?.find( const foundFavorite = favorites?.find(
(favorite) => favorite.recordId === selectedRecordId, (favorite) => favorite.recordId === selectedRecordId,
); );

View File

@ -1,13 +1,22 @@
import { DeleteRecordsActionEffect } from '@/action-menu/actions/record-actions/components/DeleteRecordsActionEffect'; import { DeleteRecordsActionEffect } from '@/action-menu/actions/record-actions/components/DeleteRecordsActionEffect';
import { ExportRecordsActionEffect } from '@/action-menu/actions/record-actions/components/ExportRecordsActionEffect'; import { ExportRecordsActionEffect } from '@/action-menu/actions/record-actions/components/ExportRecordsActionEffect';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
const actionEffects = [ExportRecordsActionEffect, DeleteRecordsActionEffect]; const actionEffects = [ExportRecordsActionEffect, DeleteRecordsActionEffect];
export const MultipleRecordsActionMenuEntriesSetter = () => { export const MultipleRecordsActionMenuEntriesSetter = ({
objectMetadataItem,
}: {
objectMetadataItem: ObjectMetadataItem;
}) => {
return ( return (
<> <>
{actionEffects.map((ActionEffect, index) => ( {actionEffects.map((ActionEffect, index) => (
<ActionEffect key={index} position={index} /> <ActionEffect
key={index}
position={index}
objectMetadataItem={objectMetadataItem}
/>
))} ))}
</> </>
); );

View File

@ -1,20 +1,44 @@
import { MultipleRecordsActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/MultipleRecordsActionMenuEntriesSetter'; import { MultipleRecordsActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/MultipleRecordsActionMenuEntriesSetter';
import { SingleRecordActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/SingleRecordActionMenuEntriesSetter'; import { SingleRecordActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/SingleRecordActionMenuEntriesSetter';
import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState'; import { contextStoreCurrentObjectMetadataIdState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdState';
import { contextStoreNumberOfSelectedRecordsState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsState';
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
export const RecordActionMenuEntriesSetter = () => { export const RecordActionMenuEntriesSetter = () => {
const contextStoreTargetedRecordIds = useRecoilValue( const contextStoreNumberOfSelectedRecords = useRecoilValue(
contextStoreTargetedRecordIdsState, contextStoreNumberOfSelectedRecordsState,
); );
if (contextStoreTargetedRecordIds.length === 0) { const contextStoreCurrentObjectMetadataId = useRecoilValue(
contextStoreCurrentObjectMetadataIdState,
);
const { objectMetadataItem } = useObjectMetadataItemById({
objectId: contextStoreCurrentObjectMetadataId ?? '',
});
if (!objectMetadataItem) {
throw new Error(
`Object metadata item not found for id ${contextStoreCurrentObjectMetadataId}`,
);
}
if (!contextStoreNumberOfSelectedRecords) {
return null; return null;
} }
if (contextStoreTargetedRecordIds.length === 1) { if (contextStoreNumberOfSelectedRecords === 1) {
return <SingleRecordActionMenuEntriesSetter />; return (
<SingleRecordActionMenuEntriesSetter
objectMetadataItem={objectMetadataItem}
/>
);
} }
return <MultipleRecordsActionMenuEntriesSetter />; return (
<MultipleRecordsActionMenuEntriesSetter
objectMetadataItem={objectMetadataItem}
/>
);
}; };

View File

@ -1,8 +1,13 @@
import { DeleteRecordsActionEffect } from '@/action-menu/actions/record-actions/components/DeleteRecordsActionEffect'; import { DeleteRecordsActionEffect } from '@/action-menu/actions/record-actions/components/DeleteRecordsActionEffect';
import { ExportRecordsActionEffect } from '@/action-menu/actions/record-actions/components/ExportRecordsActionEffect'; import { ExportRecordsActionEffect } from '@/action-menu/actions/record-actions/components/ExportRecordsActionEffect';
import { ManageFavoritesActionEffect } from '@/action-menu/actions/record-actions/components/ManageFavoritesActionEffect'; import { ManageFavoritesActionEffect } from '@/action-menu/actions/record-actions/components/ManageFavoritesActionEffect';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
export const SingleRecordActionMenuEntriesSetter = () => { export const SingleRecordActionMenuEntriesSetter = ({
objectMetadataItem,
}: {
objectMetadataItem: ObjectMetadataItem;
}) => {
const actionEffects = [ const actionEffects = [
ManageFavoritesActionEffect, ManageFavoritesActionEffect,
ExportRecordsActionEffect, ExportRecordsActionEffect,
@ -11,7 +16,11 @@ export const SingleRecordActionMenuEntriesSetter = () => {
return ( return (
<> <>
{actionEffects.map((ActionEffect, index) => ( {actionEffects.map((ActionEffect, index) => (
<ActionEffect key={index} position={index} /> <ActionEffect
key={index}
position={index}
objectMetadataItem={objectMetadataItem}
/>
))} ))}
</> </>
); );

View File

@ -0,0 +1,30 @@
import { RecordActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter';
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 { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { contextStoreCurrentObjectMetadataIdState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdState';
import { useRecoilValue } from 'recoil';
export const ActionMenu = ({ actionMenuId }: { actionMenuId: string }) => {
const contextStoreCurrentObjectMetadataId = useRecoilValue(
contextStoreCurrentObjectMetadataIdState,
);
return (
<>
{contextStoreCurrentObjectMetadataId && (
<ActionMenuComponentInstanceContext.Provider
value={{ instanceId: actionMenuId }}
>
<ActionMenuBar />
<ActionMenuDropdown />
<ActionMenuConfirmationModals />
<ActionMenuEffect />
<RecordActionMenuEntriesSetter />
</ActionMenuComponentInstanceContext.Provider>
)}
</>
);
};

View File

@ -4,7 +4,7 @@ import { ActionMenuBarEntry } from '@/action-menu/components/ActionMenuBarEntry'
import { actionMenuEntriesComponentSelector } from '@/action-menu/states/actionMenuEntriesComponentSelector'; import { actionMenuEntriesComponentSelector } from '@/action-menu/states/actionMenuEntriesComponentSelector';
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { ActionBarHotkeyScope } from '@/action-menu/types/ActionBarHotKeyScope'; import { ActionBarHotkeyScope } from '@/action-menu/types/ActionBarHotKeyScope';
import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState'; import { contextStoreNumberOfSelectedRecordsState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsState';
import { BottomBar } from '@/ui/layout/bottom-bar/components/BottomBar'; import { BottomBar } from '@/ui/layout/bottom-bar/components/BottomBar';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
@ -19,8 +19,8 @@ const StyledLabel = styled.div`
`; `;
export const ActionMenuBar = () => { export const ActionMenuBar = () => {
const contextStoreTargetedRecordIds = useRecoilValue( const contextStoreNumberOfSelectedRecords = useRecoilValue(
contextStoreTargetedRecordIdsState, contextStoreNumberOfSelectedRecordsState,
); );
const actionMenuId = useAvailableComponentInstanceIdOrThrow( const actionMenuId = useAvailableComponentInstanceIdOrThrow(
@ -42,9 +42,7 @@ export const ActionMenuBar = () => {
scope: ActionBarHotkeyScope.ActionBar, scope: ActionBarHotkeyScope.ActionBar,
}} }}
> >
<StyledLabel> <StyledLabel>{contextStoreNumberOfSelectedRecords} selected:</StyledLabel>
{contextStoreTargetedRecordIds.length} selected:
</StyledLabel>
{actionMenuEntries.map((entry, index) => ( {actionMenuEntries.map((entry, index) => (
<ActionMenuBarEntry key={index} entry={entry} /> <ActionMenuBarEntry key={index} entry={entry} />
))} ))}

View File

@ -64,7 +64,7 @@ export const ActionMenuDropdown = () => {
return ( return (
<StyledContainerActionMenuDropdown <StyledContainerActionMenuDropdown
position={actionMenuDropdownPosition} position={actionMenuDropdownPosition}
className="context-menu" className="action-menu-dropdown"
> >
<Dropdown <Dropdown
dropdownId={`action-menu-dropdown-${actionMenuId}`} dropdownId={`action-menu-dropdown-${actionMenuId}`}

View File

@ -1,6 +1,6 @@
import { useActionMenu } from '@/action-menu/hooks/useActionMenu'; import { useActionMenu } from '@/action-menu/hooks/useActionMenu';
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState'; import { contextStoreNumberOfSelectedRecordsState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsState';
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState'; import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState'; import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
@ -8,8 +8,8 @@ import { useEffect } from 'react';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
export const ActionMenuEffect = () => { export const ActionMenuEffect = () => {
const contextStoreTargetedRecordIds = useRecoilValue( const contextStoreNumberOfSelectedRecords = useRecoilValue(
contextStoreTargetedRecordIdsState, contextStoreNumberOfSelectedRecordsState,
); );
const actionMenuId = useAvailableComponentInstanceIdOrThrow( const actionMenuId = useAvailableComponentInstanceIdOrThrow(
@ -26,17 +26,17 @@ export const ActionMenuEffect = () => {
); );
useEffect(() => { useEffect(() => {
if (contextStoreTargetedRecordIds.length > 0 && !isDropdownOpen) { if (contextStoreNumberOfSelectedRecords > 0 && !isDropdownOpen) {
// We only handle opening the ActionMenuBar here, not the Dropdown. // We only handle opening the ActionMenuBar here, not the Dropdown.
// The Dropdown is already managed by sync handlers for events like // The Dropdown is already managed by sync handlers for events like
// right-click to open and click outside to close. // right-click to open and click outside to close.
openActionBar(); openActionBar();
} }
if (contextStoreTargetedRecordIds.length === 0) { if (contextStoreNumberOfSelectedRecords === 0 && isDropdownOpen) {
closeActionBar(); closeActionBar();
} }
}, [ }, [
contextStoreTargetedRecordIds, contextStoreNumberOfSelectedRecords,
openActionBar, openActionBar,
closeActionBar, closeActionBar,
isDropdownOpen, isDropdownOpen,

View File

@ -5,7 +5,8 @@ import { RecoilRoot } from 'recoil';
import { ActionMenuBar } from '@/action-menu/components/ActionMenuBar'; import { ActionMenuBar } from '@/action-menu/components/ActionMenuBar';
import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState'; import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState';
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState'; import { contextStoreNumberOfSelectedRecordsState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsState';
import { contextStoreTargetedRecordsRuleState } from '@/context-store/states/contextStoreTargetedRecordsRuleState';
import { isBottomBarOpenedComponentState } from '@/ui/layout/bottom-bar/states/isBottomBarOpenedComponentState'; import { isBottomBarOpenedComponentState } from '@/ui/layout/bottom-bar/states/isBottomBarOpenedComponentState';
import { userEvent, waitFor, within } from '@storybook/test'; import { userEvent, waitFor, within } from '@storybook/test';
import { IconCheckbox, IconTrash } from 'twenty-ui'; import { IconCheckbox, IconTrash } from 'twenty-ui';
@ -20,7 +21,11 @@ const meta: Meta<typeof ActionMenuBar> = {
(Story) => ( (Story) => (
<RecoilRoot <RecoilRoot
initializeState={({ set }) => { initializeState={({ set }) => {
set(contextStoreTargetedRecordIdsState, ['1', '2', '3']); set(contextStoreTargetedRecordsRuleState, {
mode: 'selection',
selectedRecordIds: ['1', '2', '3'],
});
set(contextStoreNumberOfSelectedRecordsState, 3);
set( set(
actionMenuEntriesComponentState.atomFamily({ actionMenuEntriesComponentState.atomFamily({
instanceId: 'story-action-menu', instanceId: 'story-action-menu',

View File

@ -7,7 +7,7 @@ import {
displayedExportProgress, displayedExportProgress,
download, download,
generateCsv, generateCsv,
} from '../useExportTableData'; } from '../useExportRecordData';
jest.useFakeTimers(); jest.useFakeTimers();

View File

@ -4,10 +4,11 @@ import { useMemo } from 'react';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { EXPORT_TABLE_DATA_DEFAULT_PAGE_SIZE } from '@/object-record/record-index/options/constants/ExportTableDataDefaultPageSize'; import { EXPORT_TABLE_DATA_DEFAULT_PAGE_SIZE } from '@/object-record/record-index/options/constants/ExportTableDataDefaultPageSize';
import { useProcessRecordsForCSVExport } from '@/object-record/record-index/options/hooks/useProcessRecordsForCSVExport'; import { useProcessRecordsForCSVExport } from '@/object-record/record-index/options/hooks/useProcessRecordsForCSVExport';
import { import {
useTableData, UseRecordDataOptions,
UseTableDataOptions, useRecordData,
} from '@/object-record/record-index/options/hooks/useTableData'; } from '@/object-record/record-index/options/hooks/useRecordData';
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { RelationDefinitionType } from '~/generated-metadata/graphql'; import { RelationDefinitionType } from '~/generated-metadata/graphql';
@ -134,21 +135,22 @@ const downloader = (mimeType: string, generator: GenerateExport) => {
export const csvDownloader = downloader('text/csv', generateCsv); export const csvDownloader = downloader('text/csv', generateCsv);
type UseExportTableDataOptions = Omit<UseTableDataOptions, 'callback'> & { type UseExportTableDataOptions = Omit<UseRecordDataOptions, 'callback'> & {
filename: string; filename: string;
}; };
export const useExportTableData = ({ export const useExportRecordData = ({
delayMs, delayMs,
filename, filename,
maximumRequests = 100, maximumRequests = 100,
objectNameSingular, objectMetadataItem,
pageSize = EXPORT_TABLE_DATA_DEFAULT_PAGE_SIZE, pageSize = EXPORT_TABLE_DATA_DEFAULT_PAGE_SIZE,
recordIndexId, recordIndexId,
viewType, viewType,
}: UseExportTableDataOptions) => { }: UseExportTableDataOptions) => {
const { processRecordsForCSVExport } = const { processRecordsForCSVExport } = useProcessRecordsForCSVExport(
useProcessRecordsForCSVExport(objectNameSingular); objectMetadataItem.nameSingular,
);
const downloadCsv = useMemo( const downloadCsv = useMemo(
() => () =>
@ -160,10 +162,10 @@ export const useExportTableData = ({
[filename, processRecordsForCSVExport], [filename, processRecordsForCSVExport],
); );
const { getTableData: download, progress } = useTableData({ const { getTableData: download, progress } = useRecordData({
delayMs, delayMs,
maximumRequests, maximumRequests,
objectNameSingular, objectMetadataItem,
pageSize, pageSize,
recordIndexId, recordIndexId,
callback: downloadCsv, callback: downloadCsv,

View File

@ -0,0 +1,6 @@
import { createState } from 'twenty-ui';
export const contextStoreNumberOfSelectedRecordsState = createState<number>({
key: 'contextStoreNumberOfSelectedRecordsState',
defaultValue: 0,
});

View File

@ -1,6 +0,0 @@
import { createState } from 'twenty-ui';
export const contextStoreTargetedRecordIdsState = createState<string[]>({
key: 'contextStoreTargetedRecordIdsState',
defaultValue: [],
});

View File

@ -0,0 +1,26 @@
import { Filter } from '@/object-record/object-filter-dropdown/types/Filter';
import { createState } from 'twenty-ui';
type ContextStoreTargetedRecordsRuleSelectionMode = {
mode: 'selection';
selectedRecordIds: string[];
};
type ContextStoreTargetedRecordsRuleExclusionMode = {
mode: 'exclusion';
excludedRecordIds: string[];
filters: Filter[];
};
export type ContextStoreTargetedRecordsRule =
| ContextStoreTargetedRecordsRuleSelectionMode
| ContextStoreTargetedRecordsRuleExclusionMode;
export const contextStoreTargetedRecordsRuleState =
createState<ContextStoreTargetedRecordsRule>({
key: 'contextStoreTargetedRecordsRuleState',
defaultValue: {
mode: 'selection',
selectedRecordIds: [],
},
});

View File

@ -0,0 +1,77 @@
import { ContextStoreTargetedRecordsRule } from '@/context-store/states/contextStoreTargetedRecordsRuleState';
import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
describe('computeContextStoreFilters', () => {
const personObjectMetadataItem = generatedMockObjectMetadataItems.find(
(item) => item.nameSingular === 'person',
)!;
it('should work for selection mode', () => {
const contextStoreTargetedRecordsRule: ContextStoreTargetedRecordsRule = {
mode: 'selection',
selectedRecordIds: ['1', '2', '3'],
};
const filters = computeContextStoreFilters(
contextStoreTargetedRecordsRule,
personObjectMetadataItem,
);
expect(filters).toEqual({
id: {
in: ['1', '2', '3'],
},
});
});
it('should work for exclusion mode', () => {
const contextStoreTargetedRecordsRule: ContextStoreTargetedRecordsRule = {
mode: 'exclusion',
filters: [
{
id: 'name-filter',
variant: 'default',
fieldMetadataId: personObjectMetadataItem.fields.find(
(field) => field.name === 'name',
)!.id,
value: 'John',
displayValue: 'John',
displayAvatarUrl: undefined,
operand: ViewFilterOperand.Contains,
definition: {
fieldMetadataId: personObjectMetadataItem.fields.find(
(field) => field.name === 'name',
)!.id,
label: 'Name',
iconName: 'person',
type: 'TEXT',
},
},
],
excludedRecordIds: ['1', '2', '3'],
};
const filters = computeContextStoreFilters(
contextStoreTargetedRecordsRule,
personObjectMetadataItem,
);
expect(filters).toEqual({
and: [
{
name: {
ilike: '%John%',
},
},
{
not: {
id: {
in: ['1', '2', '3'],
},
},
},
],
});
});
});

View File

@ -0,0 +1,42 @@
import { ContextStoreTargetedRecordsRule } from '@/context-store/states/contextStoreTargetedRecordsRuleState';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { RecordGqlOperationFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter';
import { turnFiltersIntoQueryFilter } from '@/object-record/record-filter/utils/turnFiltersIntoQueryFilter';
import { makeAndFilterVariables } from '@/object-record/utils/makeAndFilterVariables';
export const computeContextStoreFilters = (
contextStoreTargetedRecordsRule: ContextStoreTargetedRecordsRule,
objectMetadataItem: ObjectMetadataItem,
) => {
let queryFilter: RecordGqlOperationFilter | undefined;
if (contextStoreTargetedRecordsRule.mode === 'exclusion') {
queryFilter = makeAndFilterVariables([
turnFiltersIntoQueryFilter(
contextStoreTargetedRecordsRule.filters,
objectMetadataItem?.fields ?? [],
),
contextStoreTargetedRecordsRule.excludedRecordIds.length > 0
? {
not: {
id: {
in: contextStoreTargetedRecordsRule.excludedRecordIds,
},
},
}
: undefined,
]);
}
if (contextStoreTargetedRecordsRule.mode === 'selection') {
queryFilter =
contextStoreTargetedRecordsRule.selectedRecordIds.length > 0
? {
id: {
in: contextStoreTargetedRecordsRule.selectedRecordIds,
},
}
: undefined;
}
return queryFilter;
};

View File

@ -33,16 +33,11 @@ describe('useObjectMetadataItemById', () => {
expect(objectMetadataItem?.id).toBe(opportunityObjectMetadata.id); expect(objectMetadataItem?.id).toBe(opportunityObjectMetadata.id);
}); });
it('should return null when invalid ID is provided', async () => { it('should throw an error when invalid ID is provided', async () => {
const { result } = renderHook( expect(() =>
() => useObjectMetadataItemById({ objectId: 'invalid-id' }), renderHook(() => useObjectMetadataItemById({ objectId: 'invalid-id' }), {
{
wrapper: Wrapper, wrapper: Wrapper,
}, }),
); ).toThrow(`Object metadata item not found for id invalid-id`);
const { objectMetadataItem } = result.current;
expect(objectMetadataItem).toBeNull();
}); });
}); });

View File

@ -6,7 +6,7 @@ import { isDefined } from '~/utils/isDefined';
export const useObjectMetadataItemById = ({ export const useObjectMetadataItemById = ({
objectId, objectId,
}: { }: {
objectId: string | null; objectId: string;
}) => { }) => {
const objectMetadataItems = useRecoilValue(objectMetadataItemsState); const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
@ -15,9 +15,7 @@ export const useObjectMetadataItemById = ({
); );
if (!isDefined(objectMetadataItem)) { if (!isDefined(objectMetadataItem)) {
return { throw new Error(`Object metadata item not found for id ${objectId}`);
objectMetadataItem: null,
};
} }
return { return {

View File

@ -66,7 +66,7 @@ export const RecordBoard = () => {
useListenClickOutsideByClassName({ useListenClickOutsideByClassName({
classNames: ['record-board-card'], classNames: ['record-board-card'],
excludeClassNames: ['bottom-bar', 'context-menu'], excludeClassNames: ['bottom-bar', 'action-menu-dropdown'],
callback: resetRecordSelection, callback: resetRecordSelection,
}); });

View File

@ -1,5 +1,5 @@
import { Filter } from '@/object-record/object-filter-dropdown/types/Filter'; import { Filter } from '@/object-record/object-filter-dropdown/types/Filter';
import { turnObjectDropdownFilterIntoQueryFilter } from '@/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter'; import { turnFiltersIntoQueryFilter } from '@/object-record/record-filter/utils/turnFiltersIntoQueryFilter';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
import { getCompaniesMock } from '~/testing/mock-data/companies'; import { getCompaniesMock } from '~/testing/mock-data/companies';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
@ -16,7 +16,7 @@ const personMockObjectMetadataItem = generatedMockObjectMetadataItems.find(
jest.useFakeTimers().setSystemTime(new Date('2020-01-01')); jest.useFakeTimers().setSystemTime(new Date('2020-01-01'));
describe('turnObjectDropdownFilterIntoQueryFilter', () => { describe('turnFiltersIntoQueryFilter', () => {
it('should work as expected for single filter', () => { it('should work as expected for single filter', () => {
const companyMockNameFieldMetadataId = const companyMockNameFieldMetadataId =
companyMockObjectMetadataItem.fields.find( companyMockObjectMetadataItem.fields.find(
@ -37,7 +37,7 @@ describe('turnObjectDropdownFilterIntoQueryFilter', () => {
}, },
}; };
const result = turnObjectDropdownFilterIntoQueryFilter( const result = turnFiltersIntoQueryFilter(
[nameFilter], [nameFilter],
companyMockObjectMetadataItem.fields, companyMockObjectMetadataItem.fields,
); );
@ -88,7 +88,7 @@ describe('turnObjectDropdownFilterIntoQueryFilter', () => {
}, },
}; };
const result = turnObjectDropdownFilterIntoQueryFilter( const result = turnFiltersIntoQueryFilter(
[nameFilter, employeesFilter], [nameFilter, employeesFilter],
companyMockObjectMetadataItem.fields, companyMockObjectMetadataItem.fields,
); );
@ -173,7 +173,7 @@ describe('should work as expected for the different field types', () => {
}, },
}; };
const result = turnObjectDropdownFilterIntoQueryFilter( const result = turnFiltersIntoQueryFilter(
[ [
addressFilterContains, addressFilterContains,
addressFilterDoesNotContain, addressFilterDoesNotContain,
@ -554,7 +554,7 @@ describe('should work as expected for the different field types', () => {
}, },
}; };
const result = turnObjectDropdownFilterIntoQueryFilter( const result = turnFiltersIntoQueryFilter(
[ [
phonesFilterContains, phonesFilterContains,
phonesFilterDoesNotContain, phonesFilterDoesNotContain,
@ -754,7 +754,7 @@ describe('should work as expected for the different field types', () => {
}, },
}; };
const result = turnObjectDropdownFilterIntoQueryFilter( const result = turnFiltersIntoQueryFilter(
[ [
emailsFilterContains, emailsFilterContains,
emailsFilterDoesNotContain, emailsFilterDoesNotContain,
@ -908,7 +908,7 @@ describe('should work as expected for the different field types', () => {
}, },
}; };
const result = turnObjectDropdownFilterIntoQueryFilter( const result = turnFiltersIntoQueryFilter(
[ [
dateFilterIsAfter, dateFilterIsAfter,
dateFilterIsBefore, dateFilterIsBefore,
@ -1023,7 +1023,7 @@ describe('should work as expected for the different field types', () => {
}, },
}; };
const result = turnObjectDropdownFilterIntoQueryFilter( const result = turnFiltersIntoQueryFilter(
[ [
employeesFilterIsGreaterThan, employeesFilterIsGreaterThan,
employeesFilterIsLessThan, employeesFilterIsLessThan,

View File

@ -31,7 +31,7 @@ import { z } from 'zod';
// TODO: break this down into smaller functions and make the whole thing immutable // TODO: break this down into smaller functions and make the whole thing immutable
// Especially applyEmptyFilters // Especially applyEmptyFilters
export const turnObjectDropdownFilterIntoQueryFilter = ( export const turnFiltersIntoQueryFilter = (
rawUIFilters: Filter[], rawUIFilters: Filter[],
fields: Pick<Field, 'id' | 'name'>[], fields: Pick<Field, 'id' | 'name'>[],
): RecordGqlOperationFilter | undefined => { ): RecordGqlOperationFilter | undefined => {

View File

@ -2,8 +2,7 @@ import { useCallback, useEffect } from 'react';
import { useLocation, useNavigate } from 'react-router-dom'; import { useLocation, useNavigate } from 'react-router-dom';
import { useRecoilValue, useSetRecoilState } from 'recoil'; import { useRecoilValue, useSetRecoilState } from 'recoil';
import { contextStoreCurrentObjectMetadataIdState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdState'; import { contextStoreTargetedRecordsRuleState } from '@/context-store/states/contextStoreTargetedRecordsRuleState';
import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug'; import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug';
import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard'; import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard';
@ -121,32 +120,23 @@ export const RecordIndexBoardDataLoaderEffect = ({
const selectedRecordIds = useRecoilValue(selectedRecordIdsSelector()); const selectedRecordIds = useRecoilValue(selectedRecordIdsSelector());
const setContextStoreTargetedRecordIds = useSetRecoilState( const setContextStoreTargetedRecords = useSetRecoilState(
contextStoreTargetedRecordIdsState, contextStoreTargetedRecordsRuleState,
);
const setContextStoreCurrentObjectMetadataItem = useSetRecoilState(
contextStoreCurrentObjectMetadataIdState,
); );
useEffect(() => { useEffect(() => {
setContextStoreTargetedRecordIds(selectedRecordIds); setContextStoreTargetedRecords({
}, [selectedRecordIds, setContextStoreTargetedRecordIds]); mode: 'selection',
selectedRecordIds: selectedRecordIds,
useEffect(() => { });
setContextStoreTargetedRecordIds(selectedRecordIds);
setContextStoreCurrentObjectMetadataItem(objectMetadataItem?.id);
return () => { return () => {
setContextStoreTargetedRecordIds([]); setContextStoreTargetedRecords({
setContextStoreCurrentObjectMetadataItem(null); mode: 'selection',
selectedRecordIds: [],
});
}; };
}, [ }, [selectedRecordIds, setContextStoreTargetedRecords]);
objectMetadataItem?.id,
selectedRecordIds,
setContextStoreCurrentObjectMetadataItem,
setContextStoreTargetedRecordIds,
]);
return <></>; return <></>;
}; };

View File

@ -22,12 +22,8 @@ import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { SpreadsheetImportProvider } from '@/spreadsheet-import/provider/components/SpreadsheetImportProvider'; import { SpreadsheetImportProvider } from '@/spreadsheet-import/provider/components/SpreadsheetImportProvider';
import { RecordActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter'; import { ActionMenu } from '@/action-menu/components/ActionMenu';
import { ActionMenuBar } from '@/action-menu/components/ActionMenuBar'; import { contextStoreTargetedRecordsRuleState } from '@/context-store/states/contextStoreTargetedRecordsRuleState';
import { ActionMenuConfirmationModals } from '@/action-menu/components/ActionMenuConfirmationModals';
import { ActionMenuDropdown } from '@/action-menu/components/ActionMenuDropdown';
import { ActionMenuEffect } from '@/action-menu/components/ActionMenuEffect';
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { ViewBar } from '@/views/components/ViewBar'; import { ViewBar } from '@/views/components/ViewBar';
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
import { ViewField } from '@/views/types/ViewField'; import { ViewField } from '@/views/types/ViewField';
@ -106,6 +102,10 @@ export const RecordIndexContainer = () => {
[columnDefinitions, setTableColumns], [columnDefinitions, setTableColumns],
); );
const setContextStoreTargetedRecordsRule = useSetRecoilState(
contextStoreTargetedRecordsRuleState,
);
return ( return (
<StyledContainer> <StyledContainer>
<InformationBannerWrapper /> <InformationBannerWrapper />
@ -119,7 +119,7 @@ export const RecordIndexContainer = () => {
optionsDropdownButton={ optionsDropdownButton={
<RecordIndexOptionsDropdown <RecordIndexOptionsDropdown
recordIndexId={recordIndexId} recordIndexId={recordIndexId}
objectNameSingular={objectNameSingular} objectMetadataItem={objectMetadataItem}
viewType={recordIndexViewType ?? ViewType.Table} viewType={recordIndexViewType ?? ViewType.Table}
/> />
} }
@ -135,6 +135,13 @@ export const RecordIndexContainer = () => {
setRecordIndexFilters( setRecordIndexFilters(
mapViewFiltersToFilters(view.viewFilters, filterDefinitions), mapViewFiltersToFilters(view.viewFilters, filterDefinitions),
); );
setContextStoreTargetedRecordsRule((prev) => ({
...prev,
filters: mapViewFiltersToFilters(
view.viewFilters,
filterDefinitions,
),
}));
setTableSorts( setTableSorts(
mapViewSortsToSorts(view.viewSorts, sortDefinitions), mapViewSortsToSorts(view.viewSorts, sortDefinitions),
); );
@ -179,15 +186,7 @@ export const RecordIndexContainer = () => {
/> />
</StyledContainerWithPadding> </StyledContainerWithPadding>
)} )}
<ActionMenuComponentInstanceContext.Provider <ActionMenu actionMenuId={recordIndexId} />
value={{ instanceId: recordIndexId }}
>
<ActionMenuEffect />
<RecordActionMenuEntriesSetter />
<ActionMenuBar />
<ActionMenuDropdown />
<ActionMenuConfirmationModals />
</ActionMenuComponentInstanceContext.Provider>
</RecordFieldValueSelectorContextProvider> </RecordFieldValueSelectorContextProvider>
</ViewComponentInstanceContext.Provider> </ViewComponentInstanceContext.Provider>
</StyledContainer> </StyledContainer>

View File

@ -0,0 +1,66 @@
import { contextStoreNumberOfSelectedRecordsState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsState';
import { contextStoreTargetedRecordsRuleState } from '@/context-store/states/contextStoreTargetedRecordsRuleState';
import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
import { useFindManyParams } from '@/object-record/record-index/hooks/useLoadRecordIndexTable';
import { useContext, useEffect } from 'react';
import { useRecoilValue, useSetRecoilState } from 'recoil';
export const RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect =
() => {
const setContextStoreNumberOfSelectedRecords = useSetRecoilState(
contextStoreNumberOfSelectedRecordsState,
);
const contextStoreTargetedRecordsRule = useRecoilValue(
contextStoreTargetedRecordsRuleState,
);
const { objectNamePlural } = useContext(RecordIndexRootPropsContext);
const { objectNameSingular } = useObjectNameSingularFromPlural({
objectNamePlural,
});
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
});
const findManyRecordsParams = useFindManyParams(
objectMetadataItem?.nameSingular ?? '',
objectMetadataItem?.namePlural ?? '',
);
const { totalCount } = useFindManyRecords({
...findManyRecordsParams,
recordGqlFields: {
id: true,
},
filter: computeContextStoreFilters(
contextStoreTargetedRecordsRule,
objectMetadataItem,
),
limit: 1,
skip: contextStoreTargetedRecordsRule.mode === 'selection',
});
useEffect(() => {
if (contextStoreTargetedRecordsRule.mode === 'selection') {
setContextStoreNumberOfSelectedRecords(
contextStoreTargetedRecordsRule.selectedRecordIds.length,
);
}
if (contextStoreTargetedRecordsRule.mode === 'exclusion') {
setContextStoreNumberOfSelectedRecords(totalCount ?? 0);
}
}, [
contextStoreTargetedRecordsRule,
setContextStoreNumberOfSelectedRecords,
totalCount,
]);
return null;
};

View File

@ -0,0 +1,31 @@
import { contextStoreCurrentObjectMetadataIdState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdState';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
import { useContext, useEffect } from 'react';
import { useSetRecoilState } from 'recoil';
export const RecordIndexContainerContextStoreObjectMetadataEffect = () => {
const setContextStoreCurrentObjectMetadataItem = useSetRecoilState(
contextStoreCurrentObjectMetadataIdState,
);
const { objectNamePlural } = useContext(RecordIndexRootPropsContext);
const { objectNameSingular } = useObjectNameSingularFromPlural({
objectNamePlural,
});
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
});
useEffect(() => {
setContextStoreCurrentObjectMetadataItem(objectMetadataItem.id);
return () => {
setContextStoreCurrentObjectMetadataItem(null);
};
}, [objectMetadataItem.id, setContextStoreCurrentObjectMetadataItem]);
return null;
};

View File

@ -1,8 +1,7 @@
import { useContext, useEffect } from 'react'; import { useContext, useEffect } from 'react';
import { useRecoilValue, useSetRecoilState } from 'recoil'; import { useRecoilValue, useSetRecoilState } from 'recoil';
import { contextStoreCurrentObjectMetadataIdState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdState'; import { contextStoreTargetedRecordsRuleState } from '@/context-store/states/contextStoreTargetedRecordsRuleState';
import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState';
import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata'; import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext'; import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
@ -24,18 +23,12 @@ export const RecordIndexTableContainerEffect = () => {
selectedRowIdsSelector, selectedRowIdsSelector,
setOnToggleColumnFilter, setOnToggleColumnFilter,
setOnToggleColumnSort, setOnToggleColumnSort,
hasUserSelectedAllRowsState,
unselectedRowIdsSelector,
} = useRecordTable({ } = useRecordTable({
recordTableId: recordIndexId, recordTableId: recordIndexId,
}); });
const setContextStoreTargetedRecordIds = useSetRecoilState(
contextStoreTargetedRecordIdsState,
);
const setContextStoreCurrentObjectMetadataItem = useSetRecoilState(
contextStoreCurrentObjectMetadataIdState,
);
const { objectMetadataItem } = useObjectMetadataItem({ const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular, objectNameSingular,
}); });
@ -50,8 +43,6 @@ export const RecordIndexTableContainerEffect = () => {
setAvailableTableColumns(columnDefinitions); setAvailableTableColumns(columnDefinitions);
}, [columnDefinitions, setAvailableTableColumns]); }, [columnDefinitions, setAvailableTableColumns]);
const selectedRowIds = useRecoilValue(selectedRowIdsSelector());
const handleToggleColumnFilter = useHandleToggleColumnFilter({ const handleToggleColumnFilter = useHandleToggleColumnFilter({
objectNameSingular, objectNameSingular,
viewBarId, viewBarId,
@ -82,19 +73,38 @@ export const RecordIndexTableContainerEffect = () => {
); );
}, [setRecordCountInCurrentView, setOnEntityCountChange]); }, [setRecordCountInCurrentView, setOnEntityCountChange]);
const setContextStoreTargetedRecords = useSetRecoilState(
contextStoreTargetedRecordsRuleState,
);
const hasUserSelectedAllRows = useRecoilValue(hasUserSelectedAllRowsState);
const selectedRowIds = useRecoilValue(selectedRowIdsSelector());
const unselectedRowIds = useRecoilValue(unselectedRowIdsSelector());
useEffect(() => { useEffect(() => {
setContextStoreTargetedRecordIds(selectedRowIds); if (hasUserSelectedAllRows) {
setContextStoreCurrentObjectMetadataItem(objectMetadataItem?.id); setContextStoreTargetedRecords({
mode: 'exclusion',
excludedRecordIds: unselectedRowIds,
filters: [],
});
} else {
setContextStoreTargetedRecords({
mode: 'selection',
selectedRecordIds: selectedRowIds,
});
}
return () => { return () => {
setContextStoreTargetedRecordIds([]); setContextStoreTargetedRecords({
setContextStoreCurrentObjectMetadataItem(null); mode: 'selection',
selectedRecordIds: [],
});
}; };
}, [ }, [
objectMetadataItem?.id, hasUserSelectedAllRows,
selectedRowIds, selectedRowIds,
setContextStoreCurrentObjectMetadataItem, setContextStoreTargetedRecords,
setContextStoreTargetedRecordIds, unselectedRowIds,
]); ]);
return <></>; return <></>;

View File

@ -5,7 +5,7 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy'; import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy';
import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard'; import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard';
import { turnObjectDropdownFilterIntoQueryFilter } from '@/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter'; import { turnFiltersIntoQueryFilter } from '@/object-record/record-filter/utils/turnFiltersIntoQueryFilter';
import { useRecordBoardRecordGqlFields } from '@/object-record/record-index/hooks/useRecordBoardRecordGqlFields'; import { useRecordBoardRecordGqlFields } from '@/object-record/record-index/hooks/useRecordBoardRecordGqlFields';
import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/states/recordIndexFieldDefinitionsState'; import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/states/recordIndexFieldDefinitionsState';
import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState'; import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState';
@ -44,7 +44,7 @@ export const useLoadRecordIndexBoard = ({
const recordIndexFilters = useRecoilValue(recordIndexFiltersState); const recordIndexFilters = useRecoilValue(recordIndexFiltersState);
const recordIndexSorts = useRecoilValue(recordIndexSortsState); const recordIndexSorts = useRecoilValue(recordIndexSortsState);
const requestFilters = turnObjectDropdownFilterIntoQueryFilter( const requestFilters = turnFiltersIntoQueryFilter(
recordIndexFilters, recordIndexFilters,
objectMetadataItem?.fields ?? [], objectMetadataItem?.fields ?? [],
); );

View File

@ -5,7 +5,7 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy'; import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy';
import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard'; import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard';
import { turnObjectDropdownFilterIntoQueryFilter } from '@/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter'; import { turnFiltersIntoQueryFilter } from '@/object-record/record-filter/utils/turnFiltersIntoQueryFilter';
import { useRecordBoardRecordGqlFields } from '@/object-record/record-index/hooks/useRecordBoardRecordGqlFields'; import { useRecordBoardRecordGqlFields } from '@/object-record/record-index/hooks/useRecordBoardRecordGqlFields';
import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState'; import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState';
import { recordIndexSortsState } from '@/object-record/record-index/states/recordIndexSortsState'; import { recordIndexSortsState } from '@/object-record/record-index/states/recordIndexSortsState';
@ -35,7 +35,7 @@ export const useLoadRecordIndexBoardColumn = ({
const recordIndexFilters = useRecoilValue(recordIndexFiltersState); const recordIndexFilters = useRecoilValue(recordIndexFiltersState);
const recordIndexSorts = useRecoilValue(recordIndexSortsState); const recordIndexSorts = useRecoilValue(recordIndexSortsState);
const requestFilters = turnObjectDropdownFilterIntoQueryFilter( const requestFilters = turnFiltersIntoQueryFilter(
recordIndexFilters, recordIndexFilters,
objectMetadataItem?.fields ?? [], objectMetadataItem?.fields ?? [],
); );

View File

@ -5,7 +5,7 @@ import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy'; import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy';
import { turnObjectDropdownFilterIntoQueryFilter } from '@/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter'; import { turnFiltersIntoQueryFilter } from '@/object-record/record-filter/utils/turnFiltersIntoQueryFilter';
import { useRecordTableRecordGqlFields } from '@/object-record/record-index/hooks/useRecordTableRecordGqlFields'; import { useRecordTableRecordGqlFields } from '@/object-record/record-index/hooks/useRecordTableRecordGqlFields';
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates'; import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
@ -27,7 +27,7 @@ export const useFindManyParams = (
const tableFilters = useRecoilValue(tableFiltersState); const tableFilters = useRecoilValue(tableFiltersState);
const tableSorts = useRecoilValue(tableSortsState); const tableSorts = useRecoilValue(tableSortsState);
const filter = turnObjectDropdownFilterIntoQueryFilter( const filter = turnFiltersIntoQueryFilter(
tableFilters, tableFilters,
objectMetadataItem?.fields ?? [], objectMetadataItem?.fields ?? [],
); );

View File

@ -1,3 +1,4 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { RecordIndexOptionsDropdownButton } from '@/object-record/record-index/options/components/RecordIndexOptionsDropdownButton'; import { RecordIndexOptionsDropdownButton } from '@/object-record/record-index/options/components/RecordIndexOptionsDropdownButton';
import { RecordIndexOptionsDropdownContent } from '@/object-record/record-index/options/components/RecordIndexOptionsDropdownContent'; import { RecordIndexOptionsDropdownContent } from '@/object-record/record-index/options/components/RecordIndexOptionsDropdownContent';
import { RECORD_INDEX_OPTIONS_DROPDOWN_ID } from '@/object-record/record-index/options/constants/RecordIndexOptionsDropdownId'; import { RECORD_INDEX_OPTIONS_DROPDOWN_ID } from '@/object-record/record-index/options/constants/RecordIndexOptionsDropdownId';
@ -7,13 +8,13 @@ import { ViewType } from '@/views/types/ViewType';
type RecordIndexOptionsDropdownProps = { type RecordIndexOptionsDropdownProps = {
viewType: ViewType; viewType: ViewType;
objectNameSingular: string; objectMetadataItem: ObjectMetadataItem;
recordIndexId: string; recordIndexId: string;
}; };
export const RecordIndexOptionsDropdown = ({ export const RecordIndexOptionsDropdown = ({
recordIndexId, recordIndexId,
objectNameSingular, objectMetadataItem,
viewType, viewType,
}: RecordIndexOptionsDropdownProps) => { }: RecordIndexOptionsDropdownProps) => {
return ( return (
@ -26,7 +27,7 @@ export const RecordIndexOptionsDropdown = ({
dropdownComponents={ dropdownComponents={
<RecordIndexOptionsDropdownContent <RecordIndexOptionsDropdownContent
viewType={viewType} viewType={viewType}
objectNameSingular={objectNameSingular} objectMetadataItem={objectMetadataItem}
recordIndexId={recordIndexId} recordIndexId={recordIndexId}
/> />
} }

View File

@ -14,10 +14,12 @@ import {
import { useObjectNamePluralFromSingular } from '@/object-metadata/hooks/useObjectNamePluralFromSingular'; import { useObjectNamePluralFromSingular } from '@/object-metadata/hooks/useObjectNamePluralFromSingular';
import { useHandleToggleTrashColumnFilter } from '@/object-record/record-index/hooks/useHandleToggleTrashColumnFilter'; import { useHandleToggleTrashColumnFilter } from '@/object-record/record-index/hooks/useHandleToggleTrashColumnFilter';
import { RECORD_INDEX_OPTIONS_DROPDOWN_ID } from '@/object-record/record-index/options/constants/RecordIndexOptionsDropdownId'; import { RECORD_INDEX_OPTIONS_DROPDOWN_ID } from '@/object-record/record-index/options/constants/RecordIndexOptionsDropdownId';
import { import {
displayedExportProgress, displayedExportProgress,
useExportTableData, useExportRecordData,
} from '@/object-record/record-index/options/hooks/useExportTableData'; } from '@/action-menu/hooks/useExportRecordData';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useRecordIndexOptionsForBoard } from '@/object-record/record-index/options/hooks/useRecordIndexOptionsForBoard'; import { useRecordIndexOptionsForBoard } from '@/object-record/record-index/options/hooks/useRecordIndexOptionsForBoard';
import { useRecordIndexOptionsForTable } from '@/object-record/record-index/options/hooks/useRecordIndexOptionsForTable'; import { useRecordIndexOptionsForTable } from '@/object-record/record-index/options/hooks/useRecordIndexOptionsForTable';
import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope'; import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope';
@ -44,14 +46,14 @@ type RecordIndexOptionsMenu = 'fields' | 'hiddenFields';
type RecordIndexOptionsDropdownContentProps = { type RecordIndexOptionsDropdownContentProps = {
recordIndexId: string; recordIndexId: string;
objectNameSingular: string; objectMetadataItem: ObjectMetadataItem;
viewType: ViewType; viewType: ViewType;
}; };
export const RecordIndexOptionsDropdownContent = ({ export const RecordIndexOptionsDropdownContent = ({
viewType, viewType,
recordIndexId, recordIndexId,
objectNameSingular, objectMetadataItem,
}: RecordIndexOptionsDropdownContentProps) => { }: RecordIndexOptionsDropdownContentProps) => {
const { currentViewWithCombinedFiltersAndSorts } = useGetCurrentView(); const { currentViewWithCombinedFiltersAndSorts } = useGetCurrentView();
@ -68,7 +70,7 @@ export const RecordIndexOptionsDropdownContent = ({
}; };
const { objectNamePlural } = useObjectNamePluralFromSingular({ const { objectNamePlural } = useObjectNamePluralFromSingular({
objectNameSingular: objectNameSingular, objectNameSingular: objectMetadataItem.nameSingular,
}); });
const settingsUrl = getSettingsPagePath(SettingsPath.ObjectDetail, { const settingsUrl = getSettingsPagePath(SettingsPath.ObjectDetail, {
@ -92,7 +94,7 @@ export const RecordIndexOptionsDropdownContent = ({
const { handleToggleTrashColumnFilter, toggleSoftDeleteFilterState } = const { handleToggleTrashColumnFilter, toggleSoftDeleteFilterState } =
useHandleToggleTrashColumnFilter({ useHandleToggleTrashColumnFilter({
objectNameSingular, objectNameSingular: objectMetadataItem.nameSingular,
viewBarId: recordIndexId, viewBarId: recordIndexId,
}); });
@ -104,7 +106,7 @@ export const RecordIndexOptionsDropdownContent = ({
isCompactModeActive, isCompactModeActive,
setAndPersistIsCompactModeActive, setAndPersistIsCompactModeActive,
} = useRecordIndexOptionsForBoard({ } = useRecordIndexOptionsForBoard({
objectNameSingular, objectNameSingular: objectMetadataItem.nameSingular,
recordBoardId: recordIndexId, recordBoardId: recordIndexId,
viewBarId: recordIndexId, viewBarId: recordIndexId,
}); });
@ -126,12 +128,12 @@ export const RecordIndexOptionsDropdownContent = ({
: handleColumnVisibilityChange; : handleColumnVisibilityChange;
const { openObjectRecordsSpreasheetImportDialog } = const { openObjectRecordsSpreasheetImportDialog } =
useOpenObjectRecordsSpreasheetImportDialog(objectNameSingular); useOpenObjectRecordsSpreasheetImportDialog(objectMetadataItem.nameSingular);
const { progress, download } = useExportTableData({ const { progress, download } = useExportRecordData({
delayMs: 100, delayMs: 100,
filename: `${objectNameSingular}.csv`, filename: `${objectMetadataItem.nameSingular}.csv`,
objectNameSingular, objectMetadataItem,
recordIndexId, recordIndexId,
viewType, viewType,
}); });

View File

@ -1,6 +1,6 @@
import { renderHook, waitFor } from '@testing-library/react'; import { renderHook, waitFor } from '@testing-library/react';
import { act } from 'react'; import { act } from 'react';
import { percentage, sleep, useTableData } from '../useTableData'; import { percentage, sleep, useRecordData } from '../useRecordData';
import { PERSON_FRAGMENT_WITH_DEPTH_ZERO_RELATIONS } from '@/object-record/hooks/__mocks__/personFragments'; import { PERSON_FRAGMENT_WITH_DEPTH_ZERO_RELATIONS } from '@/object-record/hooks/__mocks__/personFragments';
import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard'; import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard';
@ -11,7 +11,7 @@ import { ViewType } from '@/views/types/ViewType';
import { MockedResponse } from '@apollo/client/testing'; import { MockedResponse } from '@apollo/client/testing';
import gql from 'graphql-tag'; import gql from 'graphql-tag';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; import { getJestMetadataAndApolloMocksAndContextStoreWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndContextStoreWrapper';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
const defaultResponseData = { const defaultResponseData = {
@ -127,9 +127,16 @@ const mocks: MockedResponse[] = [
}, },
]; ];
const WrapperWithResponse = getJestMetadataAndApolloMocksWrapper({ const WrapperWithResponse = getJestMetadataAndApolloMocksAndContextStoreWrapper(
apolloMocks: mocks, {
}); apolloMocks: mocks,
contextStoreTargetedRecordsRule: {
mode: 'selection',
selectedRecordIds: [],
},
contextStoreCurrentObjectMetadataNameSingular: 'person',
},
);
const graphqlEmptyResponse = [ const graphqlEmptyResponse = [
{ {
@ -145,28 +152,41 @@ const graphqlEmptyResponse = [
}, },
]; ];
const WrapperWithEmptyResponse = getJestMetadataAndApolloMocksWrapper({ const WrapperWithEmptyResponse =
apolloMocks: graphqlEmptyResponse, getJestMetadataAndApolloMocksAndContextStoreWrapper({
}); apolloMocks: graphqlEmptyResponse,
contextStoreTargetedRecordsRule: {
mode: 'selection',
selectedRecordIds: [],
},
contextStoreCurrentObjectMetadataNameSingular: 'person',
});
describe('useTableData', () => { describe('useRecordData', () => {
const recordIndexId = 'people'; const recordIndexId = 'people';
const objectNameSingular = 'person'; const objectMetadataItem = generatedMockObjectMetadataItems.find(
(item) => item.nameSingular === 'person',
);
if (!objectMetadataItem) {
throw new Error('Object metadata item not found');
}
describe('data fetching', () => { describe('data fetching', () => {
it('should handle no records', async () => { it('should handle no records', async () => {
const callback = jest.fn(); const callback = jest.fn();
const { result } = renderHook( const { result } = renderHook(
() => () =>
useTableData({ useRecordData({
recordIndexId, recordIndexId,
objectNameSingular, objectMetadataItem,
pageSize: 30, pageSize: 30,
callback, callback,
delayMs: 0, delayMs: 0,
viewType: ViewType.Kanban, viewType: ViewType.Kanban,
}), }),
{ wrapper: WrapperWithEmptyResponse }, {
wrapper: WrapperWithEmptyResponse,
},
); );
await act(async () => { await act(async () => {
@ -182,9 +202,9 @@ describe('useTableData', () => {
const callback = jest.fn(); const callback = jest.fn();
const { result } = renderHook( const { result } = renderHook(
() => () =>
useTableData({ useRecordData({
recordIndexId, recordIndexId,
objectNameSingular, objectMetadataItem,
callback, callback,
pageSize: 30, pageSize: 30,
@ -211,9 +231,9 @@ describe('useTableData', () => {
recordIndexId, recordIndexId,
); );
return { return {
tableData: useTableData({ tableData: useRecordData({
recordIndexId, recordIndexId,
objectNameSingular, objectMetadataItem,
callback, callback,
pageSize: 30, pageSize: 30,
maximumRequests: 100, maximumRequests: 100,
@ -223,7 +243,7 @@ describe('useTableData', () => {
useRecordBoardHook: useRecordBoard(recordIndexId), useRecordBoardHook: useRecordBoard(recordIndexId),
kanbanFieldName: useRecoilValue(kanbanFieldNameState), kanbanFieldName: useRecoilValue(kanbanFieldNameState),
kanbanData: useRecordIndexOptionsForBoard({ kanbanData: useRecordIndexOptionsForBoard({
objectNameSingular, objectNameSingular: objectMetadataItem.nameSingular,
recordBoardId: recordIndexId, recordBoardId: recordIndexId,
viewBarId: recordIndexId, viewBarId: recordIndexId,
}), }),
@ -304,9 +324,9 @@ describe('useTableData', () => {
recordIndexId, recordIndexId,
); );
return { return {
tableData: useTableData({ tableData: useRecordData({
recordIndexId, recordIndexId,
objectNameSingular, objectMetadataItem,
callback, callback,
pageSize: 30, pageSize: 30,
maximumRequests: 100, maximumRequests: 100,
@ -316,7 +336,7 @@ describe('useTableData', () => {
setKanbanFieldName: useRecordBoard(recordIndexId), setKanbanFieldName: useRecordBoard(recordIndexId),
kanbanFieldName: useRecoilValue(kanbanFieldNameState), kanbanFieldName: useRecoilValue(kanbanFieldNameState),
kanbanData: useRecordIndexOptionsForBoard({ kanbanData: useRecordIndexOptionsForBoard({
objectNameSingular, objectNameSingular: objectMetadataItem.nameSingular,
recordBoardId: recordIndexId, recordBoardId: recordIndexId,
viewBarId: recordIndexId, viewBarId: recordIndexId,
}), }),

View File

@ -1,43 +0,0 @@
import { useFavorites } from '@/favorites/hooks/useFavorites';
import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords';
import { UseTableDataOptions } from '@/object-record/record-index/options/hooks/useTableData';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
type UseDeleteTableDataOptions = Pick<
UseTableDataOptions,
'objectNameSingular' | 'recordIndexId'
>;
export const useDeleteTableData = ({
objectNameSingular,
recordIndexId,
}: UseDeleteTableDataOptions) => {
const { resetTableRowSelection } = useRecordTable({
recordTableId: recordIndexId,
});
const { deleteManyRecords } = useDeleteManyRecords({
objectNameSingular,
});
const { favorites, deleteFavorite } = useFavorites();
const deleteRecords = async (recordIdsToDelete: string[]) => {
resetTableRowSelection();
for (const recordIdToDelete of recordIdsToDelete) {
const foundFavorite = favorites?.find(
(favorite) => favorite.recordId === recordIdToDelete,
);
if (foundFavorite !== undefined) {
deleteFavorite(foundFavorite.id);
}
}
await deleteManyRecords(recordIdsToDelete, {
delayInMsBetweenRequests: 50,
});
};
return { deleteTableData: deleteRecords };
};

View File

@ -1,18 +1,21 @@
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useState } from 'react';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { useLazyFindManyRecords } from '@/object-record/hooks/useLazyFindManyRecords';
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates'; import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
import { contextStoreTargetedRecordsRuleState } from '@/context-store/states/contextStoreTargetedRecordsRuleState';
import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useLazyFindManyRecords } from '@/object-record/hooks/useLazyFindManyRecords';
import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates';
import { useFindManyParams } from '@/object-record/record-index/hooks/useLoadRecordIndexTable';
import { EXPORT_TABLE_DATA_DEFAULT_PAGE_SIZE } from '@/object-record/record-index/options/constants/ExportTableDataDefaultPageSize'; import { EXPORT_TABLE_DATA_DEFAULT_PAGE_SIZE } from '@/object-record/record-index/options/constants/ExportTableDataDefaultPageSize';
import { useRecordIndexOptionsForBoard } from '@/object-record/record-index/options/hooks/useRecordIndexOptionsForBoard'; import { useRecordIndexOptionsForBoard } from '@/object-record/record-index/options/hooks/useRecordIndexOptionsForBoard';
import { ViewType } from '@/views/types/ViewType'; import { ViewType } from '@/views/types/ViewType';
import { useFindManyParams } from '../../hooks/useLoadRecordIndexTable';
export const sleep = (ms: number) => export const sleep = (ms: number) =>
new Promise((resolve) => setTimeout(resolve, ms)); new Promise((resolve) => setTimeout(resolve, ms));
@ -21,10 +24,10 @@ export const percentage = (part: number, whole: number): number => {
return Math.round((part / whole) * 100); return Math.round((part / whole) * 100);
}; };
export type UseTableDataOptions = { export type UseRecordDataOptions = {
delayMs: number; delayMs: number;
maximumRequests?: number; maximumRequests?: number;
objectNameSingular: string; objectMetadataItem: ObjectMetadataItem;
pageSize?: number; pageSize?: number;
recordIndexId: string; recordIndexId: string;
callback: ( callback: (
@ -40,15 +43,15 @@ type ExportProgress = {
displayType: 'percentage' | 'number'; displayType: 'percentage' | 'number';
}; };
export const useTableData = ({ export const useRecordData = ({
objectMetadataItem,
delayMs, delayMs,
maximumRequests = 100, maximumRequests = 100,
objectNameSingular,
pageSize = EXPORT_TABLE_DATA_DEFAULT_PAGE_SIZE, pageSize = EXPORT_TABLE_DATA_DEFAULT_PAGE_SIZE,
recordIndexId, recordIndexId,
callback, callback,
viewType = ViewType.Table, viewType = ViewType.Table,
}: UseTableDataOptions) => { }: UseRecordDataOptions) => {
const [isDownloading, setIsDownloading] = useState(false); const [isDownloading, setIsDownloading] = useState(false);
const [inflight, setInflight] = useState(false); const [inflight, setInflight] = useState(false);
const [pageCount, setPageCount] = useState(0); const [pageCount, setPageCount] = useState(0);
@ -57,15 +60,10 @@ export const useTableData = ({
}); });
const [previousRecordCount, setPreviousRecordCount] = useState(0); const [previousRecordCount, setPreviousRecordCount] = useState(0);
const { const { visibleTableColumnsSelector } = useRecordTableStates(recordIndexId);
visibleTableColumnsSelector,
selectedRowIdsSelector,
tableRowIdsState,
hasUserSelectedAllRowsState,
} = useRecordTableStates(recordIndexId);
const { hiddenBoardFields } = useRecordIndexOptionsForBoard({ const { hiddenBoardFields } = useRecordIndexOptionsForBoard({
objectNameSingular, objectNameSingular: objectMetadataItem.nameSingular,
recordBoardId: recordIndexId, recordBoardId: recordIndexId,
viewBarId: recordIndexId, viewBarId: recordIndexId,
}); });
@ -76,61 +74,21 @@ export const useTableData = ({
(column) => column.metadata.fieldName === kanbanFieldMetadataName, (column) => column.metadata.fieldName === kanbanFieldMetadataName,
); );
const columns = useRecoilValue(visibleTableColumnsSelector()); const columns = useRecoilValue(visibleTableColumnsSelector());
const selectedRowIds = useRecoilValue(selectedRowIdsSelector());
const hasUserSelectedAllRows = useRecoilValue(hasUserSelectedAllRowsState); const contextStoreTargetedRecordsRule = useRecoilValue(
const tableRowIds = useRecoilValue(tableRowIdsState); contextStoreTargetedRecordsRuleState,
);
// user has checked select all and then unselected some rows const queryFilter = computeContextStoreFilters(
const userHasUnselectedSomeRows = contextStoreTargetedRecordsRule,
hasUserSelectedAllRows && selectedRowIds.length < tableRowIds.length; objectMetadataItem,
const hasSelectedRows =
selectedRowIds.length > 0 &&
!(hasUserSelectedAllRows && selectedRowIds.length === tableRowIds.length);
const unselectedRowIds = useMemo(
() =>
userHasUnselectedSomeRows
? tableRowIds.filter((id) => !selectedRowIds.includes(id))
: [],
[userHasUnselectedSomeRows, tableRowIds, selectedRowIds],
); );
const findManyRecordsParams = useFindManyParams( const findManyRecordsParams = useFindManyParams(
objectNameSingular, objectMetadataItem.nameSingular,
recordIndexId, recordIndexId,
); );
const selectedFindManyParams = {
...findManyRecordsParams,
filter: {
...findManyRecordsParams.filter,
id: {
in: selectedRowIds,
},
},
};
const unselectedFindManyParams = {
...findManyRecordsParams,
filter: {
...findManyRecordsParams.filter,
not: {
id: {
in: unselectedRowIds,
},
},
},
};
const usedFindManyParams =
hasSelectedRows && !userHasUnselectedSomeRows
? selectedFindManyParams
: userHasUnselectedSomeRows
? unselectedFindManyParams
: findManyRecordsParams;
const { const {
findManyRecords, findManyRecords,
totalCount, totalCount,
@ -138,7 +96,8 @@ export const useTableData = ({
fetchMoreRecordsWithPagination, fetchMoreRecordsWithPagination,
loading, loading,
} = useLazyFindManyRecords({ } = useLazyFindManyRecords({
...usedFindManyParams, ...findManyRecordsParams,
filter: queryFilter,
limit: pageSize, limit: pageSize,
}); });

View File

@ -1,41 +1,22 @@
import { Key } from 'ts-key-enum'; import { Key } from 'ts-key-enum';
import { SOFT_FOCUS_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/SoftFocusClickOutsideListenerId';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope'; import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener'; import { useListenClickOutsideByClassName } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import {
ClickOutsideMode,
useListenClickOutsideByClassName,
} from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
type RecordTableInternalEffectProps = { type RecordTableInternalEffectProps = {
recordTableId: string; recordTableId: string;
tableBodyRef: React.RefObject<HTMLDivElement>;
}; };
export const RecordTableInternalEffect = ({ export const RecordTableInternalEffect = ({
recordTableId, recordTableId,
tableBodyRef,
}: RecordTableInternalEffectProps) => { }: RecordTableInternalEffectProps) => {
const { leaveTableFocus, resetTableRowSelection, useMapKeyboardToSoftFocus } = const { leaveTableFocus, resetTableRowSelection, useMapKeyboardToSoftFocus } =
useRecordTable({ recordTableId }); useRecordTable({ recordTableId });
useMapKeyboardToSoftFocus(); useMapKeyboardToSoftFocus();
const { useListenClickOutside } = useClickOutsideListener(
SOFT_FOCUS_CLICK_OUTSIDE_LISTENER_ID,
);
useListenClickOutside({
refs: [tableBodyRef],
callback: () => {
leaveTableFocus();
},
mode: ClickOutsideMode.compareHTMLRef,
});
useScopedHotkeys( useScopedHotkeys(
[Key.Escape], [Key.Escape],
() => { () => {
@ -46,9 +27,9 @@ export const RecordTableInternalEffect = ({
useListenClickOutsideByClassName({ useListenClickOutsideByClassName({
classNames: ['entity-table-cell'], classNames: ['entity-table-cell'],
excludeClassNames: ['bottom-bar', 'context-menu'], excludeClassNames: ['bottom-bar', 'action-menu-dropdown'],
callback: () => { callback: () => {
resetTableRowSelection(); leaveTableFocus();
}, },
}); });

View File

@ -87,10 +87,7 @@ export const RecordTableWithWrappers = ({
onDragSelectionChange={setRowSelected} onDragSelectionChange={setRowSelected}
/> />
</StyledTableInternalContainer> </StyledTableInternalContainer>
<RecordTableInternalEffect <RecordTableInternalEffect recordTableId={recordTableId} />
recordTableId={recordTableId}
tableBodyRef={tableBodyRef}
/>
</StyledTableContainer> </StyledTableContainer>
</StyledTableWithHeader> </StyledTableWithHeader>
</RecordUpdateContext.Provider> </RecordUpdateContext.Provider>

View File

@ -6,21 +6,19 @@ import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotV
import { TableHotkeyScope } from '../../types/TableHotkeyScope'; import { TableHotkeyScope } from '../../types/TableHotkeyScope';
import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection';
import { useCloseCurrentTableCellInEditMode } from './useCloseCurrentTableCellInEditMode'; import { useCloseCurrentTableCellInEditMode } from './useCloseCurrentTableCellInEditMode';
import { useDisableSoftFocus } from './useDisableSoftFocus'; import { useDisableSoftFocus } from './useDisableSoftFocus';
import { useSetHasUserSelectedAllRows } from './useSetAllRowSelectedState';
export const useLeaveTableFocus = (recordTableId?: string) => { export const useLeaveTableFocus = (recordTableId?: string) => {
const disableSoftFocus = useDisableSoftFocus(recordTableId); const disableSoftFocus = useDisableSoftFocus(recordTableId);
const closeCurrentCellInEditMode = const closeCurrentCellInEditMode =
useCloseCurrentTableCellInEditMode(recordTableId); useCloseCurrentTableCellInEditMode(recordTableId);
const setHasUserSelectedAllRows = useSetHasUserSelectedAllRows(recordTableId);
const selectAllRows = useSetHasUserSelectedAllRows(recordTableId);
const { isSoftFocusActiveState } = useRecordTableStates(recordTableId); const { isSoftFocusActiveState } = useRecordTableStates(recordTableId);
const resetTableRowSelection = useResetTableRowSelection(recordTableId);
return useRecoilCallback( return useRecoilCallback(
({ snapshot }) => ({ snapshot }) =>
() => { () => {
@ -33,6 +31,8 @@ export const useLeaveTableFocus = (recordTableId?: string) => {
.getLoadable(currentHotkeyScopeState) .getLoadable(currentHotkeyScopeState)
.getValue(); .getValue();
resetTableRowSelection();
if (!isSoftFocusActive) { if (!isSoftFocusActive) {
return; return;
} }
@ -43,15 +43,12 @@ export const useLeaveTableFocus = (recordTableId?: string) => {
closeCurrentCellInEditMode(); closeCurrentCellInEditMode();
disableSoftFocus(); disableSoftFocus();
setHasUserSelectedAllRows(false);
selectAllRows(false);
}, },
[ [
closeCurrentCellInEditMode, closeCurrentCellInEditMode,
disableSoftFocus, disableSoftFocus,
isSoftFocusActiveState, isSoftFocusActiveState,
selectAllRows, resetTableRowSelection,
setHasUserSelectedAllRows,
], ],
); );
}; };

View File

@ -19,6 +19,7 @@ import { allRowsSelectedStatusComponentSelector } from '@/object-record/record-t
import { hiddenTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/hiddenTableColumnsComponentSelector'; import { hiddenTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/hiddenTableColumnsComponentSelector';
import { numberOfTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/numberOfTableColumnsComponentSelector'; import { numberOfTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/numberOfTableColumnsComponentSelector';
import { selectedRowIdsComponentSelector } from '@/object-record/record-table/states/selectors/selectedRowIdsComponentSelector'; import { selectedRowIdsComponentSelector } from '@/object-record/record-table/states/selectors/selectedRowIdsComponentSelector';
import { unselectedRowIdsComponentSelector } from '@/object-record/record-table/states/selectors/unselectedRowIdsComponentSelector';
import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector'; import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector';
import { softFocusPositionComponentState } from '@/object-record/record-table/states/softFocusPositionComponentState'; import { softFocusPositionComponentState } from '@/object-record/record-table/states/softFocusPositionComponentState';
import { tableColumnsComponentState } from '@/object-record/record-table/states/tableColumnsComponentState'; import { tableColumnsComponentState } from '@/object-record/record-table/states/tableColumnsComponentState';
@ -134,6 +135,10 @@ export const useRecordTableStates = (recordTableId?: string) => {
selectedRowIdsComponentSelector, selectedRowIdsComponentSelector,
scopeId, scopeId,
), ),
unselectedRowIdsSelector: extractComponentReadOnlySelector(
unselectedRowIdsComponentSelector,
scopeId,
),
visibleTableColumnsSelector: extractComponentReadOnlySelector( visibleTableColumnsSelector: extractComponentReadOnlySelector(
visibleTableColumnsComponentSelector, visibleTableColumnsComponentSelector,
scopeId, scopeId,

View File

@ -46,17 +46,16 @@ export const useSetRecordTableData = ({
const recordIds = newRecords.map((record) => record.id); const recordIds = newRecords.map((record) => record.id);
if (!isDeeplyEqual(currentRowIds, recordIds)) { if (!isDeeplyEqual(currentRowIds, recordIds)) {
set(tableRowIdsState, recordIds); if (hasUserSelectedAllRows) {
} for (const rowId of recordIds) {
set(isRowSelectedFamilyState(rowId), true);
if (hasUserSelectedAllRows) { }
for (const rowId of recordIds) {
set(isRowSelectedFamilyState(rowId), true);
} }
}
set(numberOfTableRowsState, totalCount ?? 0); set(tableRowIdsState, recordIds);
onEntityCountChange(totalCount); set(numberOfTableRowsState, totalCount ?? 0);
onEntityCountChange(totalCount);
}
}, },
[ [
numberOfTableRowsState, numberOfTableRowsState,

View File

@ -42,6 +42,7 @@ export const useRecordTable = (props?: useRecordTableProps) => {
isRecordTableInitialLoadingState, isRecordTableInitialLoadingState,
tableLastRowVisibleState, tableLastRowVisibleState,
selectedRowIdsSelector, selectedRowIdsSelector,
unselectedRowIdsSelector,
onToggleColumnFilterState, onToggleColumnFilterState,
onToggleColumnSortState, onToggleColumnSortState,
pendingRecordIdState, pendingRecordIdState,
@ -223,6 +224,7 @@ export const useRecordTable = (props?: useRecordTableProps) => {
setSoftFocusPosition, setSoftFocusPosition,
isSomeCellInEditModeState, isSomeCellInEditModeState,
selectedRowIdsSelector, selectedRowIdsSelector,
unselectedRowIdsSelector,
setHasUserSelectedAllRows, setHasUserSelectedAllRows,
setOnToggleColumnFilter, setOnToggleColumnFilter,
setOnToggleColumnSort, setOnToggleColumnSort,

View File

@ -1,9 +1,7 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useCallback, useContext } from 'react'; import { useCallback, useContext } from 'react';
import { useRecoilValue } from 'recoil';
import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext'; 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 { RecordTableTd } from '@/object-record/record-table/record-table-cell/components/RecordTableTd';
import { useSetCurrentRowSelected } from '@/object-record/record-table/record-table-row/hooks/useSetCurrentRowSelected'; import { useSetCurrentRowSelected } from '@/object-record/record-table/record-table-row/hooks/useSetCurrentRowSelected';
import { Checkbox } from '@/ui/input/components/Checkbox'; import { Checkbox } from '@/ui/input/components/Checkbox';
@ -21,19 +19,16 @@ const StyledContainer = styled.div`
export const RecordTableCellCheckbox = () => { export const RecordTableCellCheckbox = () => {
const { isSelected } = useContext(RecordTableRowContext); const { isSelected } = useContext(RecordTableRowContext);
const { recordId } = useContext(RecordTableRowContext);
const { isRowSelectedFamilyState } = useRecordTableStates();
const { setCurrentRowSelected } = useSetCurrentRowSelected(); const { setCurrentRowSelected } = useSetCurrentRowSelected();
const currentRowSelected = useRecoilValue(isRowSelectedFamilyState(recordId));
const handleClick = useCallback(() => { const handleClick = useCallback(() => {
setCurrentRowSelected(!currentRowSelected); setCurrentRowSelected(!isSelected);
}, [currentRowSelected, setCurrentRowSelected]); }, [isSelected, setCurrentRowSelected]);
return ( return (
<RecordTableTd isSelected={isSelected} hasRightBorder={false}> <RecordTableTd isSelected={isSelected} hasRightBorder={false}>
<StyledContainer onClick={handleClick}> <StyledContainer onClick={handleClick}>
<Checkbox hoverable checked={currentRowSelected} /> <Checkbox hoverable checked={isSelected} />
</StyledContainer> </StyledContainer>
</RecordTableTd> </RecordTableTd>
); );

View File

@ -37,7 +37,6 @@ export const RecordTableHeaderCheckboxColumn = () => {
setHasUserSelectedAllRows(true); setHasUserSelectedAllRows(true);
selectAllRows(); selectAllRows();
} else { } else {
setHasUserSelectedAllRows(false);
resetTableRowSelection(); resetTableRowSelection();
} }
}; };

View File

@ -0,0 +1,23 @@
import { isRowSelectedComponentFamilyState } from '@/object-record/record-table/record-table-row/states/isRowSelectedComponentFamilyState';
import { tableRowIdsComponentState } from '@/object-record/record-table/states/tableRowIdsComponentState';
import { createComponentReadOnlySelector } from '@/ui/utilities/state/component-state/utils/createComponentReadOnlySelector';
export const unselectedRowIdsComponentSelector =
createComponentReadOnlySelector<string[]>({
key: 'unselectedRowIdsComponentSelector',
get:
({ scopeId }) =>
({ get }) => {
const rowIds = get(tableRowIdsComponentState({ scopeId }));
return rowIds.filter(
(rowId) =>
get(
isRowSelectedComponentFamilyState({
scopeId,
familyKey: rowId,
}),
) === false,
);
},
});

View File

@ -1,11 +1,9 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { RecordIndexOptionsDropdown } from '@/object-record/record-index/options/components/RecordIndexOptionsDropdown';
import { RecordTableWithWrappers } from '@/object-record/record-table/components/RecordTableWithWrappers'; import { RecordTableWithWrappers } from '@/object-record/record-table/components/RecordTableWithWrappers';
import { SignInBackgroundMockContainerEffect } from '@/sign-in-background-mock/components/SignInBackgroundMockContainerEffect'; import { SignInBackgroundMockContainerEffect } from '@/sign-in-background-mock/components/SignInBackgroundMockContainerEffect';
import { ViewBar } from '@/views/components/ViewBar'; import { ViewBar } from '@/views/components/ViewBar';
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
import { ViewType } from '@/views/types/ViewType';
const StyledContainer = styled.div` const StyledContainer = styled.div`
display: flex; display: flex;
@ -26,13 +24,7 @@ export const SignInBackgroundMockContainer = () => {
<ViewBar <ViewBar
viewBarId={viewBarId} viewBarId={viewBarId}
onCurrentViewChange={async () => {}} onCurrentViewChange={async () => {}}
optionsDropdownButton={ optionsDropdownButton={<></>}
<RecordIndexOptionsDropdown
recordIndexId={recordIndexId}
objectNameSingular={objectNameSingular}
viewType={ViewType.Table}
/>
}
/> />
<SignInBackgroundMockContainerEffect <SignInBackgroundMockContainerEffect
objectNamePlural={objectNamePlural} objectNamePlural={objectNamePlural}

View File

@ -3,7 +3,7 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { formatFieldMetadataItemsAsFilterDefinitions } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions'; import { formatFieldMetadataItemsAsFilterDefinitions } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
import { formatFieldMetadataItemsAsSortDefinitions } from '@/object-metadata/utils/formatFieldMetadataItemsAsSortDefinitions'; import { formatFieldMetadataItemsAsSortDefinitions } from '@/object-metadata/utils/formatFieldMetadataItemsAsSortDefinitions';
import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy'; import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy';
import { turnObjectDropdownFilterIntoQueryFilter } from '@/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter'; import { turnFiltersIntoQueryFilter } from '@/object-record/record-filter/utils/turnFiltersIntoQueryFilter';
import { View } from '@/views/types/View'; import { View } from '@/views/types/View';
import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters'; import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters';
import { mapViewSortsToSorts } from '@/views/utils/mapViewSortsToSorts'; import { mapViewSortsToSorts } from '@/views/utils/mapViewSortsToSorts';
@ -35,7 +35,7 @@ export const getQueryVariablesFromView = ({
fields: fieldMetadataItems, fields: fieldMetadataItems,
}); });
const filter = turnObjectDropdownFilterIntoQueryFilter( const filter = turnFiltersIntoQueryFilter(
mapViewFiltersToFilters(viewFilters, filterDefinitions), mapViewFiltersToFilters(viewFilters, filterDefinitions),
objectMetadataItem?.fields ?? [], objectMetadataItem?.fields ?? [],
); );

View File

@ -5,6 +5,8 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural'; import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
import { lastShowPageRecordIdState } from '@/object-record/record-field/states/lastShowPageRecordId'; import { lastShowPageRecordIdState } from '@/object-record/record-field/states/lastShowPageRecordId';
import { RecordIndexContainer } from '@/object-record/record-index/components/RecordIndexContainer'; import { RecordIndexContainer } from '@/object-record/record-index/components/RecordIndexContainer';
import { RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect } from '@/object-record/record-index/components/RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect';
import { RecordIndexContainerContextStoreObjectMetadataEffect } from '@/object-record/record-index/components/RecordIndexContainerContextStoreObjectMetadataEffect';
import { RecordIndexPageHeader } from '@/object-record/record-index/components/RecordIndexPageHeader'; import { RecordIndexPageHeader } from '@/object-record/record-index/components/RecordIndexPageHeader';
import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext'; import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext';
import { useHandleIndexIdentifierClick } from '@/object-record/record-index/hooks/useHandleIndexIdentifierClick'; import { useHandleIndexIdentifierClick } from '@/object-record/record-index/hooks/useHandleIndexIdentifierClick';
@ -71,6 +73,8 @@ export const RecordIndexPage = () => {
<RecordIndexPageHeader /> <RecordIndexPageHeader />
<PageBody> <PageBody>
<StyledIndexContainer> <StyledIndexContainer>
<RecordIndexContainerContextStoreObjectMetadataEffect />
<RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect />
<RecordIndexContainer /> <RecordIndexContainer />
</StyledIndexContainer> </StyledIndexContainer>
</PageBody> </PageBody>

View File

@ -1,5 +1,6 @@
import { contextStoreCurrentObjectMetadataIdState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdState'; import { contextStoreCurrentObjectMetadataIdState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdState';
import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState'; import { contextStoreNumberOfSelectedRecordsState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsState';
import { contextStoreTargetedRecordsRuleState } from '@/context-store/states/contextStoreTargetedRecordsRuleState';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
@ -10,8 +11,8 @@ export const RecordShowPageContextStoreEffect = ({
}: { }: {
recordId: string; recordId: string;
}) => { }) => {
const setContextStoreTargetedRecordIds = useSetRecoilState( const setContextStoreTargetedRecordsRule = useSetRecoilState(
contextStoreTargetedRecordIdsState, contextStoreTargetedRecordsRuleState,
); );
const setContextStoreCurrentObjectMetadataId = useSetRecoilState( const setContextStoreCurrentObjectMetadataId = useSetRecoilState(
@ -24,19 +25,32 @@ export const RecordShowPageContextStoreEffect = ({
objectNameSingular: objectNameSingular ?? '', objectNameSingular: objectNameSingular ?? '',
}); });
const setContextStoreNumberOfSelectedRecords = useSetRecoilState(
contextStoreNumberOfSelectedRecordsState,
);
useEffect(() => { useEffect(() => {
setContextStoreTargetedRecordIds([recordId]); setContextStoreTargetedRecordsRule({
mode: 'selection',
selectedRecordIds: [recordId],
});
setContextStoreCurrentObjectMetadataId(objectMetadataItem?.id); setContextStoreCurrentObjectMetadataId(objectMetadataItem?.id);
setContextStoreNumberOfSelectedRecords(1);
return () => { return () => {
setContextStoreTargetedRecordIds([]); setContextStoreTargetedRecordsRule({
mode: 'selection',
selectedRecordIds: [],
});
setContextStoreCurrentObjectMetadataId(null); setContextStoreCurrentObjectMetadataId(null);
setContextStoreNumberOfSelectedRecords(0);
}; };
}, [ }, [
recordId, recordId,
setContextStoreTargetedRecordIds, setContextStoreTargetedRecordsRule,
setContextStoreCurrentObjectMetadataId, setContextStoreCurrentObjectMetadataId,
objectMetadataItem?.id, objectMetadataItem?.id,
setContextStoreNumberOfSelectedRecords,
]); ]);
return null; return null;

View File

@ -1,15 +0,0 @@
import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState';
import { useEffect } from 'react';
import { useSetRecoilState } from 'recoil';
export const RecordShowPageEffect = ({ recordId }: { recordId: string }) => {
const setContextStoreTargetedRecordIds = useSetRecoilState(
contextStoreTargetedRecordIdsState,
);
useEffect(() => {
setContextStoreTargetedRecordIds([recordId]);
}, [recordId, setContextStoreTargetedRecordIds]);
return null;
};

View File

@ -0,0 +1,49 @@
import { ReactNode, useEffect, useState } from 'react';
import { useSetRecoilState } from 'recoil';
import { contextStoreCurrentObjectMetadataIdState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdState';
import {
ContextStoreTargetedRecordsRule,
contextStoreTargetedRecordsRuleState,
} from '@/context-store/states/contextStoreTargetedRecordsRuleState';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
export const JestContextStoreSetter = ({
contextStoreTargetedRecordsRule = {
mode: 'selection',
selectedRecordIds: [],
},
contextStoreCurrentObjectMetadataNameSingular = '',
children,
}: {
contextStoreTargetedRecordsRule?: ContextStoreTargetedRecordsRule;
contextStoreCurrentObjectMetadataNameSingular?: string;
children: ReactNode;
}) => {
const setContextStoreTargetedRecordsRule = useSetRecoilState(
contextStoreTargetedRecordsRuleState,
);
const setContextStoreCurrentObjectMetadataId = useSetRecoilState(
contextStoreCurrentObjectMetadataIdState,
);
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular: contextStoreCurrentObjectMetadataNameSingular,
});
const contextStoreCurrentObjectMetadataId = objectMetadataItem.id;
const [isLoaded, setIsLoaded] = useState(false);
useEffect(() => {
setContextStoreTargetedRecordsRule(contextStoreTargetedRecordsRule);
setContextStoreCurrentObjectMetadataId(contextStoreCurrentObjectMetadataId);
setIsLoaded(true);
}, [
setContextStoreTargetedRecordsRule,
setContextStoreCurrentObjectMetadataId,
contextStoreTargetedRecordsRule,
contextStoreCurrentObjectMetadataId,
]);
return isLoaded ? <>{children}</> : null;
};

View File

@ -0,0 +1,37 @@
import { ContextStoreTargetedRecordsRule } from '@/context-store/states/contextStoreTargetedRecordsRuleState';
import { MockedResponse } from '@apollo/client/testing';
import { ReactNode } from 'react';
import { MutableSnapshot } from 'recoil';
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
import { JestContextStoreSetter } from '~/testing/jest/JestContextStoreSetter';
export const getJestMetadataAndApolloMocksAndContextStoreWrapper = ({
apolloMocks,
onInitializeRecoilSnapshot,
contextStoreTargetedRecordsRule,
contextStoreCurrentObjectMetadataNameSingular,
}: {
apolloMocks:
| readonly MockedResponse<Record<string, any>, Record<string, any>>[]
| undefined;
onInitializeRecoilSnapshot?: (snapshot: MutableSnapshot) => void;
contextStoreTargetedRecordsRule?: ContextStoreTargetedRecordsRule;
contextStoreCurrentObjectMetadataNameSingular?: string;
}) => {
const Wrapper = getJestMetadataAndApolloMocksWrapper({
apolloMocks,
onInitializeRecoilSnapshot,
});
return ({ children }: { children: ReactNode }) => (
<Wrapper>
<JestContextStoreSetter
contextStoreTargetedRecordsRule={contextStoreTargetedRecordsRule}
contextStoreCurrentObjectMetadataNameSingular={
contextStoreCurrentObjectMetadataNameSingular
}
>
{children}
</JestContextStoreSetter>
</Wrapper>
);
};