Raphaël Bosi
2025-01-24 16:10:02 +01:00
committed by GitHub
parent 29df6e64a0
commit ff41768e8f
15 changed files with 438 additions and 221 deletions

View File

@ -1,19 +1,24 @@
import { CommandGroup } from '@/command-menu/components/CommandGroup'; import { CommandGroup } from '@/command-menu/components/CommandGroup';
import { CommandMenuDefaultSelectionEffect } from '@/command-menu/components/CommandMenuDefaultSelectionEffect'; import { CommandMenuDefaultSelectionEffect } from '@/command-menu/components/CommandMenuDefaultSelectionEffect';
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem'; import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
import { ResetContextToSelectionCommandButton } from '@/command-menu/components/ResetContextToSelectionCommandButton';
import { COMMAND_MENU_SEARCH_BAR_HEIGHT } from '@/command-menu/constants/CommandMenuSearchBarHeight'; import { COMMAND_MENU_SEARCH_BAR_HEIGHT } from '@/command-menu/constants/CommandMenuSearchBarHeight';
import { COMMAND_MENU_SEARCH_BAR_PADDING } from '@/command-menu/constants/CommandMenuSearchBarPadding'; import { COMMAND_MENU_SEARCH_BAR_PADDING } from '@/command-menu/constants/CommandMenuSearchBarPadding';
import { useCommandMenuOnItemClick } from '@/command-menu/hooks/useCommandMenuOnItemClick'; import { useCommandMenuOnItemClick } from '@/command-menu/hooks/useCommandMenuOnItemClick';
import { useMatchingCommandMenuCommands } from '@/command-menu/hooks/useMatchingCommandMenuCommands'; import { useMatchingCommandMenuCommands } from '@/command-menu/hooks/useMatchingCommandMenuCommands';
import { useResetPreviousCommandMenuContext } from '@/command-menu/hooks/useResetPreviousCommandMenuContext';
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState'; import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
import { Command } from '@/command-menu/types/Command'; import { Command } from '@/command-menu/types/Command';
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem'; import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList'; import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope'; import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro'; import { useLingui } from '@lingui/react/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-ui'; import { isDefined } from 'twenty-ui';
@ -60,6 +65,8 @@ export const CommandMenu = () => {
const { t } = useLingui(); const { t } = useLingui();
const { onItemClick } = useCommandMenuOnItemClick(); const { onItemClick } = useCommandMenuOnItemClick();
const { resetPreviousCommandMenuContext } =
useResetPreviousCommandMenuContext();
const commandMenuSearch = useRecoilValue(commandMenuSearchState); const commandMenuSearch = useRecoilValue(commandMenuSearchState);
@ -84,22 +91,33 @@ export const CommandMenu = () => {
commandMenuSearch, commandMenuSearch,
}); });
const selectableItems = copilotCommands const selectableItems: Command[] = copilotCommands
.concat(matchingStandardActionRecordSelectionCommands) .concat(
.concat(matchingWorkflowRunRecordSelectionCommands) matchingStandardActionRecordSelectionCommands,
.concat(matchingStandardActionGlobalCommands) matchingWorkflowRunRecordSelectionCommands,
.concat(matchingWorkflowRunGlobalCommands) matchingStandardActionGlobalCommands,
.concat(matchingNavigateCommand) matchingWorkflowRunGlobalCommands,
.concat(peopleCommands) matchingNavigateCommand,
.concat(companyCommands) peopleCommands,
.concat(opportunityCommands) companyCommands,
.concat(noteCommands) opportunityCommands,
.concat(tasksCommands) noteCommands,
.concat(customObjectCommands) tasksCommands,
customObjectCommands,
)
.filter(isDefined); .filter(isDefined);
const previousContextStoreCurrentObjectMetadataId = useRecoilComponentValueV2(
contextStoreCurrentObjectMetadataIdComponentState,
'command-menu-previous',
);
const selectableItemIds = selectableItems.map((item) => item.id); const selectableItemIds = selectableItems.map((item) => item.id);
if (isNonEmptyString(previousContextStoreCurrentObjectMetadataId)) {
selectableItemIds.unshift('reset-context-to-selection');
}
const commandGroups: CommandGroupConfig[] = [ const commandGroups: CommandGroupConfig[] = [
{ {
heading: t`Copilot`, heading: t`Copilot`,
@ -168,6 +186,11 @@ export const CommandMenu = () => {
selectableItemIdArray={selectableItemIds} selectableItemIdArray={selectableItemIds}
hotkeyScope={AppHotkeyScope.CommandMenu} hotkeyScope={AppHotkeyScope.CommandMenu}
onEnter={(itemId) => { onEnter={(itemId) => {
if (itemId === 'reset-context-to-selection') {
resetPreviousCommandMenuContext();
return;
}
const command = selectableItems.find( const command = selectableItems.find(
(item) => item.id === itemId, (item) => item.id === itemId,
); );
@ -184,6 +207,16 @@ export const CommandMenu = () => {
} }
}} }}
> >
{isNonEmptyString(
previousContextStoreCurrentObjectMetadataId,
) && (
<CommandGroup heading={t`Context`} key={t`Context`}>
<SelectableItem itemId="reset-context-to-selection">
<ResetContextToSelectionCommandButton />
</SelectableItem>
</CommandGroup>
)}
{isNoResults && !isLoading && ( {isNoResults && !isLoading && (
<StyledEmpty>No results found</StyledEmpty> <StyledEmpty>No results found</StyledEmpty>
)} )}

View File

@ -1,6 +1,6 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
const StyledChip = styled.div` const StyledChip = styled.div<{ variant?: 'default' | 'small' }>`
align-items: center; align-items: center;
background: ${({ theme }) => theme.background.transparent.light}; background: ${({ theme }) => theme.background.transparent.light};
border: 1px solid ${({ theme }) => theme.border.color.medium}; border: 1px solid ${({ theme }) => theme.border.color.medium};
@ -8,7 +8,8 @@ const StyledChip = styled.div`
box-sizing: border-box; box-sizing: border-box;
display: flex; display: flex;
gap: ${({ theme }) => theme.spacing(1)}; gap: ${({ theme }) => theme.spacing(1)};
height: ${({ theme }) => theme.spacing(8)}; height: ${({ theme, variant }) =>
variant === 'small' ? theme.spacing(6) : theme.spacing(8)};
padding: 0 ${({ theme }) => theme.spacing(2)}; padding: 0 ${({ theme }) => theme.spacing(2)};
font-size: ${({ theme }) => theme.font.size.sm}; font-size: ${({ theme }) => theme.font.size.sm};
font-weight: ${({ theme }) => theme.font.weight.medium}; font-weight: ${({ theme }) => theme.font.weight.medium};
@ -40,13 +41,15 @@ export const CommandMenuContextChip = ({
Icons, Icons,
text, text,
withIconBackground, withIconBackground,
variant = 'default',
}: { }: {
Icons: React.ReactNode[]; Icons: React.ReactNode[];
text?: string; text?: string;
withIconBackground?: boolean; withIconBackground?: boolean;
variant?: 'default' | 'small';
}) => { }) => {
return ( return (
<StyledChip> <StyledChip variant={variant}>
<StyledIconsContainer> <StyledIconsContainer>
{Icons.map((Icon, index) => ( {Icons.map((Icon, index) => (
<StyledIconWrapper <StyledIconWrapper

View File

@ -1,14 +1,17 @@
import { CommandMenuContextChip } from '@/command-menu/components/CommandMenuContextChip'; import { CommandMenuContextChip } from '@/command-menu/components/CommandMenuContextChip';
import { CommandMenuContextRecordChipAvatars } from '@/command-menu/components/CommandMenuContextRecordChipAvatars'; import { CommandMenuContextRecordChipAvatars } from '@/command-menu/components/CommandMenuContextRecordChipAvatars';
import { getSelectedRecordsContextText } from '@/command-menu/utils/getRecordContextText';
import { useFindManyRecordsSelectedInContextStore } from '@/context-store/hooks/useFindManyRecordsSelectedInContextStore'; import { useFindManyRecordsSelectedInContextStore } from '@/context-store/hooks/useFindManyRecordsSelectedInContextStore';
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById'; import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { getObjectRecordIdentifier } from '@/object-metadata/utils/getObjectRecordIdentifier';
import { capitalize } from 'twenty-shared';
export const CommandMenuContextRecordChip = ({ export const CommandMenuContextRecordChip = ({
objectMetadataItemId, objectMetadataItemId,
instanceId,
variant = 'default',
}: { }: {
objectMetadataItemId: string; objectMetadataItemId: string;
instanceId?: string;
variant?: 'default' | 'small';
}) => { }) => {
const { objectMetadataItem } = useObjectMetadataItemById({ const { objectMetadataItem } = useObjectMetadataItemById({
objectId: objectMetadataItemId, objectId: objectMetadataItemId,
@ -17,6 +20,7 @@ export const CommandMenuContextRecordChip = ({
const { records, loading, totalCount } = const { records, loading, totalCount } =
useFindManyRecordsSelectedInContextStore({ useFindManyRecordsSelectedInContextStore({
limit: 3, limit: 3,
instanceId,
}); });
if (loading || !totalCount) { if (loading || !totalCount) {
@ -31,17 +35,16 @@ export const CommandMenuContextRecordChip = ({
/> />
)); ));
const text =
totalCount === 1
? getObjectRecordIdentifier({ objectMetadataItem, record: records[0] })
.name
: `${totalCount} ${capitalize(objectMetadataItem.namePlural)}`;
return ( return (
<CommandMenuContextChip <CommandMenuContextChip
text={text} text={getSelectedRecordsContextText(
objectMetadataItem,
records,
totalCount,
)}
Icons={Avatars} Icons={Avatars}
withIconBackground={true} withIconBackground={true}
variant={variant}
/> />
); );
}; };

View File

@ -14,7 +14,10 @@ export const CommandMenuDefaultSelectionEffect = ({
const selectedItemId = useRecoilValue(selectedItemIdState); const selectedItemId = useRecoilValue(selectedItemIdState);
useEffect(() => { useEffect(() => {
if (isDefined(selectedItemId)) { if (
isDefined(selectedItemId) &&
selectableItemIds.includes(selectedItemId)
) {
return; return;
} }

View File

@ -4,6 +4,7 @@ import { IconArrowUpRight, IconComponent, MenuItemCommand } from 'twenty-ui';
import { useCommandMenuOnItemClick } from '@/command-menu/hooks/useCommandMenuOnItemClick'; import { useCommandMenuOnItemClick } from '@/command-menu/hooks/useCommandMenuOnItemClick';
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList'; import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { ReactNode } from 'react';
export type CommandMenuItemProps = { export type CommandMenuItemProps = {
label: string; label: string;
@ -14,6 +15,7 @@ export type CommandMenuItemProps = {
firstHotKey?: string; firstHotKey?: string;
secondHotKey?: string; secondHotKey?: string;
shouldCloseCommandMenuOnClick?: boolean; shouldCloseCommandMenuOnClick?: boolean;
RightComponent?: ReactNode;
}; };
export const CommandMenuItem = ({ export const CommandMenuItem = ({
@ -25,6 +27,7 @@ export const CommandMenuItem = ({
firstHotKey, firstHotKey,
secondHotKey, secondHotKey,
shouldCloseCommandMenuOnClick, shouldCloseCommandMenuOnClick,
RightComponent,
}: CommandMenuItemProps) => { }: CommandMenuItemProps) => {
const { onItemClick } = useCommandMenuOnItemClick(); const { onItemClick } = useCommandMenuOnItemClick();
@ -49,6 +52,7 @@ export const CommandMenuItem = ({
}) })
} }
isSelected={isSelectedItemId} isSelected={isSelectedItemId}
RightComponent={RightComponent}
/> />
); );
}; };

View File

@ -0,0 +1,56 @@
import { CommandMenuContextRecordChip } from '@/command-menu/components/CommandMenuContextRecordChip';
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
import { useResetPreviousCommandMenuContext } from '@/command-menu/hooks/useResetPreviousCommandMenuContext';
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { t } from '@lingui/core/macro';
import { useRecoilValue } from 'recoil';
import { IconArrowBackUp, isDefined } from 'twenty-ui';
export const ResetContextToSelectionCommandButton = () => {
const contextStoreTargetedRecordsRule = useRecoilComponentValueV2(
contextStoreTargetedRecordsRuleComponentState,
'command-menu-previous',
);
const contextStoreCurrentObjectMetadataId = useRecoilComponentValueV2(
contextStoreCurrentObjectMetadataIdComponentState,
'command-menu-previous',
);
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const objectMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.id === contextStoreCurrentObjectMetadataId,
);
const { resetPreviousCommandMenuContext } =
useResetPreviousCommandMenuContext();
if (
!isDefined(objectMetadataItem) ||
(contextStoreTargetedRecordsRule.mode === 'selection' &&
contextStoreTargetedRecordsRule.selectedRecordIds.length === 0)
) {
return null;
}
return (
<CommandMenuItem
id="reset-context-to-selection"
Icon={IconArrowBackUp}
label={t`Reset to`}
RightComponent={
<CommandMenuContextRecordChip
objectMetadataItemId={objectMetadataItem.id}
instanceId="command-menu-previous"
variant="small"
/>
}
onClick={resetPreviousCommandMenuContext}
/>
);
};

View File

@ -4,19 +4,18 @@ import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchS
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList'; import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope'; import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
import { isDefined } from '~/utils/isDefined';
import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState';
import { CommandMenuPages } from '@/command-menu/components/CommandMenuPages'; import { CommandMenuPages } from '@/command-menu/components/CommandMenuPages';
import { useCopyContextStoreStates } from '@/command-menu/hooks/useCopyContextStoreAndActionMenuStates';
import { useResetContextStoreStates } from '@/command-menu/hooks/useResetContextStoreStates';
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState'; import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageTitle'; import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageTitle';
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState'; import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState'; import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState'; import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { mainContextStoreComponentInstanceIdState } from '@/context-store/states/mainContextStoreComponentInstanceId'; import { mainContextStoreComponentInstanceIdState } from '@/context-store/states/mainContextStoreComponentInstanceId';
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState'; import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState'; import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState';
import { emitRightDrawerCloseEvent } from '@/ui/layout/right-drawer/utils/emitRightDrawerCloseEvent'; import { emitRightDrawerCloseEvent } from '@/ui/layout/right-drawer/utils/emitRightDrawerCloseEvent';
@ -33,120 +32,22 @@ export const useCommandMenu = () => {
mainContextStoreComponentInstanceIdState, mainContextStoreComponentInstanceIdState,
); );
const { copyContextStoreStates } = useCopyContextStoreStates();
const { resetContextStoreStates } = useResetContextStoreStates();
const openCommandMenu = useRecoilCallback( const openCommandMenu = useRecoilCallback(
({ snapshot, set }) => ({ set }) =>
() => { () => {
if (isDefined(mainContextStoreComponentInstanceId)) { copyContextStoreStates({
const contextStoreCurrentObjectMetadataId = snapshot instanceIdToCopyFrom: mainContextStoreComponentInstanceId,
.getLoadable( instanceIdToCopyTo: 'command-menu',
contextStoreCurrentObjectMetadataIdComponentState.atomFamily({ });
instanceId: mainContextStoreComponentInstanceId,
}),
)
.getValue();
set(
contextStoreCurrentObjectMetadataIdComponentState.atomFamily({
instanceId: 'command-menu',
}),
contextStoreCurrentObjectMetadataId,
);
const contextStoreTargetedRecordsRule = snapshot
.getLoadable(
contextStoreTargetedRecordsRuleComponentState.atomFamily({
instanceId: mainContextStoreComponentInstanceId,
}),
)
.getValue();
set(
contextStoreTargetedRecordsRuleComponentState.atomFamily({
instanceId: 'command-menu',
}),
contextStoreTargetedRecordsRule,
);
const contextStoreNumberOfSelectedRecords = snapshot
.getLoadable(
contextStoreNumberOfSelectedRecordsComponentState.atomFamily({
instanceId: mainContextStoreComponentInstanceId,
}),
)
.getValue();
set(
contextStoreNumberOfSelectedRecordsComponentState.atomFamily({
instanceId: 'command-menu',
}),
contextStoreNumberOfSelectedRecords,
);
const contextStoreFilters = snapshot
.getLoadable(
contextStoreFiltersComponentState.atomFamily({
instanceId: mainContextStoreComponentInstanceId,
}),
)
.getValue();
set(
contextStoreFiltersComponentState.atomFamily({
instanceId: 'command-menu',
}),
contextStoreFilters,
);
const contextStoreCurrentViewId = snapshot
.getLoadable(
contextStoreCurrentViewIdComponentState.atomFamily({
instanceId: mainContextStoreComponentInstanceId,
}),
)
.getValue();
set(
contextStoreCurrentViewIdComponentState.atomFamily({
instanceId: 'command-menu',
}),
contextStoreCurrentViewId,
);
const contextStoreCurrentViewType = snapshot
.getLoadable(
contextStoreCurrentViewTypeComponentState.atomFamily({
instanceId: mainContextStoreComponentInstanceId,
}),
)
.getValue();
set(
contextStoreCurrentViewTypeComponentState.atomFamily({
instanceId: 'command-menu',
}),
contextStoreCurrentViewType,
);
}
const actionMenuEntries = snapshot
.getLoadable(
actionMenuEntriesComponentState.atomFamily({
instanceId: mainContextStoreComponentInstanceId,
}),
)
.getValue();
set(
actionMenuEntriesComponentState.atomFamily({
instanceId: 'command-menu',
}),
actionMenuEntries,
);
set(isCommandMenuOpenedState, true); set(isCommandMenuOpenedState, true);
setHotkeyScopeAndMemorizePreviousScope(AppHotkeyScope.CommandMenuOpen); setHotkeyScopeAndMemorizePreviousScope(AppHotkeyScope.CommandMenuOpen);
}, },
[ [
copyContextStoreStates,
mainContextStoreComponentInstanceId, mainContextStoreComponentInstanceId,
setHotkeyScopeAndMemorizePreviousScope, setHotkeyScopeAndMemorizePreviousScope,
], ],
@ -160,57 +61,8 @@ export const useCommandMenu = () => {
.getValue(); .getValue();
if (isCommandMenuOpened) { if (isCommandMenuOpened) {
set( resetContextStoreStates('command-menu');
contextStoreCurrentObjectMetadataIdComponentState.atomFamily({ resetContextStoreStates('command-menu-previous');
instanceId: 'command-menu',
}),
null,
);
set(
contextStoreTargetedRecordsRuleComponentState.atomFamily({
instanceId: 'command-menu',
}),
{
mode: 'selection',
selectedRecordIds: [],
},
);
set(
contextStoreNumberOfSelectedRecordsComponentState.atomFamily({
instanceId: 'command-menu',
}),
0,
);
set(
contextStoreFiltersComponentState.atomFamily({
instanceId: 'command-menu',
}),
[],
);
set(
contextStoreCurrentViewIdComponentState.atomFamily({
instanceId: 'command-menu',
}),
null,
);
set(
contextStoreCurrentViewTypeComponentState.atomFamily({
instanceId: 'command-menu',
}),
null,
);
set(
actionMenuEntriesComponentState.atomFamily({
instanceId: 'command-menu',
}),
new Map(),
);
set(viewableRecordIdState, null); set(viewableRecordIdState, null);
set(commandMenuPageState, CommandMenuPages.Root); set(commandMenuPageState, CommandMenuPages.Root);
@ -225,7 +77,7 @@ export const useCommandMenu = () => {
emitRightDrawerCloseEvent(); emitRightDrawerCloseEvent();
} }
}, },
[goBackToPreviousHotkeyScope, resetSelectedItem], [goBackToPreviousHotkeyScope, resetContextStoreStates, resetSelectedItem],
); );
const toggleCommandMenu = useRecoilCallback( const toggleCommandMenu = useRecoilCallback(
@ -258,44 +110,59 @@ export const useCommandMenu = () => {
[openCommandMenu], [openCommandMenu],
); );
const setGlobalCommandMenuContext = useRecoilCallback(({ set }) => { const setGlobalCommandMenuContext = useRecoilCallback(
return () => { ({ set }) => {
set( return () => {
contextStoreTargetedRecordsRuleComponentState.atomFamily({ copyContextStoreStates({
instanceId: 'command-menu', instanceIdToCopyFrom: 'command-menu',
}), instanceIdToCopyTo: 'command-menu-previous',
{ });
mode: 'selection',
selectedRecordIds: [],
},
);
set( set(
contextStoreNumberOfSelectedRecordsComponentState.atomFamily({ contextStoreTargetedRecordsRuleComponentState.atomFamily({
instanceId: 'command-menu', instanceId: 'command-menu',
}), }),
0, {
); mode: 'selection',
selectedRecordIds: [],
},
);
set( set(
contextStoreCurrentViewTypeComponentState.atomFamily({ contextStoreNumberOfSelectedRecordsComponentState.atomFamily({
instanceId: 'command-menu', instanceId: 'command-menu',
}), }),
null, 0,
); );
set(commandMenuPageInfoState, { set(
title: undefined, contextStoreFiltersComponentState.atomFamily({
Icon: undefined, instanceId: 'command-menu',
}); }),
}; [],
}, []); );
set(
contextStoreCurrentViewTypeComponentState.atomFamily({
instanceId: 'command-menu',
}),
ContextStoreViewType.Table,
);
set(commandMenuPageInfoState, {
title: undefined,
Icon: undefined,
});
};
},
[copyContextStoreStates],
);
return { return {
openCommandMenu, openCommandMenu,
closeCommandMenu, closeCommandMenu,
openRecordInCommandMenu, openRecordInCommandMenu,
toggleCommandMenu, toggleCommandMenu,
resetCommandMenuContext: setGlobalCommandMenuContext, setGlobalCommandMenuContext,
}; };
}; };

View File

@ -2,15 +2,17 @@ import { CommandMenuPages } from '@/command-menu/components/CommandMenuPages';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState'; import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState'; import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { useKeyboardShortcutMenu } from '@/keyboard-shortcut-menu/hooks/useKeyboardShortcutMenu'; import { useKeyboardShortcutMenu } from '@/keyboard-shortcut-menu/hooks/useKeyboardShortcutMenu';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope'; import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { isNonEmptyString } from '@sniptt/guards'; import { isNonEmptyString } from '@sniptt/guards';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { Key } from 'ts-key-enum'; import { Key } from 'ts-key-enum';
export const useCommandMenuHotKeys = () => { export const useCommandMenuHotKeys = () => {
const { closeCommandMenu, toggleCommandMenu, resetCommandMenuContext } = const { closeCommandMenu, toggleCommandMenu, setGlobalCommandMenuContext } =
useCommandMenu(); useCommandMenu();
const commandMenuSearch = useRecoilValue(commandMenuSearchState); const commandMenuSearch = useRecoilValue(commandMenuSearchState);
@ -19,6 +21,11 @@ export const useCommandMenuHotKeys = () => {
const commandMenuPage = useRecoilValue(commandMenuPageState); const commandMenuPage = useRecoilValue(commandMenuPageState);
const contextStoreTargetedRecordsRuleComponent = useRecoilComponentValueV2(
contextStoreTargetedRecordsRuleComponentState,
'command-menu',
);
useScopedHotkeys( useScopedHotkeys(
'ctrl+k,meta+k', 'ctrl+k,meta+k',
() => { () => {
@ -43,9 +50,14 @@ export const useCommandMenuHotKeys = () => {
() => { () => {
if ( if (
commandMenuPage === CommandMenuPages.Root && commandMenuPage === CommandMenuPages.Root &&
!isNonEmptyString(commandMenuSearch) !isNonEmptyString(commandMenuSearch) &&
!(
contextStoreTargetedRecordsRuleComponent.mode === 'selection' &&
contextStoreTargetedRecordsRuleComponent.selectedRecordIds.length ===
0
)
) { ) {
resetCommandMenuContext(); setGlobalCommandMenuContext();
} }
}, },
AppHotkeyScope.CommandMenuOpen, AppHotkeyScope.CommandMenuOpen,

View File

@ -0,0 +1,129 @@
import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState';
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { useRecoilCallback } from 'recoil';
export const useCopyContextStoreStates = () => {
const copyContextStoreStates = useRecoilCallback(
({ snapshot, set }) =>
({
instanceIdToCopyFrom,
instanceIdToCopyTo,
}: {
instanceIdToCopyFrom: string;
instanceIdToCopyTo: string;
}) => {
const contextStoreCurrentObjectMetadataId = snapshot
.getLoadable(
contextStoreCurrentObjectMetadataIdComponentState.atomFamily({
instanceId: instanceIdToCopyFrom,
}),
)
.getValue();
set(
contextStoreCurrentObjectMetadataIdComponentState.atomFamily({
instanceId: instanceIdToCopyTo,
}),
contextStoreCurrentObjectMetadataId,
);
const contextStoreTargetedRecordsRule = snapshot
.getLoadable(
contextStoreTargetedRecordsRuleComponentState.atomFamily({
instanceId: instanceIdToCopyFrom,
}),
)
.getValue();
set(
contextStoreTargetedRecordsRuleComponentState.atomFamily({
instanceId: instanceIdToCopyTo,
}),
contextStoreTargetedRecordsRule,
);
const contextStoreNumberOfSelectedRecords = snapshot
.getLoadable(
contextStoreNumberOfSelectedRecordsComponentState.atomFamily({
instanceId: instanceIdToCopyFrom,
}),
)
.getValue();
set(
contextStoreNumberOfSelectedRecordsComponentState.atomFamily({
instanceId: instanceIdToCopyTo,
}),
contextStoreNumberOfSelectedRecords,
);
const contextStoreFilters = snapshot
.getLoadable(
contextStoreFiltersComponentState.atomFamily({
instanceId: instanceIdToCopyFrom,
}),
)
.getValue();
set(
contextStoreFiltersComponentState.atomFamily({
instanceId: instanceIdToCopyTo,
}),
contextStoreFilters,
);
const contextStoreCurrentViewId = snapshot
.getLoadable(
contextStoreCurrentViewIdComponentState.atomFamily({
instanceId: instanceIdToCopyFrom,
}),
)
.getValue();
set(
contextStoreCurrentViewIdComponentState.atomFamily({
instanceId: instanceIdToCopyTo,
}),
contextStoreCurrentViewId,
);
const contextStoreCurrentViewType = snapshot
.getLoadable(
contextStoreCurrentViewTypeComponentState.atomFamily({
instanceId: instanceIdToCopyFrom,
}),
)
.getValue();
set(
contextStoreCurrentViewTypeComponentState.atomFamily({
instanceId: instanceIdToCopyTo,
}),
contextStoreCurrentViewType,
);
const actionMenuEntries = snapshot
.getLoadable(
actionMenuEntriesComponentState.atomFamily({
instanceId: instanceIdToCopyFrom,
}),
)
.getValue();
set(
actionMenuEntriesComponentState.atomFamily({
instanceId: instanceIdToCopyTo,
}),
actionMenuEntries,
);
},
[],
);
return { copyContextStoreStates };
};

View File

@ -0,0 +1,68 @@
import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState';
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { useRecoilCallback } from 'recoil';
export const useResetContextStoreStates = () => {
const resetContextStoreStates = useRecoilCallback(({ set }) => {
return (instanceId: string) => {
set(
contextStoreCurrentObjectMetadataIdComponentState.atomFamily({
instanceId,
}),
null,
);
set(
contextStoreTargetedRecordsRuleComponentState.atomFamily({
instanceId,
}),
{
mode: 'selection',
selectedRecordIds: [],
},
);
set(
contextStoreNumberOfSelectedRecordsComponentState.atomFamily({
instanceId,
}),
0,
);
set(
contextStoreFiltersComponentState.atomFamily({
instanceId,
}),
[],
);
set(
contextStoreCurrentViewIdComponentState.atomFamily({
instanceId,
}),
null,
);
set(
contextStoreCurrentViewTypeComponentState.atomFamily({
instanceId,
}),
null,
);
set(
actionMenuEntriesComponentState.atomFamily({
instanceId,
}),
new Map(),
);
};
}, []);
return { resetContextStoreStates };
};

View File

@ -0,0 +1,19 @@
import { useCopyContextStoreStates } from '@/command-menu/hooks/useCopyContextStoreAndActionMenuStates';
import { useResetContextStoreStates } from '@/command-menu/hooks/useResetContextStoreStates';
export const useResetPreviousCommandMenuContext = () => {
const { copyContextStoreStates } = useCopyContextStoreStates();
const { resetContextStoreStates } = useResetContextStoreStates();
const resetPreviousCommandMenuContext = () => {
copyContextStoreStates({
instanceIdToCopyFrom: 'command-menu-previous',
instanceIdToCopyTo: 'command-menu',
});
resetContextStoreStates('command-menu-previous');
};
return {
resetPreviousCommandMenuContext,
};
};

View File

@ -0,0 +1,14 @@
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { getObjectRecordIdentifier } from '@/object-metadata/utils/getObjectRecordIdentifier';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { capitalize } from 'twenty-shared';
export const getSelectedRecordsContextText = (
objectMetadataItem: ObjectMetadataItem,
records: ObjectRecord[],
totalCount: number,
) => {
return totalCount === 1
? getObjectRecordIdentifier({ objectMetadataItem, record: records[0] }).name
: `${totalCount} ${capitalize(objectMetadataItem.namePlural)}`;
};

View File

@ -53,6 +53,7 @@ export const useFindManyRecordsSelectedInContextStore = ({
contextStoreTargetedRecordsRule.mode === 'selection' && contextStoreTargetedRecordsRule.mode === 'selection' &&
contextStoreTargetedRecordsRule.selectedRecordIds.length === 0, contextStoreTargetedRecordsRule.selectedRecordIds.length === 0,
limit, limit,
fetchPolicy: 'cache-and-network',
}); });
return { return {

View File

@ -39,6 +39,7 @@ export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
const { objectMetadataItem } = useObjectMetadataItem({ const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular, objectNameSingular,
}); });
const { findManyRecordsQuery } = useFindManyRecordsQuery({ const { findManyRecordsQuery } = useFindManyRecordsQuery({
objectNameSingular, objectNameSingular,
recordGqlFields, recordGqlFields,

View File

@ -8,6 +8,7 @@ import {
import { IconComponent } from '@ui/display'; import { IconComponent } from '@ui/display';
import { useIsMobile } from '@ui/utilities/responsive/hooks/useIsMobile'; import { useIsMobile } from '@ui/utilities/responsive/hooks/useIsMobile';
import { ReactNode } from 'react';
import { MenuItemCommandHotKeys } from './MenuItemCommandHotKeys'; import { MenuItemCommandHotKeys } from './MenuItemCommandHotKeys';
const StyledMenuItemLabelText = styled(StyledMenuItemLabel)` const StyledMenuItemLabelText = styled(StyledMenuItemLabel)`
@ -72,6 +73,7 @@ export type MenuItemCommandProps = {
className?: string; className?: string;
isSelected?: boolean; isSelected?: boolean;
onClick?: () => void; onClick?: () => void;
RightComponent?: ReactNode;
}; };
export const MenuItemCommand = ({ export const MenuItemCommand = ({
@ -82,6 +84,7 @@ export const MenuItemCommand = ({
className, className,
isSelected, isSelected,
onClick, onClick,
RightComponent,
}: MenuItemCommandProps) => { }: MenuItemCommandProps) => {
const theme = useTheme(); const theme = useTheme();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
@ -99,6 +102,7 @@ export const MenuItemCommand = ({
</StyledBigIconContainer> </StyledBigIconContainer>
)} )}
<StyledMenuItemLabelText>{text}</StyledMenuItemLabelText> <StyledMenuItemLabelText>{text}</StyledMenuItemLabelText>
{RightComponent}
</StyledMenuItemLeftContent> </StyledMenuItemLeftContent>
{!isMobile && ( {!isMobile && (
<MenuItemCommandHotKeys <MenuItemCommandHotKeys