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:
@ -1,5 +1,13 @@
|
||||
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 { splitWorkflowTriggerEventName } from '@/workflow/utils/splitWorkflowTriggerEventName';
|
||||
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 { getTriggerDefaultLabel } from '@/workflow/workflow-trigger/utils/getTriggerLabel';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { SelectOption, useIcons } from 'twenty-ui';
|
||||
import styled from '@emotion/styled';
|
||||
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 = {
|
||||
trigger: WorkflowDatabaseEventTrigger;
|
||||
@ -29,20 +63,47 @@ export const WorkflowEditTriggerDatabaseEventForm = ({
|
||||
}: WorkflowEditTriggerDatabaseEventFormProps) => {
|
||||
const theme = useTheme();
|
||||
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(
|
||||
trigger.settings.eventName,
|
||||
);
|
||||
|
||||
const availableMetadata: Array<SelectOption<string>> =
|
||||
activeObjectMetadataItems.map((item) => ({
|
||||
const regularObjects = objectMetadataItems
|
||||
.filter((item) => item.isActive && !item.isSystem)
|
||||
.map((item) => ({
|
||||
label: item.labelPlural,
|
||||
value: item.nameSingular,
|
||||
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 =
|
||||
getTriggerDefaultLabel({
|
||||
type: 'DATABASE_EVENT',
|
||||
@ -54,10 +115,40 @@ export const WorkflowEditTriggerDatabaseEventForm = ({
|
||||
eventName: triggerEvent.event,
|
||||
});
|
||||
|
||||
const headerTitle = isDefined(trigger.name) ? trigger.name : 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 (
|
||||
<>
|
||||
<WorkflowStepHeader
|
||||
@ -73,34 +164,90 @@ export const WorkflowEditTriggerDatabaseEventForm = ({
|
||||
}}
|
||||
Icon={getIcon(headerIcon)}
|
||||
iconColor={theme.font.color.tertiary}
|
||||
initialTitle={headerTitle}
|
||||
initialTitle={defaultLabel}
|
||||
headerType={headerType}
|
||||
disabled={triggerOptions.readonly}
|
||||
/>
|
||||
<WorkflowStepBody>
|
||||
<Select
|
||||
dropdownId="workflow-edit-trigger-record-type"
|
||||
label="Record Type"
|
||||
fullWidth
|
||||
disabled={triggerOptions.readonly}
|
||||
value={triggerEvent?.objectType}
|
||||
emptyOption={{ label: 'Select an option', value: '' }}
|
||||
options={availableMetadata}
|
||||
onChange={(updatedRecordType) => {
|
||||
if (triggerOptions.readonly === true) {
|
||||
return;
|
||||
<StyledContainer fullWidth>
|
||||
<StyledLabel>Record Type</StyledLabel>
|
||||
<Dropdown
|
||||
dropdownId="workflow-edit-trigger-record-type"
|
||||
dropdownMenuWidth={300}
|
||||
dropdownPlacement="bottom-start"
|
||||
clickableComponent={
|
||||
<SelectControl
|
||||
isDisabled={triggerOptions.readonly}
|
||||
selectedOption={selectedOption}
|
||||
/>
|
||||
}
|
||||
|
||||
triggerOptions.onTriggerUpdate({
|
||||
...trigger,
|
||||
settings: {
|
||||
...trigger.settings,
|
||||
eventName: `${updatedRecordType}.${triggerEvent.event}`,
|
||||
},
|
||||
});
|
||||
}}
|
||||
withSearchInput
|
||||
/>
|
||||
dropdownComponents={
|
||||
<>
|
||||
{isSystemObjectsOpen ? (
|
||||
<>
|
||||
<DropdownMenuHeader
|
||||
StartComponent={
|
||||
<DropdownMenuHeaderLeftComponent
|
||||
onClick={handleBack}
|
||||
Icon={IconChevronLeft}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user