Display system objects in Workflow triggers (#11314)

Fixes #11310

Note:
- I changed "system to advanced" because I think we will link that to
the Advanced Mode in the future
- Not sure when to use useCallback / useMemo, AI added some useCallbacks
which I removed but I left some useMemo... Not sure what should be the
rule
- It had to use MenuItem because this sub-menu behavior wasn't available
in the Standard select component. We should probably rename the
"MenuItem" elements to something more generic. I didn't do it in this PR
because I'm not sure about the strategy and it would change a lot of
files.
This commit is contained in:
Félix Malfait
2025-04-01 11:48:52 +02:00
committed by GitHub
parent a26b3f54d6
commit b2012229f4

View File

@ -1,5 +1,13 @@
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { Select } from '@/ui/input/components/Select'; import { SelectControl } from '@/ui/input/components/SelectControl';
import { SelectHotkeyScope } from '@/ui/input/types/SelectHotkeyScope';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { WorkflowDatabaseEventTrigger } from '@/workflow/types/Workflow'; import { WorkflowDatabaseEventTrigger } from '@/workflow/types/Workflow';
import { splitWorkflowTriggerEventName } from '@/workflow/utils/splitWorkflowTriggerEventName'; import { splitWorkflowTriggerEventName } from '@/workflow/utils/splitWorkflowTriggerEventName';
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody'; import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
@ -7,8 +15,34 @@ import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/Workflo
import { getTriggerIcon } from '@/workflow/workflow-trigger/utils/getTriggerIcon'; import { getTriggerIcon } from '@/workflow/workflow-trigger/utils/getTriggerIcon';
import { getTriggerDefaultLabel } from '@/workflow/workflow-trigger/utils/getTriggerLabel'; import { getTriggerDefaultLabel } from '@/workflow/workflow-trigger/utils/getTriggerLabel';
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
import { isDefined } from 'twenty-shared/utils'; import styled from '@emotion/styled';
import { SelectOption, useIcons } from 'twenty-ui'; import { Trans } from '@lingui/react/macro';
import { useCallback, useMemo, useState } from 'react';
import { IconChevronLeft, IconSettings, MenuItem, useIcons } from 'twenty-ui';
const StyledLabel = styled.span`
color: ${({ theme }) => theme.font.color.light};
display: block;
font-size: ${({ theme }) => theme.font.size.xs};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
margin-bottom: ${({ theme }) => theme.spacing(1)};
`;
const StyledContainer = styled.div<{ fullWidth?: boolean }>`
width: ${({ fullWidth }) => (fullWidth ? '100%' : 'auto')};
`;
const DEFAULT_SELECTED_OPTION = { label: 'Select an option', value: '' };
const filterOptionsBySearch = <T extends { label: string }>(
options: T[],
searchValue: string,
): T[] => {
if (!searchValue) return options;
return options.filter((option) =>
option.label.toLowerCase().includes(searchValue.toLowerCase()),
);
};
type WorkflowEditTriggerDatabaseEventFormProps = { type WorkflowEditTriggerDatabaseEventFormProps = {
trigger: WorkflowDatabaseEventTrigger; trigger: WorkflowDatabaseEventTrigger;
@ -29,20 +63,47 @@ export const WorkflowEditTriggerDatabaseEventForm = ({
}: WorkflowEditTriggerDatabaseEventFormProps) => { }: WorkflowEditTriggerDatabaseEventFormProps) => {
const theme = useTheme(); const theme = useTheme();
const { getIcon } = useIcons(); const { getIcon } = useIcons();
const [searchInputValue, setSearchInputValue] = useState('');
const [isSystemObjectsOpen, setIsSystemObjectsOpen] = useState(false);
const { closeDropdown } = useDropdown('workflow-edit-trigger-record-type');
const { activeObjectMetadataItems } = useFilteredObjectMetadataItems(); const { objectMetadataItems } = useFilteredObjectMetadataItems();
const triggerEvent = splitWorkflowTriggerEventName( const triggerEvent = splitWorkflowTriggerEventName(
trigger.settings.eventName, trigger.settings.eventName,
); );
const availableMetadata: Array<SelectOption<string>> = const regularObjects = objectMetadataItems
activeObjectMetadataItems.map((item) => ({ .filter((item) => item.isActive && !item.isSystem)
.map((item) => ({
label: item.labelPlural, label: item.labelPlural,
value: item.nameSingular, value: item.nameSingular,
Icon: getIcon(item.icon), Icon: getIcon(item.icon),
})); }));
const systemObjects = objectMetadataItems
.filter((item) => item.isActive && item.isSystem)
.map((item) => ({
label: item.labelPlural,
value: item.nameSingular,
Icon: getIcon(item.icon),
}));
const selectedOption =
[...regularObjects, ...systemObjects].find(
(option) => option.value === triggerEvent?.objectType,
) || DEFAULT_SELECTED_OPTION;
const filteredRegularObjects = useMemo(
() => filterOptionsBySearch(regularObjects, searchInputValue),
[regularObjects, searchInputValue],
);
const filteredSystemObjects = useMemo(
() => filterOptionsBySearch(systemObjects, searchInputValue),
[systemObjects, searchInputValue],
);
const defaultLabel = const defaultLabel =
getTriggerDefaultLabel({ getTriggerDefaultLabel({
type: 'DATABASE_EVENT', type: 'DATABASE_EVENT',
@ -54,10 +115,40 @@ export const WorkflowEditTriggerDatabaseEventForm = ({
eventName: triggerEvent.event, eventName: triggerEvent.event,
}); });
const headerTitle = isDefined(trigger.name) ? trigger.name : defaultLabel;
const headerType = `Trigger · ${defaultLabel}`; const headerType = `Trigger · ${defaultLabel}`;
const handleOptionClick = (value: string) => {
if (triggerOptions.readonly === true) {
return;
}
triggerOptions.onTriggerUpdate({
...trigger,
settings: {
...trigger.settings,
eventName: `${value}.${triggerEvent.event}`,
},
});
closeDropdown();
};
const handleSystemObjectsClick = () => {
setIsSystemObjectsOpen(true);
setSearchInputValue('');
};
const handleBack = () => {
setIsSystemObjectsOpen(false);
setSearchInputValue('');
};
const handleSearchInputChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
setSearchInputValue(event.target.value);
},
[],
);
return ( return (
<> <>
<WorkflowStepHeader <WorkflowStepHeader
@ -73,34 +164,90 @@ export const WorkflowEditTriggerDatabaseEventForm = ({
}} }}
Icon={getIcon(headerIcon)} Icon={getIcon(headerIcon)}
iconColor={theme.font.color.tertiary} iconColor={theme.font.color.tertiary}
initialTitle={headerTitle} initialTitle={defaultLabel}
headerType={headerType} headerType={headerType}
disabled={triggerOptions.readonly} disabled={triggerOptions.readonly}
/> />
<WorkflowStepBody> <WorkflowStepBody>
<Select <StyledContainer fullWidth>
dropdownId="workflow-edit-trigger-record-type" <StyledLabel>Record Type</StyledLabel>
label="Record Type" <Dropdown
fullWidth dropdownId="workflow-edit-trigger-record-type"
disabled={triggerOptions.readonly} dropdownMenuWidth={300}
value={triggerEvent?.objectType} dropdownPlacement="bottom-start"
emptyOption={{ label: 'Select an option', value: '' }} clickableComponent={
options={availableMetadata} <SelectControl
onChange={(updatedRecordType) => { isDisabled={triggerOptions.readonly}
if (triggerOptions.readonly === true) { selectedOption={selectedOption}
return; />
} }
dropdownComponents={
triggerOptions.onTriggerUpdate({ <>
...trigger, {isSystemObjectsOpen ? (
settings: { <>
...trigger.settings, <DropdownMenuHeader
eventName: `${updatedRecordType}.${triggerEvent.event}`, StartComponent={
}, <DropdownMenuHeaderLeftComponent
}); onClick={handleBack}
}} Icon={IconChevronLeft}
withSearchInput />
/> }
>
<Trans>Advanced</Trans>
</DropdownMenuHeader>
<DropdownMenuSearchInput
autoFocus
value={searchInputValue}
onChange={handleSearchInputChange}
/>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer hasMaxHeight>
{filteredSystemObjects.map((option) => (
<MenuItem
key={option.value}
LeftIcon={option.Icon}
text={option.label}
onClick={() => handleOptionClick(option.value)}
/>
))}
</DropdownMenuItemsContainer>
</>
) : (
<>
<DropdownMenuSearchInput
autoFocus
value={searchInputValue}
onChange={handleSearchInputChange}
/>
<DropdownMenuSeparator />
<DropdownMenuItemsContainer hasMaxHeight>
{filteredRegularObjects.map((option) => (
<MenuItem
key={option.value}
LeftIcon={option.Icon}
text={option.label}
onClick={() => handleOptionClick(option.value)}
/>
))}
{(!searchInputValue ||
'advanced'.includes(
searchInputValue.toLowerCase(),
)) && (
<MenuItem
text="Advanced"
LeftIcon={IconSettings}
onClick={handleSystemObjectsClick}
hasSubMenu
/>
)}
</DropdownMenuItemsContainer>
</>
)}
</>
}
dropdownHotkeyScope={{ scope: SelectHotkeyScope.Select }}
/>
</StyledContainer>
</WorkflowStepBody> </WorkflowStepBody>
</> </>
); );