Add record picker with variables (#8813)
- Add update actions - Create a folder for workflow actions - Add a SingleRecordPicker with variables handler https://github.com/user-attachments/assets/9fd57ce1-1b8d-424a-8aa1-69468d684fa1
This commit is contained in:
@ -76,6 +76,7 @@ export const WorkflowDiagramStepNodeBase = ({
|
|||||||
</StyledStepNodeLabelIconContainer>
|
</StyledStepNodeLabelIconContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
case 'RECORD_CRUD.UPDATE':
|
||||||
case 'RECORD_CRUD.CREATE': {
|
case 'RECORD_CRUD.CREATE': {
|
||||||
return (
|
return (
|
||||||
<StyledStepNodeLabelIconContainer>
|
<StyledStepNodeLabelIconContainer>
|
||||||
@ -87,8 +88,8 @@ export const WorkflowDiagramStepNodeBase = ({
|
|||||||
</StyledStepNodeLabelIconContainer>
|
</StyledStepNodeLabelIconContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
case 'RECORD_CRUD.DELETE':
|
|
||||||
case 'RECORD_CRUD.UPDATE': {
|
case 'RECORD_CRUD.DELETE': {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,62 @@
|
|||||||
|
import { RecordChip } from '@/object-record/components/RecordChip';
|
||||||
|
import { VariableChip } from '@/object-record/record-field/form-types/components/VariableChip';
|
||||||
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
|
import {
|
||||||
|
RecordId,
|
||||||
|
Variable,
|
||||||
|
} from '@/workflow/components/WorkflowSingleRecordPicker';
|
||||||
|
import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
const StyledRecordChip = styled(RecordChip)`
|
||||||
|
margin: ${({ theme }) => theme.spacing(2)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledPlaceholder = styled.div`
|
||||||
|
color: ${({ theme }) => theme.font.color.tertiary};
|
||||||
|
font-size: ${({ theme }) => theme.font.size.md};
|
||||||
|
margin: ${({ theme }) => theme.spacing(2)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
type WorkflowSingleRecordFieldChipProps = {
|
||||||
|
draftValue:
|
||||||
|
| {
|
||||||
|
type: 'static';
|
||||||
|
value: RecordId;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'variable';
|
||||||
|
value: Variable;
|
||||||
|
};
|
||||||
|
selectedRecord?: ObjectRecord;
|
||||||
|
objectNameSingular: string;
|
||||||
|
onRemove: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WorkflowSingleRecordFieldChip = ({
|
||||||
|
draftValue,
|
||||||
|
selectedRecord,
|
||||||
|
objectNameSingular,
|
||||||
|
onRemove,
|
||||||
|
}: WorkflowSingleRecordFieldChipProps) => {
|
||||||
|
if (
|
||||||
|
!!draftValue &&
|
||||||
|
draftValue.type === 'variable' &&
|
||||||
|
isStandaloneVariableString(draftValue.value)
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<VariableChip rawVariableName={draftValue.value} onRemove={onRemove} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!!draftValue && draftValue.type === 'static' && !!selectedRecord) {
|
||||||
|
return (
|
||||||
|
<StyledRecordChip
|
||||||
|
record={selectedRecord}
|
||||||
|
objectNameSingular={objectNameSingular}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <StyledPlaceholder>Select a {objectNameSingular}</StyledPlaceholder>;
|
||||||
|
};
|
||||||
@ -0,0 +1,219 @@
|
|||||||
|
import {
|
||||||
|
IconChevronDown,
|
||||||
|
IconForbid,
|
||||||
|
isDefined,
|
||||||
|
LightIconButton,
|
||||||
|
} from 'twenty-ui';
|
||||||
|
|
||||||
|
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
||||||
|
import { StyledFormFieldInputContainer } from '@/object-record/record-field/form-types/components/StyledFormFieldInputContainer';
|
||||||
|
import { StyledFormFieldInputRowContainer } from '@/object-record/record-field/form-types/components/StyledFormFieldInputRowContainer';
|
||||||
|
import { SingleRecordSelect } from '@/object-record/relation-picker/components/SingleRecordSelect';
|
||||||
|
import { useRecordPicker } from '@/object-record/relation-picker/hooks/useRecordPicker';
|
||||||
|
import { RecordPickerComponentInstanceContext } from '@/object-record/relation-picker/states/contexts/RecordPickerComponentInstanceContext';
|
||||||
|
import { RecordForSelect } from '@/object-record/relation-picker/types/RecordForSelect';
|
||||||
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
|
import { InputLabel } from '@/ui/input/components/InputLabel';
|
||||||
|
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||||
|
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||||
|
import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
|
||||||
|
import { WorkflowSingleRecordFieldChip } from '@/workflow/components/WorkflowSingleRecordFieldChip';
|
||||||
|
import SearchVariablesDropdown from '@/workflow/search-variables/components/SearchVariablesDropdown';
|
||||||
|
import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString';
|
||||||
|
import { css } from '@emotion/react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
import { isValidUuid } from '~/utils/isValidUuid';
|
||||||
|
|
||||||
|
const StyledFormSelectContainer = styled.div`
|
||||||
|
background-color: ${({ theme }) => theme.background.transparent.lighter};
|
||||||
|
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||||
|
border-top-left-radius: ${({ theme }) => theme.border.radius.sm};
|
||||||
|
border-bottom-left-radius: ${({ theme }) => theme.border.radius.sm};
|
||||||
|
border-right: none;
|
||||||
|
border-bottom-right-radius: none;
|
||||||
|
border-top-right-radius: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
overflow: 'hidden';
|
||||||
|
width: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
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 Variable = string;
|
||||||
|
|
||||||
|
export type WorkflowSingleRecordPickerProps = {
|
||||||
|
label?: string;
|
||||||
|
defaultValue: RecordId | Variable;
|
||||||
|
onChange: (value: RecordId | Variable) => void;
|
||||||
|
objectNameSingular: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WorkflowSingleRecordPicker = ({
|
||||||
|
label,
|
||||||
|
defaultValue,
|
||||||
|
objectNameSingular,
|
||||||
|
onChange,
|
||||||
|
}: WorkflowSingleRecordPickerProps) => {
|
||||||
|
const [draftValue, setDraftValue] = useState<
|
||||||
|
| {
|
||||||
|
type: 'static';
|
||||||
|
value: RecordId;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'variable';
|
||||||
|
value: Variable;
|
||||||
|
}
|
||||||
|
>(
|
||||||
|
isStandaloneVariableString(defaultValue)
|
||||||
|
? {
|
||||||
|
type: 'variable',
|
||||||
|
value: defaultValue,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
type: 'static',
|
||||||
|
value: defaultValue || '',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const { record } = useFindOneRecord({
|
||||||
|
objectRecordId:
|
||||||
|
isDefined(defaultValue) && !isStandaloneVariableString(defaultValue)
|
||||||
|
? defaultValue
|
||||||
|
: '',
|
||||||
|
objectNameSingular,
|
||||||
|
withSoftDeleted: true,
|
||||||
|
skip: !isValidUuid(defaultValue),
|
||||||
|
});
|
||||||
|
|
||||||
|
const [selectedRecord, setSelectedRecord] = useState<
|
||||||
|
ObjectRecord | undefined
|
||||||
|
>(record);
|
||||||
|
|
||||||
|
const dropdownId = `workflow-record-picker-${objectNameSingular}`;
|
||||||
|
const variablesDropdownId = `workflow-record-picker-${objectNameSingular}-variables`;
|
||||||
|
|
||||||
|
const { closeDropdown } = useDropdown(dropdownId);
|
||||||
|
|
||||||
|
const { setRecordPickerSearchFilter } = useRecordPicker({
|
||||||
|
recordPickerInstanceId: dropdownId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleCloseRelationPickerDropdown = useCallback(() => {
|
||||||
|
setRecordPickerSearchFilter('');
|
||||||
|
}, [setRecordPickerSearchFilter]);
|
||||||
|
|
||||||
|
const handleRecordSelected = (
|
||||||
|
selectedEntity: RecordForSelect | null | undefined,
|
||||||
|
) => {
|
||||||
|
setDraftValue({
|
||||||
|
type: 'static',
|
||||||
|
value: selectedEntity?.record?.id ?? '',
|
||||||
|
});
|
||||||
|
setSelectedRecord(selectedEntity?.record);
|
||||||
|
closeDropdown();
|
||||||
|
|
||||||
|
onChange?.(selectedEntity?.record?.id ?? '');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleVariableTagInsert = (variable: string) => {
|
||||||
|
setDraftValue({
|
||||||
|
type: 'variable',
|
||||||
|
value: variable,
|
||||||
|
});
|
||||||
|
setSelectedRecord(undefined);
|
||||||
|
closeDropdown();
|
||||||
|
|
||||||
|
onChange?.(variable);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUnlinkVariable = () => {
|
||||||
|
setDraftValue({
|
||||||
|
type: 'static',
|
||||||
|
value: '',
|
||||||
|
});
|
||||||
|
closeDropdown();
|
||||||
|
|
||||||
|
onChange('');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledFormFieldInputContainer>
|
||||||
|
{label ? <InputLabel>{label}</InputLabel> : null}
|
||||||
|
<StyledFormFieldInputRowContainer>
|
||||||
|
<StyledFormSelectContainer>
|
||||||
|
<WorkflowSingleRecordFieldChip
|
||||||
|
draftValue={draftValue}
|
||||||
|
selectedRecord={selectedRecord}
|
||||||
|
objectNameSingular={objectNameSingular}
|
||||||
|
onRemove={handleUnlinkVariable}
|
||||||
|
/>
|
||||||
|
<DropdownScope dropdownScopeId={dropdownId}>
|
||||||
|
<Dropdown
|
||||||
|
dropdownId={dropdownId}
|
||||||
|
dropdownPlacement="left-start"
|
||||||
|
onClose={handleCloseRelationPickerDropdown}
|
||||||
|
clickableComponent={
|
||||||
|
<LightIconButton
|
||||||
|
className="displayOnHover"
|
||||||
|
Icon={IconChevronDown}
|
||||||
|
accent="tertiary"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
dropdownComponents={
|
||||||
|
<RecordPickerComponentInstanceContext.Provider
|
||||||
|
value={{ instanceId: dropdownId }}
|
||||||
|
>
|
||||||
|
<SingleRecordSelect
|
||||||
|
EmptyIcon={IconForbid}
|
||||||
|
emptyLabel={'No ' + objectNameSingular}
|
||||||
|
onCancel={() => closeDropdown()}
|
||||||
|
onRecordSelected={handleRecordSelected}
|
||||||
|
objectNameSingular={objectNameSingular}
|
||||||
|
recordPickerInstanceId={dropdownId}
|
||||||
|
selectedRecordIds={
|
||||||
|
draftValue?.value &&
|
||||||
|
!isStandaloneVariableString(draftValue.value)
|
||||||
|
? [draftValue.value]
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</RecordPickerComponentInstanceContext.Provider>
|
||||||
|
}
|
||||||
|
dropdownHotkeyScope={{
|
||||||
|
scope: dropdownId,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</DropdownScope>
|
||||||
|
</StyledFormSelectContainer>
|
||||||
|
<StyledSearchVariablesDropdownContainer>
|
||||||
|
<SearchVariablesDropdown
|
||||||
|
inputId={variablesDropdownId}
|
||||||
|
onVariableSelect={handleVariableTagInsert}
|
||||||
|
disabled={false}
|
||||||
|
/>
|
||||||
|
</StyledSearchVariablesDropdownContainer>
|
||||||
|
</StyledFormFieldInputRowContainer>
|
||||||
|
</StyledFormFieldInputContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,6 +1,3 @@
|
|||||||
import { WorkflowEditActionFormRecordCreate } from '@/workflow/components/WorkflowEditActionFormRecordCreate';
|
|
||||||
import { WorkflowEditActionFormSendEmail } from '@/workflow/components/WorkflowEditActionFormSendEmail';
|
|
||||||
import { WorkflowEditActionFormServerlessFunction } from '@/workflow/components/WorkflowEditActionFormServerlessFunction';
|
|
||||||
import { WorkflowEditTriggerDatabaseEventForm } from '@/workflow/components/WorkflowEditTriggerDatabaseEventForm';
|
import { WorkflowEditTriggerDatabaseEventForm } from '@/workflow/components/WorkflowEditTriggerDatabaseEventForm';
|
||||||
import { WorkflowEditTriggerManualForm } from '@/workflow/components/WorkflowEditTriggerManualForm';
|
import { WorkflowEditTriggerManualForm } from '@/workflow/components/WorkflowEditTriggerManualForm';
|
||||||
import {
|
import {
|
||||||
@ -11,6 +8,11 @@ import {
|
|||||||
import { assertUnreachable } from '@/workflow/utils/assertUnreachable';
|
import { assertUnreachable } from '@/workflow/utils/assertUnreachable';
|
||||||
import { getStepDefinitionOrThrow } from '@/workflow/utils/getStepDefinitionOrThrow';
|
import { getStepDefinitionOrThrow } from '@/workflow/utils/getStepDefinitionOrThrow';
|
||||||
import { isWorkflowRecordCreateAction } from '@/workflow/utils/isWorkflowRecordCreateAction';
|
import { isWorkflowRecordCreateAction } from '@/workflow/utils/isWorkflowRecordCreateAction';
|
||||||
|
import { isWorkflowRecordUpdateAction } from '@/workflow/utils/isWorkflowRecordUpdateAction';
|
||||||
|
import { WorkflowEditActionFormRecordCreate } from '@/workflow/workflow-actions/components/WorkflowEditActionFormRecordCreate';
|
||||||
|
import { WorkflowEditActionFormRecordUpdate } from '@/workflow/workflow-actions/components/WorkflowEditActionFormRecordUpdate';
|
||||||
|
import { WorkflowEditActionFormSendEmail } from '@/workflow/workflow-actions/components/WorkflowEditActionFormSendEmail';
|
||||||
|
import { WorkflowEditActionFormServerlessFunction } from '@/workflow/workflow-actions/components/WorkflowEditActionFormServerlessFunction';
|
||||||
import { isDefined } from 'twenty-ui';
|
import { isDefined } from 'twenty-ui';
|
||||||
|
|
||||||
type WorkflowStepDetailProps =
|
type WorkflowStepDetailProps =
|
||||||
@ -102,6 +104,15 @@ export const WorkflowStepDetail = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isWorkflowRecordUpdateAction(stepDefinition.definition)) {
|
||||||
|
return (
|
||||||
|
<WorkflowEditActionFormRecordUpdate
|
||||||
|
action={stepDefinition.definition}
|
||||||
|
actionOptions={props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,4 +25,9 @@ export const ACTIONS: Array<{
|
|||||||
type: 'RECORD_CRUD.CREATE',
|
type: 'RECORD_CRUD.CREATE',
|
||||||
icon: IconAddressBook,
|
icon: IconAddressBook,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Update Record',
|
||||||
|
type: 'RECORD_CRUD.UPDATE',
|
||||||
|
icon: IconAddressBook,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@ -92,6 +92,37 @@ it('returns a valid definition for RECORD_CRUD.CREATE actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('returns a valid definition for RECORD_CRUD.UPDATE actions', () => {
|
||||||
|
expect(
|
||||||
|
getStepDefaultDefinition({
|
||||||
|
type: 'RECORD_CRUD.UPDATE',
|
||||||
|
activeObjectMetadataItems: generatedMockObjectMetadataItems,
|
||||||
|
}),
|
||||||
|
).toStrictEqual({
|
||||||
|
id: expect.any(String),
|
||||||
|
name: 'Update Record',
|
||||||
|
type: 'RECORD_CRUD',
|
||||||
|
valid: false,
|
||||||
|
settings: {
|
||||||
|
input: {
|
||||||
|
type: 'UPDATE',
|
||||||
|
objectName: generatedMockObjectMetadataItems[0].nameSingular,
|
||||||
|
objectRecord: {},
|
||||||
|
objectRecordId: '',
|
||||||
|
},
|
||||||
|
outputSchema: {},
|
||||||
|
errorHandlingOptions: {
|
||||||
|
continueOnFailure: {
|
||||||
|
value: false,
|
||||||
|
},
|
||||||
|
retryOnFailure: {
|
||||||
|
value: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("throws for RECORD_CRUD.DELETE actions as it's not implemented yet", () => {
|
it("throws for RECORD_CRUD.DELETE actions as it's not implemented yet", () => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
getStepDefaultDefinition({
|
getStepDefaultDefinition({
|
||||||
@ -101,15 +132,6 @@ it("throws for RECORD_CRUD.DELETE actions as it's not implemented yet", () => {
|
|||||||
}).toThrow('Not implemented yet');
|
}).toThrow('Not implemented yet');
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws for RECORD_CRUD.UPDATE actions as it's not implemented yet", () => {
|
|
||||||
expect(() => {
|
|
||||||
getStepDefaultDefinition({
|
|
||||||
type: 'RECORD_CRUD.UPDATE',
|
|
||||||
activeObjectMetadataItems: generatedMockObjectMetadataItems,
|
|
||||||
});
|
|
||||||
}).toThrow('Not implemented yet');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('throws when providing an unknown type', () => {
|
it('throws when providing an unknown type', () => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
getStepDefaultDefinition({
|
getStepDefaultDefinition({
|
||||||
|
|||||||
@ -3,6 +3,18 @@ import { WorkflowStep, WorkflowStepType } from '@/workflow/types/Workflow';
|
|||||||
import { assertUnreachable } from '@/workflow/utils/assertUnreachable';
|
import { assertUnreachable } from '@/workflow/utils/assertUnreachable';
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
|
const BASE_DEFAULT_STEP_SETTINGS = {
|
||||||
|
outputSchema: {},
|
||||||
|
errorHandlingOptions: {
|
||||||
|
continueOnFailure: {
|
||||||
|
value: false,
|
||||||
|
},
|
||||||
|
retryOnFailure: {
|
||||||
|
value: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const getStepDefaultDefinition = ({
|
export const getStepDefaultDefinition = ({
|
||||||
type,
|
type,
|
||||||
activeObjectMetadataItems,
|
activeObjectMetadataItems,
|
||||||
@ -25,15 +37,7 @@ export const getStepDefaultDefinition = ({
|
|||||||
serverlessFunctionVersion: '',
|
serverlessFunctionVersion: '',
|
||||||
serverlessFunctionInput: {},
|
serverlessFunctionInput: {},
|
||||||
},
|
},
|
||||||
outputSchema: {},
|
...BASE_DEFAULT_STEP_SETTINGS,
|
||||||
errorHandlingOptions: {
|
|
||||||
continueOnFailure: {
|
|
||||||
value: false,
|
|
||||||
},
|
|
||||||
retryOnFailure: {
|
|
||||||
value: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -50,15 +54,7 @@ export const getStepDefaultDefinition = ({
|
|||||||
subject: '',
|
subject: '',
|
||||||
body: '',
|
body: '',
|
||||||
},
|
},
|
||||||
outputSchema: {},
|
...BASE_DEFAULT_STEP_SETTINGS,
|
||||||
errorHandlingOptions: {
|
|
||||||
continueOnFailure: {
|
|
||||||
value: false,
|
|
||||||
},
|
|
||||||
retryOnFailure: {
|
|
||||||
value: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -74,20 +70,27 @@ export const getStepDefaultDefinition = ({
|
|||||||
objectName: activeObjectMetadataItems[0].nameSingular,
|
objectName: activeObjectMetadataItems[0].nameSingular,
|
||||||
objectRecord: {},
|
objectRecord: {},
|
||||||
},
|
},
|
||||||
outputSchema: {},
|
...BASE_DEFAULT_STEP_SETTINGS,
|
||||||
errorHandlingOptions: {
|
|
||||||
continueOnFailure: {
|
|
||||||
value: false,
|
|
||||||
},
|
|
||||||
retryOnFailure: {
|
|
||||||
value: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case 'RECORD_CRUD.DELETE':
|
case 'RECORD_CRUD.UPDATE':
|
||||||
case 'RECORD_CRUD.UPDATE': {
|
return {
|
||||||
|
id: newStepId,
|
||||||
|
name: 'Update Record',
|
||||||
|
type: 'RECORD_CRUD',
|
||||||
|
valid: false,
|
||||||
|
settings: {
|
||||||
|
input: {
|
||||||
|
type: 'UPDATE',
|
||||||
|
objectName: activeObjectMetadataItems[0].nameSingular,
|
||||||
|
objectRecordId: '',
|
||||||
|
objectRecord: {},
|
||||||
|
},
|
||||||
|
...BASE_DEFAULT_STEP_SETTINGS,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
case 'RECORD_CRUD.DELETE': {
|
||||||
throw new Error('Not implemented yet');
|
throw new Error('Not implemented yet');
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
|
|||||||
@ -0,0 +1,12 @@
|
|||||||
|
import {
|
||||||
|
WorkflowAction,
|
||||||
|
WorkflowRecordUpdateAction,
|
||||||
|
} from '@/workflow/types/Workflow';
|
||||||
|
|
||||||
|
export const isWorkflowRecordUpdateAction = (
|
||||||
|
action: WorkflowAction,
|
||||||
|
): action is WorkflowRecordUpdateAction => {
|
||||||
|
return (
|
||||||
|
action.type === 'RECORD_CRUD' && action.settings.input.type === 'UPDATE'
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -28,7 +28,7 @@ type WorkflowEditActionFormRecordCreateProps = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
type SendEmailFormData = {
|
type CreateRecordFormData = {
|
||||||
objectName: string;
|
objectName: string;
|
||||||
[field: string]: unknown;
|
[field: string]: unknown;
|
||||||
};
|
};
|
||||||
@ -49,17 +49,17 @@ export const WorkflowEditActionFormRecordCreate = ({
|
|||||||
value: item.nameSingular,
|
value: item.nameSingular,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const [formData, setFormData] = useState<SendEmailFormData>({
|
const [formData, setFormData] = useState<CreateRecordFormData>({
|
||||||
objectName: action.settings.input.objectName,
|
objectName: action.settings.input.objectName,
|
||||||
...action.settings.input.objectRecord,
|
...action.settings.input.objectRecord,
|
||||||
});
|
});
|
||||||
const isFormDisabled = actionOptions.readonly;
|
const isFormDisabled = actionOptions.readonly;
|
||||||
|
|
||||||
const handleFieldChange = (
|
const handleFieldChange = (
|
||||||
fieldName: keyof SendEmailFormData,
|
fieldName: keyof CreateRecordFormData,
|
||||||
updatedValue: JsonValue,
|
updatedValue: JsonValue,
|
||||||
) => {
|
) => {
|
||||||
const newFormData: SendEmailFormData = {
|
const newFormData: CreateRecordFormData = {
|
||||||
...formData,
|
...formData,
|
||||||
[fieldName]: updatedValue,
|
[fieldName]: updatedValue,
|
||||||
};
|
};
|
||||||
@ -93,7 +93,7 @@ export const WorkflowEditActionFormRecordCreate = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const saveAction = useDebouncedCallback(
|
const saveAction = useDebouncedCallback(
|
||||||
async (formData: SendEmailFormData) => {
|
async (formData: CreateRecordFormData) => {
|
||||||
if (actionOptions.readonly === true) {
|
if (actionOptions.readonly === true) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -153,7 +153,7 @@ export const WorkflowEditActionFormRecordCreate = ({
|
|||||||
emptyOption={{ label: 'Select an option', value: '' }}
|
emptyOption={{ label: 'Select an option', value: '' }}
|
||||||
options={availableMetadata}
|
options={availableMetadata}
|
||||||
onChange={(updatedObjectName) => {
|
onChange={(updatedObjectName) => {
|
||||||
const newFormData: SendEmailFormData = {
|
const newFormData: CreateRecordFormData = {
|
||||||
objectName: updatedObjectName,
|
objectName: updatedObjectName,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -0,0 +1,179 @@
|
|||||||
|
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||||
|
import { Select, SelectOption } from '@/ui/input/components/Select';
|
||||||
|
import { WorkflowEditGenericFormBase } from '@/workflow/components/WorkflowEditGenericFormBase';
|
||||||
|
import { WorkflowSingleRecordPicker } from '@/workflow/components/WorkflowSingleRecordPicker';
|
||||||
|
import { WorkflowRecordUpdateAction } from '@/workflow/types/Workflow';
|
||||||
|
import { useTheme } from '@emotion/react';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import {
|
||||||
|
HorizontalSeparator,
|
||||||
|
IconAddressBook,
|
||||||
|
isDefined,
|
||||||
|
useIcons,
|
||||||
|
} from 'twenty-ui';
|
||||||
|
|
||||||
|
import { JsonValue } from 'type-fest';
|
||||||
|
import { useDebouncedCallback } from 'use-debounce';
|
||||||
|
|
||||||
|
type WorkflowEditActionFormRecordUpdateProps = {
|
||||||
|
action: WorkflowRecordUpdateAction;
|
||||||
|
actionOptions:
|
||||||
|
| {
|
||||||
|
readonly: true;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
readonly?: false;
|
||||||
|
onActionUpdate: (action: WorkflowRecordUpdateAction) => void;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type UpdateRecordFormData = {
|
||||||
|
objectName: string;
|
||||||
|
objectRecordId: string;
|
||||||
|
[field: string]: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WorkflowEditActionFormRecordUpdate = ({
|
||||||
|
action,
|
||||||
|
actionOptions,
|
||||||
|
}: WorkflowEditActionFormRecordUpdateProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const { getIcon } = useIcons();
|
||||||
|
|
||||||
|
const { activeObjectMetadataItems } = useFilteredObjectMetadataItems();
|
||||||
|
|
||||||
|
const availableMetadata: Array<SelectOption<string>> =
|
||||||
|
activeObjectMetadataItems.map((item) => ({
|
||||||
|
Icon: getIcon(item.icon),
|
||||||
|
label: item.labelPlural,
|
||||||
|
value: item.nameSingular,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState<UpdateRecordFormData>({
|
||||||
|
objectName: action.settings.input.objectName,
|
||||||
|
objectRecordId: action.settings.input.objectRecordId,
|
||||||
|
...action.settings.input.objectRecord,
|
||||||
|
});
|
||||||
|
const isFormDisabled = actionOptions.readonly;
|
||||||
|
|
||||||
|
const handleFieldChange = (
|
||||||
|
fieldName: keyof UpdateRecordFormData,
|
||||||
|
updatedValue: JsonValue,
|
||||||
|
) => {
|
||||||
|
const newFormData: UpdateRecordFormData = {
|
||||||
|
...formData,
|
||||||
|
[fieldName]: updatedValue,
|
||||||
|
};
|
||||||
|
|
||||||
|
setFormData(newFormData);
|
||||||
|
|
||||||
|
saveAction(newFormData);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setFormData({
|
||||||
|
objectName: action.settings.input.objectName,
|
||||||
|
objectRecordId: action.settings.input.objectRecordId,
|
||||||
|
...action.settings.input.objectRecord,
|
||||||
|
});
|
||||||
|
}, [action.settings.input]);
|
||||||
|
|
||||||
|
const selectedObjectMetadataItemNameSingular = formData.objectName;
|
||||||
|
|
||||||
|
const selectedObjectMetadataItem = activeObjectMetadataItems.find(
|
||||||
|
(item) => item.nameSingular === selectedObjectMetadataItemNameSingular,
|
||||||
|
);
|
||||||
|
if (!isDefined(selectedObjectMetadataItem)) {
|
||||||
|
throw new Error('Should have found the metadata item');
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveAction = useDebouncedCallback(
|
||||||
|
async (formData: UpdateRecordFormData) => {
|
||||||
|
if (actionOptions.readonly === true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
objectName: updatedObjectName,
|
||||||
|
objectRecordId: updatedObjectRecordId,
|
||||||
|
...updatedOtherFields
|
||||||
|
} = formData;
|
||||||
|
|
||||||
|
actionOptions.onActionUpdate({
|
||||||
|
...action,
|
||||||
|
settings: {
|
||||||
|
...action.settings,
|
||||||
|
input: {
|
||||||
|
type: 'UPDATE',
|
||||||
|
objectName: updatedObjectName,
|
||||||
|
objectRecordId: updatedObjectRecordId ?? '',
|
||||||
|
objectRecord: updatedOtherFields,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
1_000,
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
saveAction.flush();
|
||||||
|
};
|
||||||
|
}, [saveAction]);
|
||||||
|
|
||||||
|
const headerTitle = isDefined(action.name) ? action.name : `Update Record`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<WorkflowEditGenericFormBase
|
||||||
|
onTitleChange={(newName: string) => {
|
||||||
|
if (actionOptions.readonly === true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
actionOptions.onActionUpdate({
|
||||||
|
...action,
|
||||||
|
name: newName,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
HeaderIcon={
|
||||||
|
<IconAddressBook
|
||||||
|
color={theme.font.color.tertiary}
|
||||||
|
stroke={theme.icon.stroke.sm}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
headerTitle={headerTitle}
|
||||||
|
headerType="Action"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
dropdownId="workflow-edit-action-record-update-object-name"
|
||||||
|
label="Object"
|
||||||
|
fullWidth
|
||||||
|
disabled={isFormDisabled}
|
||||||
|
value={formData.objectName}
|
||||||
|
emptyOption={{ label: 'Select an option', value: '' }}
|
||||||
|
options={availableMetadata}
|
||||||
|
onChange={(updatedObjectName) => {
|
||||||
|
const newFormData: UpdateRecordFormData = {
|
||||||
|
objectName: updatedObjectName,
|
||||||
|
objectRecordId: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
setFormData(newFormData);
|
||||||
|
|
||||||
|
saveAction(newFormData);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<HorizontalSeparator noMargin />
|
||||||
|
|
||||||
|
<WorkflowSingleRecordPicker
|
||||||
|
label="Record"
|
||||||
|
onChange={(objectRecordId) =>
|
||||||
|
handleFieldChange('objectRecordId', objectRecordId)
|
||||||
|
}
|
||||||
|
objectNameSingular={formData.objectName}
|
||||||
|
defaultValue={formData.objectRecordId}
|
||||||
|
/>
|
||||||
|
</WorkflowEditGenericFormBase>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user