Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
1d5a91e
handle select all case for record deletion
bosiraphael Oct 14, 2024
1116747
update DeleteRecordsActionEffect
bosiraphael Oct 14, 2024
d428975
add unselectedRowIdsComponentSelector
bosiraphael Oct 14, 2024
0c567a6
fix case where selectedRowIds.length is 1
bosiraphael Oct 14, 2024
2e6dfac
move and rename files
bosiraphael Oct 15, 2024
be38710
delete only filtered records
bosiraphael Oct 15, 2024
366d05c
fix RecordActionMenuEntriesSetter
bosiraphael Oct 15, 2024
55f5877
update hooks and useTableData
bosiraphael Oct 15, 2024
8603250
rename hooks
bosiraphael Oct 15, 2024
3163cb1
make export action export everything when no record is selected
bosiraphael Oct 16, 2024
4589aac
update test
bosiraphael Oct 16, 2024
71bb537
Merge branch 'main' into 7665-handle-the-select-all-case-inside-the-a…
bosiraphael Oct 16, 2024
cdba802
updates after comments
bosiraphael Oct 16, 2024
78aa3ff
create wrapper to set context store states inside test
bosiraphael Oct 17, 2024
4c51200
fix useRecordData and useContextStoreSelectedRecords
bosiraphael Oct 17, 2024
028178d
jest config update
bosiraphael Oct 17, 2024
6fa11fd
fix delete action
bosiraphael Oct 17, 2024
43efae4
move resetTableSelection
bosiraphael Oct 17, 2024
118a593
fix reset table row selection when clicking outside
bosiraphael Oct 18, 2024
5657fc7
update RecordTableInternalEffect
bosiraphael Oct 18, 2024
cc47b73
wip
bosiraphael Oct 18, 2024
85ab4bc
Enhance performance
charlesBochet Oct 18, 2024
b8e2d98
Fix
charlesBochet Oct 18, 2024
ffbbf01
refactor context store states
bosiraphael Oct 18, 2024
0dd6e68
set numberOfSelectedRecords inside effect in record index
bosiraphael Oct 21, 2024
65826ac
update RecordShowPageContextStoreEffect
bosiraphael Oct 21, 2024
cd8e0a5
refactor to make useObjectMetadataItemById throw
bosiraphael Oct 21, 2024
9b3ed1d
rename test
bosiraphael Oct 21, 2024
2416306
Merge branch 'main' into 7665-handle-the-select-all-case-inside-the-a…
bosiraphael Oct 21, 2024
a14ed1c
fix test
bosiraphael Oct 21, 2024
f0ef4dd
fix ActionMenuBar.stories
bosiraphael Oct 21, 2024
01c5022
modifications after comments
bosiraphael Oct 21, 2024
4b63742
update filter definition
bosiraphael Oct 21, 2024
cf80505
Merge branch 'main' into 7665-handle-the-select-all-case-inside-the-a…
charlesBochet Oct 21, 2024
440dc66
Fix CI
charlesBochet Oct 21, 2024
87ca647
Fix
charlesBochet Oct 21, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
import { useContextStoreSelectedRecords } from '@/context-store/hooks/useContextStoreSelectedRecords';
import { contextStoreCurrentObjectMetadataIdState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdState';
import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState';
import { useFavorites } from '@/favorites/hooks/useFavorites';
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
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 { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { useCallback, useEffect, useState } from 'react';
import { useRecoilValue } from 'recoil';
import { IconTrash } from 'twenty-ui';
import { IconTrash, isDefined } from 'twenty-ui';

export const DeleteRecordsActionEffect = ({
position,
Expand All @@ -16,10 +18,6 @@ export const DeleteRecordsActionEffect = ({
}) => {
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();

const contextStoreTargetedRecordIds = useRecoilValue(
contextStoreTargetedRecordIdsState,
);

const contextStoreCurrentObjectMetadataId = useRecoilValue(
contextStoreCurrentObjectMetadataIdState,
);
Expand All @@ -31,21 +29,56 @@ export const DeleteRecordsActionEffect = ({
const [isDeleteRecordsModalOpen, setIsDeleteRecordsModalOpen] =
useState(false);

const { deleteTableData } = useDeleteTableData({
const { resetTableRowSelection } = useRecordTable({
recordTableId: objectMetadataItem?.namePlural ?? '',
});

const { deleteManyRecords } = useDeleteManyRecords({
objectNameSingular: objectMetadataItem?.nameSingular ?? '',
recordIndexId: objectMetadataItem?.namePlural ?? '',
});

const handleDeleteClick = useCallback(() => {
deleteTableData(contextStoreTargetedRecordIds);
}, [deleteTableData, contextStoreTargetedRecordIds]);
const { favorites, deleteFavorite } = useFavorites();

const isRemoteObject = objectMetadataItem?.isRemote ?? false;
const { totalCount: numberOfSelectedRecords, fetchAllRecordIds } =
useContextStoreSelectedRecords({
recordGqlFields: {
id: true,
},
});

const handleDeleteClick = useCallback(async () => {
const recordIdsToDelete = await fetchAllRecordIds();
bosiraphael marked this conversation as resolved.
Show resolved Hide resolved

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 numberOfSelectedRecords = contextStoreTargetedRecordIds.length;
const isRemoteObject = objectMetadataItem?.isRemote ?? false;

const canDelete =
!isRemoteObject && numberOfSelectedRecords < DELETE_MAX_COUNT;
!isRemoteObject &&
isDefined(numberOfSelectedRecords) &&
numberOfSelectedRecords < DELETE_MAX_COUNT &&
numberOfSelectedRecords > 0;

useEffect(() => {
if (canDelete) {
Expand Down Expand Up @@ -80,14 +113,18 @@ export const DeleteRecordsActionEffect = ({
} else {
removeActionMenuEntry('delete');
}

return () => {
removeActionMenuEntry('delete');
};
}, [
canDelete,
addActionMenuEntry,
removeActionMenuEntry,
canDelete,
handleDeleteClick,
isDeleteRecordsModalOpen,
numberOfSelectedRecords,
handleDeleteClick,
position,
removeActionMenuEntry,
]);

return null;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
import { contextStoreCurrentObjectMetadataIdState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdState';
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import {
displayedExportProgress,
useExportTableData,
} from '@/object-record/record-index/options/hooks/useExportTableData';
useExportRecordData,
} from '@/action-menu/hooks/useExportRecordData';
import { contextStoreCurrentObjectMetadataIdState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdState';
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';

import { useEffect } from 'react';
import { useRecoilValue } from 'recoil';
import { IconFileExport } from 'twenty-ui';
Expand All @@ -24,14 +25,10 @@ export const ExportRecordsActionEffect = ({
objectId: contextStoreCurrentObjectMetadataId,
});

const baseTableDataParams = {
const { progress, download } = useExportRecordData({
delayMs: 100,
objectNameSingular: objectMetadataItem?.nameSingular ?? '',
recordIndexId: objectMetadataItem?.namePlural ?? '',
};

const { progress, download } = useExportTableData({
...baseTableDataParams,
filename: `${objectMetadataItem?.nameSingular}.csv`,
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
import { contextStoreCurrentObjectMetadataIdState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdState';
import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState';
import { contextStoreTargetedRecordsState } from '@/context-store/states/contextStoreTargetedRecordsState';
import { useFavorites } from '@/favorites/hooks/useFavorites';
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
Expand All @@ -15,16 +15,19 @@ export const ManageFavoritesActionEffect = ({
}) => {
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();

const contextStoreTargetedRecordIds = useRecoilValue(
contextStoreTargetedRecordIdsState,
const contextStoreTargetedRecords = useRecoilValue(
contextStoreTargetedRecordsState,
);
const contextStoreCurrentObjectMetadataId = useRecoilValue(
contextStoreCurrentObjectMetadataIdState,
);

const { favorites, createFavorite, deleteFavorite } = useFavorites();

const selectedRecordId = contextStoreTargetedRecordIds[0];
const selectedRecordId =
contextStoreTargetedRecords.selectedRecordIds === 'all'
? ''
: contextStoreTargetedRecords.selectedRecordIds[0];
bosiraphael marked this conversation as resolved.
Show resolved Hide resolved

const selectedRecord = useRecoilValue(
recordStoreFamilyState(selectedRecordId),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
import { MultipleRecordsActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/MultipleRecordsActionMenuEntriesSetter';
import { SingleRecordActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/SingleRecordActionMenuEntriesSetter';
import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState';
import { useRecoilValue } from 'recoil';
import { useContextStoreSelectedRecords } from '@/context-store/hooks/useContextStoreSelectedRecords';

export const RecordActionMenuEntriesSetter = () => {
const contextStoreTargetedRecordIds = useRecoilValue(
contextStoreTargetedRecordIdsState,
);
const { totalCount } = useContextStoreSelectedRecords({ limit: 1 });

if (contextStoreTargetedRecordIds.length === 0) {
if (!totalCount) {
return null;
}

if (contextStoreTargetedRecordIds.length === 1) {
if (totalCount === 1) {
return <SingleRecordActionMenuEntriesSetter />;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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>
)}
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@ import { ActionMenuBarEntry } from '@/action-menu/components/ActionMenuBarEntry'
import { actionMenuEntriesComponentSelector } from '@/action-menu/states/actionMenuEntriesComponentSelector';
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { ActionBarHotkeyScope } from '@/action-menu/types/ActionBarHotKeyScope';
import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState';
import { useContextStoreSelectedRecords } from '@/context-store/hooks/useContextStoreSelectedRecords';
import { BottomBar } from '@/ui/layout/bottom-bar/components/BottomBar';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useRecoilValue } from 'recoil';

const StyledLabel = styled.div`
color: ${({ theme }) => theme.font.color.tertiary};
Expand All @@ -19,9 +18,10 @@ const StyledLabel = styled.div`
`;

export const ActionMenuBar = () => {
const contextStoreTargetedRecordIds = useRecoilValue(
contextStoreTargetedRecordIdsState,
);
const { totalCount: numberOfSelectedRecords } =
useContextStoreSelectedRecords({
limit: 1,
});

const actionMenuId = useAvailableComponentInstanceIdOrThrow(
ActionMenuComponentInstanceContext,
Expand All @@ -42,9 +42,7 @@ export const ActionMenuBar = () => {
scope: ActionBarHotkeyScope.ActionBar,
}}
>
<StyledLabel>
{contextStoreTargetedRecordIds.length} selected:
</StyledLabel>
<StyledLabel>{numberOfSelectedRecords} selected:</StyledLabel>
bosiraphael marked this conversation as resolved.
Show resolved Hide resolved
{actionMenuEntries.map((entry, index) => (
<ActionMenuBarEntry key={index} entry={entry} />
))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export const ActionMenuDropdown = () => {
return (
<StyledContainerActionMenuDropdown
position={actionMenuDropdownPosition}
className="context-menu"
className="action-menu-dropdown"
>
<Dropdown
dropdownId={`action-menu-dropdown-${actionMenuId}`}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import { useActionMenu } from '@/action-menu/hooks/useActionMenu';
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState';
import { contextStoreTargetedRecordsState } from '@/context-store/states/contextStoreTargetedRecordsState';
import { isOneRecordOrMoreSelected } from '@/context-store/utils/isOneRecordOrMoreSelected';
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
import { useEffect } from 'react';
import { useRecoilValue } from 'recoil';

export const ActionMenuEffect = () => {
const contextStoreTargetedRecordIds = useRecoilValue(
contextStoreTargetedRecordIdsState,
const contextStoreTargetedRecords = useRecoilValue(
contextStoreTargetedRecordsState,
);

const selectedRecords = contextStoreTargetedRecords.selectedRecordIds;

const actionMenuId = useAvailableComponentInstanceIdOrThrow(
ActionMenuComponentInstanceContext,
);
Expand All @@ -26,20 +29,21 @@ export const ActionMenuEffect = () => {
);

useEffect(() => {
if (contextStoreTargetedRecordIds.length > 0 && !isDropdownOpen) {
if (isOneRecordOrMoreSelected(selectedRecords) && !isDropdownOpen) {
// We only handle opening the ActionMenuBar here, not the Dropdown.
// The Dropdown is already managed by sync handlers for events like
// right-click to open and click outside to close.
openActionBar();
}
if (contextStoreTargetedRecordIds.length === 0) {
if (!isOneRecordOrMoreSelected(selectedRecords) && isDropdownOpen) {
closeActionBar();
}
}, [
contextStoreTargetedRecordIds,
contextStoreTargetedRecords,
openActionBar,
closeActionBar,
isDropdownOpen,
selectedRecords,
]);

return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { RecoilRoot } from 'recoil';
import { ActionMenuBar } from '@/action-menu/components/ActionMenuBar';
import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState';
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState';
import { contextStoreTargetedRecordsState } from '@/context-store/states/contextStoreTargetedRecordsState';
import { isBottomBarOpenedComponentState } from '@/ui/layout/bottom-bar/states/isBottomBarOpenedComponentState';
import { userEvent, waitFor, within } from '@storybook/test';
import { IconCheckbox, IconTrash } from 'twenty-ui';
Expand All @@ -20,7 +20,10 @@ const meta: Meta<typeof ActionMenuBar> = {
(Story) => (
<RecoilRoot
initializeState={({ set }) => {
set(contextStoreTargetedRecordIdsState, ['1', '2', '3']);
set(contextStoreTargetedRecordsState, {
selectedRecordIds: ['1', '2', '3'],
excludedRecordIds: [],
});
set(
actionMenuEntriesComponentState.atomFamily({
instanceId: 'story-action-menu',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
displayedExportProgress,
download,
generateCsv,
} from '../useExportTableData';
} from '../useExportRecordData';

jest.useFakeTimers();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import { useMemo } from 'react';
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 { useProcessRecordsForCSVExport } from '@/object-record/record-index/options/hooks/useProcessRecordsForCSVExport';

import {
useTableData,
UseTableDataOptions,
} from '@/object-record/record-index/options/hooks/useTableData';
UseRecordDataOptions,
useRecordData,
} from '@/object-record/record-index/options/hooks/useRecordData';
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { RelationDefinitionType } from '~/generated-metadata/graphql';
Expand Down Expand Up @@ -134,11 +135,11 @@ const downloader = (mimeType: string, generator: GenerateExport) => {

export const csvDownloader = downloader('text/csv', generateCsv);

type UseExportTableDataOptions = Omit<UseTableDataOptions, 'callback'> & {
type UseExportTableDataOptions = Omit<UseRecordDataOptions, 'callback'> & {
filename: string;
};

export const useExportTableData = ({
export const useExportRecordData = ({
delayMs,
filename,
maximumRequests = 100,
Expand All @@ -160,7 +161,7 @@ export const useExportTableData = ({
[filename, processRecordsForCSVExport],
);

const { getTableData: download, progress } = useTableData({
const { getTableData: download, progress } = useRecordData({
delayMs,
maximumRequests,
objectNameSingular,
Expand Down
Loading
Loading