Make workflow step name editable (#8677)

- Use TextInput in header title
- add onTitleChange prop
- rename field name instead of label

To fix :
- padding right on title comes from current TextInput component. It
needs to be refactored


https://github.com/user-attachments/assets/535cd6d3-866b-4a61-9c5d-cdbe7710396a
This commit is contained in:
Thomas Trompette
2024-11-22 16:25:01 +01:00
committed by GitHub
parent 4d8445a34a
commit 5ec6cb0e6f
24 changed files with 217 additions and 157 deletions

View File

@ -39,7 +39,7 @@ export const RightDrawerWorkflowSelectTriggerTypeContent = ({
<MenuItem
key={action.type}
LeftIcon={action.icon}
text={action.label}
text={action.name}
onClick={async () => {
await updateTrigger(
getTriggerDefaultDefinition({

View File

@ -2,7 +2,7 @@ import { WorkflowDiagramStepNodeData } from '@/workflow/types/WorkflowDiagram';
import styled from '@emotion/styled';
import { Handle, Position } from '@xyflow/react';
import React from 'react';
import { isDefined } from 'twenty-ui';
import { isDefined, OverflowingTextWithTooltip } from 'twenty-ui';
import { capitalize } from '~/utils/string/capitalize';
type Variant = 'placeholder';
@ -68,6 +68,7 @@ const StyledStepNodeLabel = styled.div<{ variant?: Variant }>`
variant === 'placeholder'
? theme.font.color.extraLight
: theme.font.color.primary};
max-width: 200px;
`;
const StyledSourceHandle = styled(Handle)`
@ -90,13 +91,13 @@ const StyledRightFloatingElementContainer = styled.div`
export const WorkflowDiagramBaseStepNode = ({
nodeType,
label,
name,
variant,
Icon,
RightFloatingElement,
}: {
nodeType: WorkflowDiagramStepNodeData['nodeType'];
label: string;
name: string;
variant?: Variant;
Icon?: React.ReactNode;
RightFloatingElement?: React.ReactNode;
@ -113,7 +114,7 @@ export const WorkflowDiagramBaseStepNode = ({
<StyledStepNodeLabel variant={variant}>
{Icon}
{label}
<OverflowingTextWithTooltip text={name} />
</StyledStepNodeLabel>
{isDefined(RightFloatingElement) ? (

View File

@ -17,7 +17,7 @@ export const WorkflowDiagramEmptyTrigger = () => {
return (
<WorkflowDiagramBaseStepNode
label="Add a Trigger"
name="Add a Trigger"
nodeType="trigger"
variant="placeholder"
Icon={

View File

@ -100,8 +100,8 @@ export const WorkflowDiagramStepNodeBase = ({
return (
<WorkflowDiagramBaseStepNode
name={data.name}
nodeType={data.nodeType}
label={data.label}
Icon={renderStepIcon()}
RightFloatingElement={RightFloatingElement}
/>

View File

@ -119,15 +119,27 @@ export const WorkflowEditActionFormRecordCreate = ({
};
}, [saveAction]);
const headerTitle = isDefined(action.name) ? action.name : `Create Record`;
return (
<WorkflowEditGenericFormBase
onTitleChange={(newName: string) => {
if (actionOptions.readonly === true) {
return;
}
actionOptions.onActionUpdate({
...action,
name: newName,
});
}}
HeaderIcon={
<IconAddressBook
color={theme.font.color.tertiary}
stroke={theme.icon.stroke.sm}
/>
}
headerTitle="Record Create"
headerTitle={headerTitle}
headerType="Action"
>
<Select

View File

@ -166,11 +166,23 @@ export const WorkflowEditActionFormSendEmail = ({
}
});
const headerTitle = isDefined(action.name) ? action.name : 'Send Email';
return (
!loading && (
<WorkflowEditGenericFormBase
onTitleChange={(newName: string) => {
if (actionOptions.readonly === true) {
return;
}
actionOptions.onActionUpdate({
...action,
name: newName,
});
}}
HeaderIcon={<IconMail color={theme.color.blue} />}
headerTitle="Send Email"
headerTitle={headerTitle}
headerType="Email"
>
<Controller

View File

@ -217,10 +217,24 @@ export const WorkflowEditActionFormServerlessFunctionInner = ({
});
};
const headerTitle = isDefined(action.name)
? action.name
: 'Code - Serverless Function';
return (
<WorkflowEditGenericFormBase
onTitleChange={(newName: string) => {
if (actionOptions.readonly === true) {
return;
}
actionOptions?.onActionUpdate({
...action,
name: newName,
});
}}
HeaderIcon={<IconCode color={theme.color.orange} />}
headerTitle="Code - Serverless Function"
headerTitle={headerTitle}
headerType="Code"
>
<Select

View File

@ -1,5 +1,7 @@
import { TextInput } from '@/ui/field/input/components/TextInput';
import styled from '@emotion/styled';
import React from 'react';
import { useDebouncedCallback } from 'use-debounce';
const StyledHeader = styled.div`
background-color: ${({ theme }) => theme.background.secondary};
@ -40,22 +42,36 @@ const StyledContentContainer = styled.div`
`;
export const WorkflowEditGenericFormBase = ({
onTitleChange,
HeaderIcon,
headerTitle,
headerType,
children,
}: {
onTitleChange: (newTitle: string) => void;
HeaderIcon: React.ReactNode;
headerTitle: string;
headerType: string;
children: React.ReactNode;
}) => {
const debouncedOnTitleChange = useDebouncedCallback(onTitleChange, 100);
return (
<>
<StyledHeader>
<StyledHeaderIconContainer>{HeaderIcon}</StyledHeaderIconContainer>
<StyledHeaderTitle>{headerTitle}</StyledHeaderTitle>
<StyledHeaderTitle>
<TextInput
value={headerTitle}
copyButton={false}
hotkeyScope="workflow-step-title"
onEnter={onTitleChange}
onEscape={onTitleChange}
onChange={debouncedOnTitleChange}
shouldTrim={false}
/>
</StyledHeaderTitle>
<StyledHeaderType>{headerType}</StyledHeaderType>
</StyledHeader>

View File

@ -1,50 +1,12 @@
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { Select, SelectOption } from '@/ui/input/components/Select';
import { WorkflowEditGenericFormBase } from '@/workflow/components/WorkflowEditGenericFormBase';
import { OBJECT_EVENT_TRIGGERS } from '@/workflow/constants/ObjectEventTriggers';
import { WorkflowDatabaseEventTrigger } from '@/workflow/types/Workflow';
import { splitWorkflowTriggerEventName } from '@/workflow/utils/splitWorkflowTriggerEventName';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconPlaylistAdd, isDefined } from 'twenty-ui';
const StyledTriggerHeader = styled.div`
background-color: ${({ theme }) => theme.background.secondary};
border-bottom: 1px solid ${({ theme }) => theme.border.color.medium};
display: flex;
flex-direction: column;
padding: ${({ theme }) => theme.spacing(6)};
`;
const StyledTriggerHeaderTitle = styled.p`
color: ${({ theme }) => theme.font.color.primary};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
font-size: ${({ theme }) => theme.font.size.xl};
margin: ${({ theme }) => theme.spacing(3)} 0;
`;
const StyledTriggerHeaderType = styled.p`
color: ${({ theme }) => theme.font.color.tertiary};
margin: 0;
`;
const StyledTriggerHeaderIconContainer = styled.div`
align-self: flex-start;
display: flex;
justify-content: center;
align-items: center;
background-color: ${({ theme }) => theme.background.transparent.light};
border-radius: ${({ theme }) => theme.border.radius.xs};
padding: ${({ theme }) => theme.spacing(1)};
`;
const StyledTriggerSettings = styled.div`
padding: ${({ theme }) => theme.spacing(6)};
display: flex;
flex-direction: column;
row-gap: ${({ theme }) => theme.spacing(4)};
`;
type WorkflowEditTriggerDatabaseEventFormProps = {
trigger: WorkflowDatabaseEventTrigger;
triggerOptions:
@ -87,92 +49,98 @@ export const WorkflowEditTriggerDatabaseEventForm = ({
)
: undefined;
const headerTitle = isDefined(trigger.name)
? trigger.name
: isDefined(recordTypeMetadata) && isDefined(selectedEvent)
? `When a ${recordTypeMetadata.labelSingular} is ${selectedEvent.label}`
: '-';
const headerType = isDefined(selectedEvent)
? `Trigger · Record is ${selectedEvent.label}`
: '-';
return (
<>
<StyledTriggerHeader>
<StyledTriggerHeaderIconContainer>
<IconPlaylistAdd color={theme.font.color.tertiary} />
</StyledTriggerHeaderIconContainer>
<WorkflowEditGenericFormBase
onTitleChange={(newName: string) => {
if (triggerOptions.readonly === true) {
return;
}
<StyledTriggerHeaderTitle>
{isDefined(recordTypeMetadata) && isDefined(selectedEvent)
? `When a ${recordTypeMetadata.labelSingular} is ${selectedEvent.label}`
: '-'}
</StyledTriggerHeaderTitle>
triggerOptions.onTriggerUpdate({
...trigger,
name: newName,
});
}}
HeaderIcon={<IconPlaylistAdd color={theme.font.color.tertiary} />}
headerTitle={headerTitle}
headerType={headerType}
>
<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;
}
<StyledTriggerHeaderType>
{isDefined(selectedEvent)
? `Trigger · Record is ${selectedEvent.label}`
: '-'}
</StyledTriggerHeaderType>
</StyledTriggerHeader>
<StyledTriggerSettings>
<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;
}
triggerOptions.onTriggerUpdate(
isDefined(trigger) && isDefined(triggerEvent)
? {
...trigger,
settings: {
...trigger.settings,
eventName: `${updatedRecordType}.${triggerEvent.event}`,
},
}
: {
type: 'DATABASE_EVENT',
settings: {
eventName: `${updatedRecordType}.${OBJECT_EVENT_TRIGGERS[0].value}`,
outputSchema: {},
},
triggerOptions.onTriggerUpdate(
isDefined(trigger) && isDefined(triggerEvent)
? {
...trigger,
settings: {
...trigger.settings,
eventName: `${updatedRecordType}.${triggerEvent.event}`,
},
);
}}
/>
<Select
dropdownId="workflow-edit-trigger-event-type"
label="Event type"
fullWidth
value={triggerEvent?.event}
emptyOption={{ label: 'Select an option', value: '' }}
options={OBJECT_EVENT_TRIGGERS}
disabled={triggerOptions.readonly}
onChange={(updatedEvent) => {
if (triggerOptions.readonly === true) {
return;
}
triggerOptions.onTriggerUpdate(
isDefined(trigger) && isDefined(triggerEvent)
? {
...trigger,
settings: {
...trigger.settings,
eventName: `${triggerEvent.objectType}.${updatedEvent}`,
},
}
: {
type: 'DATABASE_EVENT',
settings: {
eventName: `${availableMetadata[0].value}.${updatedEvent}`,
outputSchema: {},
},
}
: {
name: headerTitle,
type: 'DATABASE_EVENT',
settings: {
eventName: `${updatedRecordType}.${OBJECT_EVENT_TRIGGERS[0].value}`,
outputSchema: {},
},
);
}}
/>
</StyledTriggerSettings>
</>
},
);
}}
/>
<Select
dropdownId="workflow-edit-trigger-event-type"
label="Event type"
fullWidth
value={triggerEvent?.event}
emptyOption={{ label: 'Select an option', value: '' }}
options={OBJECT_EVENT_TRIGGERS}
disabled={triggerOptions.readonly}
onChange={(updatedEvent) => {
if (triggerOptions.readonly === true) {
return;
}
triggerOptions.onTriggerUpdate(
isDefined(trigger) && isDefined(triggerEvent)
? {
...trigger,
settings: {
...trigger.settings,
eventName: `${triggerEvent.objectType}.${updatedEvent}`,
},
}
: {
name: headerTitle,
type: 'DATABASE_EVENT',
settings: {
eventName: `${availableMetadata[0].value}.${updatedEvent}`,
outputSchema: {},
},
},
);
}}
/>
</WorkflowEditGenericFormBase>
);
};

View File

@ -44,10 +44,22 @@ export const WorkflowEditTriggerManualForm = ({
? 'WHEN_RECORD_SELECTED'
: 'EVERYWHERE';
const headerTitle = isDefined(trigger.name) ? trigger.name : 'Manual Trigger';
return (
<WorkflowEditGenericFormBase
onTitleChange={(newName: string) => {
if (triggerOptions.readonly === true) {
return;
}
triggerOptions.onTriggerUpdate({
...trigger,
name: newName,
});
}}
HeaderIcon={<IconHandMove color={theme.font.color.tertiary} />}
headerTitle="Manual Trigger"
headerTitle={headerTitle}
headerType="Trigger · Manual"
>
<Select