Add record picker in form action (#11331)
Record picker becomes a form field that could be used in another context than workflows. Settings <img width="488" alt="Capture d’écran 2025-04-02 à 10 55 53" src="https://github.com/user-attachments/assets/a9fc09ff-28cd-4ede-8aaa-af1e986cda8e" /> Execution <img width="936" alt="Capture d’écran 2025-04-02 à 10 57 36" src="https://github.com/user-attachments/assets/d796aeeb-cae1-4e59-b388-5b8d08739ea8" />
This commit is contained in:
@ -1,11 +1,11 @@
|
|||||||
import { RecordChip } from '@/object-record/components/RecordChip';
|
import { RecordChip } from '@/object-record/components/RecordChip';
|
||||||
import { VariableChipStandalone } from '@/object-record/record-field/form-types/components/VariableChipStandalone';
|
|
||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
|
||||||
import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString';
|
|
||||||
import {
|
import {
|
||||||
RecordId,
|
RecordId,
|
||||||
Variable,
|
Variable,
|
||||||
} from '@/workflow/workflow-steps/workflow-actions/components/WorkflowSingleRecordPicker';
|
} from '@/object-record/record-field/form-types/components/FormSingleRecordPicker';
|
||||||
|
import { VariableChipStandalone } from '@/object-record/record-field/form-types/components/VariableChipStandalone';
|
||||||
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
|
import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
const StyledRecordChip = styled(RecordChip)`
|
const StyledRecordChip = styled(RecordChip)`
|
||||||
@ -18,7 +18,7 @@ const StyledPlaceholder = styled.div`
|
|||||||
margin: ${({ theme }) => theme.spacing(2)};
|
margin: ${({ theme }) => theme.spacing(2)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
type WorkflowSingleRecordFieldChipProps = {
|
type FormSingleRecordFieldChipProps = {
|
||||||
draftValue:
|
draftValue:
|
||||||
| {
|
| {
|
||||||
type: 'static';
|
type: 'static';
|
||||||
@ -34,13 +34,13 @@ type WorkflowSingleRecordFieldChipProps = {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WorkflowSingleRecordFieldChip = ({
|
export const FormSingleRecordFieldChip = ({
|
||||||
draftValue,
|
draftValue,
|
||||||
selectedRecord,
|
selectedRecord,
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
onRemove,
|
onRemove,
|
||||||
disabled,
|
disabled,
|
||||||
}: WorkflowSingleRecordFieldChipProps) => {
|
}: FormSingleRecordFieldChipProps) => {
|
||||||
if (
|
if (
|
||||||
!!draftValue &&
|
!!draftValue &&
|
||||||
draftValue.type === 'variable' &&
|
draftValue.type === 'variable' &&
|
||||||
@ -2,6 +2,8 @@ import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
|||||||
import { FormFieldInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputContainer';
|
import { FormFieldInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputContainer';
|
||||||
import { FormFieldInputInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputInputContainer';
|
import { FormFieldInputInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputInputContainer';
|
||||||
import { FormFieldInputRowContainer } from '@/object-record/record-field/form-types/components/FormFieldInputRowContainer';
|
import { FormFieldInputRowContainer } from '@/object-record/record-field/form-types/components/FormFieldInputRowContainer';
|
||||||
|
import { FormSingleRecordFieldChip } from '@/object-record/record-field/form-types/components/FormSingleRecordFieldChip';
|
||||||
|
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
|
||||||
import { SingleRecordPicker } from '@/object-record/record-picker/single-record-picker/components/SingleRecordPicker';
|
import { SingleRecordPicker } from '@/object-record/record-picker/single-record-picker/components/SingleRecordPicker';
|
||||||
import { singleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSearchFilterComponentState';
|
import { singleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSearchFilterComponentState';
|
||||||
import { singleRecordPickerSelectedIdComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSelectedIdComponentState';
|
import { singleRecordPickerSelectedIdComponentState } from '@/object-record/record-picker/single-record-picker/states/singleRecordPickerSelectedIdComponentState';
|
||||||
@ -12,13 +14,10 @@ import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
|||||||
import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
|
import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
|
||||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||||
import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString';
|
import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString';
|
||||||
import { WorkflowSingleRecordFieldChip } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowSingleRecordFieldChip';
|
|
||||||
import { WorkflowVariablesDropdown } from '@/workflow/workflow-variables/components/WorkflowVariablesDropdown';
|
|
||||||
import { css } from '@emotion/react';
|
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { IconChevronDown, IconForbid, LightIconButton } from 'twenty-ui';
|
|
||||||
import { isDefined, isValidUuid } from 'twenty-shared/utils';
|
import { isDefined, isValidUuid } from 'twenty-shared/utils';
|
||||||
|
import { IconChevronDown, IconForbid, LightIconButton } from 'twenty-ui';
|
||||||
|
|
||||||
const StyledFormSelectContainer = styled(FormFieldInputInputContainer)`
|
const StyledFormSelectContainer = styled(FormFieldInputInputContainer)`
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@ -26,27 +25,10 @@ const StyledFormSelectContainer = styled(FormFieldInputInputContainer)`
|
|||||||
padding-right: ${({ theme }) => theme.spacing(1)};
|
padding-right: ${({ theme }) => theme.spacing(1)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledSearchVariablesDropdownContainer = styled.div`
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
${({ theme }) => css`
|
|
||||||
:hover {
|
|
||||||
background-color: ${theme.background.transparent.light};
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
${({ theme }) => css`
|
|
||||||
background-color: ${theme.background.transparent.lighter};
|
|
||||||
border-top-right-radius: ${theme.border.radius.sm};
|
|
||||||
border-bottom-right-radius: ${theme.border.radius.sm};
|
|
||||||
border: 1px solid ${theme.border.color.medium};
|
|
||||||
`}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export type RecordId = string;
|
export type RecordId = string;
|
||||||
export type Variable = string;
|
export type Variable = string;
|
||||||
|
|
||||||
type WorkflowSingleRecordPickerValue =
|
type FormSingleRecordPickerValue =
|
||||||
| {
|
| {
|
||||||
type: 'static';
|
type: 'static';
|
||||||
value: RecordId;
|
value: RecordId;
|
||||||
@ -56,33 +38,36 @@ type WorkflowSingleRecordPickerValue =
|
|||||||
value: Variable;
|
value: Variable;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WorkflowSingleRecordPickerProps = {
|
export type FormSingleRecordPickerProps = {
|
||||||
label?: string;
|
label?: string;
|
||||||
defaultValue: RecordId | Variable;
|
defaultValue: RecordId | Variable;
|
||||||
onChange: (value: RecordId | Variable) => void;
|
onChange: (value: RecordId | Variable) => void;
|
||||||
objectNameSingular: string;
|
objectNameSingular: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
testId?: string;
|
testId?: string;
|
||||||
|
VariablePicker?: VariablePickerComponent;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WorkflowSingleRecordPicker = ({
|
export const FormSingleRecordPicker = ({
|
||||||
label,
|
label,
|
||||||
defaultValue,
|
defaultValue,
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
onChange,
|
onChange,
|
||||||
disabled,
|
disabled,
|
||||||
testId,
|
testId,
|
||||||
}: WorkflowSingleRecordPickerProps) => {
|
VariablePicker,
|
||||||
const draftValue: WorkflowSingleRecordPickerValue =
|
}: FormSingleRecordPickerProps) => {
|
||||||
isStandaloneVariableString(defaultValue)
|
const draftValue: FormSingleRecordPickerValue = isStandaloneVariableString(
|
||||||
? {
|
defaultValue,
|
||||||
type: 'variable',
|
)
|
||||||
value: defaultValue,
|
? {
|
||||||
}
|
type: 'variable',
|
||||||
: {
|
value: defaultValue,
|
||||||
type: 'static',
|
}
|
||||||
value: defaultValue || '',
|
: {
|
||||||
};
|
type: 'static',
|
||||||
|
value: defaultValue || '',
|
||||||
|
};
|
||||||
|
|
||||||
const { record: selectedRecord } = useFindOneRecord({
|
const { record: selectedRecord } = useFindOneRecord({
|
||||||
objectRecordId:
|
objectRecordId:
|
||||||
@ -94,8 +79,8 @@ export const WorkflowSingleRecordPicker = ({
|
|||||||
skip: !isValidUuid(defaultValue),
|
skip: !isValidUuid(defaultValue),
|
||||||
});
|
});
|
||||||
|
|
||||||
const dropdownId = `workflow-record-picker-${objectNameSingular}`;
|
const dropdownId = `form-record-picker-${objectNameSingular}`;
|
||||||
const variablesDropdownId = `workflow-record-picker-${objectNameSingular}-variables`;
|
const variablesDropdownId = `form-record-picker-${objectNameSingular}-variables`;
|
||||||
|
|
||||||
const { closeDropdown } = useDropdown(dropdownId);
|
const { closeDropdown } = useDropdown(dropdownId);
|
||||||
|
|
||||||
@ -144,8 +129,10 @@ export const WorkflowSingleRecordPicker = ({
|
|||||||
<FormFieldInputContainer testId={testId}>
|
<FormFieldInputContainer testId={testId}>
|
||||||
{label ? <InputLabel>{label}</InputLabel> : null}
|
{label ? <InputLabel>{label}</InputLabel> : null}
|
||||||
<FormFieldInputRowContainer>
|
<FormFieldInputRowContainer>
|
||||||
<StyledFormSelectContainer hasRightElement={!disabled}>
|
<StyledFormSelectContainer
|
||||||
<WorkflowSingleRecordFieldChip
|
hasRightElement={isDefined(VariablePicker) && !disabled}
|
||||||
|
>
|
||||||
|
<FormSingleRecordFieldChip
|
||||||
draftValue={draftValue}
|
draftValue={draftValue}
|
||||||
selectedRecord={selectedRecord}
|
selectedRecord={selectedRecord}
|
||||||
objectNameSingular={objectNameSingular}
|
objectNameSingular={objectNameSingular}
|
||||||
@ -182,15 +169,13 @@ export const WorkflowSingleRecordPicker = ({
|
|||||||
</DropdownScope>
|
</DropdownScope>
|
||||||
)}
|
)}
|
||||||
</StyledFormSelectContainer>
|
</StyledFormSelectContainer>
|
||||||
|
{isDefined(VariablePicker) && !disabled && (
|
||||||
{!disabled && (
|
<VariablePicker
|
||||||
<StyledSearchVariablesDropdownContainer>
|
inputId={variablesDropdownId}
|
||||||
<WorkflowVariablesDropdown
|
disabled={disabled}
|
||||||
inputId={variablesDropdownId}
|
onVariableSelect={handleVariableTagInsert}
|
||||||
onVariableSelect={handleVariableTagInsert}
|
objectNameSingularToSelect={objectNameSingular}
|
||||||
objectNameSingularToSelect={objectNameSingular}
|
/>
|
||||||
/>
|
|
||||||
</StyledSearchVariablesDropdownContainer>
|
|
||||||
)}
|
)}
|
||||||
</FormFieldInputRowContainer>
|
</FormFieldInputRowContainer>
|
||||||
</FormFieldInputContainer>
|
</FormFieldInputContainer>
|
||||||
@ -3,4 +3,5 @@ export type VariablePickerComponent = React.FC<{
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
multiline?: boolean;
|
multiline?: boolean;
|
||||||
onVariableSelect: (variableName: string) => void;
|
onVariableSelect: (variableName: string) => void;
|
||||||
|
objectNameSingularToSelect?: string;
|
||||||
}>;
|
}>;
|
||||||
|
|||||||
@ -92,6 +92,7 @@ export const workflowFormActionSettingsSchema =
|
|||||||
type: z.union([
|
type: z.union([
|
||||||
z.literal(FieldMetadataType.TEXT),
|
z.literal(FieldMetadataType.TEXT),
|
||||||
z.literal(FieldMetadataType.NUMBER),
|
z.literal(FieldMetadataType.NUMBER),
|
||||||
|
z.literal('RECORD'),
|
||||||
]),
|
]),
|
||||||
placeholder: z.string().optional(),
|
placeholder: z.string().optional(),
|
||||||
settings: z.record(z.any()).optional(),
|
settings: z.record(z.any()).optional(),
|
||||||
|
|||||||
@ -1,14 +1,15 @@
|
|||||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||||
|
import { FormSingleRecordPicker } from '@/object-record/record-field/form-types/components/FormSingleRecordPicker';
|
||||||
import { Select } from '@/ui/input/components/Select';
|
import { Select } from '@/ui/input/components/Select';
|
||||||
import { WorkflowDeleteRecordAction } from '@/workflow/types/Workflow';
|
import { WorkflowDeleteRecordAction } from '@/workflow/types/Workflow';
|
||||||
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
|
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
|
||||||
import { WorkflowSingleRecordPicker } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowSingleRecordPicker';
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
|
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
|
||||||
import { useActionHeaderTypeOrThrow } from '@/workflow/workflow-steps/workflow-actions/hooks/useActionHeaderTypeOrThrow';
|
import { useActionHeaderTypeOrThrow } from '@/workflow/workflow-steps/workflow-actions/hooks/useActionHeaderTypeOrThrow';
|
||||||
import { useActionIconColorOrThrow } from '@/workflow/workflow-steps/workflow-actions/hooks/useActionIconColorOrThrow';
|
import { useActionIconColorOrThrow } from '@/workflow/workflow-steps/workflow-actions/hooks/useActionIconColorOrThrow';
|
||||||
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
|
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
|
||||||
|
import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
import { HorizontalSeparator, SelectOption, useIcons } from 'twenty-ui';
|
import { HorizontalSeparator, SelectOption, useIcons } from 'twenty-ui';
|
||||||
import { JsonValue } from 'type-fest';
|
import { JsonValue } from 'type-fest';
|
||||||
@ -154,7 +155,7 @@ export const WorkflowEditActionDeleteRecord = ({
|
|||||||
|
|
||||||
<HorizontalSeparator noMargin />
|
<HorizontalSeparator noMargin />
|
||||||
|
|
||||||
<WorkflowSingleRecordPicker
|
<FormSingleRecordPicker
|
||||||
label="Record"
|
label="Record"
|
||||||
onChange={(objectRecordId) =>
|
onChange={(objectRecordId) =>
|
||||||
handleFieldChange('objectRecordId', objectRecordId)
|
handleFieldChange('objectRecordId', objectRecordId)
|
||||||
@ -163,6 +164,7 @@ export const WorkflowEditActionDeleteRecord = ({
|
|||||||
defaultValue={formData.objectRecordId}
|
defaultValue={formData.objectRecordId}
|
||||||
testId="workflow-edit-action-record-delete-object-record-id"
|
testId="workflow-edit-action-record-delete-object-record-id"
|
||||||
disabled={isFormDisabled}
|
disabled={isFormDisabled}
|
||||||
|
VariablePicker={WorkflowVariablePicker}
|
||||||
/>
|
/>
|
||||||
</WorkflowStepBody>
|
</WorkflowStepBody>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -6,9 +6,9 @@ import { useEffect, useState } from 'react';
|
|||||||
import { formatFieldMetadataItemAsFieldDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition';
|
import { formatFieldMetadataItemAsFieldDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition';
|
||||||
import { FormFieldInput } from '@/object-record/record-field/components/FormFieldInput';
|
import { FormFieldInput } from '@/object-record/record-field/components/FormFieldInput';
|
||||||
import { FormMultiSelectFieldInput } from '@/object-record/record-field/form-types/components/FormMultiSelectFieldInput';
|
import { FormMultiSelectFieldInput } from '@/object-record/record-field/form-types/components/FormMultiSelectFieldInput';
|
||||||
|
import { FormSingleRecordPicker } from '@/object-record/record-field/form-types/components/FormSingleRecordPicker';
|
||||||
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 { WorkflowSingleRecordPicker } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowSingleRecordPicker';
|
|
||||||
import { useActionHeaderTypeOrThrow } from '@/workflow/workflow-steps/workflow-actions/hooks/useActionHeaderTypeOrThrow';
|
import { useActionHeaderTypeOrThrow } from '@/workflow/workflow-steps/workflow-actions/hooks/useActionHeaderTypeOrThrow';
|
||||||
import { useActionIconColorOrThrow } from '@/workflow/workflow-steps/workflow-actions/hooks/useActionIconColorOrThrow';
|
import { useActionIconColorOrThrow } from '@/workflow/workflow-steps/workflow-actions/hooks/useActionIconColorOrThrow';
|
||||||
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
|
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
|
||||||
@ -208,7 +208,7 @@ export const WorkflowEditActionUpdateRecord = ({
|
|||||||
|
|
||||||
<HorizontalSeparator noMargin />
|
<HorizontalSeparator noMargin />
|
||||||
|
|
||||||
<WorkflowSingleRecordPicker
|
<FormSingleRecordPicker
|
||||||
testId="workflow-edit-action-record-update-object-record-id"
|
testId="workflow-edit-action-record-update-object-record-id"
|
||||||
label="Record"
|
label="Record"
|
||||||
onChange={(objectRecordId) =>
|
onChange={(objectRecordId) =>
|
||||||
@ -217,6 +217,7 @@ export const WorkflowEditActionUpdateRecord = ({
|
|||||||
objectNameSingular={formData.objectName}
|
objectNameSingular={formData.objectName}
|
||||||
defaultValue={formData.objectRecordId}
|
defaultValue={formData.objectRecordId}
|
||||||
disabled={isFormDisabled}
|
disabled={isFormDisabled}
|
||||||
|
VariablePicker={WorkflowVariablePicker}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormMultiSelectFieldInput
|
<FormMultiSelectFieldInput
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { FormSelectFieldInput } from '@/object-record/record-field/form-types/co
|
|||||||
import { InputLabel } from '@/ui/input/components/InputLabel';
|
import { InputLabel } from '@/ui/input/components/InputLabel';
|
||||||
import { WorkflowFormFieldSettingsByType } from '@/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowFormFieldSettingsByType';
|
import { WorkflowFormFieldSettingsByType } from '@/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowFormFieldSettingsByType';
|
||||||
import { WorkflowFormActionField } from '@/workflow/workflow-steps/workflow-actions/form-action/types/WorkflowFormActionField';
|
import { WorkflowFormActionField } from '@/workflow/workflow-steps/workflow-actions/form-action/types/WorkflowFormActionField';
|
||||||
|
import { WorkflowFormFieldType } from '@/workflow/workflow-steps/workflow-actions/form-action/types/WorkflowFormFieldType';
|
||||||
import { getDefaultFormFieldSettings } from '@/workflow/workflow-steps/workflow-actions/form-action/utils/getDefaultFormFieldSettings';
|
import { getDefaultFormFieldSettings } from '@/workflow/workflow-steps/workflow-actions/form-action/utils/getDefaultFormFieldSettings';
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
@ -13,6 +14,7 @@ import {
|
|||||||
IconSettingsAutomation,
|
IconSettingsAutomation,
|
||||||
IconX,
|
IconX,
|
||||||
IllustrationIconNumbers,
|
IllustrationIconNumbers,
|
||||||
|
IllustrationIconOneToMany,
|
||||||
IllustrationIconText,
|
IllustrationIconText,
|
||||||
LightIconButton,
|
LightIconButton,
|
||||||
} from 'twenty-ui';
|
} from 'twenty-ui';
|
||||||
@ -95,6 +97,7 @@ export const WorkflowEditActionFormFieldSettings = ({
|
|||||||
</StyledTitleContainer>
|
</StyledTitleContainer>
|
||||||
<StyledCloseButtonContainer>
|
<StyledCloseButtonContainer>
|
||||||
<LightIconButton
|
<LightIconButton
|
||||||
|
testId="close-button"
|
||||||
Icon={IconX}
|
Icon={IconX}
|
||||||
size="small"
|
size="small"
|
||||||
accent="secondary"
|
accent="secondary"
|
||||||
@ -106,35 +109,47 @@ export const WorkflowEditActionFormFieldSettings = ({
|
|||||||
<FormFieldInputContainer>
|
<FormFieldInputContainer>
|
||||||
<InputLabel>Type</InputLabel>
|
<InputLabel>Type</InputLabel>
|
||||||
<FormSelectFieldInput
|
<FormSelectFieldInput
|
||||||
options={[
|
options={
|
||||||
{
|
[
|
||||||
label: getDefaultFormFieldSettings(FieldMetadataType.TEXT)
|
{
|
||||||
.label,
|
label: getDefaultFormFieldSettings(FieldMetadataType.TEXT)
|
||||||
value: FieldMetadataType.TEXT,
|
.label,
|
||||||
Icon: IllustrationIconText,
|
value: FieldMetadataType.TEXT,
|
||||||
},
|
Icon: IllustrationIconText,
|
||||||
{
|
},
|
||||||
label: getDefaultFormFieldSettings(FieldMetadataType.NUMBER)
|
{
|
||||||
.label,
|
label: getDefaultFormFieldSettings(FieldMetadataType.NUMBER)
|
||||||
value: FieldMetadataType.NUMBER,
|
.label,
|
||||||
Icon: IllustrationIconNumbers,
|
value: FieldMetadataType.NUMBER,
|
||||||
},
|
Icon: IllustrationIconNumbers,
|
||||||
]}
|
},
|
||||||
|
{
|
||||||
|
label: 'Record Picker',
|
||||||
|
value: 'RECORD',
|
||||||
|
Icon: IllustrationIconOneToMany,
|
||||||
|
},
|
||||||
|
] satisfies {
|
||||||
|
label: string;
|
||||||
|
value: WorkflowFormFieldType;
|
||||||
|
Icon: React.ElementType;
|
||||||
|
}[]
|
||||||
|
}
|
||||||
onChange={(newType: string | null) => {
|
onChange={(newType: string | null) => {
|
||||||
if (newType === null) {
|
if (newType === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const type = newType as
|
const type = newType as WorkflowFormFieldType;
|
||||||
| FieldMetadataType.TEXT
|
const { name, label, placeholder, settings } =
|
||||||
| FieldMetadataType.NUMBER;
|
getDefaultFormFieldSettings(type);
|
||||||
const { label, placeholder } = getDefaultFormFieldSettings(type);
|
|
||||||
|
|
||||||
onChange({
|
onChange({
|
||||||
...field,
|
...field,
|
||||||
type,
|
type,
|
||||||
|
name,
|
||||||
label,
|
label,
|
||||||
placeholder,
|
placeholder,
|
||||||
|
settings,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
defaultValue={field.type}
|
defaultValue={field.type}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { CmdEnterActionButton } from '@/action-menu/components/CmdEnterActionButton';
|
import { CmdEnterActionButton } from '@/action-menu/components/CmdEnterActionButton';
|
||||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||||
import { FormFieldInput } from '@/object-record/record-field/components/FormFieldInput';
|
import { FormFieldInput } from '@/object-record/record-field/components/FormFieldInput';
|
||||||
|
import { FormSingleRecordPicker } from '@/object-record/record-field/form-types/components/FormSingleRecordPicker';
|
||||||
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
import { RightDrawerFooter } from '@/ui/layout/right-drawer/components/RightDrawerFooter';
|
import { RightDrawerFooter } from '@/ui/layout/right-drawer/components/RightDrawerFooter';
|
||||||
import { useWorkflowStepContextOrThrow } from '@/workflow/states/context/WorkflowStepContext';
|
import { useWorkflowStepContextOrThrow } from '@/workflow/states/context/WorkflowStepContext';
|
||||||
@ -114,28 +115,58 @@ export const WorkflowEditActionFormFiller = ({
|
|||||||
disabled
|
disabled
|
||||||
/>
|
/>
|
||||||
<WorkflowStepBody>
|
<WorkflowStepBody>
|
||||||
{formData.map((field) => (
|
{formData.map((field) => {
|
||||||
<FormFieldInput
|
if (field.type === 'RECORD') {
|
||||||
key={field.id}
|
const objectNameSingular = field.settings?.objectName;
|
||||||
field={{
|
|
||||||
label: field.label,
|
if (!isDefined(objectNameSingular)) {
|
||||||
type: field.type,
|
return null;
|
||||||
metadata: {} as FieldMetadata,
|
|
||||||
}}
|
|
||||||
onChange={(value) => {
|
|
||||||
onFieldUpdate({
|
|
||||||
fieldId: field.id,
|
|
||||||
value,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
defaultValue={field.value ?? ''}
|
|
||||||
readonly={actionOptions.readonly}
|
|
||||||
placeholder={
|
|
||||||
field.placeholder ??
|
|
||||||
getDefaultFormFieldSettings(field.type).placeholder
|
|
||||||
}
|
}
|
||||||
/>
|
|
||||||
))}
|
const recordId = field.value?.id;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormSingleRecordPicker
|
||||||
|
key={field.id}
|
||||||
|
label={field.label}
|
||||||
|
defaultValue={recordId}
|
||||||
|
onChange={(recordId) => {
|
||||||
|
onFieldUpdate({
|
||||||
|
fieldId: field.id,
|
||||||
|
value: {
|
||||||
|
id: recordId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
objectNameSingular={objectNameSingular}
|
||||||
|
disabled={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
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</WorkflowStepBody>
|
</WorkflowStepBody>
|
||||||
{!actionOptions.readonly && (
|
{!actionOptions.readonly && (
|
||||||
<RightDrawerFooter
|
<RightDrawerFooter
|
||||||
|
|||||||
@ -1,15 +1,16 @@
|
|||||||
|
import { WorkflowFormFieldSettingsRecordPicker } from '@/workflow/workflow-steps/workflow-actions/form-action/components/WorkflowFormFieldSettingsRecordPicker';
|
||||||
import { WorkflowFormActionField } from '@/workflow/workflow-steps/workflow-actions/form-action/types/WorkflowFormActionField';
|
import { WorkflowFormActionField } from '@/workflow/workflow-steps/workflow-actions/form-action/types/WorkflowFormActionField';
|
||||||
|
import { FieldMetadataType } from 'twenty-shared/types';
|
||||||
|
import { assertUnreachable } from 'twenty-shared/utils';
|
||||||
import { WorkflowFormFieldSettingsNumber } from './WorkflowFormFieldSettingsNumber';
|
import { WorkflowFormFieldSettingsNumber } from './WorkflowFormFieldSettingsNumber';
|
||||||
import { WorkflowFormFieldSettingsText } from './WorkflowFormFieldSettingsText';
|
import { WorkflowFormFieldSettingsText } from './WorkflowFormFieldSettingsText';
|
||||||
import { assertUnreachable } from 'twenty-shared/utils';
|
|
||||||
import { FieldMetadataType } from 'twenty-shared/types';
|
|
||||||
|
|
||||||
export const WorkflowFormFieldSettingsByType = ({
|
export const WorkflowFormFieldSettingsByType = ({
|
||||||
field,
|
field,
|
||||||
onChange,
|
onChange,
|
||||||
}: {
|
}: {
|
||||||
field: WorkflowFormActionField;
|
field: WorkflowFormActionField;
|
||||||
onChange: (fieldName: string, value: string | null) => void;
|
onChange: (fieldName: string, value: unknown) => void;
|
||||||
}) => {
|
}) => {
|
||||||
switch (field.type) {
|
switch (field.type) {
|
||||||
case FieldMetadataType.TEXT:
|
case FieldMetadataType.TEXT:
|
||||||
@ -32,6 +33,16 @@ export const WorkflowFormFieldSettingsByType = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
case 'RECORD':
|
||||||
|
return (
|
||||||
|
<WorkflowFormFieldSettingsRecordPicker
|
||||||
|
label={field.label}
|
||||||
|
settings={field.settings}
|
||||||
|
onChange={(fieldName, value) => {
|
||||||
|
onChange(fieldName, value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return assertUnreachable(field.type, 'Unknown form field type');
|
return assertUnreachable(field.type, 'Unknown form field type');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,69 @@
|
|||||||
|
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||||
|
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 { Select } from '@/ui/input/components/Select';
|
||||||
|
import { getDefaultFormFieldSettings } from '@/workflow/workflow-steps/workflow-actions/form-action/utils/getDefaultFormFieldSettings';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { SelectOption, useIcons } from 'twenty-ui';
|
||||||
|
|
||||||
|
type WorkflowFormFieldSettingsRecordPickerProps = {
|
||||||
|
label?: string;
|
||||||
|
settings?: Record<string, any>;
|
||||||
|
onChange: (fieldName: string, value: unknown) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: ${({ theme }) => theme.spacing(2)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const WorkflowFormFieldSettingsRecordPicker = ({
|
||||||
|
label,
|
||||||
|
settings,
|
||||||
|
onChange,
|
||||||
|
}: WorkflowFormFieldSettingsRecordPickerProps) => {
|
||||||
|
const { getIcon } = useIcons();
|
||||||
|
|
||||||
|
const { activeObjectMetadataItems } = useFilteredObjectMetadataItems();
|
||||||
|
|
||||||
|
const availableMetadata: Array<SelectOption<string>> =
|
||||||
|
activeObjectMetadataItems.map((item) => ({
|
||||||
|
Icon: getIcon(item.icon),
|
||||||
|
label: item.labelPlural,
|
||||||
|
value: item.nameSingular,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledContainer>
|
||||||
|
<FormFieldInputContainer>
|
||||||
|
<Select
|
||||||
|
dropdownId="workflow-form-field-settings-record-picker-object-name"
|
||||||
|
label="Object"
|
||||||
|
fullWidth
|
||||||
|
value={settings?.objectName}
|
||||||
|
emptyOption={{ label: 'Select an option', value: '' }}
|
||||||
|
options={availableMetadata}
|
||||||
|
onChange={(updatedObjectName) => {
|
||||||
|
onChange('settings', {
|
||||||
|
...settings,
|
||||||
|
objectName: updatedObjectName,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
withSearchInput
|
||||||
|
/>
|
||||||
|
</FormFieldInputContainer>
|
||||||
|
<FormFieldInputContainer>
|
||||||
|
<InputLabel>Label</InputLabel>
|
||||||
|
<FormTextFieldInput
|
||||||
|
onChange={(newLabel: string | null) => {
|
||||||
|
onChange('label', newLabel);
|
||||||
|
}}
|
||||||
|
defaultValue={label}
|
||||||
|
placeholder={getDefaultFormFieldSettings('RECORD').label}
|
||||||
|
/>
|
||||||
|
</FormFieldInputContainer>
|
||||||
|
</StyledContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,11 +1,12 @@
|
|||||||
import { WorkflowFormAction } from '@/workflow/types/Workflow';
|
import { WorkflowFormAction } from '@/workflow/types/Workflow';
|
||||||
import { Meta, StoryObj } from '@storybook/react';
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
import { expect, fn, userEvent, within } from '@storybook/test';
|
import { expect, fn, userEvent, within } from '@storybook/test';
|
||||||
|
import { FieldMetadataType } from 'twenty-shared/types';
|
||||||
import { ComponentDecorator } from 'twenty-ui';
|
import { ComponentDecorator } from 'twenty-ui';
|
||||||
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
|
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
|
||||||
|
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
|
||||||
import { WorkflowStepActionDrawerDecorator } from '~/testing/decorators/WorkflowStepActionDrawerDecorator';
|
import { WorkflowStepActionDrawerDecorator } from '~/testing/decorators/WorkflowStepActionDrawerDecorator';
|
||||||
import { WorkflowEditActionFormFieldSettings } from '../WorkflowEditActionFormFieldSettings';
|
import { WorkflowEditActionFormFieldSettings } from '../WorkflowEditActionFormFieldSettings';
|
||||||
import { FieldMetadataType } from 'twenty-shared/types';
|
|
||||||
|
|
||||||
const meta: Meta<typeof WorkflowEditActionFormFieldSettings> = {
|
const meta: Meta<typeof WorkflowEditActionFormFieldSettings> = {
|
||||||
title: 'Modules/Workflow/Actions/Form/WorkflowEditActionFormFieldSettings',
|
title: 'Modules/Workflow/Actions/Form/WorkflowEditActionFormFieldSettings',
|
||||||
@ -14,6 +15,7 @@ const meta: Meta<typeof WorkflowEditActionFormFieldSettings> = {
|
|||||||
WorkflowStepActionDrawerDecorator,
|
WorkflowStepActionDrawerDecorator,
|
||||||
ComponentDecorator,
|
ComponentDecorator,
|
||||||
I18nFrontDecorator,
|
I18nFrontDecorator,
|
||||||
|
ObjectMetadataItemsDecorator,
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -90,3 +92,32 @@ export const NumberFieldSettings: Story = {
|
|||||||
expect(args.onClose).toHaveBeenCalled();
|
expect(args.onClose).toHaveBeenCalled();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const SingleRecordFieldSettings: Story = {
|
||||||
|
args: {
|
||||||
|
field: {
|
||||||
|
id: 'field-3',
|
||||||
|
name: 'record',
|
||||||
|
label: 'Record',
|
||||||
|
type: 'RECORD',
|
||||||
|
settings: {
|
||||||
|
objectName: 'company',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onClose: fn(),
|
||||||
|
},
|
||||||
|
|
||||||
|
play: async ({ canvasElement, args }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
|
||||||
|
const typeSelect = await canvas.findByText('Record');
|
||||||
|
expect(typeSelect).toBeVisible();
|
||||||
|
|
||||||
|
const objectSelect = await canvas.findByText('Companies');
|
||||||
|
expect(objectSelect).toBeVisible();
|
||||||
|
|
||||||
|
const closeButton = await canvas.findByTestId('close-button');
|
||||||
|
await userEvent.click(closeButton);
|
||||||
|
expect(args.onClose).toHaveBeenCalled();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|||||||
@ -54,6 +54,16 @@ const mockAction: WorkflowFormAction = {
|
|||||||
placeholder: 'Enter number',
|
placeholder: 'Enter number',
|
||||||
settings: {},
|
settings: {},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'field-3',
|
||||||
|
name: 'record',
|
||||||
|
label: 'Record',
|
||||||
|
type: 'RECORD',
|
||||||
|
placeholder: 'Select a record',
|
||||||
|
settings: {
|
||||||
|
objectName: 'company',
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
outputSchema: {},
|
outputSchema: {},
|
||||||
errorHandlingOptions: {
|
errorHandlingOptions: {
|
||||||
@ -78,6 +88,9 @@ export const Default: Story = {
|
|||||||
|
|
||||||
const numberField = await canvas.findByText('Number Field');
|
const numberField = await canvas.findByText('Number Field');
|
||||||
expect(numberField).toBeVisible();
|
expect(numberField).toBeVisible();
|
||||||
|
|
||||||
|
const recordField = await canvas.findByText('Record');
|
||||||
|
expect(recordField).toBeVisible();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,11 @@
|
|||||||
import { JsonValue } from 'type-fest';
|
import { WorkflowFormFieldType } from '@/workflow/workflow-steps/workflow-actions/form-action/types/WorkflowFormFieldType';
|
||||||
import { FieldMetadataType } from 'twenty-shared/types';
|
|
||||||
|
|
||||||
export type WorkflowFormActionField = {
|
export type WorkflowFormActionField = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
label: string;
|
label: string;
|
||||||
type: FieldMetadataType.TEXT | FieldMetadataType.NUMBER;
|
type: WorkflowFormFieldType;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
settings?: Record<string, unknown>;
|
settings?: Record<string, any>;
|
||||||
value?: JsonValue;
|
value?: any;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,6 @@
|
|||||||
|
import { FieldMetadataType } from 'twenty-shared/types';
|
||||||
|
|
||||||
|
export type WorkflowFormFieldType =
|
||||||
|
| FieldMetadataType.TEXT
|
||||||
|
| FieldMetadataType.NUMBER
|
||||||
|
| 'RECORD';
|
||||||
@ -1,7 +1,9 @@
|
|||||||
import { v4 } from 'uuid';
|
import { WorkflowFormFieldType } from '@/workflow/workflow-steps/workflow-actions/form-action/types/WorkflowFormFieldType';
|
||||||
import { FieldMetadataType } from 'twenty-shared/types';
|
import { FieldMetadataType } from 'twenty-shared/types';
|
||||||
|
import { assertUnreachable } from 'twenty-shared/utils';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
export const getDefaultFormFieldSettings = (type: FieldMetadataType) => {
|
export const getDefaultFormFieldSettings = (type: WorkflowFormFieldType) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case FieldMetadataType.TEXT:
|
case FieldMetadataType.TEXT:
|
||||||
return {
|
return {
|
||||||
@ -17,12 +19,17 @@ export const getDefaultFormFieldSettings = (type: FieldMetadataType) => {
|
|||||||
label: 'Number',
|
label: 'Number',
|
||||||
placeholder: '1000',
|
placeholder: '1000',
|
||||||
};
|
};
|
||||||
default:
|
case 'RECORD':
|
||||||
return {
|
return {
|
||||||
id: v4(),
|
id: v4(),
|
||||||
name: '',
|
name: 'record',
|
||||||
label: type.charAt(0).toUpperCase() + type.slice(1),
|
label: 'Record',
|
||||||
placeholder: 'Enter your value',
|
placeholder: 'Select a record',
|
||||||
|
settings: {
|
||||||
|
objectName: 'company',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
default:
|
||||||
|
assertUnreachable(type);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -41,6 +41,7 @@ export const WorkflowVariablePicker: VariablePickerComponent = ({
|
|||||||
disabled,
|
disabled,
|
||||||
multiline,
|
multiline,
|
||||||
onVariableSelect,
|
onVariableSelect,
|
||||||
|
objectNameSingularToSelect,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<StyledSearchVariablesDropdownContainer
|
<StyledSearchVariablesDropdownContainer
|
||||||
@ -51,6 +52,7 @@ export const WorkflowVariablePicker: VariablePickerComponent = ({
|
|||||||
inputId={inputId}
|
inputId={inputId}
|
||||||
onVariableSelect={onVariableSelect}
|
onVariableSelect={onVariableSelect}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
objectNameSingularToSelect={objectNameSingularToSelect}
|
||||||
/>
|
/>
|
||||||
</StyledSearchVariablesDropdownContainer>
|
</StyledSearchVariablesDropdownContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,10 +1,24 @@
|
|||||||
import { FieldMetadataType } from 'twenty-shared/types';
|
import { FieldMetadataType } from 'twenty-shared/types';
|
||||||
|
|
||||||
|
import { mockObjectMetadataItemsWithFieldMaps } from 'src/engine/core-modules/search/__mocks__/mockObjectMetadataItemsWithFieldMaps';
|
||||||
import { generateFakeFormResponse } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-form-response';
|
import { generateFakeFormResponse } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-form-response';
|
||||||
|
import { FormFieldMetadata } from 'src/modules/workflow/workflow-executor/workflow-actions/form/types/workflow-form-action-settings.type';
|
||||||
|
|
||||||
|
const companyMockObjectMetadataItem = mockObjectMetadataItemsWithFieldMaps.find(
|
||||||
|
(item) => item.nameSingular === 'company',
|
||||||
|
)!;
|
||||||
|
|
||||||
describe('generateFakeFormResponse', () => {
|
describe('generateFakeFormResponse', () => {
|
||||||
it('should generate fake responses for a form schema', () => {
|
let objectMetadataRepository;
|
||||||
const schema = [
|
|
||||||
|
beforeEach(() => {
|
||||||
|
objectMetadataRepository = {
|
||||||
|
findOneOrFail: jest.fn().mockResolvedValue(companyMockObjectMetadataItem),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate fake responses for a form schema', async () => {
|
||||||
|
const schema: FormFieldMetadata[] = [
|
||||||
{
|
{
|
||||||
id: '96939213-49ac-4dee-949d-56e6c7be98e6',
|
id: '96939213-49ac-4dee-949d-56e6c7be98e6',
|
||||||
name: 'name',
|
name: 'name',
|
||||||
@ -19,34 +33,22 @@ describe('generateFakeFormResponse', () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '96939213-49ac-4dee-949d-56e6c7be98e8',
|
id: '96939213-49ac-4dee-949d-56e6c7be98e8',
|
||||||
name: 'email',
|
name: 'company',
|
||||||
type: FieldMetadataType.EMAILS,
|
type: 'RECORD',
|
||||||
label: 'Email',
|
label: 'Company',
|
||||||
|
settings: {
|
||||||
|
objectName: 'company',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const result = generateFakeFormResponse(schema);
|
const result = await generateFakeFormResponse({
|
||||||
|
formMetadata: schema,
|
||||||
|
workspaceId: '1',
|
||||||
|
objectMetadataRepository,
|
||||||
|
});
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
email: {
|
|
||||||
isLeaf: false,
|
|
||||||
label: 'Email',
|
|
||||||
value: {
|
|
||||||
additionalEmails: {
|
|
||||||
isLeaf: true,
|
|
||||||
label: ' Additional Emails',
|
|
||||||
type: FieldMetadataType.RAW_JSON,
|
|
||||||
value: null,
|
|
||||||
},
|
|
||||||
primaryEmail: {
|
|
||||||
isLeaf: true,
|
|
||||||
label: ' Primary Email',
|
|
||||||
type: FieldMetadataType.TEXT,
|
|
||||||
value: 'My text',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
icon: undefined,
|
|
||||||
},
|
|
||||||
name: {
|
name: {
|
||||||
isLeaf: true,
|
isLeaf: true,
|
||||||
label: 'Name',
|
label: 'Name',
|
||||||
@ -61,6 +63,22 @@ describe('generateFakeFormResponse', () => {
|
|||||||
value: 20,
|
value: 20,
|
||||||
icon: undefined,
|
icon: undefined,
|
||||||
},
|
},
|
||||||
|
company: {
|
||||||
|
isLeaf: false,
|
||||||
|
label: 'Company',
|
||||||
|
value: {
|
||||||
|
_outputSchemaType: 'RECORD',
|
||||||
|
fields: {},
|
||||||
|
object: {
|
||||||
|
isLeaf: true,
|
||||||
|
label: 'Company',
|
||||||
|
fieldIdName: 'id',
|
||||||
|
icon: undefined,
|
||||||
|
nameSingular: 'company',
|
||||||
|
value: 'A company',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,19 +1,58 @@
|
|||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
|
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||||
import {
|
import {
|
||||||
Leaf,
|
Leaf,
|
||||||
Node,
|
Node,
|
||||||
} from 'src/modules/workflow/workflow-builder/workflow-schema/types/output-schema.type';
|
} from 'src/modules/workflow/workflow-builder/workflow-schema/types/output-schema.type';
|
||||||
import { generateFakeField } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-field';
|
import { generateFakeField } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-field';
|
||||||
|
import { generateFakeObjectRecord } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record';
|
||||||
import { FormFieldMetadata } from 'src/modules/workflow/workflow-executor/workflow-actions/form/types/workflow-form-action-settings.type';
|
import { FormFieldMetadata } from 'src/modules/workflow/workflow-executor/workflow-actions/form/types/workflow-form-action-settings.type';
|
||||||
|
|
||||||
export const generateFakeFormResponse = (
|
export const generateFakeFormResponse = async ({
|
||||||
formMetadata: FormFieldMetadata[],
|
formMetadata,
|
||||||
): Record<string, Leaf | Node> => {
|
workspaceId,
|
||||||
return formMetadata.reduce((acc, formFieldMetadata) => {
|
objectMetadataRepository,
|
||||||
acc[formFieldMetadata.name] = generateFakeField({
|
}: {
|
||||||
type: formFieldMetadata.type,
|
formMetadata: FormFieldMetadata[];
|
||||||
label: formFieldMetadata.label,
|
workspaceId: string;
|
||||||
});
|
objectMetadataRepository: Repository<ObjectMetadataEntity>;
|
||||||
|
}): Promise<Record<string, Leaf | Node>> => {
|
||||||
|
const result = await Promise.all(
|
||||||
|
formMetadata.map(async (formFieldMetadata) => {
|
||||||
|
if (formFieldMetadata.type === 'RECORD') {
|
||||||
|
if (!formFieldMetadata?.settings?.objectName) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
return acc;
|
const objectMetadata = await objectMetadataRepository.findOneOrFail({
|
||||||
|
where: {
|
||||||
|
nameSingular: formFieldMetadata?.settings?.objectName,
|
||||||
|
workspaceId,
|
||||||
|
},
|
||||||
|
relations: ['fields'],
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
[formFieldMetadata.name]: {
|
||||||
|
isLeaf: false,
|
||||||
|
label: formFieldMetadata.label,
|
||||||
|
value: generateFakeObjectRecord(objectMetadata),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
[formFieldMetadata.name]: generateFakeField({
|
||||||
|
type: formFieldMetadata.type,
|
||||||
|
label: formFieldMetadata.label,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return result.filter(isDefined).reduce((acc, curr) => {
|
||||||
|
return { ...acc, ...curr };
|
||||||
}, {});
|
}, {});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
import { Repository } from 'typeorm';
|
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
||||||
import { checkStringIsDatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/utils/check-string-is-database-event-action';
|
import { checkStringIsDatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/utils/check-string-is-database-event-action';
|
||||||
@ -83,6 +83,8 @@ export class WorkflowSchemaWorkspaceService {
|
|||||||
case WorkflowActionType.FORM:
|
case WorkflowActionType.FORM:
|
||||||
return this.computeFormActionOutputSchema({
|
return this.computeFormActionOutputSchema({
|
||||||
formMetadata: step.settings.input,
|
formMetadata: step.settings.input,
|
||||||
|
workspaceId,
|
||||||
|
objectMetadataRepository: this.objectMetadataRepository,
|
||||||
});
|
});
|
||||||
case WorkflowActionType.CODE: // StepOutput schema is computed on serverlessFunction draft execution
|
case WorkflowActionType.CODE: // StepOutput schema is computed on serverlessFunction draft execution
|
||||||
default:
|
default:
|
||||||
@ -182,11 +184,19 @@ export class WorkflowSchemaWorkspaceService {
|
|||||||
return { success: { isLeaf: true, type: 'boolean', value: true } };
|
return { success: { isLeaf: true, type: 'boolean', value: true } };
|
||||||
}
|
}
|
||||||
|
|
||||||
private computeFormActionOutputSchema({
|
private async computeFormActionOutputSchema({
|
||||||
formMetadata,
|
formMetadata,
|
||||||
|
workspaceId,
|
||||||
|
objectMetadataRepository,
|
||||||
}: {
|
}: {
|
||||||
formMetadata: FormFieldMetadata[];
|
formMetadata: FormFieldMetadata[];
|
||||||
}): OutputSchema {
|
workspaceId: string;
|
||||||
return generateFakeFormResponse(formMetadata);
|
objectMetadataRepository: Repository<ObjectMetadataEntity>;
|
||||||
|
}): Promise<OutputSchema> {
|
||||||
|
return generateFakeFormResponse({
|
||||||
|
formMetadata,
|
||||||
|
workspaceId,
|
||||||
|
objectMetadataRepository,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,11 @@
|
|||||||
import { FieldMetadataType } from 'twenty-shared/types';
|
import { WorkflowFormFieldType } from 'src/modules/workflow/workflow-executor/workflow-actions/form/types/workflow-form-field-type.type';
|
||||||
|
|
||||||
import { BaseWorkflowActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action-settings.type';
|
import { BaseWorkflowActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action-settings.type';
|
||||||
|
|
||||||
export type FormFieldMetadata = {
|
export type FormFieldMetadata = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
label: string;
|
label: string;
|
||||||
type: FieldMetadataType;
|
type: WorkflowFormFieldType;
|
||||||
value?: any;
|
value?: any;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
settings?: Record<string, any>;
|
settings?: Record<string, any>;
|
||||||
|
|||||||
@ -0,0 +1,6 @@
|
|||||||
|
import { FieldMetadataType } from 'twenty-shared/types';
|
||||||
|
|
||||||
|
export type WorkflowFormFieldType =
|
||||||
|
| FieldMetadataType.TEXT
|
||||||
|
| FieldMetadataType.NUMBER
|
||||||
|
| 'RECORD';
|
||||||
Reference in New Issue
Block a user