8414 add records selection context inside the command menu (#8610)

Closes #8414



https://github.com/user-attachments/assets/a6aeb50a-b57d-43db-a839-4627c49b4155
This commit is contained in:
Raphaël Bosi
2024-11-21 17:56:53 +01:00
committed by GitHub
parent 52df5301a8
commit 8f5515cab3
28 changed files with 762 additions and 225 deletions

View File

@ -5,13 +5,21 @@ import { Note } from '@/activities/types/Note';
import { Task } from '@/activities/types/Task';
import { CommandGroup } from '@/command-menu/components/CommandGroup';
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
import { CommandMenuTopBar } from '@/command-menu/components/CommandMenuTopBar';
import { COMMAND_MENU_SEARCH_BAR_HEIGHT } from '@/command-menu/constants/CommandMenuSearchBarHeight';
import { COMMAND_MENU_SEARCH_BAR_PADDING } from '@/command-menu/constants/CommandMenuSearchBarPadding';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { commandMenuCommandsState } from '@/command-menu/states/commandMenuCommandsState';
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
import { Command, CommandType } from '@/command-menu/types/Command';
import {
Command,
CommandScope,
CommandType,
} from '@/command-menu/types/Command';
import { Company } from '@/companies/types/Company';
import { mainContextStoreComponentInstanceIdState } from '@/context-store/states/mainContextStoreComponentInstanceId';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { useKeyboardShortcutMenu } from '@/keyboard-shortcut-menu/hooks/useKeyboardShortcutMenu';
import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
@ -27,6 +35,7 @@ import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import styled from '@emotion/styled';
import { isNonEmptyString } from '@sniptt/guards';
@ -40,16 +49,12 @@ import {
IconComponent,
IconNotes,
IconSparkles,
IconX,
LightIconButton,
isDefined,
} from 'twenty-ui';
import { useDebounce } from 'use-debounce';
import { getLogoUrlFromDomainName } from '~/utils';
import { capitalize } from '~/utils/string/capitalize';
const SEARCH_BAR_HEIGHT = 56;
const SEARCH_BAR_PADDING = 3;
const MOBILE_NAVIGATION_BAR_HEIGHT = 64;
type CommandGroupConfig = {
@ -80,48 +85,6 @@ const StyledCommandMenu = styled.div`
z-index: 1000;
`;
const StyledInputContainer = styled.div`
align-items: center;
background-color: ${({ theme }) => theme.background.transparent.lighter};
border: none;
border-bottom: 1px solid ${({ theme }) => theme.border.color.medium};
border-radius: 0;
display: flex;
font-size: ${({ theme }) => theme.font.size.lg};
height: ${SEARCH_BAR_HEIGHT}px;
margin: 0;
outline: none;
position: relative;
padding: 0 ${({ theme }) => theme.spacing(SEARCH_BAR_PADDING)};
`;
const StyledInput = styled.input`
border: none;
border-radius: 0;
background-color: transparent;
color: ${({ theme }) => theme.font.color.primary};
font-size: ${({ theme }) => theme.font.size.md};
margin: 0;
outline: none;
height: 24px;
padding: 0;
width: ${({ theme }) => `calc(100% - ${theme.spacing(8)})`};
&::placeholder {
color: ${({ theme }) => theme.font.color.light};
font-weight: ${({ theme }) => theme.font.weight.medium};
}
`;
const StyledCloseButtonContainer = styled.div`
align-items: center;
display: flex;
height: 32px;
justify-content: center;
`;
const StyledList = styled.div`
background: ${({ theme }) => theme.background.secondary};
overscroll-behavior: contain;
@ -132,10 +95,12 @@ const StyledList = styled.div`
const StyledInnerList = styled.div<{ isMobile: boolean }>`
max-height: ${({ isMobile }) =>
isMobile
? `calc(100dvh - ${SEARCH_BAR_HEIGHT}px - ${
SEARCH_BAR_PADDING * 2
? `calc(100dvh - ${COMMAND_MENU_SEARCH_BAR_HEIGHT}px - ${
COMMAND_MENU_SEARCH_BAR_PADDING * 2
}px - ${MOBILE_NAVIGATION_BAR_HEIGHT}px)`
: `calc(100dvh - ${SEARCH_BAR_HEIGHT}px - ${SEARCH_BAR_PADDING * 2}px)`};
: `calc(100dvh - ${COMMAND_MENU_SEARCH_BAR_HEIGHT}px - ${
COMMAND_MENU_SEARCH_BAR_PADDING * 2
}px)`};
padding-left: ${({ theme }) => theme.spacing(2)};
padding-right: ${({ theme }) => theme.spacing(2)};
padding-top: ${({ theme }) => theme.spacing(1)};
@ -165,9 +130,14 @@ export const CommandMenu = () => {
const [deferredCommandMenuSearch] = useDebounce(commandMenuSearch, 300); // 200ms - 500ms
const commandMenuCommands = useRecoilValue(commandMenuCommandsState);
const { closeKeyboardShortcutMenu } = useKeyboardShortcutMenu();
const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setCommandMenuSearch(event.target.value);
};
const setContextStoreTargetedRecordsRule = useSetRecoilComponentStateV2(
contextStoreTargetedRecordsRuleComponentState,
);
const setContextStoreNumberOfSelectedRecords = useSetRecoilComponentStateV2(
contextStoreNumberOfSelectedRecordsComponentState,
);
const isMobile = useIsMobile();
@ -190,6 +160,25 @@ export const CommandMenu = () => {
[closeCommandMenu],
);
useScopedHotkeys(
[Key.Backspace, Key.Delete],
() => {
if (!isNonEmptyString(commandMenuSearch)) {
setContextStoreTargetedRecordsRule({
mode: 'selection',
selectedRecordIds: [],
});
setContextStoreNumberOfSelectedRecords(0);
}
},
AppHotkeyScope.CommandMenuOpen,
[closeCommandMenu],
{
preventDefault: false,
},
);
const {
matchesSearchFilterObjectRecordsQueryResult,
matchesSearchFilterObjectRecordsLoading: loading,
@ -378,20 +367,45 @@ export const CommandMenu = () => {
: true) && cmd.type === CommandType.Create,
);
const matchingStandardActionCommands = commandMenuCommands.filter(
const matchingStandardActionRecordSelectionCommands =
commandMenuCommands.filter(
(cmd) =>
(deferredCommandMenuSearch.length > 0
? checkInShortcuts(cmd, deferredCommandMenuSearch) ||
checkInLabels(cmd, deferredCommandMenuSearch)
: true) &&
cmd.type === CommandType.StandardAction &&
cmd.scope === CommandScope.RecordSelection,
);
const matchingStandardActionGlobalCommands = commandMenuCommands.filter(
(cmd) =>
(deferredCommandMenuSearch.length > 0
? checkInShortcuts(cmd, deferredCommandMenuSearch) ||
checkInLabels(cmd, deferredCommandMenuSearch)
: true) && cmd.type === CommandType.StandardAction,
: true) &&
cmd.type === CommandType.StandardAction &&
cmd.scope === CommandScope.Global,
);
const matchingWorkflowRunCommands = commandMenuCommands.filter(
const matchingWorkflowRunRecordSelectionCommands = commandMenuCommands.filter(
(cmd) =>
(deferredCommandMenuSearch.length > 0
? checkInShortcuts(cmd, deferredCommandMenuSearch) ||
checkInLabels(cmd, deferredCommandMenuSearch)
: true) && cmd.type === CommandType.WorkflowRun,
: true) &&
cmd.type === CommandType.WorkflowRun &&
cmd.scope === CommandScope.RecordSelection,
);
const matchingWorkflowRunGlobalCommands = commandMenuCommands.filter(
(cmd) =>
(deferredCommandMenuSearch.length > 0
? checkInShortcuts(cmd, deferredCommandMenuSearch) ||
checkInLabels(cmd, deferredCommandMenuSearch)
: true) &&
cmd.type === CommandType.WorkflowRun &&
cmd.scope === CommandScope.Global,
);
useListenClickOutside({
@ -419,8 +433,10 @@ export const CommandMenu = () => {
const selectableItemIds = copilotCommands
.map((cmd) => cmd.id)
.concat(matchingStandardActionCommands.map((cmd) => cmd.id))
.concat(matchingWorkflowRunCommands.map((cmd) => cmd.id))
.concat(matchingStandardActionRecordSelectionCommands.map((cmd) => cmd.id))
.concat(matchingWorkflowRunRecordSelectionCommands.map((cmd) => cmd.id))
.concat(matchingStandardActionGlobalCommands.map((cmd) => cmd.id))
.concat(matchingWorkflowRunGlobalCommands.map((cmd) => cmd.id))
.concat(matchingCreateCommand.map((cmd) => cmd.id))
.concat(matchingNavigateCommand.map((cmd) => cmd.id))
.concat(people?.map((person) => person.id))
@ -437,8 +453,10 @@ export const CommandMenu = () => {
);
const isNoResults =
!matchingStandardActionCommands.length &&
!matchingWorkflowRunCommands.length &&
!matchingStandardActionRecordSelectionCommands.length &&
!matchingWorkflowRunRecordSelectionCommands.length &&
!matchingStandardActionGlobalCommands.length &&
!matchingWorkflowRunGlobalCommands.length &&
!matchingCreateCommand.length &&
!matchingNavigateCommand.length &&
!people?.length &&
@ -450,10 +468,6 @@ export const CommandMenu = () => {
const isLoading = loading || isNotesLoading || isTasksLoading;
const mainContextStoreComponentInstanceId = useRecoilValue(
mainContextStoreComponentInstanceIdState,
);
const commandGroups: CommandGroupConfig[] = [
{
heading: 'Navigate',
@ -575,24 +589,10 @@ export const CommandMenu = () => {
<>
{isCommandMenuOpened && (
<StyledCommandMenu ref={commandMenuRef} className="command-menu">
<StyledInputContainer>
<StyledInput
autoFocus
value={commandMenuSearch}
placeholder="Search"
onChange={handleSearchChange}
/>
{!isMobile && (
<StyledCloseButtonContainer>
<LightIconButton
accent={'tertiary'}
size={'medium'}
Icon={IconX}
onClick={closeCommandMenu}
/>
</StyledCloseButtonContainer>
)}
</StyledInputContainer>
<CommandMenuTopBar
commandMenuSearch={commandMenuSearch}
setCommandMenuSearch={setCommandMenuSearch}
/>
<StyledList>
<ScrollWrapper contextProviderName="commandMenu">
<StyledInnerList isMobile={isMobile}>
@ -632,45 +632,83 @@ export const CommandMenu = () => {
</SelectableItem>
</CommandGroup>
)}
{mainContextStoreComponentInstanceId && (
<>
<CommandGroup heading="Standard Actions">
{matchingStandardActionCommands?.map(
(standardActionCommand) => (
<SelectableItem
itemId={standardActionCommand.id}
key={standardActionCommand.id}
>
<CommandMenuItem
id={standardActionCommand.id}
label={standardActionCommand.label}
Icon={standardActionCommand.Icon}
onClick={standardActionCommand.onCommandClick}
/>
</SelectableItem>
),
)}
</CommandGroup>
<CommandGroup heading="Workflows">
{matchingWorkflowRunCommands?.map(
(workflowRunCommand) => (
<SelectableItem
itemId={workflowRunCommand.id}
key={workflowRunCommand.id}
>
<CommandMenuItem
id={workflowRunCommand.id}
label={workflowRunCommand.label}
Icon={workflowRunCommand.Icon}
onClick={workflowRunCommand.onCommandClick}
/>
</SelectableItem>
),
)}
</CommandGroup>
</>
<CommandGroup heading="Record Selection">
{matchingStandardActionRecordSelectionCommands?.map(
(standardActionrecordSelectionCommand) => (
<SelectableItem
itemId={standardActionrecordSelectionCommand.id}
key={standardActionrecordSelectionCommand.id}
>
<CommandMenuItem
id={standardActionrecordSelectionCommand.id}
label={standardActionrecordSelectionCommand.label}
Icon={standardActionrecordSelectionCommand.Icon}
onClick={
standardActionrecordSelectionCommand.onCommandClick
}
/>
</SelectableItem>
),
)}
{matchingWorkflowRunRecordSelectionCommands?.map(
(workflowRunRecordSelectionCommand) => (
<SelectableItem
itemId={workflowRunRecordSelectionCommand.id}
key={workflowRunRecordSelectionCommand.id}
>
<CommandMenuItem
id={workflowRunRecordSelectionCommand.id}
label={workflowRunRecordSelectionCommand.label}
Icon={workflowRunRecordSelectionCommand.Icon}
onClick={
workflowRunRecordSelectionCommand.onCommandClick
}
/>
</SelectableItem>
),
)}
</CommandGroup>
{matchingStandardActionGlobalCommands?.length > 0 && (
<CommandGroup heading="View">
{matchingStandardActionGlobalCommands?.map(
(standardActionGlobalCommand) => (
<SelectableItem
itemId={standardActionGlobalCommand.id}
key={standardActionGlobalCommand.id}
>
<CommandMenuItem
id={standardActionGlobalCommand.id}
label={standardActionGlobalCommand.label}
Icon={standardActionGlobalCommand.Icon}
onClick={
standardActionGlobalCommand.onCommandClick
}
/>
</SelectableItem>
),
)}
</CommandGroup>
)}
{matchingWorkflowRunGlobalCommands?.length > 0 && (
<CommandGroup heading="Workflows">
{matchingWorkflowRunGlobalCommands?.map(
(workflowRunGlobalCommand) => (
<SelectableItem
itemId={workflowRunGlobalCommand.id}
key={workflowRunGlobalCommand.id}
>
<CommandMenuItem
id={workflowRunGlobalCommand.id}
label={workflowRunGlobalCommand.label}
Icon={workflowRunGlobalCommand.Icon}
onClick={workflowRunGlobalCommand.onCommandClick}
/>
</SelectableItem>
),
)}
</CommandGroup>
)}
{commandGroups.map(({ heading, items, renderItem }) =>
items?.length ? (
<CommandGroup heading={heading} key={heading}>