Make upsert action body common with create record instead of update record (#15442)
We do not want the fields to update multiselect for upsert record action. We want all available fields displayed by default. This makes upsert record action closer to create record than update record. This PR: - deletes WorkflowUpdateRecordBody that was common between update and upsert and put back content into update - creates WorkflowCreateRecordBody that is now common between create and upsert - simplifies shouldDisplayFormField Before - using fields to update as update record action <img width="546" height="823" alt="Capture d’écran 2025-10-30 à 10 00 24" src="https://github.com/user-attachments/assets/9206bc8b-75c2-40fa-a8de-e708b6b2cd05" /> After - displaying all fields as create record action <img width="546" height="823" alt="Capture d’écran 2025-10-30 à 10 00 04" src="https://github.com/user-attachments/assets/87141a47-946f-4604-be55-f4c21ff4a3d8" />
This commit is contained in:
+239
@@ -0,0 +1,239 @@
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { formatFieldMetadataItemAsFieldDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition';
|
||||
import { FormFieldInput } from '@/object-record/record-field/ui/components/FormFieldInput';
|
||||
import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
|
||||
import { useViewOrDefaultViewFromPrefetchedViews } from '@/views/hooks/useViewOrDefaultViewFromPrefetchedViews';
|
||||
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
|
||||
import { shouldDisplayFormField } from '@/workflow/workflow-steps/workflow-actions/utils/shouldDisplayFormField';
|
||||
import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { canObjectBeManagedByWorkflow } from 'twenty-shared/workflow';
|
||||
import { HorizontalSeparator, useIcons } from 'twenty-ui/display';
|
||||
import { type SelectOption } from 'twenty-ui/input';
|
||||
import { type JsonValue } from 'type-fest';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import { RelationType } from '~/generated-metadata/graphql';
|
||||
|
||||
type RelationManyToOneField = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type CreateRecordFormData = {
|
||||
objectName: string;
|
||||
[field: string]: RelationManyToOneField | JsonValue;
|
||||
};
|
||||
|
||||
type WorkflowCreateRecordBodyProps = {
|
||||
defaultObjectName: string;
|
||||
defaultObjectRecord: Record<string, unknown>;
|
||||
readonly: boolean;
|
||||
actionType: 'CREATE_RECORD' | 'UPSERT_RECORD';
|
||||
onUpdate: (formData: CreateRecordFormData) => void;
|
||||
};
|
||||
|
||||
const sortByViewFieldPosition = (
|
||||
a: { viewFieldPosition?: number },
|
||||
b: { viewFieldPosition?: number },
|
||||
) => {
|
||||
if (isDefined(a.viewFieldPosition) && isDefined(b.viewFieldPosition)) {
|
||||
return a.viewFieldPosition - b.viewFieldPosition;
|
||||
}
|
||||
|
||||
if (isDefined(a.viewFieldPosition)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (isDefined(b.viewFieldPosition)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
export const WorkflowCreateRecordBody = ({
|
||||
defaultObjectName,
|
||||
defaultObjectRecord,
|
||||
readonly,
|
||||
actionType,
|
||||
onUpdate,
|
||||
}: WorkflowCreateRecordBodyProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const { getIcon } = useIcons();
|
||||
|
||||
const { activeNonSystemObjectMetadataItems } =
|
||||
useFilteredObjectMetadataItems();
|
||||
|
||||
const availableMetadata: Array<SelectOption<string>> =
|
||||
activeNonSystemObjectMetadataItems
|
||||
.filter((objectMetadataItem) =>
|
||||
canObjectBeManagedByWorkflow({
|
||||
nameSingular: objectMetadataItem.nameSingular,
|
||||
isSystem: objectMetadataItem.isSystem,
|
||||
}),
|
||||
)
|
||||
.map((item) => ({
|
||||
Icon: getIcon(item.icon),
|
||||
label: item.labelPlural,
|
||||
value: item.nameSingular,
|
||||
}));
|
||||
|
||||
const [formData, setFormData] = useState<CreateRecordFormData>({
|
||||
objectName: defaultObjectName,
|
||||
...defaultObjectRecord,
|
||||
});
|
||||
|
||||
const objectNameSingular = formData.objectName;
|
||||
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(item) => item.nameSingular === objectNameSingular,
|
||||
);
|
||||
|
||||
const { view: indexView } = useViewOrDefaultViewFromPrefetchedViews({
|
||||
objectMetadataItemId: objectMetadataItem?.id ?? '',
|
||||
});
|
||||
|
||||
const viewFields = indexView?.viewFields ?? [];
|
||||
|
||||
const inlineFieldMetadataItems = objectMetadataItem?.fields
|
||||
.filter((fieldMetadataItem) =>
|
||||
shouldDisplayFormField({ fieldMetadataItem, actionType }),
|
||||
)
|
||||
.map((fieldMetadataItem) => {
|
||||
const viewField = viewFields.find(
|
||||
(viewField) => viewField.fieldMetadataId === fieldMetadataItem.id,
|
||||
);
|
||||
return {
|
||||
...fieldMetadataItem,
|
||||
viewFieldPosition: viewField?.position,
|
||||
};
|
||||
})
|
||||
.sort(sortByViewFieldPosition);
|
||||
|
||||
const inlineFieldDefinitions = isDefined(objectMetadataItem)
|
||||
? inlineFieldMetadataItems?.map((fieldMetadataItem) =>
|
||||
formatFieldMetadataItemAsFieldDefinition({
|
||||
field: fieldMetadataItem,
|
||||
objectMetadataItem,
|
||||
showLabel: true,
|
||||
labelWidth: 90,
|
||||
}),
|
||||
)
|
||||
: [];
|
||||
|
||||
const handleFieldChange = (
|
||||
fieldName: keyof CreateRecordFormData,
|
||||
updatedValue: JsonValue,
|
||||
) => {
|
||||
const fieldDefinition = inlineFieldDefinitions?.find(
|
||||
(definition) => definition.metadata.fieldName === fieldName,
|
||||
);
|
||||
|
||||
if (!isDefined(fieldDefinition)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isFieldRelationManyToOne =
|
||||
isFieldRelation(fieldDefinition) &&
|
||||
fieldDefinition.metadata.relationType === RelationType.MANY_TO_ONE;
|
||||
|
||||
const fieldValue = isFieldRelationManyToOne
|
||||
? {
|
||||
id: updatedValue,
|
||||
}
|
||||
: updatedValue;
|
||||
|
||||
const newFormData: CreateRecordFormData = {
|
||||
...formData,
|
||||
[fieldName]: fieldValue,
|
||||
};
|
||||
|
||||
setFormData(newFormData);
|
||||
|
||||
saveAction(newFormData);
|
||||
};
|
||||
|
||||
const saveAction = useDebouncedCallback(
|
||||
async (formData: CreateRecordFormData) => {
|
||||
if (readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { objectName: updatedObjectName, ...updatedOtherFields } = formData;
|
||||
|
||||
onUpdate({
|
||||
objectName: updatedObjectName,
|
||||
...updatedOtherFields,
|
||||
});
|
||||
},
|
||||
1_000,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
saveAction.flush();
|
||||
};
|
||||
}, [saveAction]);
|
||||
|
||||
return (
|
||||
<WorkflowStepBody>
|
||||
<Select
|
||||
dropdownId="workflow-create-record-object-name"
|
||||
label="Object"
|
||||
fullWidth
|
||||
disabled={readonly}
|
||||
value={formData.objectName}
|
||||
emptyOption={{ label: 'Select an option', value: '' }}
|
||||
options={availableMetadata}
|
||||
onChange={(updatedObjectName) => {
|
||||
const newFormData: CreateRecordFormData = {
|
||||
objectName: updatedObjectName,
|
||||
};
|
||||
|
||||
setFormData(newFormData);
|
||||
|
||||
saveAction(newFormData);
|
||||
}}
|
||||
withSearchInput
|
||||
dropdownOffset={{ y: parseInt(theme.spacing(1), 10) }}
|
||||
dropdownWidth={GenericDropdownContentWidth.ExtraLarge}
|
||||
/>
|
||||
|
||||
<HorizontalSeparator noMargin />
|
||||
|
||||
{inlineFieldDefinitions?.map((fieldDefinition) => {
|
||||
const isFieldRelationManyToOne =
|
||||
isFieldRelation(fieldDefinition) &&
|
||||
fieldDefinition.metadata.relationType === RelationType.MANY_TO_ONE;
|
||||
|
||||
const currentValue = isFieldRelationManyToOne
|
||||
? (
|
||||
formData[
|
||||
fieldDefinition.metadata.fieldName
|
||||
] as RelationManyToOneField
|
||||
)?.id
|
||||
: (formData[fieldDefinition.metadata.fieldName] as JsonValue);
|
||||
|
||||
return (
|
||||
<FormFieldInput
|
||||
key={fieldDefinition.metadata.fieldName}
|
||||
defaultValue={currentValue}
|
||||
field={fieldDefinition}
|
||||
onChange={(value) => {
|
||||
handleFieldChange(fieldDefinition.metadata.fieldName, value);
|
||||
}}
|
||||
VariablePicker={WorkflowVariablePicker}
|
||||
readonly={readonly}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</WorkflowStepBody>
|
||||
);
|
||||
};
|
||||
+31
-224
@@ -1,28 +1,13 @@
|
||||
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { formatFieldMetadataItemAsFieldDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition';
|
||||
import { FormFieldInput } from '@/object-record/record-field/ui/components/FormFieldInput';
|
||||
import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
|
||||
import { useViewOrDefaultViewFromPrefetchedViews } from '@/views/hooks/useViewOrDefaultViewFromPrefetchedViews';
|
||||
import { type WorkflowCreateRecordAction } from '@/workflow/types/Workflow';
|
||||
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
|
||||
import { WorkflowStepFooter } from '@/workflow/workflow-steps/components/WorkflowStepFooter';
|
||||
import {
|
||||
WorkflowCreateRecordBody,
|
||||
type CreateRecordFormData,
|
||||
} from '@/workflow/workflow-steps/workflow-actions/components/WorkflowCreateRecordBody';
|
||||
import { CREATE_RECORD_ACTION } from '@/workflow/workflow-steps/workflow-actions/constants/actions/CreateRecordAction';
|
||||
import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-actions/hooks/useWorkflowActionHeader';
|
||||
import { shouldDisplayFormField } from '@/workflow/workflow-steps/workflow-actions/utils/shouldDisplayFormField';
|
||||
import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { canObjectBeManagedByWorkflow } from 'twenty-shared/workflow';
|
||||
import { HorizontalSeparator, useIcons } from 'twenty-ui/display';
|
||||
import { type SelectOption } from 'twenty-ui/input';
|
||||
import { type JsonValue } from 'type-fest';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import { RelationType } from '~/generated-metadata/graphql';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
|
||||
type WorkflowEditActionCreateRecordProps = {
|
||||
action: WorkflowCreateRecordAction;
|
||||
@@ -36,172 +21,39 @@ type WorkflowEditActionCreateRecordProps = {
|
||||
};
|
||||
};
|
||||
|
||||
type RelationManyToOneField = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
type CreateRecordFormData = {
|
||||
objectName: string;
|
||||
[field: string]: RelationManyToOneField | JsonValue;
|
||||
};
|
||||
|
||||
const sortByViewFieldPosition = (
|
||||
a: { viewFieldPosition?: number },
|
||||
b: { viewFieldPosition?: number },
|
||||
) => {
|
||||
if (isDefined(a.viewFieldPosition) && isDefined(b.viewFieldPosition)) {
|
||||
return a.viewFieldPosition - b.viewFieldPosition;
|
||||
}
|
||||
|
||||
if (isDefined(a.viewFieldPosition)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (isDefined(b.viewFieldPosition)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
export const WorkflowEditActionCreateRecord = ({
|
||||
action,
|
||||
actionOptions,
|
||||
}: WorkflowEditActionCreateRecordProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const { getIcon } = useIcons();
|
||||
|
||||
const { activeNonSystemObjectMetadataItems } =
|
||||
useFilteredObjectMetadataItems();
|
||||
|
||||
const availableMetadata: Array<SelectOption<string>> =
|
||||
activeNonSystemObjectMetadataItems
|
||||
.filter((objectMetadataItem) =>
|
||||
canObjectBeManagedByWorkflow({
|
||||
nameSingular: objectMetadataItem.nameSingular,
|
||||
isSystem: objectMetadataItem.isSystem,
|
||||
}),
|
||||
)
|
||||
.map((item) => ({
|
||||
Icon: getIcon(item.icon),
|
||||
label: item.labelPlural,
|
||||
value: item.nameSingular,
|
||||
}));
|
||||
|
||||
const [formData, setFormData] = useState<CreateRecordFormData>({
|
||||
objectName: action.settings.input.objectName,
|
||||
...action.settings.input.objectRecord,
|
||||
});
|
||||
|
||||
const isFormDisabled = actionOptions.readonly;
|
||||
|
||||
const objectNameSingular = formData.objectName;
|
||||
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(item) => item.nameSingular === objectNameSingular,
|
||||
);
|
||||
|
||||
const { view: indexView } = useViewOrDefaultViewFromPrefetchedViews({
|
||||
objectMetadataItemId: objectMetadataItem?.id ?? '',
|
||||
});
|
||||
|
||||
const viewFields = indexView?.viewFields ?? [];
|
||||
|
||||
const inlineFieldMetadataItems = objectMetadataItem?.fields
|
||||
.filter((fieldMetadataItem) =>
|
||||
shouldDisplayFormField({ fieldMetadataItem, actionType: action.type }),
|
||||
)
|
||||
.map((fieldMetadataItem) => {
|
||||
const viewField = viewFields.find(
|
||||
(viewField) => viewField.fieldMetadataId === fieldMetadataItem.id,
|
||||
);
|
||||
return {
|
||||
...fieldMetadataItem,
|
||||
viewFieldPosition: viewField?.position,
|
||||
};
|
||||
})
|
||||
.sort(sortByViewFieldPosition);
|
||||
|
||||
const inlineFieldDefinitions = isDefined(objectMetadataItem)
|
||||
? inlineFieldMetadataItems?.map((fieldMetadataItem) =>
|
||||
formatFieldMetadataItemAsFieldDefinition({
|
||||
field: fieldMetadataItem,
|
||||
objectMetadataItem,
|
||||
showLabel: true,
|
||||
labelWidth: 90,
|
||||
}),
|
||||
)
|
||||
: [];
|
||||
|
||||
const handleFieldChange = (
|
||||
fieldName: keyof CreateRecordFormData,
|
||||
updatedValue: JsonValue,
|
||||
) => {
|
||||
const fieldDefinition = inlineFieldDefinitions?.find(
|
||||
(definition) => definition.metadata.fieldName === fieldName,
|
||||
);
|
||||
|
||||
if (!isDefined(fieldDefinition)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isFieldRelationManyToOne =
|
||||
isFieldRelation(fieldDefinition) &&
|
||||
fieldDefinition.metadata.relationType === RelationType.MANY_TO_ONE;
|
||||
|
||||
const fieldValue = isFieldRelationManyToOne
|
||||
? {
|
||||
id: updatedValue,
|
||||
}
|
||||
: updatedValue;
|
||||
|
||||
const newFormData: CreateRecordFormData = {
|
||||
...formData,
|
||||
[fieldName]: fieldValue,
|
||||
};
|
||||
|
||||
setFormData(newFormData);
|
||||
|
||||
saveAction(newFormData);
|
||||
};
|
||||
|
||||
const saveAction = useDebouncedCallback(
|
||||
async (formData: CreateRecordFormData) => {
|
||||
if (actionOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { objectName: updatedObjectName, ...updatedOtherFields } = formData;
|
||||
|
||||
actionOptions.onActionUpdate({
|
||||
...action,
|
||||
settings: {
|
||||
...action.settings,
|
||||
input: {
|
||||
objectName: updatedObjectName,
|
||||
objectRecord: updatedOtherFields,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
1_000,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
saveAction.flush();
|
||||
};
|
||||
}, [saveAction]);
|
||||
|
||||
const { headerTitle, headerIcon, headerIconColor, headerType } =
|
||||
useWorkflowActionHeader({
|
||||
action,
|
||||
defaultTitle: CREATE_RECORD_ACTION.defaultLabel,
|
||||
});
|
||||
|
||||
const handleUpdate = (formData: CreateRecordFormData) => {
|
||||
if (actionOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { objectName: updatedObjectName, ...updatedOtherFields } = formData;
|
||||
|
||||
actionOptions.onActionUpdate({
|
||||
...action,
|
||||
settings: {
|
||||
...action.settings,
|
||||
input: {
|
||||
objectName: updatedObjectName,
|
||||
objectRecord: updatedOtherFields,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<SidePanelHeader
|
||||
@@ -222,58 +74,13 @@ export const WorkflowEditActionCreateRecord = ({
|
||||
disabled={isFormDisabled}
|
||||
iconTooltip={CREATE_RECORD_ACTION.defaultLabel}
|
||||
/>
|
||||
<WorkflowStepBody>
|
||||
<Select
|
||||
dropdownId="workflow-edit-action-record-create-object-name"
|
||||
label="Object"
|
||||
fullWidth
|
||||
disabled={isFormDisabled}
|
||||
value={formData.objectName}
|
||||
emptyOption={{ label: 'Select an option', value: '' }}
|
||||
options={availableMetadata}
|
||||
onChange={(updatedObjectName) => {
|
||||
const newFormData: CreateRecordFormData = {
|
||||
objectName: updatedObjectName,
|
||||
};
|
||||
|
||||
setFormData(newFormData);
|
||||
|
||||
saveAction(newFormData);
|
||||
}}
|
||||
withSearchInput
|
||||
dropdownOffset={{ y: parseInt(theme.spacing(1), 10) }}
|
||||
dropdownWidth={GenericDropdownContentWidth.ExtraLarge}
|
||||
/>
|
||||
|
||||
<HorizontalSeparator noMargin />
|
||||
|
||||
{inlineFieldDefinitions?.map((fieldDefinition) => {
|
||||
const isFieldRelationManyToOne =
|
||||
isFieldRelation(fieldDefinition) &&
|
||||
fieldDefinition.metadata.relationType === RelationType.MANY_TO_ONE;
|
||||
|
||||
const currentValue = isFieldRelationManyToOne
|
||||
? (
|
||||
formData[
|
||||
fieldDefinition.metadata.fieldName
|
||||
] as RelationManyToOneField
|
||||
)?.id
|
||||
: (formData[fieldDefinition.metadata.fieldName] as JsonValue);
|
||||
|
||||
return (
|
||||
<FormFieldInput
|
||||
key={fieldDefinition.metadata.fieldName}
|
||||
defaultValue={currentValue}
|
||||
field={fieldDefinition}
|
||||
onChange={(value) => {
|
||||
handleFieldChange(fieldDefinition.metadata.fieldName, value);
|
||||
}}
|
||||
VariablePicker={WorkflowVariablePicker}
|
||||
readonly={isFormDisabled}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</WorkflowStepBody>
|
||||
<WorkflowCreateRecordBody
|
||||
defaultObjectName={action.settings.input.objectName}
|
||||
defaultObjectRecord={action.settings.input.objectRecord}
|
||||
readonly={isFormDisabled ?? false}
|
||||
actionType="CREATE_RECORD"
|
||||
onUpdate={handleUpdate}
|
||||
/>
|
||||
{!actionOptions.readonly && <WorkflowStepFooter stepId={action.id} />}
|
||||
</>
|
||||
);
|
||||
|
||||
+10
-10
@@ -2,11 +2,13 @@ import { GMAIL_SEND_SCOPE } from '@/accounts/constants/GmailSendScope';
|
||||
import { MICROSOFT_SEND_SCOPE } from '@/accounts/constants/MicrosoftSendScope';
|
||||
import { type ConnectedAccount } from '@/accounts/types/ConnectedAccount';
|
||||
import { useUploadAttachmentFile } from '@/activities/files/hooks/useUploadAttachmentFile';
|
||||
import { WorkflowSendEmailAttachments } from '@/advanced-text-editor/components/WorkflowSendEmailAttachments';
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { FormAdvancedTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormAdvancedTextFieldInput';
|
||||
import { FormTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormTextFieldInput';
|
||||
import { useTriggerApisOAuth } from '@/settings/accounts/hooks/useTriggerApiOAuth';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
@@ -16,6 +18,7 @@ import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/ho
|
||||
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||
import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState';
|
||||
import { type WorkflowSendEmailAction } from '@/workflow/types/Workflow';
|
||||
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
|
||||
import { WorkflowStepFooter } from '@/workflow/workflow-steps/components/WorkflowStepFooter';
|
||||
import { SEND_EMAIL_ACTION } from '@/workflow/workflow-steps/workflow-actions/constants/actions/SendEmailAction';
|
||||
import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-actions/hooks/useWorkflowActionHeader';
|
||||
@@ -31,9 +34,6 @@ import { type SelectOption } from 'twenty-ui/input';
|
||||
import { type JsonValue } from 'type-fest';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
||||
import { WorkflowSendEmailAttachments } from '@/advanced-text-editor/components/WorkflowSendEmailAttachments';
|
||||
import { FormAdvancedTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormAdvancedTextFieldInput';
|
||||
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
|
||||
|
||||
const EMAIL_EDITOR_MIN_HEIGHT = 340;
|
||||
|
||||
@@ -331,13 +331,6 @@ export const WorkflowEditActionSendEmail = ({
|
||||
}}
|
||||
VariablePicker={WorkflowVariablePicker}
|
||||
/>
|
||||
<WorkflowSendEmailAttachments
|
||||
label="Attachments"
|
||||
files={formData.files}
|
||||
onChange={(files) => {
|
||||
handleFieldChange('files', files);
|
||||
}}
|
||||
/>
|
||||
<FormAdvancedTextFieldInput
|
||||
label="Body"
|
||||
placeholder="Enter email body"
|
||||
@@ -366,6 +359,13 @@ export const WorkflowEditActionSendEmail = ({
|
||||
minHeight={EMAIL_EDITOR_MIN_HEIGHT}
|
||||
maxWidth={EMAIL_EDITOR_MAX_WIDTH}
|
||||
/>
|
||||
<WorkflowSendEmailAttachments
|
||||
label="Attachments"
|
||||
files={formData.files}
|
||||
onChange={(files) => {
|
||||
handleFieldChange('files', files);
|
||||
}}
|
||||
/>
|
||||
</WorkflowStepBody>
|
||||
{!actionOptions.readonly && <WorkflowStepFooter stepId={action.id} />}
|
||||
</>
|
||||
|
||||
+212
-32
@@ -1,12 +1,29 @@
|
||||
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { formatFieldMetadataItemAsFieldDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition';
|
||||
import { FormFieldInput } from '@/object-record/record-field/ui/components/FormFieldInput';
|
||||
import { FormSingleRecordPicker } from '@/object-record/record-field/ui/form-types/components/FormSingleRecordPicker';
|
||||
import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
|
||||
import { WorkflowFieldsMultiSelect } from '@/workflow/components/WorkflowEditUpdateEventFieldsMultiSelect';
|
||||
import { type WorkflowUpdateRecordAction } from '@/workflow/types/Workflow';
|
||||
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
|
||||
import { WorkflowStepFooter } from '@/workflow/workflow-steps/components/WorkflowStepFooter';
|
||||
import { WorkflowUpdateRecordBody } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowUpdateRecordBody';
|
||||
|
||||
import { UPDATE_RECORD_ACTION } from '@/workflow/workflow-steps/workflow-actions/constants/actions/UpdateRecordAction';
|
||||
import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-actions/hooks/useWorkflowActionHeader';
|
||||
import { type UpdateRecordFormData } from '@/workflow/workflow-steps/workflow-actions/types/update-record-form-data.type';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
import { shouldDisplayFormField } from '@/workflow/workflow-steps/workflow-actions/utils/shouldDisplayFormField';
|
||||
import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { canObjectBeManagedByWorkflow } from 'twenty-shared/workflow';
|
||||
import { HorizontalSeparator, useIcons } from 'twenty-ui/display';
|
||||
import { type SelectOption } from 'twenty-ui/input';
|
||||
import { type JsonValue } from 'type-fest';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import { RelationType } from '~/generated-metadata/graphql';
|
||||
|
||||
type WorkflowEditActionUpdateRecordProps = {
|
||||
action: WorkflowUpdateRecordAction;
|
||||
@@ -24,35 +41,119 @@ export const WorkflowEditActionUpdateRecord = ({
|
||||
action,
|
||||
actionOptions,
|
||||
}: WorkflowEditActionUpdateRecordProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const { getIcon } = useIcons();
|
||||
|
||||
const { activeNonSystemObjectMetadataItems } =
|
||||
useFilteredObjectMetadataItems();
|
||||
|
||||
const availableMetadata: Array<SelectOption<string>> =
|
||||
activeNonSystemObjectMetadataItems
|
||||
.filter((objectMetadataItem) =>
|
||||
canObjectBeManagedByWorkflow({
|
||||
nameSingular: objectMetadataItem.nameSingular,
|
||||
isSystem: objectMetadataItem.isSystem,
|
||||
}),
|
||||
)
|
||||
.map((item) => ({
|
||||
Icon: getIcon(item.icon),
|
||||
label: item.labelPlural,
|
||||
value: item.nameSingular,
|
||||
}));
|
||||
|
||||
const [formData, setFormData] = useState<UpdateRecordFormData>({
|
||||
objectNameSingular: action.settings.input.objectName,
|
||||
objectRecordId: action.settings.input.objectRecordId,
|
||||
fieldsToUpdate: action.settings.input.fieldsToUpdate ?? [],
|
||||
...action.settings.input.objectRecord,
|
||||
});
|
||||
|
||||
const isFormDisabled = actionOptions.readonly === true;
|
||||
|
||||
const { headerTitle, headerIcon, headerIconColor, headerType } =
|
||||
useWorkflowActionHeader({
|
||||
action,
|
||||
defaultTitle: UPDATE_RECORD_ACTION.defaultLabel,
|
||||
});
|
||||
|
||||
const { getIcon } = useIcons();
|
||||
const handleFieldChange = (
|
||||
fieldName: keyof UpdateRecordFormData,
|
||||
updatedValue: JsonValue,
|
||||
) => {
|
||||
const newFormData: UpdateRecordFormData = {
|
||||
...formData,
|
||||
[fieldName]: updatedValue,
|
||||
};
|
||||
|
||||
const isFormDisabled = actionOptions.readonly === true;
|
||||
setFormData(newFormData);
|
||||
|
||||
const handleUpdate = (formData: UpdateRecordFormData) => {
|
||||
if (actionOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
actionOptions.onActionUpdate({
|
||||
...action,
|
||||
settings: {
|
||||
...action.settings,
|
||||
input: {
|
||||
objectName: formData.objectNameSingular,
|
||||
objectRecord: formData,
|
||||
objectRecordId: formData.objectRecordId ?? '',
|
||||
fieldsToUpdate: formData.fieldsToUpdate,
|
||||
},
|
||||
},
|
||||
});
|
||||
saveAction(newFormData);
|
||||
};
|
||||
|
||||
const selectedObjectMetadataItem = activeNonSystemObjectMetadataItems.find(
|
||||
(item) => item.nameSingular === formData.objectNameSingular,
|
||||
);
|
||||
|
||||
const objectNameSingular = selectedObjectMetadataItem?.nameSingular;
|
||||
|
||||
const inlineFieldMetadataItems = selectedObjectMetadataItem?.fields
|
||||
.filter((fieldMetadataItem) =>
|
||||
shouldDisplayFormField({
|
||||
fieldMetadataItem,
|
||||
actionType: 'UPDATE_RECORD',
|
||||
}),
|
||||
)
|
||||
.sort((fieldMetadataItemA, fieldMetadataItemB) =>
|
||||
fieldMetadataItemA.name.localeCompare(fieldMetadataItemB.name),
|
||||
);
|
||||
|
||||
const inlineFieldDefinitions = isDefined(selectedObjectMetadataItem)
|
||||
? inlineFieldMetadataItems?.map((fieldMetadataItem) =>
|
||||
formatFieldMetadataItemAsFieldDefinition({
|
||||
field: fieldMetadataItem,
|
||||
objectMetadataItem: selectedObjectMetadataItem,
|
||||
showLabel: true,
|
||||
labelWidth: 90,
|
||||
}),
|
||||
)
|
||||
: [];
|
||||
|
||||
const saveAction = useDebouncedCallback(
|
||||
async (formData: UpdateRecordFormData) => {
|
||||
if (actionOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
objectNameSingular: updatedObjectName,
|
||||
objectRecordId: updatedObjectRecordId,
|
||||
fieldsToUpdate: updatedFieldsToUpdate,
|
||||
...updatedOtherFields
|
||||
} = formData;
|
||||
|
||||
actionOptions.onActionUpdate({
|
||||
...action,
|
||||
settings: {
|
||||
...action.settings,
|
||||
input: {
|
||||
objectName: updatedObjectName,
|
||||
objectRecordId: updatedObjectRecordId ?? '',
|
||||
fieldsToUpdate: updatedFieldsToUpdate ?? [],
|
||||
objectRecord: updatedOtherFields,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
1_000,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
saveAction.flush();
|
||||
};
|
||||
}, [saveAction]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SidePanelHeader
|
||||
@@ -73,16 +174,95 @@ export const WorkflowEditActionUpdateRecord = ({
|
||||
disabled={isFormDisabled}
|
||||
iconTooltip={UPDATE_RECORD_ACTION.defaultLabel}
|
||||
/>
|
||||
<WorkflowUpdateRecordBody
|
||||
defaultObjectNameSingular={action.settings.input.objectName}
|
||||
defaultObjectRecordId={action.settings.input.objectRecordId}
|
||||
defaultFieldsToUpdate={action.settings.input.fieldsToUpdate}
|
||||
defaultObjectRecord={action.settings.input.objectRecord}
|
||||
readonly={isFormDisabled}
|
||||
actionType="UPDATE_RECORD"
|
||||
onUpdate={handleUpdate}
|
||||
shouldPickRecord={true}
|
||||
/>
|
||||
<WorkflowStepBody>
|
||||
<Select
|
||||
dropdownId="workflow-update-record-object-name"
|
||||
label="Object"
|
||||
fullWidth
|
||||
disabled={isFormDisabled}
|
||||
value={formData.objectNameSingular}
|
||||
emptyOption={{ label: 'Select an option', value: '' }}
|
||||
options={availableMetadata}
|
||||
onChange={(updatedObjectName) => {
|
||||
const newFormData: UpdateRecordFormData = {
|
||||
objectNameSingular: updatedObjectName,
|
||||
objectRecordId: '',
|
||||
fieldsToUpdate: [],
|
||||
};
|
||||
|
||||
setFormData(newFormData);
|
||||
|
||||
saveAction(newFormData);
|
||||
}}
|
||||
withSearchInput
|
||||
dropdownOffset={{ y: parseInt(theme.spacing(1), 10) }}
|
||||
dropdownWidth={GenericDropdownContentWidth.ExtraLarge}
|
||||
/>
|
||||
|
||||
<HorizontalSeparator noMargin />
|
||||
|
||||
{isDefined(objectNameSingular) && (
|
||||
<FormSingleRecordPicker
|
||||
testId="workflow-update-record-object-record-id"
|
||||
label="Record"
|
||||
onChange={(objectRecordId) =>
|
||||
handleFieldChange('objectRecordId', objectRecordId)
|
||||
}
|
||||
objectNameSingulars={[objectNameSingular]}
|
||||
defaultValue={formData.objectRecordId}
|
||||
disabled={isFormDisabled}
|
||||
VariablePicker={WorkflowVariablePicker}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isDefined(selectedObjectMetadataItem) && (
|
||||
<WorkflowFieldsMultiSelect
|
||||
label="Fields to update"
|
||||
placeholder="Select fields to update"
|
||||
objectMetadataItem={selectedObjectMetadataItem}
|
||||
handleFieldsChange={(fieldsToUpdate) =>
|
||||
handleFieldChange('fieldsToUpdate', fieldsToUpdate)
|
||||
}
|
||||
readonly={isFormDisabled ?? false}
|
||||
defaultFields={formData.fieldsToUpdate}
|
||||
/>
|
||||
)}
|
||||
|
||||
<HorizontalSeparator noMargin />
|
||||
|
||||
{formData.fieldsToUpdate.map((fieldName) => {
|
||||
const fieldDefinition = inlineFieldDefinitions?.find((definition) => {
|
||||
const isFieldRelationManyToOne =
|
||||
isFieldRelation(definition) &&
|
||||
definition.metadata.relationType === RelationType.MANY_TO_ONE;
|
||||
|
||||
const value = isFieldRelationManyToOne
|
||||
? `${definition.metadata.fieldName}Id`
|
||||
: definition.metadata.fieldName;
|
||||
|
||||
return value === fieldName;
|
||||
});
|
||||
|
||||
if (!isDefined(fieldDefinition)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const currentValue = formData[fieldName] as JsonValue;
|
||||
|
||||
return (
|
||||
<FormFieldInput
|
||||
key={fieldName}
|
||||
defaultValue={currentValue}
|
||||
field={fieldDefinition}
|
||||
onChange={(value) => {
|
||||
handleFieldChange(fieldName, value);
|
||||
}}
|
||||
VariablePicker={WorkflowVariablePicker}
|
||||
readonly={isFormDisabled}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</WorkflowStepBody>
|
||||
{!actionOptions.readonly && <WorkflowStepFooter stepId={action.id} />}
|
||||
</>
|
||||
);
|
||||
|
||||
+11
-10
@@ -1,11 +1,13 @@
|
||||
import { SidePanelHeader } from '@/command-menu/components/SidePanelHeader';
|
||||
import { type WorkflowUpsertRecordAction } from '@/workflow/types/Workflow';
|
||||
import { WorkflowStepFooter } from '@/workflow/workflow-steps/components/WorkflowStepFooter';
|
||||
import { WorkflowUpdateRecordBody } from '@/workflow/workflow-steps/workflow-actions/components/WorkflowUpdateRecordBody';
|
||||
import {
|
||||
WorkflowCreateRecordBody,
|
||||
type CreateRecordFormData,
|
||||
} from '@/workflow/workflow-steps/workflow-actions/components/WorkflowCreateRecordBody';
|
||||
|
||||
import { UPSERT_RECORD_ACTION } from '@/workflow/workflow-steps/workflow-actions/constants/actions/UpsertRecordAction';
|
||||
import { useWorkflowActionHeader } from '@/workflow/workflow-steps/workflow-actions/hooks/useWorkflowActionHeader';
|
||||
import { type UpdateRecordFormData } from '@/workflow/workflow-steps/workflow-actions/types/update-record-form-data.type';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
|
||||
type WorkflowEditActionUpsertRecordProps = {
|
||||
@@ -34,19 +36,20 @@ export const WorkflowEditActionUpsertRecord = ({
|
||||
|
||||
const isFormDisabled = actionOptions.readonly === true;
|
||||
|
||||
const handleUpdate = (formData: UpdateRecordFormData) => {
|
||||
const handleUpdate = (formData: CreateRecordFormData) => {
|
||||
if (actionOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { objectName: updatedObjectName, ...updatedOtherFields } = formData;
|
||||
|
||||
actionOptions.onActionUpdate({
|
||||
...action,
|
||||
settings: {
|
||||
...action.settings,
|
||||
input: {
|
||||
objectName: formData.objectNameSingular,
|
||||
objectRecord: formData,
|
||||
fieldsToUpdate: formData.fieldsToUpdate,
|
||||
objectName: updatedObjectName,
|
||||
objectRecord: updatedOtherFields,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -72,14 +75,12 @@ export const WorkflowEditActionUpsertRecord = ({
|
||||
disabled={isFormDisabled}
|
||||
iconTooltip={UPSERT_RECORD_ACTION.defaultLabel}
|
||||
/>
|
||||
<WorkflowUpdateRecordBody
|
||||
defaultObjectNameSingular={action.settings.input.objectName}
|
||||
defaultFieldsToUpdate={action.settings.input.fieldsToUpdate}
|
||||
<WorkflowCreateRecordBody
|
||||
defaultObjectName={action.settings.input.objectName}
|
||||
defaultObjectRecord={action.settings.input.objectRecord}
|
||||
readonly={isFormDisabled}
|
||||
actionType="UPSERT_RECORD"
|
||||
onUpdate={handleUpdate}
|
||||
shouldPickRecord={false}
|
||||
/>
|
||||
{!actionOptions.readonly && <WorkflowStepFooter stepId={action.id} />}
|
||||
</>
|
||||
|
||||
-232
@@ -1,232 +0,0 @@
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { formatFieldMetadataItemAsFieldDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition';
|
||||
import { FormFieldInput } from '@/object-record/record-field/ui/components/FormFieldInput';
|
||||
import { FormSingleRecordPicker } from '@/object-record/record-field/ui/form-types/components/FormSingleRecordPicker';
|
||||
import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation';
|
||||
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
|
||||
import { WorkflowFieldsMultiSelect } from '@/workflow/components/WorkflowEditUpdateEventFieldsMultiSelect';
|
||||
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
|
||||
import { type UpdateRecordFormData } from '@/workflow/workflow-steps/workflow-actions/types/update-record-form-data.type';
|
||||
import { shouldDisplayFormField } from '@/workflow/workflow-steps/workflow-actions/utils/shouldDisplayFormField';
|
||||
import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { canObjectBeManagedByWorkflow } from 'twenty-shared/workflow';
|
||||
import { HorizontalSeparator, useIcons } from 'twenty-ui/display';
|
||||
import { type SelectOption } from 'twenty-ui/input';
|
||||
import { type JsonValue } from 'type-fest';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import { RelationType } from '~/generated-metadata/graphql';
|
||||
|
||||
type WorkflowUpdateRecordBodyProps = {
|
||||
defaultObjectNameSingular: string;
|
||||
defaultFieldsToUpdate: string[];
|
||||
defaultObjectRecord: Record<string, unknown>;
|
||||
defaultObjectRecordId?: string;
|
||||
actionType: 'UPDATE_RECORD' | 'UPSERT_RECORD';
|
||||
readonly: boolean;
|
||||
onUpdate: (formData: UpdateRecordFormData) => void;
|
||||
shouldPickRecord: boolean;
|
||||
};
|
||||
|
||||
export const WorkflowUpdateRecordBody = ({
|
||||
defaultObjectNameSingular,
|
||||
defaultFieldsToUpdate,
|
||||
defaultObjectRecord,
|
||||
defaultObjectRecordId,
|
||||
readonly,
|
||||
actionType,
|
||||
onUpdate,
|
||||
shouldPickRecord,
|
||||
}: WorkflowUpdateRecordBodyProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const { getIcon } = useIcons();
|
||||
|
||||
const { activeNonSystemObjectMetadataItems } =
|
||||
useFilteredObjectMetadataItems();
|
||||
|
||||
const availableMetadata: Array<SelectOption<string>> =
|
||||
activeNonSystemObjectMetadataItems
|
||||
.filter((objectMetadataItem) =>
|
||||
canObjectBeManagedByWorkflow({
|
||||
nameSingular: objectMetadataItem.nameSingular,
|
||||
isSystem: objectMetadataItem.isSystem,
|
||||
}),
|
||||
)
|
||||
.map((item) => ({
|
||||
Icon: getIcon(item.icon),
|
||||
label: item.labelPlural,
|
||||
value: item.nameSingular,
|
||||
}));
|
||||
|
||||
const [formData, setFormData] = useState<UpdateRecordFormData>({
|
||||
objectNameSingular: defaultObjectNameSingular,
|
||||
objectRecordId: defaultObjectRecordId,
|
||||
fieldsToUpdate: defaultFieldsToUpdate ?? [],
|
||||
...defaultObjectRecord,
|
||||
});
|
||||
|
||||
const handleFieldChange = (
|
||||
fieldName: keyof UpdateRecordFormData,
|
||||
updatedValue: JsonValue,
|
||||
) => {
|
||||
const newFormData: UpdateRecordFormData = {
|
||||
...formData,
|
||||
[fieldName]: updatedValue,
|
||||
};
|
||||
|
||||
setFormData(newFormData);
|
||||
|
||||
saveAction(newFormData);
|
||||
};
|
||||
|
||||
const selectedObjectMetadataItem = activeNonSystemObjectMetadataItems.find(
|
||||
(item) => item.nameSingular === formData.objectNameSingular,
|
||||
);
|
||||
|
||||
const objectNameSingular = selectedObjectMetadataItem?.nameSingular;
|
||||
|
||||
const inlineFieldMetadataItems = selectedObjectMetadataItem?.fields
|
||||
.filter((fieldMetadataItem) =>
|
||||
shouldDisplayFormField({ fieldMetadataItem, actionType }),
|
||||
)
|
||||
.sort((fieldMetadataItemA, fieldMetadataItemB) =>
|
||||
fieldMetadataItemA.name.localeCompare(fieldMetadataItemB.name),
|
||||
);
|
||||
|
||||
const inlineFieldDefinitions = isDefined(selectedObjectMetadataItem)
|
||||
? inlineFieldMetadataItems?.map((fieldMetadataItem) =>
|
||||
formatFieldMetadataItemAsFieldDefinition({
|
||||
field: fieldMetadataItem,
|
||||
objectMetadataItem: selectedObjectMetadataItem,
|
||||
showLabel: true,
|
||||
labelWidth: 90,
|
||||
}),
|
||||
)
|
||||
: [];
|
||||
|
||||
const saveAction = useDebouncedCallback(
|
||||
async (formData: UpdateRecordFormData) => {
|
||||
if (readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
objectNameSingular: updatedObjectName,
|
||||
objectRecordId: updatedObjectRecordId,
|
||||
fieldsToUpdate: updatedFieldsToUpdate,
|
||||
...updatedOtherFields
|
||||
} = formData;
|
||||
|
||||
onUpdate({
|
||||
objectNameSingular: updatedObjectName,
|
||||
objectRecordId: updatedObjectRecordId ?? '',
|
||||
fieldsToUpdate: updatedFieldsToUpdate ?? [],
|
||||
...updatedOtherFields,
|
||||
});
|
||||
},
|
||||
1_000,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
saveAction.flush();
|
||||
};
|
||||
}, [saveAction]);
|
||||
|
||||
return (
|
||||
<WorkflowStepBody>
|
||||
<Select
|
||||
dropdownId="workflow-update-record-object-name"
|
||||
label="Object"
|
||||
fullWidth
|
||||
disabled={readonly}
|
||||
value={formData.objectNameSingular}
|
||||
emptyOption={{ label: 'Select an option', value: '' }}
|
||||
options={availableMetadata}
|
||||
onChange={(updatedObjectName) => {
|
||||
const newFormData: UpdateRecordFormData = {
|
||||
objectNameSingular: updatedObjectName,
|
||||
objectRecordId: '',
|
||||
fieldsToUpdate: [],
|
||||
};
|
||||
|
||||
setFormData(newFormData);
|
||||
|
||||
saveAction(newFormData);
|
||||
}}
|
||||
withSearchInput
|
||||
dropdownOffset={{ y: parseInt(theme.spacing(1), 10) }}
|
||||
dropdownWidth={GenericDropdownContentWidth.ExtraLarge}
|
||||
/>
|
||||
|
||||
<HorizontalSeparator noMargin />
|
||||
|
||||
{isDefined(objectNameSingular) && shouldPickRecord && (
|
||||
<FormSingleRecordPicker
|
||||
testId="workflow-update-record-object-record-id"
|
||||
label="Record"
|
||||
onChange={(objectRecordId) =>
|
||||
handleFieldChange('objectRecordId', objectRecordId)
|
||||
}
|
||||
objectNameSingulars={[objectNameSingular]}
|
||||
defaultValue={formData.objectRecordId}
|
||||
disabled={readonly}
|
||||
VariablePicker={WorkflowVariablePicker}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isDefined(selectedObjectMetadataItem) && (
|
||||
<WorkflowFieldsMultiSelect
|
||||
label="Fields to update"
|
||||
placeholder="Select fields to update"
|
||||
objectMetadataItem={selectedObjectMetadataItem}
|
||||
handleFieldsChange={(fieldsToUpdate) =>
|
||||
handleFieldChange('fieldsToUpdate', fieldsToUpdate)
|
||||
}
|
||||
readonly={readonly ?? false}
|
||||
defaultFields={formData.fieldsToUpdate}
|
||||
/>
|
||||
)}
|
||||
|
||||
<HorizontalSeparator noMargin />
|
||||
|
||||
{formData.fieldsToUpdate.map((fieldName) => {
|
||||
const fieldDefinition = inlineFieldDefinitions?.find((definition) => {
|
||||
const isFieldRelationManyToOne =
|
||||
isFieldRelation(definition) &&
|
||||
definition.metadata.relationType === RelationType.MANY_TO_ONE;
|
||||
|
||||
const value = isFieldRelationManyToOne
|
||||
? `${definition.metadata.fieldName}Id`
|
||||
: definition.metadata.fieldName;
|
||||
|
||||
return value === fieldName;
|
||||
});
|
||||
|
||||
if (!isDefined(fieldDefinition)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const currentValue = formData[fieldName] as JsonValue;
|
||||
|
||||
return (
|
||||
<FormFieldInput
|
||||
key={fieldName}
|
||||
defaultValue={currentValue}
|
||||
field={fieldDefinition}
|
||||
onChange={(value) => {
|
||||
handleFieldChange(fieldName, value);
|
||||
}}
|
||||
VariablePicker={WorkflowVariablePicker}
|
||||
readonly={readonly}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</WorkflowStepBody>
|
||||
);
|
||||
};
|
||||
+11
-22
@@ -3,7 +3,7 @@ import { type WorkflowActionType } from '@/workflow/types/Workflow';
|
||||
import { CustomError } from 'twenty-shared/utils';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
const COMMON_DISPLAYABLE_FIELD_TYPES = [
|
||||
const SUPPORTED_FORM_FIELD_TYPES = [
|
||||
FieldMetadataType.TEXT,
|
||||
FieldMetadataType.NUMBER,
|
||||
FieldMetadataType.DATE,
|
||||
@@ -19,10 +19,6 @@ const COMMON_DISPLAYABLE_FIELD_TYPES = [
|
||||
FieldMetadataType.DATE_TIME,
|
||||
FieldMetadataType.RAW_JSON,
|
||||
FieldMetadataType.UUID,
|
||||
];
|
||||
|
||||
const FIND_RECORDS_DISPLAYABLE_FIELD_TYPES = [
|
||||
...COMMON_DISPLAYABLE_FIELD_TYPES,
|
||||
FieldMetadataType.ARRAY,
|
||||
FieldMetadataType.RELATION,
|
||||
];
|
||||
@@ -34,35 +30,28 @@ export const shouldDisplayFormField = ({
|
||||
fieldMetadataItem: FieldMetadataItem;
|
||||
actionType: WorkflowActionType;
|
||||
}) => {
|
||||
let isTypeAllowedForAction = false;
|
||||
if (!SUPPORTED_FORM_FIELD_TYPES.includes(fieldMetadataItem.type)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isIdField = fieldMetadataItem.name === 'id';
|
||||
const isNotSupportedRelation =
|
||||
fieldMetadataItem.type === FieldMetadataType.RELATION &&
|
||||
fieldMetadataItem.settings?.['relationType'] !== 'MANY_TO_ONE';
|
||||
|
||||
switch (actionType) {
|
||||
case 'CREATE_RECORD':
|
||||
isTypeAllowedForAction =
|
||||
fieldMetadataItem.type !== FieldMetadataType.RELATION ||
|
||||
fieldMetadataItem.settings?.['relationType'] === 'MANY_TO_ONE';
|
||||
return (
|
||||
isTypeAllowedForAction &&
|
||||
!fieldMetadataItem.isSystem &&
|
||||
fieldMetadataItem.isActive
|
||||
);
|
||||
case 'UPDATE_RECORD':
|
||||
case 'UPSERT_RECORD':
|
||||
isTypeAllowedForAction =
|
||||
COMMON_DISPLAYABLE_FIELD_TYPES.includes(fieldMetadataItem.type) ||
|
||||
fieldMetadataItem.settings?.['relationType'] === 'MANY_TO_ONE';
|
||||
return (
|
||||
isTypeAllowedForAction &&
|
||||
!isNotSupportedRelation &&
|
||||
!fieldMetadataItem.isUIReadOnly &&
|
||||
!fieldMetadataItem.isSystem &&
|
||||
fieldMetadataItem.isActive
|
||||
);
|
||||
case 'FIND_RECORDS':
|
||||
isTypeAllowedForAction = FIND_RECORDS_DISPLAYABLE_FIELD_TYPES.includes(
|
||||
fieldMetadataItem.type,
|
||||
);
|
||||
return (
|
||||
isTypeAllowedForAction &&
|
||||
!isNotSupportedRelation &&
|
||||
(!fieldMetadataItem.isSystem || isIdField) &&
|
||||
fieldMetadataItem.isActive
|
||||
);
|
||||
|
||||
+7
-4
@@ -95,10 +95,13 @@ export const WorkflowEditTriggerWebhookForm = ({
|
||||
return;
|
||||
}
|
||||
|
||||
triggerOptions.onTriggerUpdate({
|
||||
...trigger,
|
||||
name: newName,
|
||||
});
|
||||
triggerOptions.onTriggerUpdate(
|
||||
{
|
||||
...trigger,
|
||||
name: newName,
|
||||
},
|
||||
{ computeOutputSchema: false },
|
||||
);
|
||||
}}
|
||||
Icon={getIcon(headerIcon)}
|
||||
iconColor={getTriggerIconColor({ theme, triggerType: trigger.type })}
|
||||
|
||||
+5
-8
@@ -30,13 +30,8 @@ export class UpsertRecordService {
|
||||
) {}
|
||||
|
||||
async execute(params: UpsertRecordParams): Promise<ToolOutput> {
|
||||
const {
|
||||
objectName,
|
||||
objectRecord,
|
||||
fieldsToUpdate,
|
||||
workspaceId,
|
||||
rolePermissionConfig,
|
||||
} = params;
|
||||
const { objectName, objectRecord, workspaceId, rolePermissionConfig } =
|
||||
params;
|
||||
|
||||
if (!workspaceId) {
|
||||
return {
|
||||
@@ -54,7 +49,9 @@ export class UpsertRecordService {
|
||||
rolePermissionConfig,
|
||||
);
|
||||
|
||||
const fieldsToUpdateArray = fieldsToUpdate || Object.keys(objectRecord);
|
||||
const fieldsToUpdateArray = Object.keys(objectRecord).filter((field) =>
|
||||
isDefined(objectRecord[field]),
|
||||
);
|
||||
|
||||
const { objectMetadataItemWithFieldsMaps } =
|
||||
await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps(
|
||||
|
||||
-1
@@ -43,5 +43,4 @@ export type FindRecordsInput = {
|
||||
export type UpsertRecordInput = {
|
||||
objectName: string;
|
||||
objectRecord: ObjectRecordProperties;
|
||||
fieldsToUpdate?: string[];
|
||||
};
|
||||
|
||||
-1
@@ -74,7 +74,6 @@ export class UpsertRecordWorkflowAction implements WorkflowAction {
|
||||
const toolOutput = await this.upsertRecordService.execute({
|
||||
objectName: workflowActionInput.objectName,
|
||||
objectRecord: workflowActionInput.objectRecord,
|
||||
fieldsToUpdate: workflowActionInput.fieldsToUpdate,
|
||||
workspaceId,
|
||||
rolePermissionConfig: executionContext.rolePermissionConfig,
|
||||
});
|
||||
|
||||
@@ -7,6 +7,5 @@ export const workflowUpsertRecordActionSettingsSchema =
|
||||
input: z.object({
|
||||
objectName: z.string(),
|
||||
objectRecord: objectRecordSchema,
|
||||
fieldsToUpdate: z.array(z.string()),
|
||||
}),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user