Compare commits

...

2 Commits

Author SHA1 Message Date
Thomas Trompette 9154f7a4c2 Use datetime design 2026-04-01 14:29:15 +02:00
Thomas Trompette 1ce64f5028 Fix workflow dates 2026-03-31 17:03:55 +02:00
13 changed files with 295 additions and 249 deletions
@@ -56,29 +56,29 @@ import { FieldMetadataType } from 'twenty-shared/types';
import { type JsonValue } from 'type-fest';
type FormFieldInputProps = {
field: Pick<FieldDefinition<FieldMetadata>, 'label' | 'metadata' | 'type'>;
defaultValue: JsonValue;
error?: string;
field: Pick<FieldDefinition<FieldMetadata>, 'label' | 'metadata' | 'type'>;
onChange: (value: JsonValue) => void;
onClear?: () => void;
VariablePicker?: VariablePickerComponent;
readonly?: boolean;
placeholder?: string;
error?: string;
onError?: (error: string | undefined) => void;
placeholder?: string;
readonly?: boolean;
timeZone?: string;
VariablePicker?: VariablePickerComponent;
};
export const FormFieldInput = ({
field,
defaultValue,
error,
field,
onChange,
onClear,
VariablePicker,
readonly,
placeholder,
error,
onError,
placeholder,
readonly,
timeZone,
VariablePicker,
}: FormFieldInputProps) => {
return isFieldNumber(field) || field.type === FieldMetadataType.NUMERIC ? (
<FormNumberFieldInput
@@ -168,21 +168,23 @@ export const FormFieldInput = ({
/>
) : isFieldDate(field) ? (
<FormDateFieldInput
label={field.label}
defaultValue={defaultValue as string | undefined}
error={error}
label={field.label}
onChange={onChange}
VariablePicker={VariablePicker}
readonly={readonly}
onError={onError}
placeholder={placeholder}
readonly={readonly}
VariablePicker={VariablePicker}
/>
) : isFieldDateTime(field) ? (
<FormDateTimeFieldInput
label={field.label}
defaultValue={defaultValue as string | undefined}
label={field.label}
onChange={onChange}
VariablePicker={VariablePicker}
readonly={readonly}
timeZone={timeZone}
VariablePicker={VariablePicker}
/>
) : isFieldMultiSelect(field) ? (
<FormMultiSelectFieldInput
@@ -1,17 +1,16 @@
import { useDateTimeFormat } from '@/localization/hooks/useDateTimeFormat';
import { FormFieldInputContainer } from '@/object-record/record-field/ui/form-types/components/FormFieldInputContainer';
import { FormFieldInputInnerContainer } from '@/object-record/record-field/ui/form-types/components/FormFieldInputInnerContainer';
import { FormFieldInputRowContainer } from '@/object-record/record-field/ui/form-types/components/FormFieldInputRowContainer';
import { VariableChipStandalone } from '@/object-record/record-field/ui/form-types/components/VariableChipStandalone';
import { type VariablePickerComponent } from '@/object-record/record-field/ui/form-types/types/VariablePickerComponent';
import { InputHint } from '@/ui/input/components/InputHint';
import { InputLabel } from '@/ui/input/components/InputLabel';
import { DatePicker } from '@/ui/input/components/internal/date/components/DatePicker';
import { DatePickerInput } from '@/ui/input/components/internal/date/components/DatePickerInput';
import {
MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID,
MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID,
} from '@/ui/input/components/internal/date/components/DateTimePicker';
import { useParseDateInputStringToPlainDate } from '@/ui/input/components/internal/date/hooks/useParseDateInputStringToPlainDate';
import { useParsePlainDateToDateInputString } from '@/ui/input/components/internal/date/hooks/useParsePlainDateToDateInputString';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer';
@@ -20,25 +19,21 @@ import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useLis
import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString';
import { styled } from '@linaria/react';
import { isNonEmptyString } from '@sniptt/guards';
import {
useId,
useRef,
useState,
type ChangeEvent,
type KeyboardEvent,
} from 'react';
import { useId, useRef, useState } from 'react';
import { Key } from 'ts-key-enum';
import { isDefined } from 'twenty-shared/utils';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { type Nullable } from 'twenty-ui/utilities';
import { getDateFormatStringForDatePickerInputMask } from '~/utils/date-utils';
const StyledInputContainerWrapper = styled.div`
display: grid;
flex: 1 1 auto;
grid-template-columns: 1fr;
grid-template-rows: 1fr 0;
min-width: 0;
overflow: visible;
position: relative;
width: 100%;
`;
const StyledDateInputAbsoluteContainer = styled.div`
@@ -46,31 +41,11 @@ const StyledDateInputAbsoluteContainer = styled.div`
top: ${themeCssVariables.spacing[1]};
`;
const StyledDateInput = styled.input<{ hasError?: boolean }>`
background-color: transparent;
border: none;
color: ${({ hasError }) =>
hasError
? themeCssVariables.color.red
: themeCssVariables.font.color.primary};
color: ${themeCssVariables.font.color.primary};
font-family: ${themeCssVariables.font.family};
font-size: inherit;
font-weight: inherit;
outline: none;
&::placeholder,
&::-webkit-input-placeholder {
color: ${themeCssVariables.font.color.light};
font-family: ${themeCssVariables.font.family};
font-weight: ${themeCssVariables.font.weight.medium};
}
&:disabled {
color: ${themeCssVariables.font.color.tertiary};
}
padding: ${themeCssVariables.spacing[0]} ${themeCssVariables.spacing[2]};
const StyledDateInputTextContainer = styled.div`
align-items: center;
display: flex;
min-width: 0;
width: 100%;
`;
const StyledDateInputContainer = styled.div`
@@ -90,29 +65,31 @@ type DraftValue =
};
type FormDateFieldInputProps = {
label?: string;
defaultValue: string | undefined;
error?: string;
label?: string;
onChange: (value: string | null) => void;
onError?: (error: string | undefined) => void;
placeholder?: string;
VariablePicker?: VariablePickerComponent;
readonly?: boolean;
VariablePicker?: VariablePickerComponent;
};
export const FormDateFieldInput = ({
label,
defaultValue,
error: errorFromProps,
label,
onChange,
VariablePicker,
onError,
placeholder: _placeholder,
readonly,
placeholder,
VariablePicker,
}: FormDateFieldInputProps) => {
const instanceId = useId();
const { dateFormat } = useDateTimeFormat();
const { parsePlainDateToDateInputString } =
useParsePlainDateToDateInputString();
const { parseDateInputStringToPlainDate } =
useParseDateInputStringToPlainDate();
const [errorMessage, setErrorMessage] = useState<string | undefined>(
undefined,
);
const [draftValue, setDraftValue] = useState<DraftValue>(
isStandaloneVariableString(defaultValue)
@@ -139,12 +116,6 @@ export const FormDateFieldInput = ({
const datePickerWrapperRef = useRef<HTMLDivElement>(null);
const [inputDate, setInputDate] = useState(
isDefined(draftValueAsDate) && !isStandaloneVariableString(defaultValue)
? parsePlainDateToDateInputString(draftValueAsDate)
: '',
);
const persistDate = (newDate: Nullable<string>) => {
if (!isDefined(newDate)) {
onChange(null);
@@ -159,11 +130,6 @@ export const FormDateFieldInput = ({
const displayDatePicker =
draftValue.type === 'static' && draftValue.mode === 'edit';
const defaultPlaceHolder =
getDateFormatStringForDatePickerInputMask(dateFormat);
const placeholderToDisplay = placeholder ?? defaultPlaceHolder;
useListenClickOutside({
refs: [datePickerWrapperRef],
listenerId: 'FormDateTimeFieldInputBase',
@@ -182,16 +148,15 @@ export const FormDateFieldInput = ({
});
const handlePickerChange = (newDate: Nullable<string>) => {
setErrorMessage(undefined);
onError?.(undefined);
setDraftValue({
type: 'static',
mode: 'edit',
value: newDate ?? null,
});
setInputDate(
isDefined(newDate) ? parsePlainDateToDateInputString(newDate) : '',
);
setPickerDate(newDate);
persistDate(newDate);
@@ -216,6 +181,9 @@ export const FormDateFieldInput = ({
};
const handlePickerClear = () => {
setErrorMessage(undefined);
onError?.(undefined);
setDraftValue({
type: 'static',
value: null,
@@ -224,12 +192,13 @@ export const FormDateFieldInput = ({
setPickerDate(null);
setInputDate('');
persistDate(null);
};
const handlePickerMouseSelect = (newDate: Nullable<string>) => {
setErrorMessage(undefined);
onError?.(undefined);
setDraftValue({
type: 'static',
value: newDate ?? null,
@@ -238,10 +207,6 @@ export const FormDateFieldInput = ({
setPickerDate(newDate);
setInputDate(
isDefined(newDate) ? parsePlainDateToDateInputString(newDate) : '',
);
persistDate(newDate);
};
@@ -253,55 +218,42 @@ export const FormDateFieldInput = ({
});
};
const handleInputChange = (event: ChangeEvent<HTMLInputElement>) => {
setInputDate(event.target.value);
};
const handleMaskedDateChange = (newPlainDate: string | null) => {
setErrorMessage(undefined);
onError?.(undefined);
const handleInputKeydown = (event: KeyboardEvent<HTMLInputElement>) => {
if (event.key !== 'Enter') {
return;
}
const inputDateTime = inputDate.trim();
if (inputDateTime === '') {
if (!isDefined(newPlainDate)) {
handlePickerClear();
return;
}
const parsedInputPlainDate = parseDateInputStringToPlainDate(inputDateTime);
if (!isDefined(parsedInputPlainDate)) {
return;
}
let validatedDate = parsedInputPlainDate;
setDraftValue({
type: 'static',
value: validatedDate,
mode: 'edit',
value: newPlainDate,
});
setPickerDate(validatedDate);
setPickerDate(newPlainDate);
setInputDate(parsePlainDateToDateInputString(validatedDate));
persistDate(validatedDate);
persistDate(newPlainDate);
};
const handleVariableTagInsert = (variableName: string) => {
setErrorMessage(undefined);
onError?.(undefined);
setDraftValue({
type: 'variable',
value: variableName,
});
setInputDate('');
onChange(variableName);
};
const handleUnlinkVariable = () => {
setErrorMessage(undefined);
onError?.(undefined);
setDraftValue({
type: 'static',
value: null,
@@ -313,6 +265,8 @@ export const FormDateFieldInput = ({
onChange(null);
};
const error = errorMessage ?? errorFromProps;
useHotkeysOnFocusedElement({
keys: [Key.Escape],
callback: handlePickerEscape,
@@ -332,16 +286,15 @@ export const FormDateFieldInput = ({
>
{draftValue.type === 'static' ? (
<>
<StyledDateInput
type="text"
placeholder={placeholderToDisplay}
value={inputDate}
onFocus={handleInputFocus}
onChange={handleInputChange}
onKeyDown={handleInputKeydown}
disabled={readonly}
/>
<StyledDateInputTextContainer>
<DatePickerInput
date={pickerDate ?? null}
hasError={isDefined(error)}
onChange={handleMaskedDateChange}
onFocus={handleInputFocus}
readonly={readonly}
/>
</StyledDateInputTextContainer>
{draftValue.mode === 'edit' ? (
<StyledDateInputContainer>
<StyledDateInputAbsoluteContainer>
@@ -377,6 +330,8 @@ export const FormDateFieldInput = ({
/>
) : null}
</FormFieldInputRowContainer>
{error ? <InputHint danger>{error}</InputHint> : null}
</FormFieldInputContainer>
);
};
@@ -28,10 +28,13 @@ import { type Nullable } from 'twenty-ui/utilities';
const StyledInputContainerWrapper = styled.div`
display: grid;
flex: 1 1 auto;
grid-template-columns: 1fr;
grid-template-rows: 1fr 0;
min-width: 0;
overflow: visible;
position: relative;
width: 100%;
`;
const StyledDateInputAbsoluteContainer = styled.div`
@@ -42,6 +45,8 @@ const StyledDateInputAbsoluteContainer = styled.div`
const StyledDateInputTextContainer = styled.div`
align-items: center;
display: flex;
min-width: 0;
width: 100%;
`;
const StyledDateInputContainer = styled.div`
@@ -61,22 +66,22 @@ type DraftValue =
};
type FormDateTimeFieldInputProps = {
label?: string;
defaultValue: string | undefined;
label?: string;
onChange: (value: string | null) => void;
placeholder?: string;
VariablePicker?: VariablePickerComponent;
readonly?: boolean;
timeZone?: string;
VariablePicker?: VariablePickerComponent;
};
export const FormDateTimeFieldInput = ({
label,
defaultValue,
label,
onChange,
VariablePicker,
readonly,
timeZone,
VariablePicker,
}: FormDateTimeFieldInputProps) => {
const instanceId = useId();
@@ -259,6 +264,7 @@ export const FormDateTimeFieldInput = ({
<StyledDateInputTextContainer>
<DateTimePickerInput
date={dateValue}
fullWidth
onChange={handleInputChange}
onFocus={handleInputFocus}
readonly={readonly}
@@ -3,6 +3,7 @@ import { styled } from '@linaria/react';
const StyledFormFieldInputContainer = styled.div`
display: flex;
flex-direction: column;
min-width: 0;
width: 100%;
`;
@@ -23,8 +23,10 @@ const StyledFormFieldInputInnerContainer = styled.div<
border-bottom-left-radius: ${themeCssVariables.border.radius.sm};
border-bottom-right-radius: ${({ multiline, hasRightElement }) =>
multiline || !hasRightElement ? themeCssVariables.border.radius.sm : '0'};
border-right: ${({ multiline, hasRightElement }) =>
multiline || !hasRightElement ? 'auto' : 'none'};
border-right: ${({ hasRightElement }) =>
hasRightElement
? 'none'
: `1px solid ${themeCssVariables.border.color.medium}`};
border-top-left-radius: ${themeCssVariables.border.radius.sm};
border-top-right-radius: ${({ multiline, hasRightElement }) =>
multiline || !hasRightElement ? themeCssVariables.border.radius.sm : '0'};
@@ -9,14 +9,16 @@ const StyledFormFieldInputRowContainer = styled.div<{
display: flex;
flex-direction: row;
height: ${({ multiline }) => (multiline ? 'auto' : '32px')};
line-height: ${({ multiline }) =>
multiline ? `${LINE_HEIGHT}px` : 'normal'};
max-height: ${({ multiline, maxHeight }) =>
multiline ? `${maxHeight ?? 5 * LINE_HEIGHT}px` : 'none'};
min-height: ${({ multiline }) =>
multiline ? `${3 * LINE_HEIGHT}px` : 'auto'};
min-width: 0;
position: relative;
width: 100%;
`;
export const FormFieldInputRowContainer = StyledFormFieldInputRowContainer;
@@ -30,9 +30,13 @@ import { IconLogin2, IconLogout, IconStepInto } from 'twenty-ui/display';
import { themeCssVariables } from 'twenty-ui/theme-constants';
const StyledContainer = styled.div`
align-items: stretch;
box-sizing: border-box;
display: flex;
flex-direction: column;
height: 100%;
min-width: 0;
width: 100%;
`;
const StyledTabListContainer = styled.div`
@@ -22,6 +22,7 @@ const StyledInputContainer = styled.div`
border-top-right-radius: ${themeCssVariables.border.radius.md};
display: flex;
height: ${themeCssVariables.spacing[8]};
min-width: 0;
width: 100%;
`;
@@ -32,19 +33,34 @@ const StyledInput = styled.input<{ hasError?: boolean }>`
hasError
? themeCssVariables.color.red
: themeCssVariables.font.color.primary};
flex: 1 1 auto;
font-size: ${themeCssVariables.font.size.md};
font-weight: 500;
min-width: 0;
outline: none;
padding: 4px 8px 4px 8px;
width: 100%;
&:disabled {
color: ${themeCssVariables.font.color.tertiary};
}
`;
type DatePickerInputProps = {
onChange?: (date: string | null) => void;
date: string | null;
hasError?: boolean;
onChange?: (date: string | null) => void;
onFocus?: () => void;
readonly?: boolean;
};
export const DatePickerInput = ({ date, onChange }: DatePickerInputProps) => {
export const DatePickerInput = ({
date,
hasError,
onChange,
onFocus,
readonly,
}: DatePickerInputProps) => {
const { dateFormat } = useDateTimeFormat();
const [internalDate, setInternalDate] = useState(date);
@@ -71,15 +87,17 @@ export const DatePickerInput = ({ date, onChange }: DatePickerInputProps) => {
? (parsePlainDateToDateInputString(internalDate) ?? undefined)
: undefined;
const { ref, setValue, value } = useIMask(
const { ref, setValue } = useIMask(
{
mask: Date,
pattern,
blocks,
min: MIN_DATE,
max: MAX_DATE,
format: (date: any) =>
isDefined(date) ? parseIMaskJSDateIMaskDateInputString(date) : '',
format: (dateValue: unknown) =>
isDefined(dateValue) && dateValue instanceof Date
? parseIMaskJSDateIMaskDateInputString(dateValue)
: '',
parse: parseIMaskDateInputStringToJSDate,
lazy: false,
autofix: true,
@@ -89,25 +107,35 @@ export const DatePickerInput = ({ date, onChange }: DatePickerInputProps) => {
onComplete: (newValue) => {
const parsedDate = parseDateInputStringToPlainDate(newValue);
onChange?.(parsedDate);
onChange?.(isDefined(parsedDate) ? parsedDate : null);
},
},
);
useEffect(() => {
if (isDefined(date) && internalDate !== date) {
setInternalDate(date);
if (date === internalDate) {
return;
}
setInternalDate(date);
if (isDefined(date)) {
setValue(parsePlainDateToDateInputString(date));
} else {
setValue('');
}
}, [date, internalDate, parsePlainDateToDateInputString, setValue]);
const shouldDisplayReadOnly = readonly === true;
return (
<StyledInputContainer>
<StyledInput
disabled={shouldDisplayReadOnly}
hasError={hasError}
type="text"
ref={ref as any}
value={value}
onChange={() => {}} // Prevent React warning
onFocus={!shouldDisplayReadOnly ? onFocus : undefined}
/>
</StyledInputContainer>
);
@@ -20,30 +20,33 @@ import { isDefined } from 'twenty-shared/utils';
import { isDifferentZonedDateTime } from '~/utils/dates/isDifferentZonedDateTime';
import { themeCssVariables } from 'twenty-ui/theme-constants';
const StyledInputContainer = styled.div`
const StyledInputContainer = styled.div<{ fullWidth?: boolean }>`
align-items: center;
border-top-left-radius: ${themeCssVariables.border.radius.md};
border-top-right-radius: ${themeCssVariables.border.radius.md};
display: flex;
height: ${themeCssVariables.spacing[8]};
min-width: ${({ fullWidth }) => (fullWidth === true ? '0' : 'auto')};
width: 100%;
`;
const StyledInput = styled.input<{ hasError?: boolean }>`
const StyledInput = styled.input<{ fullWidth?: boolean; hasError?: boolean }>`
background: transparent;
border: none;
color: ${themeCssVariables.font.color.primary};
flex: ${({ fullWidth }) => (fullWidth === true ? '1 1 auto' : 'none')};
font-size: ${themeCssVariables.font.size.md};
font-weight: 500;
min-width: ${({ fullWidth }) => (fullWidth === true ? '0' : 'auto')};
outline: none;
padding-left: ${themeCssVariables.spacing[2]};
width: 140px;
width: ${({ fullWidth }) => (fullWidth === true ? '100%' : '140px')};
`;
type DateTimePickerInputProps = {
onChange?: (date: Temporal.ZonedDateTime | null) => void;
date: Temporal.ZonedDateTime | null;
fullWidth?: boolean;
onChange?: (date: Temporal.ZonedDateTime | null) => void;
onFocus?: () => void;
readonly?: boolean;
timeZone?: string;
@@ -51,6 +54,7 @@ type DateTimePickerInputProps = {
export const DateTimePickerInput = ({
date,
fullWidth = false,
onChange,
onFocus,
readonly,
@@ -167,9 +171,10 @@ export const DateTimePickerInput = ({
internalDate?.toInstant() ?? Temporal.Now.instant();
return (
<StyledInputContainer>
<StyledInputContainer fullWidth={fullWidth}>
<StyledInput
disabled={shouldDisplayReadOnly}
fullWidth={fullWidth}
type="text"
ref={ref as any}
onFocus={!shouldDisplayReadOnly ? onFocus : undefined}
@@ -13,17 +13,20 @@ const StyledWorkflowStepBody = styled.div<{
gridTemplateRows?: string;
}>`
background: ${themeCssVariables.background.primary};
box-sizing: border-box;
display: ${({ display }) => display ?? 'flex'};
flex: 1 1 auto;
flex-direction: column;
grid-template-rows: ${({ gridTemplateRows }) => gridTemplateRows ?? 'none'};
height: 100%;
min-width: 0;
overflow: ${({ overflow }) => overflow ?? 'hidden scroll'};
padding-block: ${({ paddingBlock }) =>
paddingBlock ?? themeCssVariables.spacing[4]};
padding-inline: ${({ paddingInline }) =>
paddingInline ?? themeCssVariables.spacing[3]};
row-gap: ${({ rowGap }) => rowGap ?? themeCssVariables.spacing[4]};
width: 100%;
`;
export const WorkflowStepBody = ({
@@ -17,8 +17,8 @@ import { getDefaultFormFieldSettings } from '@/workflow/workflow-steps/workflow-
import { styled } from '@linaria/react';
import { type OnDragEndResponder } from '@hello-pangea/dnd';
import { useLingui } from '@lingui/react/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { useContext, useEffect, useState } from 'react';
import { isNonEmptyString } from '@sniptt/guards';
import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import {
@@ -83,6 +83,8 @@ const StyledTrashButtonContainer = styled.div`
const StyledFormFieldInputContainerWrapper = styled.div`
grid-area: input;
min-width: 0;
width: 100%;
`;
const StyledOpenedSettingsContainer = styled.div`
@@ -93,11 +95,13 @@ const StyledFieldContainer = styled.div<{
readonly?: boolean;
}>`
align-items: center;
align-self: stretch;
background: transparent;
border: none;
cursor: ${({ readonly }) => (readonly ? 'default' : 'pointer')};
display: flex;
font-family: inherit;
min-height: 0;
padding-left: ${themeCssVariables.spacing[2]};
padding-right: ${themeCssVariables.spacing[2]};
@@ -116,10 +120,19 @@ const StyledPlaceholderContainer = styled.div`
width: 100%;
`;
const StyledAddFieldButtonContainer = styled.div`
padding-left: ${themeCssVariables.spacing[7]};
padding-right: ${themeCssVariables.spacing[7]};
const StyledAddFieldButtonOuterGrid = styled.div`
column-gap: ${themeCssVariables.spacing[1]};
display: grid;
grid-template-columns: 24px 1fr 24px;
min-width: 0;
padding-top: ${themeCssVariables.spacing[2]};
width: 100%;
`;
const StyledAddFieldButtonGridCenter = styled.div`
grid-column: 2;
min-width: 0;
width: 100%;
`;
const StyledAddFieldButtonContentContainer = styled.div`
@@ -323,8 +336,7 @@ export const WorkflowEditActionFormBuilder = ({
>
<StyledPlaceholderContainer>
<FormFieldPlaceholder>
{isDefined(field.placeholder) &&
isNonEmptyString(field.placeholder)
{isNonEmptyString(field.placeholder)
? field.placeholder
: getDefaultFormFieldSettings(field.type)
.placeholder}
@@ -391,47 +403,49 @@ export const WorkflowEditActionFormBuilder = ({
/>
{!actionOptions.readonly && (
<StyledAddFieldButtonContainer>
<FormFieldInputContainer>
<FormFieldInputRowContainer>
<FormFieldInputInnerContainer
formFieldInputInstanceId="add-field-button"
hasRightElement={false}
onClick={() => {
const { label, name } = getDefaultFormFieldSettings(
FieldMetadataType.TEXT,
);
<StyledAddFieldButtonOuterGrid>
<StyledAddFieldButtonGridCenter>
<FormFieldInputContainer>
<FormFieldInputRowContainer>
<FormFieldInputInnerContainer
formFieldInputInstanceId="add-field-button"
hasRightElement={false}
onClick={() => {
const { label, name } = getDefaultFormFieldSettings(
FieldMetadataType.TEXT,
);
const newField: WorkflowFormActionField = {
id: v4(),
name,
type: FieldMetadataType.TEXT,
label,
};
const newField: WorkflowFormActionField = {
id: v4(),
name,
type: FieldMetadataType.TEXT,
label,
};
setFormData([...formData, newField]);
setFormData([...formData, newField]);
actionOptions.onActionUpdate({
...action,
settings: {
...action.settings,
input: [...action.settings.input, newField],
},
});
actionOptions.onActionUpdate({
...action,
settings: {
...action.settings,
input: [...action.settings.input, newField],
},
});
setSelectedField(newField.id);
}}
>
<StyledFieldContainer>
<StyledAddFieldButtonContentContainer>
<IconPlus size={theme.icon.size.sm} />
{t`Add Field`}
</StyledAddFieldButtonContentContainer>
</StyledFieldContainer>
</FormFieldInputInnerContainer>
</FormFieldInputRowContainer>
</FormFieldInputContainer>
</StyledAddFieldButtonContainer>
setSelectedField(newField.id);
}}
>
<StyledFieldContainer>
<StyledAddFieldButtonContentContainer>
<IconPlus size={theme.icon.size.sm} />
{t`Add Field`}
</StyledAddFieldButtonContentContainer>
</StyledFieldContainer>
</FormFieldInputInnerContainer>
</FormFieldInputRowContainer>
</FormFieldInputContainer>
</StyledAddFieldButtonGridCenter>
</StyledAddFieldButtonOuterGrid>
)}
</WorkflowStepBody>
{!actionOptions.readonly && <WorkflowStepFooter stepId={action.id} />}
@@ -1,22 +1,44 @@
import { WorkflowStepCmdEnterButton } from '@/workflow/workflow-steps/components/WorkflowStepCmdEnterButton';
import { useSidePanelHistory } from '@/side-panel/hooks/useSidePanelHistory';
import { FormFieldInput } from '@/object-record/record-field/ui/components/FormFieldInput';
import { FormSingleRecordPicker } from '@/object-record/record-field/ui/form-types/components/FormSingleRecordPicker';
import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldMetadata';
import { useSidePanelHistory } from '@/side-panel/hooks/useSidePanelHistory';
import { SidePanelFooter } from '@/ui/layout/side-panel/components/SidePanelFooter';
import { useWorkflowRunIdOrThrow } from '@/workflow/hooks/useWorkflowRunIdOrThrow';
import { type WorkflowFormAction } from '@/workflow/types/Workflow';
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
import { WorkflowStepCmdEnterButton } from '@/workflow/workflow-steps/components/WorkflowStepCmdEnterButton';
import { useUpdateWorkflowRunStep } from '@/workflow/workflow-steps/hooks/useUpdateWorkflowRunStep';
import { WorkflowFormFieldInput } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowFormFieldInput';
import { useSubmitFormStep } from '@/workflow/workflow-steps/workflow-actions/form-action/hooks/useSubmitFormStep';
import { type WorkflowFormActionField } from '@/workflow/workflow-steps/workflow-actions/form-action/types/WorkflowFormActionField';
import { getDefaultFormFieldSettings } from '@/workflow/workflow-steps/workflow-actions/form-action/utils/getDefaultFormFieldSettings';
import { styled } from '@linaria/react';
import { useLingui } from '@lingui/react/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { useEffect, useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { useDebouncedCallback } from 'use-debounce';
const StyledWorkflowFormFillerRoot = styled.div`
box-sizing: border-box;
display: flex;
flex: 1 1 auto;
flex-direction: column;
min-height: 0;
min-width: 0;
width: 100%;
`;
const StyledWorkflowFormFillerFields = styled.div`
display: flex;
flex: 1 1 auto;
flex-direction: column;
gap: ${themeCssVariables.spacing[4]};
min-width: 0;
width: 100%;
`;
export type WorkflowEditActionFormFillerProps = {
action: WorkflowFormAction;
actionOptions: {
@@ -99,86 +121,89 @@ export const WorkflowEditActionFormFiller = ({
}, [saveAction]);
return (
<>
<StyledWorkflowFormFillerRoot>
<WorkflowStepBody>
{formData.map((field) => {
if (field.type === 'RECORD') {
const objectNameSingular = field.settings?.objectName;
<StyledWorkflowFormFillerFields>
{formData.map((field) => {
if (field.type === 'RECORD') {
const objectNameSingular = field.settings?.objectName;
if (!isDefined(objectNameSingular)) {
return null;
if (!isDefined(objectNameSingular)) {
return null;
}
const recordId = field.value?.id;
return (
<FormSingleRecordPicker
key={field.id}
label={field.label}
defaultValue={recordId}
onChange={(recordId) => {
onFieldUpdate({
fieldId: field.id,
value: {
id: recordId,
},
});
}}
objectNameSingulars={[objectNameSingular]}
disabled={actionOptions.readonly}
/>
);
}
const recordId = field.value?.id;
if (field.type === 'SELECT' || field.type === 'MULTI_SELECT') {
const selectedFieldId = field.settings?.selectedFieldId;
return (
<FormSingleRecordPicker
key={field.id}
label={field.label}
defaultValue={recordId}
onChange={(recordId) => {
onFieldUpdate({
fieldId: field.id,
value: {
id: recordId,
},
});
}}
objectNameSingulars={[objectNameSingular]}
disabled={actionOptions.readonly}
/>
);
}
if (!isDefined(selectedFieldId)) {
return null;
}
if (field.type === 'SELECT' || field.type === 'MULTI_SELECT') {
const selectedFieldId = field.settings?.selectedFieldId;
if (!isDefined(selectedFieldId)) {
return null;
return (
<WorkflowFormFieldInput
key={field.id}
fieldMetadataId={selectedFieldId}
defaultValue={field.value}
readonly={actionOptions.readonly}
onChange={(value) => {
onFieldUpdate({
fieldId: field.id,
value,
});
}}
/>
);
}
return (
<WorkflowFormFieldInput
<FormFieldInput
key={field.id}
fieldMetadataId={selectedFieldId}
defaultValue={field.value}
readonly={actionOptions.readonly}
field={{
label: field.label,
type: field.type,
metadata: {} as FieldMetadata,
}}
onChange={(value) => {
onFieldUpdate({
fieldId: field.id,
value,
});
}}
onError={(error) => {
setError(error);
}}
placeholder={
isNonEmptyString(field.placeholder)
? field.placeholder
: getDefaultFormFieldSettings(field.type).placeholder
}
readonly={actionOptions.readonly}
/>
);
}
return (
<FormFieldInput
key={field.id}
field={{
label: field.label,
type: field.type,
metadata: {} as FieldMetadata,
}}
onChange={(value) => {
onFieldUpdate({
fieldId: field.id,
value,
});
}}
defaultValue={field.value}
readonly={actionOptions.readonly}
placeholder={
field.placeholder ??
getDefaultFormFieldSettings(field.type).placeholder
}
onError={(error) => {
setError(error);
}}
/>
);
})}
})}
</StyledWorkflowFormFillerFields>
</WorkflowStepBody>
{!actionOptions.readonly && (
<SidePanelFooter
@@ -191,6 +216,6 @@ export const WorkflowEditActionFormFiller = ({
]}
/>
)}
</>
</StyledWorkflowFormFillerRoot>
);
};
@@ -69,7 +69,6 @@ const mockAction: WorkflowFormAction = {
name: 'date',
label: 'Date',
type: FieldMetadataType.DATE,
placeholder: 'mm/dd/yyyy',
settings: {},
},
],