Add board Action bar and context menu (#3680)

* Add board Action bar and context menu

* Fix according to review
This commit is contained in:
Charles Bochet
2024-01-30 09:21:02 +01:00
committed by GitHub
parent c5ea2dfe1e
commit e951fb70f8
27 changed files with 404 additions and 341 deletions

View File

@ -1,226 +0,0 @@
import { useCallback } from 'react';
import { isNonEmptyString } from '@sniptt/guards';
import { useRecoilCallback, useRecoilValue, useSetRecoilState } from 'recoil';
import { useOpenCreateActivityDrawerForSelectedRowIds } from '@/activities/hooks/useOpenCreateActivityDrawerForSelectedRowIds';
import { useFavorites } from '@/favorites/hooks/useFavorites';
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords';
import { useExecuteQuickActionOnOneRecord } from '@/object-record/hooks/useExecuteQuickActionOnOneRecord';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import {
IconCheckbox,
IconClick,
IconHeart,
IconHeartOff,
IconMail,
IconNotes,
IconPuzzle,
IconTrash,
} from '@/ui/display/icon';
import { actionBarEntriesState } from '@/ui/navigation/action-bar/states/actionBarEntriesState';
import { contextMenuEntriesState } from '@/ui/navigation/context-menu/states/contextMenuEntriesState';
import { ContextMenuEntry } from '@/ui/navigation/context-menu/types/ContextMenuEntry';
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
type useRecordTableContextMenuEntriesProps = {
objectNamePlural: string;
recordTableId: string;
};
// TODO: refactor this
export const useRecordTableContextMenuEntries = (
props: useRecordTableContextMenuEntriesProps,
) => {
const setContextMenuEntries = useSetRecoilState(contextMenuEntriesState);
const setActionBarEntriesState = useSetRecoilState(actionBarEntriesState);
const { getSelectedRowIdsSelector } = useRecordTableStates(
props?.recordTableId,
);
const selectedRowIds = useRecoilValue(getSelectedRowIdsSelector());
const { resetTableRowSelection } = useRecordTable({
recordTableId: props?.recordTableId,
});
const { objectNameSingular } = useObjectNameSingularFromPlural({
objectNamePlural: props.objectNamePlural,
});
const { createFavorite, favorites, deleteFavorite } = useFavorites();
const handleFavoriteButtonClick = useRecoilCallback(({ snapshot }) => () => {
const selectedRowIds = getSnapshotValue(
snapshot,
getSelectedRowIdsSelector(),
);
const selectedRowId = selectedRowIds.length === 1 ? selectedRowIds[0] : '';
const selectedRecord = snapshot
.getLoadable(recordStoreFamilyState(selectedRowId))
.getValue();
const foundFavorite = favorites?.find(
(favorite) => favorite.recordId === selectedRowId,
);
const isFavorite = !!selectedRowId && !!foundFavorite;
resetTableRowSelection();
if (isFavorite) {
deleteFavorite(foundFavorite.id);
} else if (selectedRecord) {
createFavorite(selectedRecord, objectNameSingular);
}
});
const { deleteManyRecords } = useDeleteManyRecords({
objectNameSingular,
});
const { executeQuickActionOnOneRecord } = useExecuteQuickActionOnOneRecord({
objectNameSingular,
});
const handleDeleteClick = useRecoilCallback(
({ snapshot }) =>
async () => {
const rowIdsToDelete = getSnapshotValue(
snapshot,
getSelectedRowIdsSelector(),
);
resetTableRowSelection();
await deleteManyRecords(rowIdsToDelete);
},
[deleteManyRecords, resetTableRowSelection, getSelectedRowIdsSelector],
);
const handleExecuteQuickActionOnClick = useRecoilCallback(
({ snapshot }) =>
async () => {
const rowIdsToExecuteQuickActionOn = getSnapshotValue(
snapshot,
getSelectedRowIdsSelector(),
);
resetTableRowSelection();
await Promise.all(
rowIdsToExecuteQuickActionOn.map(async (rowId) => {
await executeQuickActionOnOneRecord(rowId);
}),
);
},
[
executeQuickActionOnOneRecord,
resetTableRowSelection,
getSelectedRowIdsSelector,
],
);
const dataExecuteQuickActionOnmentEnabled = useIsFeatureEnabled(
'IS_QUICK_ACTIONS_ENABLED',
);
const openCreateActivityDrawer = useOpenCreateActivityDrawerForSelectedRowIds(
props.recordTableId,
);
return {
setContextMenuEntries: useCallback(() => {
const selectedRowId =
selectedRowIds.length === 1 ? selectedRowIds[0] : '';
const isFavorite =
isNonEmptyString(selectedRowId) &&
!!favorites?.find((favorite) => favorite.recordId === selectedRowId);
const contextMenuEntries = [
// {
// label: 'New task',
// Icon: IconCheckbox,
// onClick: () => {},
// },
// {
// label: 'New note',
// Icon: IconNotes,
// onClick: () => {},
// },
{
label: 'Delete',
Icon: IconTrash,
accent: 'danger',
onClick: () => handleDeleteClick(),
},
] as ContextMenuEntry[];
if (selectedRowIds.length === 1) {
contextMenuEntries.unshift({
label: isFavorite ? 'Remove from favorites' : 'Add to favorites',
Icon: isFavorite ? IconHeartOff : IconHeart,
onClick: () => handleFavoriteButtonClick(),
});
}
setContextMenuEntries(contextMenuEntries);
}, [
selectedRowIds,
favorites,
handleDeleteClick,
handleFavoriteButtonClick,
setContextMenuEntries,
]),
setActionBarEntries: useRecoilCallback(() => () => {
setActionBarEntriesState([
{
label: 'Task',
Icon: IconCheckbox,
onClick: () => {
openCreateActivityDrawer('Task', objectNameSingular);
},
},
{
label: 'Note',
Icon: IconNotes,
onClick: () => {
openCreateActivityDrawer('Note', objectNameSingular);
},
},
...(dataExecuteQuickActionOnmentEnabled
? [
{
label: 'Actions',
Icon: IconClick,
subActions: [
{
label: 'Enrich',
Icon: IconPuzzle,
onClick: () => handleExecuteQuickActionOnClick(),
},
{
label: 'Send to mailjet',
Icon: IconMail,
},
],
},
]
: []),
{
label: 'Delete',
Icon: IconTrash,
accent: 'danger',
onClick: () => handleDeleteClick(),
},
]);
}),
};
};

View File

@ -0,0 +1,168 @@
import { useCallback, useMemo } from 'react';
import { isNonEmptyString } from '@sniptt/guards';
import { useRecoilCallback, useSetRecoilState } from 'recoil';
import { useFavorites } from '@/favorites/hooks/useFavorites';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords';
import { useExecuteQuickActionOnOneRecord } from '@/object-record/hooks/useExecuteQuickActionOnOneRecord';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import {
IconClick,
IconHeart,
IconHeartOff,
IconMail,
IconPuzzle,
IconTrash,
} from '@/ui/display/icon';
import { actionBarEntriesState } from '@/ui/navigation/action-bar/states/actionBarEntriesState';
import { contextMenuEntriesState } from '@/ui/navigation/context-menu/states/contextMenuEntriesState';
import { ContextMenuEntry } from '@/ui/navigation/context-menu/types/ContextMenuEntry';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
type useRecordActionBarProps = {
objectMetadataItem: ObjectMetadataItem;
selectedRecordIds: string[];
callback?: () => void;
};
export const useRecordActionBar = ({
objectMetadataItem,
selectedRecordIds,
callback,
}: useRecordActionBarProps) => {
const setContextMenuEntries = useSetRecoilState(contextMenuEntriesState);
const setActionBarEntriesState = useSetRecoilState(actionBarEntriesState);
const { createFavorite, favorites, deleteFavorite } = useFavorites();
const { deleteManyRecords } = useDeleteManyRecords({
objectNameSingular: objectMetadataItem.nameSingular,
});
const { executeQuickActionOnOneRecord } = useExecuteQuickActionOnOneRecord({
objectNameSingular: objectMetadataItem.nameSingular,
});
const handleFavoriteButtonClick = useRecoilCallback(({ snapshot }) => () => {
if (selectedRecordIds.length > 1) {
return;
}
const selectedRecordId = selectedRecordIds[0];
const selectedRecord = snapshot
.getLoadable(recordStoreFamilyState(selectedRecordId))
.getValue();
const foundFavorite = favorites?.find(
(favorite) => favorite.recordId === selectedRecordId,
);
const isFavorite = !!selectedRecordId && !!foundFavorite;
if (isFavorite) {
deleteFavorite(foundFavorite.id);
} else if (selectedRecord) {
createFavorite(selectedRecord, objectMetadataItem.nameSingular);
}
callback?.();
});
const handleDeleteClick = useCallback(async () => {
callback?.();
await deleteManyRecords(selectedRecordIds);
}, [callback, deleteManyRecords, selectedRecordIds]);
const handleExecuteQuickActionOnClick = useCallback(async () => {
callback?.();
await Promise.all(
selectedRecordIds.map(async (recordId) => {
await executeQuickActionOnOneRecord(recordId);
}),
);
}, [callback, executeQuickActionOnOneRecord, selectedRecordIds]);
const baseActions: ContextMenuEntry[] = useMemo(
() => [
{
label: 'Delete',
Icon: IconTrash,
accent: 'danger',
onClick: () => handleDeleteClick(),
},
],
[handleDeleteClick],
);
const dataExecuteQuickActionOnmentEnabled = useIsFeatureEnabled(
'IS_QUICK_ACTIONS_ENABLED',
);
const hasOnlyOneRecordSelected = selectedRecordIds.length === 1;
const isFavorite =
isNonEmptyString(selectedRecordIds[0]) &&
!!favorites?.find((favorite) => favorite.recordId === selectedRecordIds[0]);
return {
setContextMenuEntries: useCallback(() => {
setContextMenuEntries([
...baseActions,
...(isFavorite && hasOnlyOneRecordSelected
? [
{
label: 'Remove from favorites',
Icon: IconHeartOff,
onClick: handleFavoriteButtonClick,
},
]
: []),
...(!isFavorite && hasOnlyOneRecordSelected
? [
{
label: 'Add to favorites',
Icon: IconHeart,
onClick: handleFavoriteButtonClick,
},
]
: []),
]);
}, [
baseActions,
handleFavoriteButtonClick,
hasOnlyOneRecordSelected,
isFavorite,
setContextMenuEntries,
]),
setActionBarEntries: useCallback(() => {
setActionBarEntriesState([
...(dataExecuteQuickActionOnmentEnabled
? [
{
label: 'Actions',
Icon: IconClick,
subActions: [
{
label: 'Enrich',
Icon: IconPuzzle,
onClick: handleExecuteQuickActionOnClick,
},
{
label: 'Send to mailjet',
Icon: IconMail,
},
],
},
]
: []),
...baseActions,
]);
}, [
baseActions,
dataExecuteQuickActionOnmentEnabled,
handleExecuteQuickActionOnClick,
setActionBarEntriesState,
]),
};
};

View File

@ -7,5 +7,10 @@ import { ActionBar } from '@/ui/navigation/action-bar/components/ActionBar';
export const RecordBoardDeprecatedActionBar = () => {
const { selectedCardIdsSelector } = useRecordBoardDeprecatedScopedStates();
const selectedCardIds = useRecoilValue(selectedCardIdsSelector);
return <ActionBar selectedIds={selectedCardIds}></ActionBar>;
if (!selectedCardIds.length) {
return null;
}
return <ActionBar />;
};

View File

@ -6,5 +6,9 @@ import { ContextMenu } from '@/ui/navigation/context-menu/components/ContextMenu
export const RecordBoardDeprecatedContextMenu = () => {
const { selectedCardIdsSelector } = useRecordBoardDeprecatedScopedStates();
const selectedCardIds = useRecoilValue(selectedCardIdsSelector);
return <ContextMenu selectedIds={selectedCardIds}></ContextMenu>;
if (!selectedCardIds.length) {
return null;
}
return <ContextMenu />;
};

View File

@ -0,0 +1,22 @@
import { useRecoilValue } from 'recoil';
import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates';
import { ActionBar } from '@/ui/navigation/action-bar/components/ActionBar';
type RecordBoardActionBarProps = {
recordBoardId: string;
};
export const RecordBoardActionBar = ({
recordBoardId,
}: RecordBoardActionBarProps) => {
const { getSelectedRecordIdsSelector } = useRecordBoardStates(recordBoardId);
const selectedRecordIds = useRecoilValue(getSelectedRecordIdsSelector());
if (!selectedRecordIds.length) {
return null;
}
return <ActionBar />;
};

View File

@ -0,0 +1,22 @@
import { useRecoilValue } from 'recoil';
import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates';
import { ContextMenu } from '@/ui/navigation/context-menu/components/ContextMenu';
type RecordBoardContextMenuProps = {
recordBoardId: string;
};
export const RecordBoardContextMenu = ({
recordBoardId,
}: RecordBoardContextMenuProps) => {
const { getSelectedRecordIdsSelector } = useRecordBoardStates(recordBoardId);
const selectedRecordIds = useRecoilValue(getSelectedRecordIdsSelector());
if (!selectedRecordIds.length) {
return null;
}
return <ContextMenu />;
};

View File

@ -10,6 +10,7 @@ import { recordBoardObjectSingularNameStateScopeMap } from '@/object-record/reco
import { recordBoardRecordIdsByColumnIdFamilyStateScopeMap } from '@/object-record/record-board/states/recordBoardRecordIdsByColumnIdFamilyStateScopeMap';
import { recordBoardSortsStateScopeMap } from '@/object-record/record-board/states/recordBoardSortsStateScopeMap';
import { recordBoardColumnsFamilySelectorScopeMap } from '@/object-record/record-board/states/selectors/recordBoardColumnsFamilySelectorScopeMap';
import { recordBoardSelectedRecordIdsSelectorScopeMap } from '@/object-record/record-board/states/selectors/recordBoardSelectedRecordIdsSelectorScopeMap';
import { recordBoardVisibleFieldDefinitionsScopedSelector } from '@/object-record/record-board/states/selectors/recordBoardVisibleFieldDefinitionsScopedSelector';
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
import { getFamilyState } from '@/ui/utilities/recoil-scope/utils/getFamilyState';
@ -54,7 +55,7 @@ export const useRecordBoardStates = (recordBoardId?: string) => {
scopeId,
),
recordBoardRecordIdsByColumnIdFamilyState: getFamilyState(
recordIdsByColumnIdFamilyState: getFamilyState(
recordBoardRecordIdsByColumnIdFamilyStateScopeMap,
scopeId,
),
@ -62,6 +63,10 @@ export const useRecordBoardStates = (recordBoardId?: string) => {
isRecordBoardCardSelectedFamilyStateScopeMap,
scopeId,
),
getSelectedRecordIdsSelector: getSelectorReadOnly(
recordBoardSelectedRecordIdsSelectorScopeMap,
scopeId,
),
getIsCompactModeActiveState: getState(
isRecordBoardCompactModeActiveStateScopeMap,

View File

@ -7,7 +7,7 @@ import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
export const useSetRecordBoardRecordIds = (recordBoardId?: string) => {
const {
scopeId,
recordBoardRecordIdsByColumnIdFamilyState,
recordIdsByColumnIdFamilyState,
columnsFamilySelector,
getColumnIdsState,
} = useRecordBoardStates(recordBoardId);
@ -23,7 +23,7 @@ export const useSetRecordBoardRecordIds = (recordBoardId?: string) => {
.getValue();
const existingColumnRecordIds = snapshot
.getLoadable(recordBoardRecordIdsByColumnIdFamilyState(columnId))
.getLoadable(recordIdsByColumnIdFamilyState(columnId))
.getValue();
const columnRecordIds = records
@ -31,18 +31,11 @@ export const useSetRecordBoardRecordIds = (recordBoardId?: string) => {
.map((record) => record.id);
if (!isDeeplyEqual(existingColumnRecordIds, columnRecordIds)) {
set(
recordBoardRecordIdsByColumnIdFamilyState(columnId),
columnRecordIds,
);
set(recordIdsByColumnIdFamilyState(columnId), columnRecordIds);
}
});
},
[
columnsFamilySelector,
getColumnIdsState,
recordBoardRecordIdsByColumnIdFamilyState,
],
[columnsFamilySelector, getColumnIdsState, recordIdsByColumnIdFamilyState],
);
return {

View File

@ -5,8 +5,12 @@ import { useSetRecordBoardColumns } from '@/object-record/record-board/hooks/int
import { useSetRecordBoardRecordIds } from '@/object-record/record-board/hooks/internal/useSetRecordBoardRecordIds';
export const useRecordBoard = (recordBoardId?: string) => {
const { scopeId, getFieldDefinitionsState, getObjectSingularNameState } =
useRecordBoardStates(recordBoardId);
const {
scopeId,
getFieldDefinitionsState,
getObjectSingularNameState,
getSelectedRecordIdsSelector,
} = useRecordBoardStates(recordBoardId);
const { setColumns } = useSetRecordBoardColumns(recordBoardId);
const { setRecordIds } = useSetRecordBoardRecordIds(recordBoardId);
@ -19,5 +23,6 @@ export const useRecordBoard = (recordBoardId?: string) => {
setRecordIds,
setFieldDefinitions,
setObjectSingularName,
getSelectedRecordIdsSelector,
};
};

View File

@ -0,0 +1,24 @@
import { useRecoilCallback } from 'recoil';
import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates';
export const useResetBoardRecordSelection = (recordBoardId?: string) => {
const { getSelectedRecordIdsSelector, isRecordBoardCardSelectedFamilyState } =
useRecordBoardStates(recordBoardId);
const resetRecordSelection = useRecoilCallback(
({ snapshot, set }) =>
() => {
const recordIds = snapshot
.getLoadable(getSelectedRecordIdsSelector())
.getValue();
for (const recordId of recordIds) {
set(isRecordBoardCardSelectedFamilyState(recordId), false);
}
},
[getSelectedRecordIdsSelector, isRecordBoardCardSelectedFamilyState],
);
return { resetRecordSelection };
};

View File

@ -32,7 +32,7 @@ export const RecordBoardColumn = ({
isFirstColumnFamilyState,
isLastColumnFamilyState,
columnsFamilySelector,
recordBoardRecordIdsByColumnIdFamilyState,
recordIdsByColumnIdFamilyState,
} = useRecordBoardStates();
const columnDefinition = useRecoilValue(
columnsFamilySelector(recordBoardColumnId),
@ -47,7 +47,7 @@ export const RecordBoardColumn = ({
);
const recordIds = useRecoilValue(
recordBoardRecordIdsByColumnIdFamilyState(recordBoardColumnId),
recordIdsByColumnIdFamilyState(recordBoardColumnId),
);
if (!columnDefinition) {

View File

@ -14,7 +14,6 @@ const StyledHeader = styled.div`
cursor: pointer;
display: flex;
flex-direction: row;
height: 24px;
justify-content: left;
margin-bottom: ${({ theme }) => theme.spacing(2)};
width: 100%;
@ -31,7 +30,7 @@ const StyledNumChildren = styled.div`
border-radius: ${({ theme }) => theme.border.radius.rounded};
color: ${({ theme }) => theme.font.color.tertiary};
display: flex;
height: 20px;
height: 24px;
justify-content: center;
line-height: ${({ theme }) => theme.text.lineHeight.lg};
margin-left: auto;

View File

@ -0,0 +1,35 @@
import { isRecordBoardCardSelectedFamilyStateScopeMap } from '@/object-record/record-board/states/isRecordBoardCardSelectedFamilyStateScopeMap';
import { recordBoardColumnIdsStateScopeMap } from '@/object-record/record-board/states/recordBoardColumnIdsStateScopeMap';
import { recordBoardRecordIdsByColumnIdFamilyStateScopeMap } from '@/object-record/record-board/states/recordBoardRecordIdsByColumnIdFamilyStateScopeMap';
import { createSelectorReadOnlyScopeMap } from '@/ui/utilities/recoil-scope/utils/createSelectorReadOnlyScopeMap';
export const recordBoardSelectedRecordIdsSelectorScopeMap =
createSelectorReadOnlyScopeMap<string[]>({
key: 'recordBoardSelectedRecordIdsSelectorScopeMap',
get:
({ scopeId }) =>
({ get }) => {
const columnIds = get(recordBoardColumnIdsStateScopeMap({ scopeId }));
const recordIdsByColumn = columnIds.map((columnId) =>
get(
recordBoardRecordIdsByColumnIdFamilyStateScopeMap({
scopeId,
familyKey: columnId,
}),
),
);
const recordIds = recordIdsByColumn.flat();
return recordIds.filter(
(recordId) =>
get(
isRecordBoardCardSelectedFamilyStateScopeMap({
scopeId,
familyKey: recordId,
}),
) === true,
);
},
});

View File

@ -2,7 +2,9 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { RecordBoardActionBar } from '@/object-record/record-board/action-bar/components/RecordBoardActionBar';
import { RecordBoard } from '@/object-record/record-board/components/RecordBoard';
import { RecordBoardContextMenu } from '@/object-record/record-board/context-menu/components/RecordBoardContextMenu';
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
type RecordIndexBoardContainerProps = {
@ -32,6 +34,8 @@ export const RecordIndexBoardContainer = ({
}}
>
<RecordBoard recordBoardId={recordBoardId} />
<RecordBoardActionBar recordBoardId={recordBoardId} />
<RecordBoardContextMenu recordBoardId={recordBoardId} />
</RecordBoardContext.Provider>
);
};

View File

@ -1,8 +1,11 @@
import { useCallback, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useRecordActionBar } from '@/object-record/record-action-bar/hooks/useRecordActionBar';
import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard';
import { useResetBoardRecordSelection } from '@/object-record/record-board/hooks/useResetBoardRecordSelection';
import { useLoadRecordIndexBoard } from '@/object-record/record-index/hooks/useLoadRecordIndexBoard';
import { computeRecordBoardColumnDefinitionsFromObjectMetadata } from '@/object-record/utils/computeRecordBoardColumnDefinitionsFromObjectMetadata';
@ -28,7 +31,9 @@ export const RecordIndexBoardContainerEffect = ({
navigate(`/settings/objects/${objectMetadataItem.namePlural}`);
}, [navigate, objectMetadataItem.namePlural]);
const { setColumns, setObjectSingularName } = useRecordBoard(recordBoardId);
const { setColumns, setObjectSingularName, getSelectedRecordIdsSelector } =
useRecordBoard(recordBoardId);
const { resetRecordSelection } = useResetBoardRecordSelection(recordBoardId);
useEffect(() => {
setObjectSingularName(objectNameSingular);
@ -48,5 +53,18 @@ export const RecordIndexBoardContainerEffect = ({
setColumns,
]);
const selectedRecordIds = useRecoilValue(getSelectedRecordIdsSelector());
const { setActionBarEntries, setContextMenuEntries } = useRecordActionBar({
objectMetadataItem,
selectedRecordIds,
callback: resetRecordSelection,
});
useEffect(() => {
setActionBarEntries?.();
setContextMenuEntries?.();
}, [setActionBarEntries, setContextMenuEntries]);
return <></>;
};

View File

@ -1,8 +1,9 @@
import { useEffect } from 'react';
import { useRecoilValue } from 'recoil';
import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useRecordTableContextMenuEntries } from '@/object-record/hooks/useRecordTableContextMenuEntries';
import { useRecordActionBar } from '@/object-record/record-action-bar/hooks/useRecordActionBar';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { filterAvailableTableColumns } from '@/object-record/utils/filterAvailableTableColumns';
import { useViewBar } from '@/views/hooks/useViewBar';
@ -18,7 +19,12 @@ export const RecordIndexTableContainerEffect = ({
recordTableId,
viewBarId,
}: RecordIndexTableContainerEffectProps) => {
const { setAvailableTableColumns, setOnEntityCountChange } = useRecordTable({
const {
setAvailableTableColumns,
setOnEntityCountChange,
resetTableRowSelection,
getSelectedRowIdsSelector,
} = useRecordTable({
recordTableId,
});
@ -47,11 +53,13 @@ export const RecordIndexTableContainerEffect = ({
setAvailableTableColumns,
]);
const { setActionBarEntries, setContextMenuEntries } =
useRecordTableContextMenuEntries({
objectNamePlural: objectMetadataItem.namePlural,
recordTableId,
});
const selectedRowIds = useRecoilValue(getSelectedRowIdsSelector());
const { setActionBarEntries, setContextMenuEntries } = useRecordActionBar({
objectMetadataItem,
selectedRecordIds: selectedRowIds,
callback: resetTableRowSelection,
});
useEffect(() => {
setActionBarEntries?.();

View File

@ -12,5 +12,9 @@ export const RecordTableActionBar = ({
const selectedRowIds = useRecoilValue(getSelectedRowIdsSelector());
return <ActionBar selectedIds={selectedRowIds} />;
if (!selectedRowIds.length) {
return null;
}
return <ActionBar />;
};

View File

@ -12,5 +12,9 @@ export const RecordTableContextMenu = ({
const selectedRowIds = useRecoilValue(getSelectedRowIdsSelector());
return <ContextMenu selectedIds={selectedRowIds} />;
if (!selectedRowIds.length) {
return null;
}
return <ContextMenu />;
};

View File

@ -2,7 +2,7 @@ import { useEffect } from 'react';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
import { useRecordTableContextMenuEntries } from '@/object-record/hooks/useRecordTableContextMenuEntries';
import { useRecordActionBar } from '@/object-record/record-action-bar/hooks/useRecordActionBar';
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
import { filterAvailableTableColumns } from '@/object-record/utils/filterAvailableTableColumns';
import {
@ -30,6 +30,7 @@ export const SignInBackgroundMockContainerEffect = ({
setOnEntityCountChange,
setRecordTableData,
setTableColumns,
resetTableRowSelection,
} = useRecordTable({
recordTableId,
});
@ -80,11 +81,11 @@ export const SignInBackgroundMockContainerEffect = ({
setTableColumns,
]);
const { setActionBarEntries, setContextMenuEntries } =
useRecordTableContextMenuEntries({
objectNamePlural,
recordTableId,
});
const { setActionBarEntries, setContextMenuEntries } = useRecordActionBar({
objectMetadataItem,
selectedRecordIds: [],
callback: resetTableRowSelection,
});
useEffect(() => {
setActionBarEntries?.();

View File

@ -9,10 +9,6 @@ import { actionBarOpenState } from '../states/actionBarIsOpenState';
import { ActionBarItem } from './ActionBarItem';
type ActionBarProps = {
selectedIds: string[];
};
const StyledContainerActionBar = styled.div`
align-items: center;
background: ${({ theme }) => theme.background.secondary};
@ -33,30 +29,24 @@ const StyledContainerActionBar = styled.div`
z-index: 1;
`;
export const ActionBar = ({ selectedIds }: ActionBarProps) => {
export const ActionBar = () => {
const actionBarOpen = useRecoilValue(actionBarOpenState);
const contextMenuIsOpen = useRecoilValue(contextMenuIsOpenState);
const actionBarEntries = useRecoilValue(actionBarEntriesState);
const wrapperRef = useRef<HTMLDivElement>(null);
if (selectedIds.length === 0 || !actionBarOpen || contextMenuIsOpen) {
if (!actionBarOpen || contextMenuIsOpen) {
return null;
}
return (
<StyledContainerActionBar
data-select-disable
className="action-bar"
ref={wrapperRef}
>
{actionBarEntries.map((item) => (
<ActionBarItem
Icon={item.Icon}
accent={item.accent}
label={item.label}
onClick={item.onClick}
key={item.label}
subActions={item?.subActions}
/>
{actionBarEntries.map((item, index) => (
<ActionBarItem key={index} item={item} />
))}
</StyledContainerActionBar>
);

View File

@ -3,22 +3,17 @@ import styled from '@emotion/styled';
import { MenuItem } from 'tsup.ui.index';
import { IconChevronDown } from '@/ui/display/icon';
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { ActionBarItemAccent } from '../types/ActionBarItemAccent';
import { ActionBarEntry } from '@/ui/navigation/action-bar/types/ActionBarEntry';
import { MenuItemAccent } from '@/ui/navigation/menu-item/types/MenuItemAccent';
type ActionBarItemProps = {
Icon: IconComponent;
label: string;
accent?: ActionBarItemAccent;
onClick?: () => void;
subActions?: ActionBarItemProps[];
item: ActionBarEntry;
};
const StyledButton = styled.div<{ accent: ActionBarItemAccent }>`
const StyledButton = styled.div<{ accent: MenuItemAccent }>`
border-radius: ${({ theme }) => theme.border.radius.sm};
color: ${(props) =>
props.accent === 'danger'
@ -45,19 +40,13 @@ const StyledButtonLabel = styled.div`
margin-left: ${({ theme }) => theme.spacing(1)};
`;
export const ActionBarItem = ({
label,
Icon,
accent = 'standard',
onClick,
subActions,
}: ActionBarItemProps) => {
export const ActionBarItem = ({ item }: ActionBarItemProps) => {
const theme = useTheme();
const dropdownId = `action-bar-item-${label}`;
const dropdownId = `action-bar-item-${item.label}`;
const { toggleDropdown, closeDropdown } = useDropdown(dropdownId);
return (
<>
{Array.isArray(subActions) ? (
{Array.isArray(item.subActions) ? (
<Dropdown
dropdownId={dropdownId}
dropdownPlacement="top-start"
@ -65,15 +54,18 @@ export const ActionBarItem = ({
scope: dropdownId,
}}
clickableComponent={
<StyledButton accent={accent} onClick={toggleDropdown}>
{Icon && <Icon size={theme.icon.size.md} />}
<StyledButtonLabel>{label}</StyledButtonLabel>
<StyledButton
accent={item.accent ?? 'default'}
onClick={toggleDropdown}
>
{item.Icon && <item.Icon size={theme.icon.size.md} />}
<StyledButtonLabel>{item.label}</StyledButtonLabel>
<IconChevronDown size={theme.icon.size.md} />
</StyledButton>
}
dropdownComponents={
<DropdownMenuItemsContainer>
{subActions.map((subAction) => (
{item.subActions.map((subAction) => (
<MenuItem
key={subAction.label}
text={subAction.label}
@ -88,9 +80,12 @@ export const ActionBarItem = ({
}
/>
) : (
<StyledButton accent={accent} onClick={onClick}>
{Icon && <Icon size={theme.icon.size.md} />}
<StyledButtonLabel>{label}</StyledButtonLabel>
<StyledButton
accent={item.accent ?? 'default'}
onClick={() => item.onClick?.()}
>
{item.Icon && <item.Icon size={theme.icon.size.md} />}
<StyledButtonLabel>{item.label}</StyledButtonLabel>
</StyledButton>
)}
</>

View File

@ -8,10 +8,10 @@ import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { actionBarOpenState } from '../../states/actionBarIsOpenState';
import { ActionBar } from '../ActionBar';
const FilledActionBar = (props: { selectedIds: string[] }) => {
const FilledActionBar = () => {
const setActionBarOpenState = useSetRecoilState(actionBarOpenState);
setActionBarOpenState(true);
return <ActionBar selectedIds={props.selectedIds} />;
return <ActionBar />;
};
const meta: Meta<typeof ActionBar> = {

View File

@ -1,11 +1,10 @@
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
import { ActionBarItemAccent } from './ActionBarItemAccent';
import { MenuItemAccent } from '@/ui/navigation/menu-item/types/MenuItemAccent';
export type ActionBarEntry = {
label: string;
Icon: IconComponent;
accent?: ActionBarItemAccent;
accent?: MenuItemAccent;
onClick?: () => void;
subActions?: ActionBarEntry[];
};

View File

@ -14,10 +14,6 @@ import { PositionType } from '../types/PositionType';
import { ContextMenuItem } from './ContextMenuItem';
type ContextMenuProps = {
selectedIds: string[];
};
type StyledContainerProps = {
position: PositionType;
};
@ -41,7 +37,7 @@ const StyledContainerContextMenu = styled.div<StyledContainerProps>`
z-index: 2;
`;
export const ContextMenu = ({ selectedIds }: ContextMenuProps) => {
export const ContextMenu = () => {
const contextMenuPosition = useRecoilValue(contextMenuPositionState);
const contextMenuIsOpen = useRecoilValue(contextMenuIsOpenState);
const contextMenuEntries = useRecoilValue(contextMenuEntriesState);
@ -57,7 +53,7 @@ export const ContextMenu = ({ selectedIds }: ContextMenuProps) => {
},
});
if (selectedIds.length === 0 || !contextMenuIsOpen) {
if (!contextMenuIsOpen) {
return null;
}
@ -75,15 +71,9 @@ export const ContextMenu = ({ selectedIds }: ContextMenuProps) => {
>
<DropdownMenu data-select-disable width={width}>
<DropdownMenuItemsContainer>
{contextMenuEntries.map((item) => (
<ContextMenuItem
Icon={item.Icon}
label={item.label}
accent={item.accent}
onClick={item.onClick}
key={item.label}
/>
))}
{contextMenuEntries.map((item, index) => {
return <ContextMenuItem key={index} item={item} />;
})}
</DropdownMenuItemsContainer>
</DropdownMenu>
</StyledContainerContextMenu>

View File

@ -1,20 +1,15 @@
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
import { ContextMenuEntry } from '@/ui/navigation/context-menu/types/ContextMenuEntry';
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import { ContextMenuItemAccent } from '../types/ContextMenuItemAccent';
type ContextMenuItemProps = {
Icon: IconComponent;
label: string;
accent?: ContextMenuItemAccent;
onClick: () => void;
item: ContextMenuEntry;
};
export const ContextMenuItem = ({
label,
Icon,
accent = 'default',
onClick,
}: ContextMenuItemProps) => (
<MenuItem LeftIcon={Icon} onClick={onClick} accent={accent} text={label} />
export const ContextMenuItem = ({ item }: ContextMenuItemProps) => (
<MenuItem
LeftIcon={item.Icon}
onClick={item.onClick}
accent={item.accent}
text={item.label}
/>
);

View File

@ -9,7 +9,7 @@ import { contextMenuIsOpenState } from '../../states/contextMenuIsOpenState';
import { contextMenuPositionState } from '../../states/contextMenuPositionState';
import { ContextMenu } from '../ContextMenu';
const FilledContextMenu = (props: { selectedIds: string[] }) => {
const FilledContextMenu = () => {
const setContextMenuPosition = useSetRecoilState(contextMenuPositionState);
setContextMenuPosition({
x: 100,
@ -17,7 +17,7 @@ const FilledContextMenu = (props: { selectedIds: string[] }) => {
});
const setContextMenuOpenState = useSetRecoilState(contextMenuIsOpenState);
setContextMenuOpenState(true);
return <ContextMenu selectedIds={props.selectedIds} />;
return <ContextMenu />;
};
const meta: Meta<typeof ContextMenu> = {

View File

@ -1,10 +1,9 @@
import { IconComponent } from '@/ui/display/icon/types/IconComponent';
import { ContextMenuItemAccent } from './ContextMenuItemAccent';
import { MenuItemAccent } from '@/ui/navigation/menu-item/types/MenuItemAccent';
export type ContextMenuEntry = {
label: string;
Icon: IconComponent;
accent?: ContextMenuItemAccent;
accent?: MenuItemAccent;
onClick: () => void;
};