Form action field base settings (#11035)
- Add settings for text and number fields - Settings are for now the same but I still separated with two components because they will evolve https://github.com/user-attachments/assets/96b7fffd-c3a1-45b9-aeaa-45d63505de3c
This commit is contained in:
@ -4,12 +4,12 @@ import { FormFieldInputRowContainer } from '@/object-record/record-field/form-ty
|
|||||||
import { TextVariableEditor } from '@/object-record/record-field/form-types/components/TextVariableEditor';
|
import { TextVariableEditor } from '@/object-record/record-field/form-types/components/TextVariableEditor';
|
||||||
import { useTextVariableEditor } from '@/object-record/record-field/form-types/hooks/useTextVariableEditor';
|
import { useTextVariableEditor } from '@/object-record/record-field/form-types/hooks/useTextVariableEditor';
|
||||||
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
|
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
|
||||||
|
import { InputErrorHelper } from '@/ui/input/components/InputErrorHelper';
|
||||||
|
import { InputHint } from '@/ui/input/components/InputHint';
|
||||||
import { InputLabel } from '@/ui/input/components/InputLabel';
|
import { InputLabel } from '@/ui/input/components/InputLabel';
|
||||||
import { parseEditorContent } from '@/workflow/workflow-variables/utils/parseEditorContent';
|
import { parseEditorContent } from '@/workflow/workflow-variables/utils/parseEditorContent';
|
||||||
import { useId } from 'react';
|
import { useId } from 'react';
|
||||||
import { isDefined } from 'twenty-shared';
|
import { isDefined } from 'twenty-shared';
|
||||||
import { InputErrorHelper } from '@/ui/input/components/InputErrorHelper';
|
|
||||||
import { InputHint } from '@/ui/input/components/InputHint';
|
|
||||||
|
|
||||||
type FormTextFieldInputProps = {
|
type FormTextFieldInputProps = {
|
||||||
label?: string;
|
label?: string;
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { Editor, EditorContent } from '@tiptap/react';
|
import { Editor, EditorContent } from '@tiptap/react';
|
||||||
|
|
||||||
const StyledEditor = styled.div<{ multiline?: boolean; readonly?: boolean }>`
|
const StyledEditor = styled.div<{
|
||||||
|
multiline?: boolean;
|
||||||
|
readonly?: boolean;
|
||||||
|
}>`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|||||||
@ -88,9 +88,12 @@ export const workflowFormActionSettingsSchema =
|
|||||||
z.object({
|
z.object({
|
||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
label: z.string(),
|
label: z.string(),
|
||||||
type: z.nativeEnum(FieldMetadataType),
|
type: z.union([
|
||||||
|
z.literal(FieldMetadataType.TEXT),
|
||||||
|
z.literal(FieldMetadataType.NUMBER),
|
||||||
|
]),
|
||||||
placeholder: z.string().optional(),
|
placeholder: z.string().optional(),
|
||||||
settings: z.record(z.any()),
|
settings: z.record(z.any()).optional(),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -4,9 +4,9 @@ import { getStepDefinitionOrThrow } from '@/workflow/utils/getStepDefinitionOrTh
|
|||||||
import { WorkflowEditActionCreateRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionCreateRecord';
|
import { WorkflowEditActionCreateRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionCreateRecord';
|
||||||
import { WorkflowEditActionDeleteRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionDeleteRecord';
|
import { WorkflowEditActionDeleteRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionDeleteRecord';
|
||||||
import { WorkflowEditActionFindRecords } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFindRecords';
|
import { WorkflowEditActionFindRecords } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionFindRecords';
|
||||||
import { WorkflowEditActionForm } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionForm';
|
|
||||||
import { WorkflowEditActionSendEmail } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionSendEmail';
|
import { WorkflowEditActionSendEmail } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionSendEmail';
|
||||||
import { WorkflowEditActionUpdateRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionUpdateRecord';
|
import { WorkflowEditActionUpdateRecord } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionUpdateRecord';
|
||||||
|
import { WorkflowEditActionForm } from '@/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionForm';
|
||||||
import { WorkflowEditTriggerCronForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerCronForm';
|
import { WorkflowEditTriggerCronForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerCronForm';
|
||||||
import { WorkflowEditTriggerDatabaseEventForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerDatabaseEventForm';
|
import { WorkflowEditTriggerDatabaseEventForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerDatabaseEventForm';
|
||||||
import { WorkflowEditTriggerManualForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerManualForm';
|
import { WorkflowEditTriggerManualForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerManualForm';
|
||||||
|
|||||||
@ -5,11 +5,13 @@ import { InputLabel } from '@/ui/input/components/InputLabel';
|
|||||||
import { WorkflowFormAction } from '@/workflow/types/Workflow';
|
import { WorkflowFormAction } from '@/workflow/types/Workflow';
|
||||||
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
|
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
|
||||||
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
|
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
|
||||||
|
import { WorkflowEditActionFormFieldSettings } from '@/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionFormFieldSettings';
|
||||||
|
import { getDefaultFormFieldSettings } from '@/workflow/workflow-steps/workflow-actions/form-action/utils/getDefaultFormFieldSettings';
|
||||||
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
|
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useLingui } from '@lingui/react/macro';
|
import { useLingui } from '@lingui/react/macro';
|
||||||
import { useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { FieldMetadataType, isDefined } from 'twenty-shared';
|
import { FieldMetadataType, isDefined } from 'twenty-shared';
|
||||||
import {
|
import {
|
||||||
IconChevronDown,
|
IconChevronDown,
|
||||||
@ -18,9 +20,11 @@ import {
|
|||||||
IconTrash,
|
IconTrash,
|
||||||
useIcons,
|
useIcons,
|
||||||
} from 'twenty-ui';
|
} from 'twenty-ui';
|
||||||
|
import { useDebouncedCallback } from 'use-debounce';
|
||||||
|
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
type WorkflowEditActionFormProps = {
|
export type WorkflowEditActionFormProps = {
|
||||||
action: WorkflowFormAction;
|
action: WorkflowFormAction;
|
||||||
actionOptions:
|
actionOptions:
|
||||||
| {
|
| {
|
||||||
@ -32,6 +36,16 @@ type WorkflowEditActionFormProps = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type WorkflowFormActionField = {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
type: FieldMetadataType.TEXT | FieldMetadataType.NUMBER;
|
||||||
|
placeholder?: string;
|
||||||
|
settings?: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type FormData = WorkflowFormActionField[];
|
||||||
|
|
||||||
const StyledRowContainer = styled.div`
|
const StyledRowContainer = styled.div`
|
||||||
column-gap: ${({ theme }) => theme.spacing(1)};
|
column-gap: ${({ theme }) => theme.spacing(1)};
|
||||||
display: grid;
|
display: grid;
|
||||||
@ -92,6 +106,9 @@ export const WorkflowEditActionForm = ({
|
|||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { getIcon } = useIcons();
|
const { getIcon } = useIcons();
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState<FormData>(action.settings.input);
|
||||||
|
|
||||||
const headerTitle = isDefined(action.name) ? action.name : `Form`;
|
const headerTitle = isDefined(action.name) ? action.name : `Form`;
|
||||||
const headerIcon = getActionIcon(action.type);
|
const headerIcon = getActionIcon(action.type);
|
||||||
const [selectedField, setSelectedField] = useState<string | null>(null);
|
const [selectedField, setSelectedField] = useState<string | null>(null);
|
||||||
@ -108,6 +125,40 @@ export const WorkflowEditActionForm = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onFieldUpdate = (updatedField: WorkflowFormActionField) => {
|
||||||
|
if (actionOptions.readonly === true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedFormData = formData.map((currentField) =>
|
||||||
|
currentField.id === updatedField.id ? updatedField : currentField,
|
||||||
|
);
|
||||||
|
|
||||||
|
setFormData(updatedFormData);
|
||||||
|
|
||||||
|
saveAction(updatedFormData);
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveAction = useDebouncedCallback(async (formData: FormData) => {
|
||||||
|
if (actionOptions.readonly === true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
actionOptions.onActionUpdate({
|
||||||
|
...action,
|
||||||
|
settings: {
|
||||||
|
...action.settings,
|
||||||
|
input: formData,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, 1_000);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
saveAction.flush();
|
||||||
|
};
|
||||||
|
}, [saveAction]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<WorkflowStepHeader
|
<WorkflowStepHeader
|
||||||
@ -128,7 +179,7 @@ export const WorkflowEditActionForm = ({
|
|||||||
disabled={actionOptions.readonly}
|
disabled={actionOptions.readonly}
|
||||||
/>
|
/>
|
||||||
<WorkflowStepBody>
|
<WorkflowStepBody>
|
||||||
{action.settings.input.map((field) => (
|
{formData.map((field) => (
|
||||||
<FormFieldInputContainer key={field.id}>
|
<FormFieldInputContainer key={field.id}>
|
||||||
{field.label ? <InputLabel>{field.label}</InputLabel> : null}
|
{field.label ? <InputLabel>{field.label}</InputLabel> : null}
|
||||||
|
|
||||||
@ -166,19 +217,32 @@ export const WorkflowEditActionForm = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updatedFormData = formData.filter(
|
||||||
|
(currentField) => currentField.id !== field.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
setFormData(updatedFormData);
|
||||||
|
|
||||||
actionOptions.onActionUpdate({
|
actionOptions.onActionUpdate({
|
||||||
...action,
|
...action,
|
||||||
settings: {
|
settings: {
|
||||||
...action.settings,
|
...action.settings,
|
||||||
input: action.settings.input.filter(
|
input: updatedFormData,
|
||||||
(f) => f.id !== field.id,
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</StyledIconButtonContainer>
|
</StyledIconButtonContainer>
|
||||||
)}
|
)}
|
||||||
|
{isFieldSelected(field.id) && (
|
||||||
|
<WorkflowEditActionFormFieldSettings
|
||||||
|
field={field}
|
||||||
|
onChange={onFieldUpdate}
|
||||||
|
onClose={() => {
|
||||||
|
setSelectedField(null);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</StyledRowContainer>
|
</StyledRowContainer>
|
||||||
</FormFieldInputContainer>
|
</FormFieldInputContainer>
|
||||||
))}
|
))}
|
||||||
@ -189,20 +253,24 @@ export const WorkflowEditActionForm = ({
|
|||||||
<FormFieldInputInputContainer
|
<FormFieldInputInputContainer
|
||||||
hasRightElement={false}
|
hasRightElement={false}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
const { label, placeholder } = getDefaultFormFieldSettings(
|
||||||
|
FieldMetadataType.TEXT,
|
||||||
|
);
|
||||||
|
|
||||||
|
const newField: WorkflowFormActionField = {
|
||||||
|
id: v4(),
|
||||||
|
type: FieldMetadataType.TEXT,
|
||||||
|
label,
|
||||||
|
placeholder,
|
||||||
|
};
|
||||||
|
|
||||||
|
setFormData([...formData, newField]);
|
||||||
|
|
||||||
actionOptions.onActionUpdate({
|
actionOptions.onActionUpdate({
|
||||||
...action,
|
...action,
|
||||||
settings: {
|
settings: {
|
||||||
...action.settings,
|
...action.settings,
|
||||||
input: [
|
input: [...action.settings.input, newField],
|
||||||
...action.settings.input,
|
|
||||||
{
|
|
||||||
id: v4(),
|
|
||||||
type: FieldMetadataType.TEXT,
|
|
||||||
label: 'New Field',
|
|
||||||
placeholder: 'New Field',
|
|
||||||
settings: {},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@ -0,0 +1,140 @@
|
|||||||
|
import { FormFieldInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputContainer';
|
||||||
|
import { FormSelectFieldInput } from '@/object-record/record-field/form-types/components/FormSelectFieldInput';
|
||||||
|
import { InputLabel } from '@/ui/input/components/InputLabel';
|
||||||
|
import { WorkflowFormActionField } from '@/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionForm';
|
||||||
|
import { WorkflowFormFieldSettingsByType } from '@/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowFormFieldSettingsByType';
|
||||||
|
import { getDefaultFormFieldSettings } from '@/workflow/workflow-steps/workflow-actions/form-action/utils/getDefaultFormFieldSettings';
|
||||||
|
import { useTheme } from '@emotion/react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { t } from '@lingui/core/macro';
|
||||||
|
import { FieldMetadataType } from 'twenty-shared';
|
||||||
|
import {
|
||||||
|
IconSettingsAutomation,
|
||||||
|
IconX,
|
||||||
|
IllustrationIconNumbers,
|
||||||
|
IllustrationIconText,
|
||||||
|
LightIconButton,
|
||||||
|
} from 'twenty-ui';
|
||||||
|
|
||||||
|
type WorkflowEditActionFormFieldSettingsProps = {
|
||||||
|
field: WorkflowFormActionField;
|
||||||
|
onChange: (field: WorkflowFormActionField) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledFormFieldSettingsContainer = styled.div`
|
||||||
|
background-color: ${({ theme }) => theme.background.transparent.lighter};
|
||||||
|
border: 1px solid ${({ theme }) => theme.border.color.light};
|
||||||
|
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
|
margin-top: ${({ theme }) => theme.spacing(3)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledSettingsContent = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: ${({ theme }) => theme.spacing(2)};
|
||||||
|
padding: ${({ theme }) => theme.spacing(3)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledSettingsHeader = styled.div`
|
||||||
|
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
|
||||||
|
display: grid;
|
||||||
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
|
padding-inline: ${({ theme }) => theme.spacing(3)};
|
||||||
|
grid-template-columns: 1fr 24px;
|
||||||
|
padding-bottom: ${({ theme }) => theme.spacing(3)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledTitleContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
|
padding-top: ${({ theme }) => theme.spacing(3)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledCloseButtonContainer = styled.div`
|
||||||
|
padding-top: ${({ theme }) => theme.spacing(2)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const WorkflowEditActionFormFieldSettings = ({
|
||||||
|
field,
|
||||||
|
onChange,
|
||||||
|
onClose,
|
||||||
|
}: WorkflowEditActionFormFieldSettingsProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const onSubFieldUpdate = (fieldName: string, value: any) => {
|
||||||
|
onChange({
|
||||||
|
...field,
|
||||||
|
[fieldName]: value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledFormFieldSettingsContainer>
|
||||||
|
<StyledSettingsHeader>
|
||||||
|
<StyledTitleContainer>
|
||||||
|
<IconSettingsAutomation
|
||||||
|
size={theme.icon.size.md}
|
||||||
|
color={theme.font.color.primary}
|
||||||
|
/>
|
||||||
|
{t`Input settings`}
|
||||||
|
</StyledTitleContainer>
|
||||||
|
<StyledCloseButtonContainer>
|
||||||
|
<LightIconButton
|
||||||
|
Icon={IconX}
|
||||||
|
size="small"
|
||||||
|
accent="secondary"
|
||||||
|
onClick={onClose}
|
||||||
|
/>
|
||||||
|
</StyledCloseButtonContainer>
|
||||||
|
</StyledSettingsHeader>
|
||||||
|
<StyledSettingsContent>
|
||||||
|
<FormFieldInputContainer>
|
||||||
|
<InputLabel>Type</InputLabel>
|
||||||
|
<FormSelectFieldInput
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
label: getDefaultFormFieldSettings(FieldMetadataType.TEXT)
|
||||||
|
.label,
|
||||||
|
value: FieldMetadataType.TEXT,
|
||||||
|
icon: IllustrationIconText,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: getDefaultFormFieldSettings(FieldMetadataType.NUMBER)
|
||||||
|
.label,
|
||||||
|
value: FieldMetadataType.NUMBER,
|
||||||
|
icon: IllustrationIconNumbers,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
onPersist={(newType: string | null) => {
|
||||||
|
if (newType === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = newType as
|
||||||
|
| FieldMetadataType.TEXT
|
||||||
|
| FieldMetadataType.NUMBER;
|
||||||
|
const { label, placeholder } = getDefaultFormFieldSettings(type);
|
||||||
|
|
||||||
|
onChange({
|
||||||
|
...field,
|
||||||
|
type,
|
||||||
|
label,
|
||||||
|
placeholder,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
defaultValue={field.type}
|
||||||
|
preventDisplayPadding
|
||||||
|
/>
|
||||||
|
</FormFieldInputContainer>
|
||||||
|
<WorkflowFormFieldSettingsByType
|
||||||
|
field={field}
|
||||||
|
onChange={onSubFieldUpdate}
|
||||||
|
/>
|
||||||
|
</StyledSettingsContent>
|
||||||
|
</StyledFormFieldSettingsContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
import { WorkflowFormActionField } from '@/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionForm';
|
||||||
|
import { assertUnreachable, FieldMetadataType } from 'twenty-shared';
|
||||||
|
import { WorkflowFormFieldSettingsNumber } from './WorkflowFormFieldSettingsNumber';
|
||||||
|
import { WorkflowFormFieldSettingsText } from './WorkflowFormFieldSettingsText';
|
||||||
|
|
||||||
|
export const WorkflowFormFieldSettingsByType = ({
|
||||||
|
field,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
field: WorkflowFormActionField;
|
||||||
|
onChange: (fieldName: string, value: string | null) => void;
|
||||||
|
}) => {
|
||||||
|
switch (field.type) {
|
||||||
|
case FieldMetadataType.TEXT:
|
||||||
|
return (
|
||||||
|
<WorkflowFormFieldSettingsText
|
||||||
|
label={field.label}
|
||||||
|
placeholder={field.placeholder}
|
||||||
|
onChange={(fieldName, value) => {
|
||||||
|
onChange(fieldName, value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case FieldMetadataType.NUMBER:
|
||||||
|
return (
|
||||||
|
<WorkflowFormFieldSettingsNumber
|
||||||
|
label={field.label}
|
||||||
|
placeholder={field.placeholder}
|
||||||
|
onChange={(fieldName, value) => {
|
||||||
|
onChange(fieldName, value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return assertUnreachable(field.type, 'Unknown form field type');
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
import { FormFieldInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputContainer';
|
||||||
|
import { FormTextFieldInput } from '@/object-record/record-field/form-types/components/FormTextFieldInput';
|
||||||
|
import { InputLabel } from '@/ui/input/components/InputLabel';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { t } from '@lingui/core/macro';
|
||||||
|
|
||||||
|
type WorkflowFormFieldSettingsNumberProps = {
|
||||||
|
label?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
onChange: (fieldName: string, value: string | null) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: ${({ theme }) => theme.spacing(2)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const WorkflowFormFieldSettingsNumber = ({
|
||||||
|
label,
|
||||||
|
placeholder,
|
||||||
|
onChange,
|
||||||
|
}: WorkflowFormFieldSettingsNumberProps) => {
|
||||||
|
return (
|
||||||
|
<StyledContainer>
|
||||||
|
<FormFieldInputContainer>
|
||||||
|
<InputLabel>Label</InputLabel>
|
||||||
|
<FormTextFieldInput
|
||||||
|
onPersist={(newLabel: string | null) => {
|
||||||
|
onChange('label', newLabel);
|
||||||
|
}}
|
||||||
|
defaultValue={label ?? t`Number`}
|
||||||
|
placeholder={t`Text`}
|
||||||
|
/>
|
||||||
|
</FormFieldInputContainer>
|
||||||
|
<FormFieldInputContainer>
|
||||||
|
<InputLabel>Placeholder</InputLabel>
|
||||||
|
<FormTextFieldInput
|
||||||
|
onPersist={(newPlaceholder: string | null) => {
|
||||||
|
onChange('placeholder', newPlaceholder);
|
||||||
|
}}
|
||||||
|
defaultValue={placeholder ?? '1000'}
|
||||||
|
placeholder={'1000'}
|
||||||
|
/>
|
||||||
|
</FormFieldInputContainer>
|
||||||
|
</StyledContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
import { FormFieldInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputContainer';
|
||||||
|
import { FormTextFieldInput } from '@/object-record/record-field/form-types/components/FormTextFieldInput';
|
||||||
|
import { InputLabel } from '@/ui/input/components/InputLabel';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
type WorkflowFormFieldSettingsTextProps = {
|
||||||
|
label?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
onChange: (fieldName: string, value: string | null) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: ${({ theme }) => theme.spacing(2)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const WorkflowFormFieldSettingsText = ({
|
||||||
|
label,
|
||||||
|
placeholder,
|
||||||
|
onChange,
|
||||||
|
}: WorkflowFormFieldSettingsTextProps) => {
|
||||||
|
return (
|
||||||
|
<StyledContainer>
|
||||||
|
<FormFieldInputContainer>
|
||||||
|
<InputLabel>Label</InputLabel>
|
||||||
|
<FormTextFieldInput
|
||||||
|
onPersist={(newLabel: string | null) => {
|
||||||
|
onChange('label', newLabel);
|
||||||
|
}}
|
||||||
|
defaultValue={label}
|
||||||
|
placeholder={'Text'}
|
||||||
|
/>
|
||||||
|
</FormFieldInputContainer>
|
||||||
|
<FormFieldInputContainer>
|
||||||
|
<InputLabel>Placeholder</InputLabel>
|
||||||
|
<FormTextFieldInput
|
||||||
|
onPersist={(newPlaceholder: string | null) => {
|
||||||
|
onChange('placeholder', newPlaceholder);
|
||||||
|
}}
|
||||||
|
defaultValue={placeholder}
|
||||||
|
placeholder={'Enter your text'}
|
||||||
|
/>
|
||||||
|
</FormFieldInputContainer>
|
||||||
|
</StyledContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { WorkflowFormAction } from '@/workflow/types/Workflow';
|
import { WorkflowFormAction } from '@/workflow/types/Workflow';
|
||||||
import { WorkflowEditActionForm } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowEditActionForm';
|
import { WorkflowEditActionForm } from '@/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowEditActionForm';
|
||||||
import { Meta, StoryObj } from '@storybook/react';
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
import { expect, fn, within } from '@storybook/test';
|
import { expect, fn, within } from '@storybook/test';
|
||||||
import { FieldMetadataType } from 'twenty-shared';
|
import { FieldMetadataType } from 'twenty-shared';
|
||||||
@ -44,7 +44,7 @@ const DEFAULT_ACTION = {
|
|||||||
} satisfies WorkflowFormAction;
|
} satisfies WorkflowFormAction;
|
||||||
|
|
||||||
const meta: Meta<typeof WorkflowEditActionForm> = {
|
const meta: Meta<typeof WorkflowEditActionForm> = {
|
||||||
title: 'Modules/Workflow/WorkflowEditActionForm',
|
title: 'Modules/Workflow/Actions/Form/WorkflowEditActionForm',
|
||||||
component: WorkflowEditActionForm,
|
component: WorkflowEditActionForm,
|
||||||
parameters: {
|
parameters: {
|
||||||
msw: graphqlMocks,
|
msw: graphqlMocks,
|
||||||
@ -0,0 +1,90 @@
|
|||||||
|
import { WorkflowFormAction } from '@/workflow/types/Workflow';
|
||||||
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { expect, fn, userEvent, within } from '@storybook/test';
|
||||||
|
import { FieldMetadataType } from 'twenty-shared';
|
||||||
|
import { ComponentDecorator } from 'twenty-ui';
|
||||||
|
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
|
||||||
|
import { WorkflowStepActionDrawerDecorator } from '~/testing/decorators/WorkflowStepActionDrawerDecorator';
|
||||||
|
import { WorkflowEditActionFormFieldSettings } from '../WorkflowEditActionFormFieldSettings';
|
||||||
|
|
||||||
|
const meta: Meta<typeof WorkflowEditActionFormFieldSettings> = {
|
||||||
|
title: 'Modules/Workflow/Actions/Form/WorkflowEditActionFormFieldSettings',
|
||||||
|
component: WorkflowEditActionFormFieldSettings,
|
||||||
|
decorators: [
|
||||||
|
WorkflowStepActionDrawerDecorator,
|
||||||
|
ComponentDecorator,
|
||||||
|
I18nFrontDecorator,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof WorkflowEditActionFormFieldSettings>;
|
||||||
|
|
||||||
|
const mockAction: WorkflowFormAction = {
|
||||||
|
id: 'form-action-1',
|
||||||
|
type: 'FORM',
|
||||||
|
name: 'Test Form',
|
||||||
|
valid: true,
|
||||||
|
settings: {
|
||||||
|
input: [
|
||||||
|
{
|
||||||
|
id: 'field-1',
|
||||||
|
label: 'Text Field',
|
||||||
|
type: FieldMetadataType.TEXT,
|
||||||
|
placeholder: 'Enter text',
|
||||||
|
settings: {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
outputSchema: {},
|
||||||
|
errorHandlingOptions: {
|
||||||
|
retryOnFailure: { value: false },
|
||||||
|
continueOnFailure: { value: false },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TextFieldSettings: Story = {
|
||||||
|
args: {
|
||||||
|
field: mockAction.settings.input[0],
|
||||||
|
onClose: fn(),
|
||||||
|
},
|
||||||
|
play: async ({ canvasElement, args }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
|
||||||
|
const typeSelect = await canvas.findByText('Text');
|
||||||
|
expect(typeSelect).toBeVisible();
|
||||||
|
|
||||||
|
const placeholderInput = await canvas.findByText('Enter text');
|
||||||
|
expect(placeholderInput).toBeVisible();
|
||||||
|
|
||||||
|
const closeButton = await canvas.findByRole('button');
|
||||||
|
await userEvent.click(closeButton);
|
||||||
|
expect(args.onClose).toHaveBeenCalled();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NumberFieldSettings: Story = {
|
||||||
|
args: {
|
||||||
|
field: {
|
||||||
|
id: 'field-2',
|
||||||
|
label: 'Number Field',
|
||||||
|
type: FieldMetadataType.NUMBER,
|
||||||
|
placeholder: 'Enter number',
|
||||||
|
settings: {},
|
||||||
|
},
|
||||||
|
onClose: fn(),
|
||||||
|
},
|
||||||
|
play: async ({ canvasElement, args }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
|
||||||
|
const typeSelect = await canvas.findByText('Number');
|
||||||
|
expect(typeSelect).toBeVisible();
|
||||||
|
|
||||||
|
const placeholderInput = await canvas.findByText('Enter number');
|
||||||
|
expect(placeholderInput).toBeInTheDocument();
|
||||||
|
|
||||||
|
const closeButton = await canvas.findByRole('button');
|
||||||
|
await userEvent.click(closeButton);
|
||||||
|
expect(args.onClose).toHaveBeenCalled();
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
import { FieldMetadataType } from 'twenty-shared';
|
||||||
|
|
||||||
|
export const getDefaultFormFieldSettings = (type: FieldMetadataType) => {
|
||||||
|
switch (type) {
|
||||||
|
case FieldMetadataType.TEXT:
|
||||||
|
return {
|
||||||
|
label: 'Text',
|
||||||
|
placeholder: 'Enter your text',
|
||||||
|
};
|
||||||
|
case FieldMetadataType.NUMBER:
|
||||||
|
return {
|
||||||
|
label: 'Number',
|
||||||
|
placeholder: '1000',
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
label: type.charAt(0).toUpperCase() + type.slice(1),
|
||||||
|
placeholder: 'Enter your value',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user