7339 implement contextual actions inside the commandmenu (#8000)

Closes #7339


https://github.com/user-attachments/assets/b623caa4-c1b3-448e-8880-4a8301802ba8
This commit is contained in:
Raphaël Bosi 2024-10-29 15:10:45 +01:00 committed by GitHub
parent 8bb07c4a4f
commit fe2c8bb43b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 399 additions and 237 deletions

View File

@ -1,5 +1,5 @@
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
import { ActionMenuType } from '@/action-menu/types/ActionMenuType';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters';
@ -12,17 +12,15 @@ import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTabl
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useContext, useEffect, useState } from 'react';
import { IconTrash, isDefined } from 'twenty-ui';
export const DeleteRecordsActionEffect = ({
position,
objectMetadataItem,
actionMenuType,
}: {
position: number;
objectMetadataItem: ObjectMetadataItem;
actionMenuType: ActionMenuType;
}) => {
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
@ -93,6 +91,9 @@ export const DeleteRecordsActionEffect = ({
contextStoreNumberOfSelectedRecords < DELETE_MAX_COUNT &&
contextStoreNumberOfSelectedRecords > 0;
const { isInRightDrawer, onActionExecutedCallback } =
useContext(ActionMenuContext);
useEffect(() => {
if (canDelete) {
addActionMenuEntry({
@ -120,17 +121,14 @@ export const DeleteRecordsActionEffect = ({
} can be recovered from the Options menu.`}
onConfirmClick={() => {
handleDeleteClick();
if (actionMenuType === 'recordShow') {
onActionExecutedCallback?.();
if (isInRightDrawer) {
closeRightDrawer();
}
}}
deleteButtonText={`Delete ${
contextStoreNumberOfSelectedRecords > 1 ? 'Records' : 'Record'
}`}
modalVariant={
actionMenuType === 'recordShow' ? 'tertiary' : 'primary'
}
/>
),
});
@ -142,13 +140,14 @@ export const DeleteRecordsActionEffect = ({
removeActionMenuEntry('delete');
};
}, [
actionMenuType,
addActionMenuEntry,
canDelete,
closeRightDrawer,
contextStoreNumberOfSelectedRecords,
handleDeleteClick,
isDeleteRecordsModalOpen,
isInRightDrawer,
onActionExecutedCallback,
position,
removeActionMenuEntry,
]);

View File

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

View File

@ -1,16 +1,23 @@
import { MultipleRecordsActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/MultipleRecordsActionMenuEntriesSetter';
import { SingleRecordActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/SingleRecordActionMenuEntriesSetter';
import { ActionMenuType } from '@/action-menu/types/ActionMenuType';
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';
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
export const RecordActionMenuEntriesSetter = ({
actionMenuType,
}: {
actionMenuType: ActionMenuType;
}) => {
const singleRecordActionEffects = [
ManageFavoritesActionEffect,
ExportRecordsActionEffect,
DeleteRecordsActionEffect,
];
const multipleRecordActionEffects = [
ExportRecordsActionEffect,
DeleteRecordsActionEffect,
];
export const RecordActionMenuEntriesSetter = () => {
const contextStoreNumberOfSelectedRecords = useRecoilComponentValueV2(
contextStoreNumberOfSelectedRecordsComponentState,
);
@ -33,19 +40,20 @@ export const RecordActionMenuEntriesSetter = ({
return null;
}
if (contextStoreNumberOfSelectedRecords === 1) {
return (
<SingleRecordActionMenuEntriesSetter
objectMetadataItem={objectMetadataItem}
actionMenuType={actionMenuType}
/>
);
}
const actions =
contextStoreNumberOfSelectedRecords === 1
? singleRecordActionEffects
: multipleRecordActionEffects;
return (
<MultipleRecordsActionMenuEntriesSetter
objectMetadataItem={objectMetadataItem}
actionMenuType={actionMenuType}
/>
<>
{actions.map((ActionEffect, index) => (
<ActionEffect
key={index}
position={index}
objectMetadataItem={objectMetadataItem}
/>
))}
</>
);
};

View File

@ -1,31 +0,0 @@
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';
import { ActionMenuType } from '@/action-menu/types/ActionMenuType';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
export const SingleRecordActionMenuEntriesSetter = ({
objectMetadataItem,
actionMenuType,
}: {
objectMetadataItem: ObjectMetadataItem;
actionMenuType: ActionMenuType;
}) => {
const actionEffects = [
ManageFavoritesActionEffect,
ExportRecordsActionEffect,
DeleteRecordsActionEffect,
];
return (
<>
{actionEffects.map((ActionEffect, index) => (
<ActionEffect
key={index}
position={index}
objectMetadataItem={objectMetadataItem}
actionMenuType={actionMenuType}
/>
))}
</>
);
};

View File

@ -3,16 +3,12 @@ import { ActionMenuConfirmationModals } from '@/action-menu/components/ActionMen
import { RecordIndexActionMenuBar } from '@/action-menu/components/RecordIndexActionMenuBar';
import { RecordIndexActionMenuDropdown } from '@/action-menu/components/RecordIndexActionMenuDropdown';
import { RecordIndexActionMenuEffect } from '@/action-menu/components/RecordIndexActionMenuEffect';
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
export const RecordIndexActionMenu = ({
actionMenuId,
}: {
actionMenuId: string;
}) => {
export const RecordIndexActionMenu = () => {
const contextStoreCurrentObjectMetadataId = useRecoilComponentValueV2(
contextStoreCurrentObjectMetadataIdComponentState,
);
@ -20,15 +16,18 @@ export const RecordIndexActionMenu = ({
return (
<>
{contextStoreCurrentObjectMetadataId && (
<ActionMenuComponentInstanceContext.Provider
value={{ instanceId: actionMenuId }}
<ActionMenuContext.Provider
value={{
isInRightDrawer: false,
onActionExecutedCallback: () => {},
}}
>
<RecordIndexActionMenuBar />
<RecordIndexActionMenuDropdown />
<ActionMenuConfirmationModals />
<RecordIndexActionMenuEffect />
<RecordActionMenuEntriesSetter actionMenuType="recordIndex" />
</ActionMenuComponentInstanceContext.Provider>
<RecordActionMenuEntriesSetter />
</ActionMenuContext.Provider>
)}
</>
);

View File

@ -1,7 +1,9 @@
import { useActionMenu } from '@/action-menu/hooks/useActionMenu';
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
@ -25,6 +27,9 @@ export const RecordIndexActionMenuEffect = () => {
`action-menu-dropdown-${actionMenuId}`,
),
);
const { isRightDrawerOpen } = useRightDrawer();
const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState);
useEffect(() => {
if (contextStoreNumberOfSelectedRecords > 0 && !isDropdownOpen) {
@ -43,5 +48,11 @@ export const RecordIndexActionMenuEffect = () => {
isDropdownOpen,
]);
useEffect(() => {
if (isRightDrawerOpen || isCommandMenuOpened) {
closeActionBar();
}
}, [closeActionBar, isRightDrawerOpen, isCommandMenuOpened]);
return null;
};

View File

@ -1,30 +1,53 @@
import { RecordActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter';
import { ActionMenuConfirmationModals } from '@/action-menu/components/ActionMenuConfirmationModals';
import { RecordShowActionMenuBar } from '@/action-menu/components/RecordShowActionMenuBar';
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { RecordShowPageBaseHeader } from '~/pages/object-record/RecordShowPageBaseHeader';
export const RecordShowActionMenu = ({
actionMenuId,
isFavorite,
handleFavoriteButtonClick,
record,
objectMetadataItem,
objectNameSingular,
}: {
actionMenuId: string;
isFavorite: boolean;
handleFavoriteButtonClick: () => void;
record: ObjectRecord | undefined;
objectMetadataItem: ObjectMetadataItem;
objectNameSingular: string;
}) => {
const contextStoreCurrentObjectMetadataId = useRecoilComponentValueV2(
contextStoreCurrentObjectMetadataIdComponentState,
);
// TODO: refactor RecordShowPageBaseHeader to use the context store
return (
<>
{contextStoreCurrentObjectMetadataId && (
<ActionMenuComponentInstanceContext.Provider
value={{ instanceId: actionMenuId }}
<ActionMenuContext.Provider
value={{
isInRightDrawer: false,
onActionExecutedCallback: () => {},
}}
>
<RecordShowActionMenuBar />
<RecordShowPageBaseHeader
{...{
isFavorite,
handleFavoriteButtonClick,
record,
objectMetadataItem,
objectNameSingular,
}}
/>
<ActionMenuConfirmationModals />
<RecordActionMenuEntriesSetter actionMenuType="recordShow" />
</ActionMenuComponentInstanceContext.Provider>
<RecordActionMenuEntriesSetter />
</ActionMenuContext.Provider>
)}
</>
);

View File

@ -0,0 +1,30 @@
import { RecordActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter';
import { ActionMenuConfirmationModals } from '@/action-menu/components/ActionMenuConfirmationModals';
import { RecordShowRightDrawerActionMenuBar } from '@/action-menu/components/RecordShowRightDrawerActionMenuBar';
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
export const RecordShowRightDrawerActionMenu = () => {
const contextStoreCurrentObjectMetadataId = useRecoilComponentValueV2(
contextStoreCurrentObjectMetadataIdComponentState,
);
return (
<>
{contextStoreCurrentObjectMetadataId && (
<ActionMenuContext.Provider
value={{
isInRightDrawer: true,
onActionExecutedCallback: () => {},
}}
>
<RecordShowRightDrawerActionMenuBar />
<ActionMenuConfirmationModals />
<RecordActionMenuEntriesSetter />
</ActionMenuContext.Provider>
)}
</>
);
};

View File

@ -2,7 +2,7 @@ import { RecordShowActionMenuBarEntry } from '@/action-menu/components/RecordSho
import { actionMenuEntriesComponentSelector } from '@/action-menu/states/actionMenuEntriesComponentSelector';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
export const RecordShowActionMenuBar = () => {
export const RecordShowRightDrawerActionMenuBar = () => {
const actionMenuEntries = useRecoilComponentValueV2(
actionMenuEntriesComponentSelector,
);

View File

@ -2,7 +2,7 @@ import { expect, jest } from '@storybook/jest';
import { Meta, StoryObj } from '@storybook/react';
import { RecoilRoot } from 'recoil';
import { RecordShowActionMenuBar } from '@/action-menu/components/RecordShowActionMenuBar';
import { RecordShowRightDrawerActionMenuBar } from '@/action-menu/components/RecordShowRightDrawerActionMenuBar';
import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState';
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
@ -20,9 +20,9 @@ const deleteMock = jest.fn();
const addToFavoritesMock = jest.fn();
const exportMock = jest.fn();
const meta: Meta<typeof RecordShowActionMenuBar> = {
title: 'Modules/ActionMenu/RecordShowActionMenuBar',
component: RecordShowActionMenuBar,
const meta: Meta<typeof RecordShowRightDrawerActionMenuBar> = {
title: 'Modules/ActionMenu/RecordShowRightDrawerActionMenuBar',
component: RecordShowRightDrawerActionMenuBar,
decorators: [
(Story) => (
<RecoilRoot
@ -98,7 +98,7 @@ const meta: Meta<typeof RecordShowActionMenuBar> = {
export default meta;
type Story = StoryObj<typeof RecordShowActionMenuBar>;
type Story = StoryObj<typeof RecordShowRightDrawerActionMenuBar>;
export const Default: Story = {
args: {

View File

@ -0,0 +1,11 @@
import { createContext } from 'react';
type ActionMenuContextType = {
isInRightDrawer: boolean;
onActionExecutedCallback: () => void;
};
export const ActionMenuContext = createContext<ActionMenuContextType>({
isInRightDrawer: false,
onActionExecutedCallback: () => {},
});

View File

@ -1 +0,0 @@
export type ActionMenuType = 'recordIndex' | 'recordShow';

View File

@ -10,6 +10,7 @@ import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchS
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
import { Command, CommandType } from '@/command-menu/types/Command';
import { Company } from '@/companies/types/Company';
import { mainContextStoreComponentInstanceIdState } from '@/context-store/states/mainContextStoreComponentInstanceId';
import { useKeyboardShortcutMenu } from '@/keyboard-shortcut-menu/hooks/useKeyboardShortcutMenu';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { getCompanyDomainName } from '@/object-metadata/utils/getCompanyDomainName';
@ -287,6 +288,14 @@ export const CommandMenu = () => {
: true) && cmd.type === CommandType.Create,
);
const matchingActionCommands = commandMenuCommands.filter(
(cmd) =>
(deferredCommandMenuSearch.length > 0
? checkInShortcuts(cmd, deferredCommandMenuSearch) ||
checkInLabels(cmd, deferredCommandMenuSearch)
: true) && cmd.type === CommandType.Action,
);
useListenClickOutside({
refs: [commandMenuRef],
callback: closeCommandMenu,
@ -312,6 +321,7 @@ export const CommandMenu = () => {
const selectableItemIds = copilotCommands
.map((cmd) => cmd.id)
.concat(matchingActionCommands.map((cmd) => cmd.id))
.concat(matchingCreateCommand.map((cmd) => cmd.id))
.concat(matchingNavigateCommand.map((cmd) => cmd.id))
.concat(people?.map((person) => person.id))
@ -320,22 +330,28 @@ export const CommandMenu = () => {
.concat(notes?.map((note) => note.id));
const isNoResults =
!matchingActionCommands.length &&
!matchingCreateCommand.length &&
!matchingNavigateCommand.length &&
!people?.length &&
!companies?.length &&
!notes?.length &&
!opportunities?.length;
const isLoading =
isPeopleLoading ||
isNotesLoading ||
isOpportunitiesLoading ||
isCompaniesLoading;
const mainContextStoreComponentInstanceId = useRecoilValue(
mainContextStoreComponentInstanceIdState,
);
return (
<>
{isCommandMenuOpened && (
<StyledCommandMenu ref={commandMenuRef}>
<StyledCommandMenu ref={commandMenuRef} className="command-menu">
<StyledInputContainer>
<StyledInput
autoFocus
@ -393,6 +409,23 @@ export const CommandMenu = () => {
</SelectableItem>
</CommandGroup>
)}
{mainContextStoreComponentInstanceId && (
<CommandGroup heading="Actions">
{matchingActionCommands?.map((actionCommand) => (
<SelectableItem
itemId={actionCommand.id}
key={actionCommand.id}
>
<CommandMenuItem
id={actionCommand.id}
label={actionCommand.label}
Icon={actionCommand.Icon}
onClick={actionCommand.onCommandClick}
/>
</SelectableItem>
))}
</CommandGroup>
)}
<CommandGroup heading="Create">
{matchingCreateCommand.map((cmd) => (
<SelectableItem itemId={cmd.id} key={cmd.id}>

View File

@ -1,7 +1,7 @@
import { isNonEmptyString } from '@sniptt/guards';
import { useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { useRecoilCallback, useSetRecoilState } from 'recoil';
import { useRecoilCallback, useRecoilValue, useSetRecoilState } from 'recoil';
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
@ -9,7 +9,9 @@ import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousH
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
import { isDefined } from '~/utils/isDefined';
import { actionMenuEntriesComponentSelector } from '@/action-menu/states/actionMenuEntriesComponentSelector';
import { COMMAND_MENU_COMMANDS } from '@/command-menu/constants/CommandMenuCommands';
import { mainContextStoreComponentInstanceIdState } from '@/context-store/states/mainContextStoreComponentInstanceId';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { ALL_ICONS } from '@ui/display/icon/providers/internal/AllIcons';
import { sortByProperty } from '~/utils/array/sortByProperty';
@ -27,10 +29,43 @@ export const useCommandMenu = () => {
goBackToPreviousHotkeyScope,
} = usePreviousHotkeyScope();
const openCommandMenu = useCallback(() => {
setIsCommandMenuOpened(true);
setHotkeyScopeAndMemorizePreviousScope(AppHotkeyScope.CommandMenuOpen);
}, [setHotkeyScopeAndMemorizePreviousScope, setIsCommandMenuOpened]);
const mainContextStoreComponentInstanceId = useRecoilValue(
mainContextStoreComponentInstanceIdState,
);
const openCommandMenu = useRecoilCallback(
({ snapshot }) =>
() => {
if (isDefined(mainContextStoreComponentInstanceId)) {
const actionMenuEntries = snapshot.getLoadable(
actionMenuEntriesComponentSelector.selectorFamily({
instanceId: mainContextStoreComponentInstanceId,
}),
);
const actionCommands = actionMenuEntries
.getValue()
?.map((actionMenuEntry) => ({
id: actionMenuEntry.key,
label: actionMenuEntry.label,
Icon: actionMenuEntry.Icon,
onCommandClick: actionMenuEntry.onClick,
type: CommandType.Action,
}));
setCommands(actionCommands);
}
setIsCommandMenuOpened(true);
setHotkeyScopeAndMemorizePreviousScope(AppHotkeyScope.CommandMenuOpen);
},
[
mainContextStoreComponentInstanceId,
setCommands,
setHotkeyScopeAndMemorizePreviousScope,
setIsCommandMenuOpened,
],
);
const closeCommandMenu = useRecoilCallback(
({ snapshot }) =>

View File

@ -3,13 +3,14 @@ import { IconComponent } from 'twenty-ui';
export enum CommandType {
Navigate = 'Navigate',
Create = 'Create',
Action = 'Action',
}
export type Command = {
id: string;
to: string;
to?: string;
label: string;
type: CommandType.Navigate | CommandType.Create;
type: CommandType.Navigate | CommandType.Create | CommandType.Action;
Icon?: IconComponent;
firstHotKey?: string;
secondHotKey?: string;

View File

@ -3,7 +3,7 @@ import { mainContextStoreComponentInstanceIdState } from '@/context-store/states
import { useContext, useEffect } from 'react';
import { useSetRecoilState } from 'recoil';
export const SetMainContextStoreComponentInstanceIdEffect = () => {
export const MainContextStoreComponentInstanceIdSetterEffect = () => {
const setMainContextStoreComponentInstanceId = useSetRecoilState(
mainContextStoreComponentInstanceIdState,
);

View File

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

View File

@ -206,6 +206,7 @@ export const RecordIndexContainer = () => {
viewBarId={recordIndexId}
/>
</SpreadsheetImportProvider>
{recordIndexViewType === ViewType.Table && (
<>
<RecordIndexTableContainer
@ -232,7 +233,7 @@ export const RecordIndexContainer = () => {
/>
</StyledContainerWithPadding>
)}
<RecordIndexActionMenu actionMenuId={recordIndexId} />
<RecordIndexActionMenu />
</RecordFieldValueSelectorContextProvider>
</StyledContainer>
);

View File

@ -1,5 +1,7 @@
import { useRecoilValue } from 'recoil';
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { isNewViewableRecordLoadingState } from '@/object-record/record-right-drawer/states/isNewViewableRecordLoading';
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState';
@ -38,19 +40,29 @@ export const RightDrawerRecord = () => {
);
return (
<StyledRightDrawerRecord>
<RecordFieldValueSelectorContextProvider>
{!isNewViewableRecordLoading && (
<RecordValueSetterEffect recordId={objectRecordId} />
)}
<RecordShowContainer
objectNameSingular={objectNameSingular}
objectRecordId={objectRecordId}
loading={false}
isInRightDrawer={true}
isNewRightDrawerItemLoading={isNewViewableRecordLoading}
/>
</RecordFieldValueSelectorContextProvider>
</StyledRightDrawerRecord>
<ContextStoreComponentInstanceContext.Provider
value={{
instanceId: `record-show-${objectRecordId}`,
}}
>
<ActionMenuComponentInstanceContext.Provider
value={{ instanceId: `record-show-${objectRecordId}` }}
>
<StyledRightDrawerRecord>
<RecordFieldValueSelectorContextProvider>
{!isNewViewableRecordLoading && (
<RecordValueSetterEffect recordId={objectRecordId} />
)}
<RecordShowContainer
objectNameSingular={objectNameSingular}
objectRecordId={objectRecordId}
loading={false}
isInRightDrawer={true}
isNewRightDrawerItemLoading={isNewViewableRecordLoading}
/>
</RecordFieldValueSelectorContextProvider>
</StyledRightDrawerRecord>
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
);
};

View File

@ -1,10 +1,11 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { ShowPageContainer } from '@/ui/layout/page/components/ShowPageContainer';
import { SetMainContextStoreComponentInstanceIdEffect } from '@/context-store/components/SetMainContextStoreComponentInstanceIdEffect';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { MainContextStoreComponentInstanceIdSetterEffect } from '@/context-store/components/MainContextStoreComponentInstanceIdSetterEffect';
import { InformationBannerDeletedRecord } from '@/information-banner/components/deleted-record/InformationBannerDeletedRecord';
import { RecordShowContainerContextStoreEffect } from '@/object-record/record-show/components/RecordShowContainerContextStoreEffect';
import { RecordShowContainerContextStoreObjectMetadataIdEffect } from '@/object-record/record-show/components/RecordShowContainerContextStoreObjectMetadataIdEffect';
import { RecordShowContainerContextStoreTargetedRecordsEffect } from '@/object-record/record-show/components/RecordShowContainerContextStoreTargetedRecordsEffect';
import { useRecordShowContainerData } from '@/object-record/record-show/hooks/useRecordShowContainerData';
import { useRecordShowContainerTabs } from '@/object-record/record-show/hooks/useRecordShowContainerTabs';
import { ShowPageSubContainer } from '@/ui/layout/show-page/components/ShowPageSubContainer';
@ -41,16 +42,15 @@ export const RecordShowContainer = ({
);
return (
<ContextStoreComponentInstanceContext.Provider
value={{
instanceId: 'record-show',
}}
>
<RecordShowContainerContextStoreEffect
<>
<RecordShowContainerContextStoreObjectMetadataIdEffect
recordId={objectRecordId}
objectNameSingular={objectNameSingular}
/>
{!isInRightDrawer && <SetMainContextStoreComponentInstanceIdEffect />}
<RecordShowContainerContextStoreTargetedRecordsEffect
recordId={objectRecordId}
/>
{!isInRightDrawer && <MainContextStoreComponentInstanceIdSetterEffect />}
{recordFromStore && recordFromStore.deletedAt && (
<InformationBannerDeletedRecord
recordId={objectRecordId}
@ -69,6 +69,6 @@ export const RecordShowContainer = ({
isNewRightDrawerItemLoading={isNewRightDrawerItemLoading}
/>
</ShowPageContainer>
</ContextStoreComponentInstanceContext.Provider>
</>
);
};

View File

@ -0,0 +1,30 @@
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { useEffect } from 'react';
export const RecordShowContainerContextStoreObjectMetadataIdEffect = ({
recordId,
objectNameSingular,
}: {
recordId: string;
objectNameSingular: string;
}) => {
const setContextStoreCurrentObjectMetadataId = useSetRecoilComponentStateV2(
contextStoreCurrentObjectMetadataIdComponentState,
);
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular: objectNameSingular,
});
useEffect(() => {
setContextStoreCurrentObjectMetadataId(objectMetadataItem?.id);
return () => {
setContextStoreCurrentObjectMetadataId(null);
};
}, [recordId, setContextStoreCurrentObjectMetadataId, objectMetadataItem.id]);
return null;
};

View File

@ -1,29 +1,17 @@
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { useEffect } from 'react';
export const RecordShowContainerContextStoreEffect = ({
export const RecordShowContainerContextStoreTargetedRecordsEffect = ({
recordId,
objectNameSingular,
}: {
recordId: string;
objectNameSingular: string;
}) => {
const setContextStoreTargetedRecordsRule = useSetRecoilComponentStateV2(
contextStoreTargetedRecordsRuleComponentState,
);
const setContextStoreCurrentObjectMetadataId = useSetRecoilComponentStateV2(
contextStoreCurrentObjectMetadataIdComponentState,
);
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular: objectNameSingular,
});
const setContextStoreNumberOfSelectedRecords = useSetRecoilComponentStateV2(
contextStoreNumberOfSelectedRecordsComponentState,
);
@ -33,7 +21,6 @@ export const RecordShowContainerContextStoreEffect = ({
mode: 'selection',
selectedRecordIds: [recordId],
});
setContextStoreCurrentObjectMetadataId(objectMetadataItem?.id);
setContextStoreNumberOfSelectedRecords(1);
return () => {
@ -41,14 +28,11 @@ export const RecordShowContainerContextStoreEffect = ({
mode: 'selection',
selectedRecordIds: [],
});
setContextStoreCurrentObjectMetadataId(null);
setContextStoreNumberOfSelectedRecords(0);
};
}, [
recordId,
setContextStoreTargetedRecordsRule,
setContextStoreCurrentObjectMetadataId,
objectMetadataItem?.id,
setContextStoreNumberOfSelectedRecords,
]);

View File

@ -27,7 +27,7 @@ export const RecordTableInternalEffect = ({
useListenClickOutsideByClassName({
classNames: ['entity-table-cell'],
excludeClassNames: ['bottom-bar', 'action-menu-dropdown'],
excludeClassNames: ['bottom-bar', 'action-menu-dropdown', 'command-menu'],
callback: () => {
leaveTableFocus();
},

View File

@ -1,11 +1,13 @@
import { useRecoilCallback } from 'recoil';
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { recordIndexActionMenuDropdownPositionComponentState } from '@/action-menu/states/recordIndexActionMenuDropdownPositionComponentState';
import { isRowSelectedComponentFamilyState } from '@/object-record/record-table/record-table-row/states/isRowSelectedComponentFamilyState';
import { isBottomBarOpenedComponentState } from '@/ui/layout/bottom-bar/states/isBottomBarOpenedComponentState';
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { extractComponentFamilyState } from '@/ui/utilities/state/component-state/utils/extractComponentFamilyState';
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
@ -14,6 +16,10 @@ export const useTriggerActionMenuDropdown = ({
}: {
recordTableId: string;
}) => {
const actionMenuInstanceId = useAvailableComponentInstanceIdOrThrow(
ActionMenuComponentInstanceContext,
);
const triggerActionMenuDropdown = useRecoilCallback(
({ set, snapshot }) =>
(event: React.MouseEvent, recordId: string) => {
@ -24,7 +30,7 @@ export const useTriggerActionMenuDropdown = ({
set(
extractComponentState(
recordIndexActionMenuDropdownPositionComponentState,
`action-menu-dropdown-${recordTableId}`,
`action-menu-dropdown-${actionMenuInstanceId}`,
),
{
x: event.clientX,
@ -48,19 +54,19 @@ export const useTriggerActionMenuDropdown = ({
const isActionMenuDropdownOpenState = extractComponentState(
isDropdownOpenComponentState,
`action-menu-dropdown-${recordTableId}`,
`action-menu-dropdown-${actionMenuInstanceId}`,
);
const isActionBarOpenState = isBottomBarOpenedComponentState.atomFamily(
{
instanceId: `action-bar-${recordTableId}`,
instanceId: `action-bar-${actionMenuInstanceId}`,
},
);
set(isActionBarOpenState, false);
set(isActionMenuDropdownOpenState, true);
},
[recordTableId],
[actionMenuInstanceId, recordTableId],
);
return { triggerActionMenuDropdown };

View File

@ -1,5 +1,7 @@
import styled from '@emotion/styled';
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { RecordTableWithWrappers } from '@/object-record/record-table/components/RecordTableWithWrappers';
import { SignInBackgroundMockContainerEffect } from '@/sign-in-background-mock/components/SignInBackgroundMockContainerEffect';
import { ViewBar } from '@/views/components/ViewBar';
@ -21,22 +23,32 @@ export const SignInBackgroundMockContainer = () => {
return (
<StyledContainer>
<ViewComponentInstanceContext.Provider value={{ instanceId: viewBarId }}>
<ViewBar
viewBarId={viewBarId}
onCurrentViewChange={async () => {}}
optionsDropdownButton={<></>}
/>
<SignInBackgroundMockContainerEffect
objectNamePlural={objectNamePlural}
recordTableId={recordIndexId}
viewId={viewBarId}
/>
<RecordTableWithWrappers
objectNameSingular={objectNameSingular}
recordTableId={recordIndexId}
viewBarId={viewBarId}
updateRecordMutation={() => {}}
/>
<ContextStoreComponentInstanceContext.Provider
value={{
instanceId: recordIndexId,
}}
>
<ActionMenuComponentInstanceContext.Provider
value={{ instanceId: recordIndexId }}
>
<ViewBar
viewBarId={viewBarId}
onCurrentViewChange={async () => {}}
optionsDropdownButton={<></>}
/>
<SignInBackgroundMockContainerEffect
objectNamePlural={objectNamePlural}
recordTableId={recordIndexId}
viewId={viewBarId}
/>
<RecordTableWithWrappers
objectNameSingular={objectNameSingular}
recordTableId={recordIndexId}
viewBarId={viewBarId}
updateRecordMutation={() => {}}
/>
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
</ViewComponentInstanceContext.Provider>
</StyledContainer>
);

View File

@ -219,6 +219,7 @@ export const Modal = ({
return (
<StyledBackDrop
className="modal-backdrop"
onMouseDown={stopEventPropagation}
modalVariant={modalVariant}
>

View File

@ -47,6 +47,10 @@ const StyledContainer = styled(motion.div)<{ isRightDrawerMinimized: boolean }>`
right: 0;
top: 0;
z-index: 100;
.modal-backdrop {
background: ${({ theme }) => theme.background.overlayTertiary};
}
`;
const StyledRightDrawer = styled.div`

View File

@ -1,4 +1,4 @@
import { RecordShowActionMenu } from '@/action-menu/components/RecordShowActionMenu';
import { RecordShowRightDrawerActionMenu } from '@/action-menu/components/RecordShowRightDrawerActionMenu';
import { Calendar } from '@/activities/calendar/components/Calendar';
import { EmailThreads } from '@/activities/emails/components/EmailThreads';
import { Attachments } from '@/activities/files/components/Attachments';
@ -221,7 +221,7 @@ export const ShowPageSubContainer = ({
</StyledContentContainer>
{isInRightDrawer && recordFromStore && !recordFromStore.deletedAt && (
<StyledButtonContainer>
<RecordShowActionMenu actionMenuId={'right-drawer-action-menu'} />
<RecordShowRightDrawerActionMenu />
</StyledButtonContainer>
)}
</StyledShowPageRightContainer>

View File

@ -1,7 +1,8 @@
import styled from '@emotion/styled';
import { useParams } from 'react-router-dom';
import { SetMainContextStoreComponentInstanceIdEffect } from '@/context-store/components/SetMainContextStoreComponentInstanceIdEffect';
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { MainContextStoreComponentInstanceIdSetterEffect } from '@/context-store/components/MainContextStoreComponentInstanceIdSetterEffect';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
@ -81,13 +82,19 @@ export const RecordIndexPage = () => {
<StyledIndexContainer>
<ContextStoreComponentInstanceContext.Provider
value={{
instanceId: 'record-index',
instanceId: `record-index-${objectMetadataItem.id}`,
}}
>
<RecordIndexContainerContextStoreObjectMetadataEffect />
<RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect />
<SetMainContextStoreComponentInstanceIdEffect />
<RecordIndexContainer />
<ActionMenuComponentInstanceContext.Provider
value={{
instanceId: `record-index-${objectMetadataItem.id}`,
}}
>
<RecordIndexContainerContextStoreObjectMetadataEffect />
<RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect />
<MainContextStoreComponentInstanceIdSetterEffect />
<RecordIndexContainer />
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
</StyledIndexContainer>
</PageBody>

View File

@ -1,6 +1,9 @@
import { useParams } from 'react-router-dom';
import { RecordShowActionMenu } from '@/action-menu/components/RecordShowActionMenu';
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { TimelineActivityContext } from '@/activities/timeline-activities/contexts/TimelineActivityContext';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { RecordShowContainer } from '@/object-record/record-show/components/RecordShowContainer';
import { useRecordShowPage } from '@/object-record/record-show/hooks/useRecordShowPage';
@ -11,7 +14,6 @@ import { PageContainer } from '@/ui/layout/page/components/PageContainer';
import { PageTitle } from '@/ui/utilities/page-title/components/PageTitle';
import { RecordShowPageWorkflowHeader } from '@/workflow/components/RecordShowPageWorkflowHeader';
import { RecordShowPageWorkflowVersionHeader } from '@/workflow/components/RecordShowPageWorkflowVersionHeader';
import { RecordShowPageBaseHeader } from '~/pages/object-record/RecordShowPageBaseHeader';
import { RecordShowPageHeader } from '~/pages/object-record/RecordShowPageHeader';
export const RecordShowPage = () => {
@ -38,49 +40,61 @@ export const RecordShowPage = () => {
return (
<RecordFieldValueSelectorContextProvider>
<RecordValueSetterEffect recordId={objectRecordId} />
<PageContainer>
<PageTitle title={pageTitle} />
<RecordShowPageHeader
objectNameSingular={objectNameSingular}
objectRecordId={objectRecordId}
headerIcon={headerIcon}
<ContextStoreComponentInstanceContext.Provider
value={{
instanceId: `record-show-${objectRecordId}`,
}}
>
<ActionMenuComponentInstanceContext.Provider
value={{ instanceId: `record-show-${objectRecordId}` }}
>
<>
{objectNameSingular === CoreObjectNameSingular.Workflow ? (
<RecordShowPageWorkflowHeader workflowId={objectRecordId} />
) : objectNameSingular ===
CoreObjectNameSingular.WorkflowVersion ? (
<RecordShowPageWorkflowVersionHeader
workflowVersionId={objectRecordId}
/>
) : (
<RecordShowPageBaseHeader
{...{
isFavorite,
handleFavoriteButtonClick,
record,
objectMetadataItem,
objectNameSingular,
}}
/>
)}
</>
</RecordShowPageHeader>
<PageBody>
<TimelineActivityContext.Provider
value={{
labelIdentifierValue: pageName,
}}
>
<RecordShowContainer
<RecordValueSetterEffect recordId={objectRecordId} />
<PageContainer>
<PageTitle title={pageTitle} />
<RecordShowPageHeader
objectNameSingular={objectNameSingular}
objectRecordId={objectRecordId}
loading={loading}
/>
</TimelineActivityContext.Provider>
</PageBody>
</PageContainer>
headerIcon={headerIcon}
>
<>
{objectNameSingular === CoreObjectNameSingular.Workflow ? (
<RecordShowPageWorkflowHeader workflowId={objectRecordId} />
) : objectNameSingular ===
CoreObjectNameSingular.WorkflowVersion ? (
<RecordShowPageWorkflowVersionHeader
workflowVersionId={objectRecordId}
/>
) : (
<>
<RecordShowActionMenu
{...{
isFavorite,
handleFavoriteButtonClick,
record,
objectMetadataItem,
objectNameSingular,
}}
/>
</>
)}
</>
</RecordShowPageHeader>
<PageBody>
<TimelineActivityContext.Provider
value={{
labelIdentifierValue: pageName,
}}
>
<RecordShowContainer
objectNameSingular={objectNameSingular}
objectRecordId={objectRecordId}
loading={loading}
/>
</TimelineActivityContext.Provider>
</PageBody>
</PageContainer>
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
</RecordFieldValueSelectorContextProvider>
);
};