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

Refactor action menu #7586

Merged
merged 11 commits into from
Oct 11, 2024
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
import { contextStoreCurrentObjectMetadataIdState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdState';
import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState';
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 { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { useCallback, useEffect, useState } from 'react';
import { useRecoilValue } from 'recoil';
import { IconTrash } from 'twenty-ui';

export const DeleteRecordsActionEffect = ({
position,
}: {
position: number;
}) => {
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();

const contextStoreTargetedRecordIds = useRecoilValue(
contextStoreTargetedRecordIdsState,
);

const contextStoreCurrentObjectMetadataId = useRecoilValue(
contextStoreCurrentObjectMetadataIdState,
);

const { objectMetadataItem } = useObjectMetadataItemById({
objectId: contextStoreCurrentObjectMetadataId,
});

const [isDeleteRecordsModalOpen, setIsDeleteRecordsModalOpen] =
useState(false);

const { deleteTableData } = useDeleteTableData({
objectNameSingular: objectMetadataItem?.nameSingular ?? '',
recordIndexId: objectMetadataItem?.namePlural ?? '',
bosiraphael marked this conversation as resolved.
Show resolved Hide resolved
});

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

const isRemoteObject = objectMetadataItem?.isRemote ?? false;

const numberOfSelectedRecords = contextStoreTargetedRecordIds.length;

const canDelete =
!isRemoteObject && numberOfSelectedRecords < DELETE_MAX_COUNT;

useEffect(() => {
if (canDelete) {
addActionMenuEntry({
key: 'delete',
label: 'Delete',
position,
Icon: IconTrash,
accent: 'danger',
onClick: () => {
setIsDeleteRecordsModalOpen(true);
},
ConfirmationModal: (
<ConfirmationModal
isOpen={isDeleteRecordsModalOpen}
setIsOpen={setIsDeleteRecordsModalOpen}
title={`Delete ${numberOfSelectedRecords} ${
numberOfSelectedRecords === 1 ? `record` : 'records'
}`}
subtitle={`Are you sure you want to delete ${
numberOfSelectedRecords === 1 ? 'this record' : 'these records'
}? ${
numberOfSelectedRecords === 1 ? 'It' : 'They'
} can be recovered from the Options menu.`}
onConfirmClick={() => handleDeleteClick()}
deleteButtonText={`Delete ${
numberOfSelectedRecords > 1 ? 'Records' : 'Record'
}`}
/>
),
});
} else {
removeActionMenuEntry('delete');
}
}, [
canDelete,
addActionMenuEntry,
removeActionMenuEntry,
isDeleteRecordsModalOpen,
numberOfSelectedRecords,
handleDeleteClick,
position,
]);

return null;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
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';
import { useEffect } from 'react';
import { useRecoilValue } from 'recoil';
import { IconFileExport } from 'twenty-ui';

export const ExportRecordsActionEffect = ({
position,
}: {
position: number;
}) => {
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();

const contextStoreCurrentObjectMetadataId = useRecoilValue(
contextStoreCurrentObjectMetadataIdState,
);

const { objectMetadataItem } = useObjectMetadataItemById({
objectId: contextStoreCurrentObjectMetadataId,
});

const baseTableDataParams = {
delayMs: 100,
objectNameSingular: objectMetadataItem?.nameSingular ?? '',
recordIndexId: objectMetadataItem?.namePlural ?? '',
};
bosiraphael marked this conversation as resolved.
Show resolved Hide resolved

const { progress, download } = useExportTableData({
...baseTableDataParams,
filename: `${objectMetadataItem?.nameSingular}.csv`,
bosiraphael marked this conversation as resolved.
Show resolved Hide resolved
});

useEffect(() => {
addActionMenuEntry({
key: 'export',
position,
label: displayedExportProgress(progress),
Icon: IconFileExport,
accent: 'default',
onClick: () => download(),
});

return () => {
removeActionMenuEntry('export');
};
}, [download, progress, addActionMenuEntry, removeActionMenuEntry, position]);
bosiraphael marked this conversation as resolved.
Show resolved Hide resolved
return null;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
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 { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { useEffect } from 'react';
import { useRecoilValue } from 'recoil';
import { IconHeart, IconHeartOff, isDefined } from 'twenty-ui';

export const ManageFavoritesActionEffect = ({
position,
}: {
position: number;
}) => {
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();

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

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

const selectedRecordId = contextStoreTargetedRecordIds[0];
bosiraphael marked this conversation as resolved.
Show resolved Hide resolved

const selectedRecord = useRecoilValue(
recordStoreFamilyState(selectedRecordId),
);

const { objectMetadataItem } = useObjectMetadataItemById({
objectId: contextStoreCurrentObjectMetadataId,
});

const foundFavorite = favorites?.find(
(favorite) => favorite.recordId === selectedRecordId,
);

const isFavorite = !!selectedRecordId && !!foundFavorite;
bosiraphael marked this conversation as resolved.
Show resolved Hide resolved

useEffect(() => {
if (!isDefined(objectMetadataItem) || objectMetadataItem.isRemote) {
return;
}

addActionMenuEntry({
key: 'manage-favorites',
label: isFavorite ? 'Remove from favorites' : 'Add to favorites',
position,
Icon: isFavorite ? IconHeartOff : IconHeart,
onClick: () => {
if (isFavorite && isDefined(foundFavorite?.id)) {
deleteFavorite(foundFavorite.id);
} else if (isDefined(selectedRecord)) {
createFavorite(selectedRecord, objectMetadataItem.nameSingular);
}
},
});

return () => {
removeActionMenuEntry('manage-favorites');
};
}, [
addActionMenuEntry,
createFavorite,
deleteFavorite,
foundFavorite?.id,
isFavorite,
objectMetadataItem,
position,
removeActionMenuEntry,
selectedRecord,
]);
bosiraphael marked this conversation as resolved.
Show resolved Hide resolved

return null;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { DeleteRecordsActionEffect } from '@/action-menu/actions/record-actions/components/DeleteRecordsActionEffect';
import { ExportRecordsActionEffect } from '@/action-menu/actions/record-actions/components/ExportRecordsActionEffect';

const actionEffects = [ExportRecordsActionEffect, DeleteRecordsActionEffect];

export const MultipleRecordsActionMenuEntriesSetter = () => {
return (
<>
{actionEffects.map((ActionEffect, index) => (
<ActionEffect key={index} position={index} />
bosiraphael marked this conversation as resolved.
Show resolved Hide resolved
))}
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
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';

export const RecordActionMenuEntriesSetter = () => {
const contextStoreTargetedRecordIds = useRecoilValue(
contextStoreTargetedRecordIdsState,
);

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

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

return <MultipleRecordsActionMenuEntriesSetter />;
};
bosiraphael marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { DeleteRecordsActionEffect } from '@/action-menu/actions/record-actions/components/DeleteRecordsActionEffect';
import { ExportRecordsActionEffect } from '@/action-menu/actions/record-actions/components/ExportRecordsActionEffect';
import { ManageFavoritesActionEffect } from '@/action-menu/actions/record-actions/components/ManageFavoritesActionEffect';

export const SingleRecordActionMenuEntriesSetter = () => {
const actionEffects = [
ExportRecordsActionEffect,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice!

DeleteRecordsActionEffect,
ManageFavoritesActionEffect,
];
return (
<>
{actionEffects.map((ActionEffect, index) => (
<ActionEffect key={index} position={index} />
))}
bosiraphael marked this conversation as resolved.
Show resolved Hide resolved
</>
);
};
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import styled from '@emotion/styled';

import { ActionMenuBarEntry } from '@/action-menu/components/ActionMenuBarEntry';
import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState';
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';
Expand All @@ -28,9 +28,13 @@ export const ActionMenuBar = () => {
);

const actionMenuEntries = useRecoilComponentValueV2(
actionMenuEntriesComponentState,
actionMenuEntriesComponentSelector,
);

if (actionMenuEntries.length === 0) {
return null;
}

return (
<BottomBar
bottomBarId={`action-bar-${actionMenuId}`}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState';
import { actionMenuEntriesComponentSelector } from '@/action-menu/states/actionMenuEntriesComponentSelector';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';

export const ActionMenuConfirmationModals = () => {
const actionMenuEntries = useRecoilComponentValueV2(
actionMenuEntriesComponentState,
actionMenuEntriesComponentSelector,
);

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useRecoilValue } from 'recoil';
import { PositionType } from '../types/PositionType';

import { actionMenuDropdownPositionComponentState } from '@/action-menu/states/actionMenuDropdownPositionComponentState';
import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState';
import { actionMenuEntriesComponentSelector } from '@/action-menu/states/actionMenuEntriesComponentSelector';
bosiraphael marked this conversation as resolved.
Show resolved Hide resolved
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { ActionMenuDropdownHotkeyScope } from '@/action-menu/types/ActionMenuDropdownHotKeyScope';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
Expand Down Expand Up @@ -36,7 +36,7 @@ const StyledContainerActionMenuDropdown = styled.div<StyledContainerProps>`

export const ActionMenuDropdown = () => {
const actionMenuEntries = useRecoilComponentValueV2(
actionMenuEntriesComponentState,
actionMenuEntriesComponentSelector,
);

const actionMenuId = useAvailableComponentInstanceIdOrThrow(
Expand All @@ -50,6 +50,10 @@ export const ActionMenuDropdown = () => {
),
);

if (actionMenuEntries.length === 0) {
return null;
}

//TODO: remove this
const width = actionMenuEntries.some(
(actionMenuEntry) => actionMenuEntry.label === 'Remove from favorites',
Expand Down

This file was deleted.

This file was deleted.

Loading
Loading