mirror of
https://github.com/twentyhq/twenty.git
synced 2024-12-22 19:41:53 +03:00
Add fields to update in update record action (#9108)
- update backend action so it handles composite fields - add fields to update multiselect - generate form based on that field - add icons
This commit is contained in:
parent
b6508cc615
commit
94676215ad
@ -11,6 +11,7 @@ import { SelectOption } from '@/spreadsheet-import/types';
|
||||
import { MultiSelectDisplay } from '@/ui/field/display/components/MultiSelectDisplay';
|
||||
import { MultiSelectInput } from '@/ui/field/input/components/MultiSelectInput';
|
||||
import { InputLabel } from '@/ui/input/components/InputLabel';
|
||||
import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContainer';
|
||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||
import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString';
|
||||
import { useId, useState } from 'react';
|
||||
@ -20,9 +21,9 @@ import { isDefined } from '~/utils/isDefined';
|
||||
type FormMultiSelectFieldInputProps = {
|
||||
label?: string;
|
||||
defaultValue: FieldMultiSelectValue | string | undefined;
|
||||
options: SelectOption[];
|
||||
onPersist: (value: FieldMultiSelectValue | string) => void;
|
||||
VariablePicker?: VariablePickerComponent;
|
||||
options: SelectOption[];
|
||||
};
|
||||
|
||||
const StyledDisplayModeContainer = styled.button`
|
||||
@ -50,9 +51,9 @@ const StyledSelectInputContainer = styled.div`
|
||||
export const FormMultiSelectFieldInput = ({
|
||||
label,
|
||||
defaultValue,
|
||||
options,
|
||||
onPersist,
|
||||
VariablePicker,
|
||||
options,
|
||||
}: FormMultiSelectFieldInputProps) => {
|
||||
const inputId = useId();
|
||||
|
||||
@ -189,13 +190,15 @@ export const FormMultiSelectFieldInput = ({
|
||||
<StyledSelectInputContainer>
|
||||
{draftValue.type === 'static' &&
|
||||
draftValue.editingMode === 'edit' && (
|
||||
<MultiSelectInput
|
||||
hotkeyScope={hotkeyScope}
|
||||
options={options}
|
||||
onCancel={onCancel}
|
||||
onOptionSelected={onOptionSelected}
|
||||
values={draftValue.value}
|
||||
/>
|
||||
<OverlayContainer>
|
||||
<MultiSelectInput
|
||||
hotkeyScope={hotkeyScope}
|
||||
options={options}
|
||||
onCancel={onCancel}
|
||||
onOptionSelected={onOptionSelected}
|
||||
values={draftValue.value}
|
||||
/>
|
||||
</OverlayContainer>
|
||||
)}
|
||||
</StyledSelectInputContainer>
|
||||
|
||||
|
@ -40,6 +40,7 @@ export const MultiSelectDisplay = ({
|
||||
key={index}
|
||||
color={selectedOption.color ?? 'transparent'}
|
||||
text={selectedOption.label}
|
||||
Icon={selectedOption.icon ?? undefined}
|
||||
/>
|
||||
))}
|
||||
</StyledContainer>
|
||||
|
@ -127,6 +127,7 @@ export const MultiSelectInput = ({
|
||||
selected={values?.includes(option.value) || false}
|
||||
text={option.label}
|
||||
color={option.color ?? 'transparent'}
|
||||
Icon={option.icon ?? undefined}
|
||||
onClick={() =>
|
||||
onOptionSelected(formatNewSelectedOptions(option.value))
|
||||
}
|
||||
|
@ -44,6 +44,7 @@ export type WorkflowUpdateRecordActionSettings = BaseWorkflowActionSettings & {
|
||||
objectName: string;
|
||||
objectRecord: ObjectRecord;
|
||||
objectRecordId: string;
|
||||
fieldsToUpdate: string[];
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -12,9 +12,14 @@ import {
|
||||
useIcons,
|
||||
} from 'twenty-ui';
|
||||
|
||||
import { formatFieldMetadataItemAsFieldDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition';
|
||||
import { FormFieldInput } from '@/object-record/record-field/components/FormFieldInput';
|
||||
import { FormMultiSelectFieldInput } from '@/object-record/record-field/form-types/components/FormMultiSelectFieldInput';
|
||||
import { WorkflowStepBody } from '@/workflow/components/WorkflowStepBody';
|
||||
import { WorkflowVariablePicker } from '@/workflow/components/WorkflowVariablePicker';
|
||||
import { JsonValue } from 'type-fest';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
type WorkflowEditActionFormUpdateRecordProps = {
|
||||
action: WorkflowUpdateRecordAction;
|
||||
@ -31,9 +36,23 @@ type WorkflowEditActionFormUpdateRecordProps = {
|
||||
type UpdateRecordFormData = {
|
||||
objectName: string;
|
||||
objectRecordId: string;
|
||||
fieldsToUpdate: string[];
|
||||
[field: string]: unknown;
|
||||
};
|
||||
|
||||
const AVAILABLE_FIELD_METADATA_TYPES = [
|
||||
FieldMetadataType.Text,
|
||||
FieldMetadataType.Number,
|
||||
FieldMetadataType.Date,
|
||||
FieldMetadataType.Boolean,
|
||||
FieldMetadataType.Select,
|
||||
FieldMetadataType.MultiSelect,
|
||||
FieldMetadataType.Emails,
|
||||
FieldMetadataType.Links,
|
||||
FieldMetadataType.FullName,
|
||||
FieldMetadataType.Address,
|
||||
];
|
||||
|
||||
export const WorkflowEditActionFormUpdateRecord = ({
|
||||
action,
|
||||
actionOptions,
|
||||
@ -53,6 +72,7 @@ export const WorkflowEditActionFormUpdateRecord = ({
|
||||
const [formData, setFormData] = useState<UpdateRecordFormData>({
|
||||
objectName: action.settings.input.objectName,
|
||||
objectRecordId: action.settings.input.objectRecordId,
|
||||
fieldsToUpdate: action.settings.input.fieldsToUpdate ?? [],
|
||||
...action.settings.input.objectRecord,
|
||||
});
|
||||
const isFormDisabled = actionOptions.readonly;
|
||||
@ -75,6 +95,7 @@ export const WorkflowEditActionFormUpdateRecord = ({
|
||||
setFormData({
|
||||
objectName: action.settings.input.objectName,
|
||||
objectRecordId: action.settings.input.objectRecordId,
|
||||
fieldsToUpdate: action.settings.input.fieldsToUpdate ?? [],
|
||||
...action.settings.input.objectRecord,
|
||||
});
|
||||
}, [action.settings.input]);
|
||||
@ -88,6 +109,27 @@ export const WorkflowEditActionFormUpdateRecord = ({
|
||||
throw new Error('Should have found the metadata item');
|
||||
}
|
||||
|
||||
const inlineFieldMetadataItems = selectedObjectMetadataItem.fields
|
||||
.filter(
|
||||
(fieldMetadataItem) =>
|
||||
!fieldMetadataItem.isSystem &&
|
||||
fieldMetadataItem.isActive &&
|
||||
AVAILABLE_FIELD_METADATA_TYPES.includes(fieldMetadataItem.type),
|
||||
)
|
||||
.sort((fieldMetadataItemA, fieldMetadataItemB) =>
|
||||
fieldMetadataItemA.name.localeCompare(fieldMetadataItemB.name),
|
||||
);
|
||||
|
||||
const inlineFieldDefinitions = inlineFieldMetadataItems.map(
|
||||
(fieldMetadataItem) =>
|
||||
formatFieldMetadataItemAsFieldDefinition({
|
||||
field: fieldMetadataItem,
|
||||
objectMetadataItem: selectedObjectMetadataItem,
|
||||
showLabel: true,
|
||||
labelWidth: 90,
|
||||
}),
|
||||
);
|
||||
|
||||
const saveAction = useDebouncedCallback(
|
||||
async (formData: UpdateRecordFormData) => {
|
||||
if (actionOptions.readonly === true) {
|
||||
@ -97,6 +139,7 @@ export const WorkflowEditActionFormUpdateRecord = ({
|
||||
const {
|
||||
objectName: updatedObjectName,
|
||||
objectRecordId: updatedObjectRecordId,
|
||||
fieldsToUpdate: updatedFieldsToUpdate,
|
||||
...updatedOtherFields
|
||||
} = formData;
|
||||
|
||||
@ -108,6 +151,7 @@ export const WorkflowEditActionFormUpdateRecord = ({
|
||||
objectName: updatedObjectName,
|
||||
objectRecordId: updatedObjectRecordId ?? '',
|
||||
objectRecord: updatedOtherFields,
|
||||
fieldsToUpdate: updatedFieldsToUpdate ?? [],
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -154,6 +198,7 @@ export const WorkflowEditActionFormUpdateRecord = ({
|
||||
const newFormData: UpdateRecordFormData = {
|
||||
objectName: updatedObjectName,
|
||||
objectRecordId: '',
|
||||
fieldsToUpdate: [],
|
||||
};
|
||||
|
||||
setFormData(newFormData);
|
||||
@ -172,6 +217,48 @@ export const WorkflowEditActionFormUpdateRecord = ({
|
||||
objectNameSingular={formData.objectName}
|
||||
defaultValue={formData.objectRecordId}
|
||||
/>
|
||||
|
||||
<FormMultiSelectFieldInput
|
||||
label="Fields to update"
|
||||
defaultValue={formData.fieldsToUpdate}
|
||||
options={inlineFieldDefinitions.map((field) => ({
|
||||
label: field.label,
|
||||
value: field.metadata.fieldName,
|
||||
icon: getIcon(field.iconName),
|
||||
color: 'gray',
|
||||
}))}
|
||||
onPersist={(fieldsToUpdate) =>
|
||||
handleFieldChange('fieldsToUpdate', fieldsToUpdate)
|
||||
}
|
||||
/>
|
||||
|
||||
<HorizontalSeparator noMargin />
|
||||
|
||||
{formData.fieldsToUpdate.map((fieldName) => {
|
||||
const fieldDefinition = inlineFieldDefinitions.find(
|
||||
(definition) => definition.metadata.fieldName === fieldName,
|
||||
);
|
||||
|
||||
if (!isDefined(fieldDefinition)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const currentValue = formData[
|
||||
fieldDefinition.metadata.fieldName
|
||||
] as JsonValue;
|
||||
|
||||
return (
|
||||
<FormFieldInput
|
||||
key={fieldDefinition.metadata.fieldName}
|
||||
defaultValue={currentValue}
|
||||
field={fieldDefinition}
|
||||
onPersist={(value) => {
|
||||
handleFieldChange(fieldDefinition.metadata.fieldName, value);
|
||||
}}
|
||||
VariablePicker={WorkflowVariablePicker}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</WorkflowStepBody>
|
||||
</>
|
||||
);
|
||||
|
@ -4,6 +4,7 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { BASE_TYPESCRIPT_PROJECT_INPUT_SCHEMA } from 'src/engine/core-modules/serverless/drivers/constants/base-typescript-project-input-schema';
|
||||
import { WorkflowActionDTO } from 'src/engine/core-modules/workflow/dtos/workflow-step.dto';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
|
||||
@ -20,7 +21,6 @@ import {
|
||||
WorkflowActionType,
|
||||
} from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
import { isDefined } from 'src/utils/is-defined';
|
||||
import { BASE_TYPESCRIPT_PROJECT_INPUT_SCHEMA } from 'src/engine/core-modules/serverless/drivers/constants/base-typescript-project-input-schema';
|
||||
|
||||
const TRIGGER_STEP_ID = 'trigger';
|
||||
|
||||
@ -152,6 +152,7 @@ export class WorkflowVersionStepWorkspaceService {
|
||||
objectName: activeObjectMetadataItem?.nameSingular || '',
|
||||
objectRecord: {},
|
||||
objectRecordId: '',
|
||||
fieldsToUpdate: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -14,6 +14,7 @@ export type WorkflowUpdateRecordActionInput = {
|
||||
objectName: string;
|
||||
objectRecord: ObjectRecord;
|
||||
objectRecordId: string;
|
||||
fieldsToUpdate: string[];
|
||||
};
|
||||
|
||||
export type WorkflowDeleteRecordActionInput = {
|
||||
|
@ -2,7 +2,11 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { WorkflowAction } from 'src/modules/workflow/workflow-executor/interfaces/workflow-action.interface';
|
||||
|
||||
import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util';
|
||||
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
|
||||
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
|
||||
import { formatData } from 'src/engine/twenty-orm/utils/format-data.util';
|
||||
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
||||
import {
|
||||
RecordCRUDActionException,
|
||||
RecordCRUDActionExceptionCode,
|
||||
@ -12,7 +16,11 @@ import { WorkflowActionResult } from 'src/modules/workflow/workflow-executor/wor
|
||||
|
||||
@Injectable()
|
||||
export class UpdateRecordWorkflowAction implements WorkflowAction {
|
||||
constructor(private readonly twentyORMManager: TwentyORMManager) {}
|
||||
constructor(
|
||||
private readonly twentyORMManager: TwentyORMManager,
|
||||
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
|
||||
private readonly scopedWorkspaceContextFactory: ScopedWorkspaceContextFactory,
|
||||
) {}
|
||||
|
||||
async execute(
|
||||
workflowActionInput: WorkflowUpdateRecordActionInput,
|
||||
@ -34,14 +42,85 @@ export class UpdateRecordWorkflowAction implements WorkflowAction {
|
||||
);
|
||||
}
|
||||
|
||||
const workspaceId = this.scopedWorkspaceContextFactory.create().workspaceId;
|
||||
|
||||
if (!workspaceId) {
|
||||
throw new RecordCRUDActionException(
|
||||
'Failed to read: Workspace ID is required',
|
||||
RecordCRUDActionExceptionCode.INVALID_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
const currentCacheVersion =
|
||||
await this.workspaceCacheStorageService.getMetadataVersion(workspaceId);
|
||||
|
||||
if (currentCacheVersion === undefined) {
|
||||
throw new RecordCRUDActionException(
|
||||
'Failed to read: Metadata cache version not found',
|
||||
RecordCRUDActionExceptionCode.INVALID_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
const objectMetadataMaps =
|
||||
await this.workspaceCacheStorageService.getObjectMetadataMaps(
|
||||
workspaceId,
|
||||
currentCacheVersion,
|
||||
);
|
||||
|
||||
if (!objectMetadataMaps) {
|
||||
throw new RecordCRUDActionException(
|
||||
'Failed to read: Object metadata collection not found',
|
||||
RecordCRUDActionExceptionCode.INVALID_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
const objectMetadataItemWithFieldsMaps =
|
||||
getObjectMetadataMapItemByNameSingular(
|
||||
objectMetadataMaps,
|
||||
workflowActionInput.objectName,
|
||||
);
|
||||
|
||||
if (!objectMetadataItemWithFieldsMaps) {
|
||||
throw new RecordCRUDActionException(
|
||||
`Failed to read: Object ${workflowActionInput.objectName} not found`,
|
||||
RecordCRUDActionExceptionCode.INVALID_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
if (workflowActionInput.fieldsToUpdate.length === 0) {
|
||||
return {
|
||||
result: {
|
||||
...objectRecord,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const objectRecordWithFilteredFields = Object.keys(
|
||||
workflowActionInput.objectRecord,
|
||||
).reduce((acc, key) => {
|
||||
if (workflowActionInput.fieldsToUpdate.includes(key)) {
|
||||
return {
|
||||
...acc,
|
||||
[key]: workflowActionInput.objectRecord[key],
|
||||
};
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const objectRecordFormatted = formatData(
|
||||
objectRecordWithFilteredFields,
|
||||
objectMetadataItemWithFieldsMaps,
|
||||
);
|
||||
|
||||
await repository.update(workflowActionInput.objectRecordId, {
|
||||
...workflowActionInput.objectRecord,
|
||||
...objectRecordFormatted,
|
||||
});
|
||||
|
||||
return {
|
||||
result: {
|
||||
...objectRecord,
|
||||
...workflowActionInput.objectRecord,
|
||||
...objectRecordWithFilteredFields,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Tag } from '@ui/display';
|
||||
import { IconComponent, Tag } from '@ui/display';
|
||||
import { Checkbox, CheckboxShape, CheckboxSize } from '@ui/input';
|
||||
import { ThemeColor } from '@ui/theme';
|
||||
import {
|
||||
@ -13,6 +13,7 @@ type MenuItemMultiSelectTagProps = {
|
||||
onClick?: () => void;
|
||||
color: ThemeColor | 'transparent';
|
||||
text: string;
|
||||
Icon?: IconComponent;
|
||||
};
|
||||
|
||||
export const MenuItemMultiSelectTag = ({
|
||||
@ -22,6 +23,7 @@ export const MenuItemMultiSelectTag = ({
|
||||
onClick,
|
||||
isKeySelected,
|
||||
text,
|
||||
Icon,
|
||||
}: MenuItemMultiSelectTagProps) => {
|
||||
return (
|
||||
<StyledMenuItemBase
|
||||
@ -35,7 +37,7 @@ export const MenuItemMultiSelectTag = ({
|
||||
checked={selected}
|
||||
/>
|
||||
<StyledMenuItemLeftContent>
|
||||
<Tag color={color} text={text} />
|
||||
<Tag color={color} text={text} Icon={Icon} />
|
||||
</StyledMenuItemLeftContent>
|
||||
</StyledMenuItemBase>
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user