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 { 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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user