Allow bulk records for manual trigger (#14725)
https://github.com/user-attachments/assets/d6c565eb-9a29-4830-9396-5f979c8caa7b - Added a new component for manual trigger (mostly duplicated from previous one). Will remove the old one once all data are migrated - Updated schema output so the current item of the iterator can be typed Todo left: - migrate old triggers - add an util to search iterator output. Today current item fields will be displayed as not found - set new manual triggers for workflow runs
This commit is contained in:
@@ -444,6 +444,8 @@ export type ClientAiModelConfig = {
|
||||
export type ComputeStepOutputSchemaInput = {
|
||||
/** Step JSON format */
|
||||
step: Scalars['JSON'];
|
||||
/** Workflow version ID */
|
||||
workflowVersionId: Scalars['UUID'];
|
||||
};
|
||||
|
||||
export enum ConfigSource {
|
||||
|
||||
@@ -444,6 +444,8 @@ export type ClientAiModelConfig = {
|
||||
export type ComputeStepOutputSchemaInput = {
|
||||
/** Step JSON format */
|
||||
step: Scalars['JSON'];
|
||||
/** Workflow version ID */
|
||||
workflowVersionId: Scalars['UUID'];
|
||||
};
|
||||
|
||||
export enum ConfigSource {
|
||||
|
||||
+6
@@ -12,10 +12,12 @@ import { DATABASE_TRIGGER_TYPES } from '@/workflow/workflow-trigger/constants/Da
|
||||
import { OTHER_TRIGGER_TYPES } from '@/workflow/workflow-trigger/constants/OtherTriggerTypes';
|
||||
import { useUpdateWorkflowVersionTrigger } from '@/workflow/workflow-trigger/hooks/useUpdateWorkflowVersionTrigger';
|
||||
import { getTriggerDefaultDefinition } from '@/workflow/workflow-trigger/utils/getTriggerDefaultDefinition';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { TRIGGER_STEP_ID } from 'twenty-shared/workflow';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
import { MenuItem } from 'twenty-ui/navigation';
|
||||
import { FeatureFlagKey } from '~/generated/graphql';
|
||||
|
||||
export const CommandMenuWorkflowSelectTriggerTypeContent = ({
|
||||
workflow,
|
||||
@@ -32,6 +34,9 @@ export const CommandMenuWorkflowSelectTriggerTypeContent = ({
|
||||
workflowSelectedNodeComponentState,
|
||||
);
|
||||
const { openWorkflowEditStepInCommandMenu } = useWorkflowCommandMenu();
|
||||
const isIteratorEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_WORKFLOW_ITERATOR_ENABLED,
|
||||
);
|
||||
|
||||
const handleTriggerTypeClick = ({
|
||||
type,
|
||||
@@ -48,6 +53,7 @@ export const CommandMenuWorkflowSelectTriggerTypeContent = ({
|
||||
defaultLabel,
|
||||
type,
|
||||
activeNonSystemObjectMetadataItems,
|
||||
isIteratorEnabled,
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
+1
-1
@@ -1,9 +1,9 @@
|
||||
import { useSearchVariable } from '@/workflow/workflow-variables/hooks/useSearchVariable';
|
||||
import { extractRawVariableNamePart } from '@/workflow/workflow-variables/utils/extractRawVariableNamePart';
|
||||
import { css, useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { extractRawVariableNamePart } from 'twenty-shared/workflow';
|
||||
import { IconAlertTriangle, IconX } from 'twenty-ui/display';
|
||||
|
||||
const StyledChip = styled.div<{ deletable: boolean; danger: boolean }>`
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import {
|
||||
type BulkRecordsAvailability,
|
||||
type GlobalAvailability,
|
||||
type SingleRecordAvailability,
|
||||
type workflowAiAgentActionSchema,
|
||||
type workflowCodeActionSchema,
|
||||
type workflowCreateRecordActionSchema,
|
||||
@@ -82,6 +85,11 @@ export type WorkflowManualTriggerAvailability =
|
||||
| 'EVERYWHERE'
|
||||
| 'WHEN_RECORD_SELECTED';
|
||||
|
||||
export type WorkflowManualTriggerAvailabilityV2 =
|
||||
| GlobalAvailability
|
||||
| SingleRecordAvailability
|
||||
| BulkRecordsAvailability;
|
||||
|
||||
export type WorkflowTrigger = z.infer<typeof workflowTriggerSchema>;
|
||||
export type WorkflowTriggerType = WorkflowTrigger['type'];
|
||||
|
||||
|
||||
+21
-2
@@ -18,9 +18,12 @@ import { WorkflowEditActionHttpRequest } from '@/workflow/workflow-steps/workflo
|
||||
import { WorkflowEditActionIterator } from '@/workflow/workflow-steps/workflow-actions/iterator-action/WorkflowEditActionIterator';
|
||||
import { WorkflowEditTriggerCronForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerCronForm';
|
||||
import { WorkflowEditTriggerDatabaseEventForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerDatabaseEventForm';
|
||||
import { WorkflowEditTriggerManualForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerManualForm';
|
||||
import { WorkflowEditTriggerManual } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerManual';
|
||||
import { WorkflowEditTriggerManualDeprecated } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerManualDeprecated';
|
||||
import { WorkflowEditTriggerWebhookForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerWebhookForm';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { assertUnreachable, isDefined } from 'twenty-shared/utils';
|
||||
import { FeatureFlagKey } from '~/generated/graphql';
|
||||
|
||||
type WorkflowRunStepNodeDetailProps = {
|
||||
stepId: string;
|
||||
@@ -41,6 +44,10 @@ export const WorkflowRunStepNodeDetail = ({
|
||||
steps,
|
||||
});
|
||||
|
||||
const isIteratorEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_WORKFLOW_ITERATOR_ENABLED,
|
||||
);
|
||||
|
||||
if (!isDefined(stepDefinition) || !isDefined(stepDefinition.definition)) {
|
||||
return null;
|
||||
}
|
||||
@@ -60,8 +67,20 @@ export const WorkflowRunStepNodeDetail = ({
|
||||
);
|
||||
}
|
||||
case 'MANUAL': {
|
||||
if (isIteratorEnabled) {
|
||||
return (
|
||||
<WorkflowEditTriggerManual
|
||||
key={stepId}
|
||||
trigger={stepDefinition.definition}
|
||||
triggerOptions={{
|
||||
readonly: true,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<WorkflowEditTriggerManualForm
|
||||
<WorkflowEditTriggerManualDeprecated
|
||||
key={stepId}
|
||||
trigger={stepDefinition.definition}
|
||||
triggerOptions={{
|
||||
|
||||
+18
-2
@@ -17,9 +17,12 @@ import { WorkflowEditActionHttpRequest } from '@/workflow/workflow-steps/workflo
|
||||
import { WorkflowEditActionIterator } from '@/workflow/workflow-steps/workflow-actions/iterator-action/WorkflowEditActionIterator';
|
||||
import { WorkflowEditTriggerCronForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerCronForm';
|
||||
import { WorkflowEditTriggerDatabaseEventForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerDatabaseEventForm';
|
||||
import { WorkflowEditTriggerManualForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerManualForm';
|
||||
import { WorkflowEditTriggerManual } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerManual';
|
||||
import { WorkflowEditTriggerManualDeprecated } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerManualDeprecated';
|
||||
import { WorkflowEditTriggerWebhookForm } from '@/workflow/workflow-trigger/components/WorkflowEditTriggerWebhookForm';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { assertUnreachable, isDefined } from 'twenty-shared/utils';
|
||||
import { FeatureFlagKey } from '~/generated/graphql';
|
||||
|
||||
type WorkflowStepDetailProps = {
|
||||
stepId: string;
|
||||
@@ -45,6 +48,9 @@ export const WorkflowStepDetail = ({
|
||||
steps,
|
||||
...props
|
||||
}: WorkflowStepDetailProps) => {
|
||||
const isIteratorEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_WORKFLOW_ITERATOR_ENABLED,
|
||||
);
|
||||
const stepDefinition = getStepDefinitionOrThrow({
|
||||
stepId,
|
||||
trigger,
|
||||
@@ -68,8 +74,18 @@ export const WorkflowStepDetail = ({
|
||||
);
|
||||
}
|
||||
case 'MANUAL': {
|
||||
if (isIteratorEnabled) {
|
||||
return (
|
||||
<WorkflowEditTriggerManual
|
||||
key={stepId}
|
||||
trigger={stepDefinition.definition}
|
||||
triggerOptions={props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<WorkflowEditTriggerManualForm
|
||||
<WorkflowEditTriggerManualDeprecated
|
||||
key={stepId}
|
||||
trigger={stepDefinition.definition}
|
||||
triggerOptions={props}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { type WorkflowStep } from '@/workflow/types/Workflow';
|
||||
import { CAPTURE_ALL_VARIABLE_TAG_INNER_REGEX } from '@/workflow/workflow-variables/constants/CaptureAllVariableTagInnerRegex';
|
||||
import { isObject, isString } from '@sniptt/guards';
|
||||
import { CAPTURE_ALL_VARIABLE_TAG_INNER_REGEX } from 'twenty-shared/workflow';
|
||||
import { type JsonValue } from 'type-fest';
|
||||
|
||||
function* resolveVariables(value: JsonValue): Generator<string> {
|
||||
|
||||
+1
-1
@@ -15,7 +15,6 @@ import { getStepFilterOperands } from '@/workflow/workflow-steps/workflow-action
|
||||
import { useVariableDropdown } from '@/workflow/workflow-variables/hooks/useVariableDropdown';
|
||||
import { isRecordOutputSchemaV2 } from '@/workflow/workflow-variables/types/guards/isRecordOutputSchemaV2';
|
||||
import { type StepOutputSchemaV2 } from '@/workflow/workflow-variables/types/StepOutputSchemaV2';
|
||||
import { extractRawVariableNamePart } from '@/workflow/workflow-variables/utils/extractRawVariableNamePart';
|
||||
import { getCurrentSubStepFromPath } from '@/workflow/workflow-variables/utils/getCurrentSubStepFromPath';
|
||||
import { getStepHeaderLabel } from '@/workflow/workflow-variables/utils/getStepHeaderLabel';
|
||||
import { getStepItemIcon } from '@/workflow/workflow-variables/utils/getStepItemIcon';
|
||||
@@ -25,6 +24,7 @@ import { useLingui } from '@lingui/react/macro';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { type StepFilter } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { extractRawVariableNamePart } from 'twenty-shared/workflow';
|
||||
import {
|
||||
IconChevronLeft,
|
||||
OverflowingTextWithTooltip,
|
||||
|
||||
+1
-1
@@ -9,12 +9,12 @@ import { useAvailableVariablesInWorkflowStep } from '@/workflow/workflow-variabl
|
||||
import { useSearchVariable } from '@/workflow/workflow-variables/hooks/useSearchVariable';
|
||||
import { type StepOutputSchemaV2 } from '@/workflow/workflow-variables/types/StepOutputSchemaV2';
|
||||
|
||||
import { extractRawVariableNamePart } from '@/workflow/workflow-variables/utils/extractRawVariableNamePart';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useContext, useState } from 'react';
|
||||
import { type StepFilter } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { extractRawVariableNamePart } from 'twenty-shared/workflow';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
|
||||
+241
@@ -0,0 +1,241 @@
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { IconPicker } from '@/ui/input/components/IconPicker';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
import { SelectControl } from '@/ui/input/components/SelectControl';
|
||||
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
|
||||
import { type WorkflowManualTrigger } from '@/workflow/types/Workflow';
|
||||
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
|
||||
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
|
||||
import { MANUAL_TRIGGER_AVAILABILITY_TYPE_OPTIONS } from '@/workflow/workflow-trigger/constants/ManualTriggerAvailabilityTypeOptions';
|
||||
import { MANUAL_TRIGGER_IS_PINNED_OPTIONS } from '@/workflow/workflow-trigger/constants/ManualTriggerIsPinnedOptions';
|
||||
import { getManualTriggerDefaultSettings } from '@/workflow/workflow-trigger/utils/getManualTriggerDefaultSettings';
|
||||
import { getTriggerDefaultLabel } from '@/workflow/workflow-trigger/utils/getTriggerDefaultLabel';
|
||||
import { getTriggerHeaderType } from '@/workflow/workflow-trigger/utils/getTriggerHeaderType';
|
||||
import { getTriggerIcon } from '@/workflow/workflow-trigger/utils/getTriggerIcon';
|
||||
import { getTriggerIconColor } from '@/workflow/workflow-trigger/utils/getTriggerIconColor';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
import { type SelectOption } from 'twenty-ui/input';
|
||||
|
||||
type WorkflowEditTriggerManualProps = {
|
||||
trigger: WorkflowManualTrigger;
|
||||
triggerOptions:
|
||||
| {
|
||||
readonly: true;
|
||||
onTriggerUpdate?: undefined;
|
||||
}
|
||||
| {
|
||||
readonly?: false;
|
||||
onTriggerUpdate: (trigger: WorkflowManualTrigger) => void;
|
||||
};
|
||||
};
|
||||
|
||||
const StyledLabel = styled.span`
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
font-size: ${({ theme }) => theme.font.size.xs};
|
||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||
margin-bottom: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const StyledDescription = styled.span`
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
margin-top: ${({ theme }) => theme.spacing(0.25)};
|
||||
`;
|
||||
|
||||
const StyledIconPickerContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
export const WorkflowEditTriggerManual = ({
|
||||
trigger,
|
||||
triggerOptions,
|
||||
}: WorkflowEditTriggerManualProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const { t } = useLingui();
|
||||
|
||||
const { getIcon } = useIcons();
|
||||
|
||||
const { activeNonSystemObjectMetadataItems } =
|
||||
useFilteredObjectMetadataItems();
|
||||
|
||||
const availableMetadata: Array<SelectOption<string>> =
|
||||
activeNonSystemObjectMetadataItems.map((item) => ({
|
||||
label: item.labelPlural,
|
||||
value: item.nameSingular,
|
||||
Icon: getIcon(item.icon),
|
||||
}));
|
||||
|
||||
const availability = trigger.settings.availability;
|
||||
|
||||
const headerTitle = trigger.name ?? getTriggerDefaultLabel(trigger);
|
||||
|
||||
const headerIcon = getTriggerIcon(trigger);
|
||||
|
||||
const headerType = getTriggerHeaderType(trigger);
|
||||
|
||||
const availabilityDescriptions = {
|
||||
SINGLE_RECORD: t`The selected record will be passed to your workflow`,
|
||||
BULK_RECORDS: t`The selected records will be passed to your workflow`,
|
||||
GLOBAL: t`No record is required to trigger this workflow`,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<WorkflowStepHeader
|
||||
onTitleChange={(newName: string) => {
|
||||
if (triggerOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
triggerOptions.onTriggerUpdate({
|
||||
...trigger,
|
||||
name: newName,
|
||||
});
|
||||
}}
|
||||
Icon={getIcon(headerIcon)}
|
||||
iconColor={getTriggerIconColor({ theme, triggerType: trigger.type })}
|
||||
initialTitle={headerTitle}
|
||||
headerType={headerType}
|
||||
disabled={triggerOptions.readonly}
|
||||
/>
|
||||
<WorkflowStepBody>
|
||||
<Select
|
||||
dropdownId="workflow-edit-manual-trigger-availability"
|
||||
label={t`Availability`}
|
||||
description={
|
||||
availability?.type
|
||||
? availabilityDescriptions[availability.type]
|
||||
: undefined
|
||||
}
|
||||
fullWidth
|
||||
disabled={triggerOptions.readonly}
|
||||
value={availability?.type}
|
||||
options={MANUAL_TRIGGER_AVAILABILITY_TYPE_OPTIONS}
|
||||
onChange={(availabilityType) => {
|
||||
if (triggerOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
triggerOptions.onTriggerUpdate({
|
||||
...trigger,
|
||||
settings: getManualTriggerDefaultSettings({
|
||||
availabilityType,
|
||||
activeNonSystemObjectMetadataItems,
|
||||
icon: trigger.settings.icon,
|
||||
isPinned: trigger.settings.isPinned,
|
||||
}),
|
||||
});
|
||||
}}
|
||||
dropdownOffset={{ y: parseInt(theme.spacing(1), 10) }}
|
||||
dropdownWidth={GenericDropdownContentWidth.ExtraLarge}
|
||||
/>
|
||||
|
||||
{availability?.type === 'SINGLE_RECORD' ||
|
||||
availability?.type === 'BULK_RECORDS' ? (
|
||||
<Select
|
||||
dropdownId="workflow-edit-manual-trigger-object"
|
||||
label={t`Object`}
|
||||
description={t`On which object(s) should this trigger be available`}
|
||||
fullWidth
|
||||
value={availability?.objectNameSingular}
|
||||
options={availableMetadata}
|
||||
disabled={triggerOptions.readonly}
|
||||
onChange={(objectNameSingular) => {
|
||||
if (triggerOptions.readonly === true || !availability) {
|
||||
return;
|
||||
}
|
||||
|
||||
triggerOptions.onTriggerUpdate({
|
||||
...trigger,
|
||||
settings: {
|
||||
...trigger.settings,
|
||||
availability: {
|
||||
type: availability.type,
|
||||
objectNameSingular,
|
||||
},
|
||||
outputSchema: {},
|
||||
},
|
||||
});
|
||||
}}
|
||||
dropdownOffset={{ y: parseInt(theme.spacing(1), 10) }}
|
||||
dropdownWidth={GenericDropdownContentWidth.ExtraLarge}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<IconPicker
|
||||
dropdownId="workflow-edit-manual-trigger-icon"
|
||||
selectedIconKey={trigger.settings.icon}
|
||||
dropdownOffset={{ y: -parseInt(theme.spacing(3), 10) }}
|
||||
dropdownWidth={GenericDropdownContentWidth.ExtraLarge}
|
||||
maxIconsVisible={9 * 8} // 9 columns * 8 lines
|
||||
disabled={triggerOptions.readonly}
|
||||
clickableComponent={
|
||||
<StyledIconPickerContainer
|
||||
onClick={(e) => {
|
||||
if (triggerOptions.readonly === true) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<StyledLabel>{t`Command Icon`}</StyledLabel>
|
||||
<SelectControl
|
||||
isDisabled={triggerOptions.readonly}
|
||||
selectedOption={{
|
||||
Icon: getIcon(trigger.settings.icon),
|
||||
value: trigger.settings.icon || null,
|
||||
label: '',
|
||||
}}
|
||||
/>
|
||||
<StyledDescription>{t`The icon your workflow trigger will display in the command menu`}</StyledDescription>
|
||||
</StyledIconPickerContainer>
|
||||
}
|
||||
onChange={({ iconKey }) => {
|
||||
if (triggerOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
triggerOptions.onTriggerUpdate({
|
||||
...trigger,
|
||||
settings: {
|
||||
...trigger.settings,
|
||||
icon: iconKey,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
<Select
|
||||
dropdownId="workflow-edit-manual-trigger-navbar"
|
||||
label={t`Navbar`}
|
||||
description={t`Display a button in the top navbar to trigger this workflow`}
|
||||
fullWidth
|
||||
value={trigger.settings.isPinned}
|
||||
options={MANUAL_TRIGGER_IS_PINNED_OPTIONS}
|
||||
disabled={triggerOptions.readonly}
|
||||
onChange={(updatedValue) => {
|
||||
if (triggerOptions.readonly === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
triggerOptions.onTriggerUpdate({
|
||||
...trigger,
|
||||
settings: {
|
||||
...trigger.settings,
|
||||
isPinned: updatedValue,
|
||||
outputSchema: {},
|
||||
},
|
||||
});
|
||||
}}
|
||||
dropdownOffset={{ y: parseInt(theme.spacing(1), 10) }}
|
||||
dropdownWidth={GenericDropdownContentWidth.ExtraLarge}
|
||||
/>
|
||||
</WorkflowStepBody>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+5
-5
@@ -11,7 +11,7 @@ import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowS
|
||||
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
|
||||
import { MANUAL_TRIGGER_AVAILABILITY_OPTIONS } from '@/workflow/workflow-trigger/constants/ManualTriggerAvailabilityOptions';
|
||||
import { MANUAL_TRIGGER_IS_PINNED_OPTIONS } from '@/workflow/workflow-trigger/constants/ManualTriggerIsPinnedOptions';
|
||||
import { getManualTriggerDefaultSettings } from '@/workflow/workflow-trigger/utils/getManualTriggerDefaultSettings';
|
||||
import { getManualTriggerDefaultSettingsDeprecated } from '@/workflow/workflow-trigger/utils/getManualTriggerDefaultSettingsDeprecated';
|
||||
import { getTriggerDefaultLabel } from '@/workflow/workflow-trigger/utils/getTriggerDefaultLabel';
|
||||
import { getTriggerHeaderType } from '@/workflow/workflow-trigger/utils/getTriggerHeaderType';
|
||||
import { getTriggerIcon } from '@/workflow/workflow-trigger/utils/getTriggerIcon';
|
||||
@@ -23,7 +23,7 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
import { type SelectOption } from 'twenty-ui/input';
|
||||
|
||||
type WorkflowEditTriggerManualFormProps = {
|
||||
type WorkflowEditTriggerManualDeprecatedProps = {
|
||||
trigger: WorkflowManualTrigger;
|
||||
triggerOptions:
|
||||
| {
|
||||
@@ -54,10 +54,10 @@ const StyledIconPickerContainer = styled.div`
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
export const WorkflowEditTriggerManualForm = ({
|
||||
export const WorkflowEditTriggerManualDeprecated = ({
|
||||
trigger,
|
||||
triggerOptions,
|
||||
}: WorkflowEditTriggerManualFormProps) => {
|
||||
}: WorkflowEditTriggerManualDeprecatedProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const { t } = useLingui();
|
||||
@@ -168,7 +168,7 @@ export const WorkflowEditTriggerManualForm = ({
|
||||
|
||||
triggerOptions.onTriggerUpdate({
|
||||
...trigger,
|
||||
settings: getManualTriggerDefaultSettings({
|
||||
settings: getManualTriggerDefaultSettingsDeprecated({
|
||||
availability: updatedTriggerType,
|
||||
activeNonSystemObjectMetadataItems,
|
||||
icon: trigger.settings.icon,
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import {
|
||||
IconCheckbox,
|
||||
type IconComponent,
|
||||
IconId,
|
||||
IconListDetails,
|
||||
} from 'twenty-ui/display';
|
||||
|
||||
export const MANUAL_TRIGGER_AVAILABILITY_TYPE_OPTIONS: Array<{
|
||||
label: string;
|
||||
value: 'GLOBAL' | 'SINGLE_RECORD' | 'BULK_RECORDS';
|
||||
Icon: IconComponent;
|
||||
}> = [
|
||||
{
|
||||
label: 'Global',
|
||||
value: 'GLOBAL',
|
||||
Icon: IconCheckbox,
|
||||
},
|
||||
{
|
||||
label: 'Single',
|
||||
value: 'SINGLE_RECORD',
|
||||
Icon: IconId,
|
||||
},
|
||||
{
|
||||
label: 'Bulk',
|
||||
value: 'BULK_RECORDS',
|
||||
Icon: IconListDetails,
|
||||
},
|
||||
];
|
||||
+4
-1
@@ -52,7 +52,10 @@ describe('useUpdateWorkflowVersionTrigger', () => {
|
||||
});
|
||||
|
||||
expect(mockGetUpdatableWorkflowVersion).toHaveBeenCalled();
|
||||
expect(mockComputeStepOutputSchema).toHaveBeenCalledWith({ step: trigger });
|
||||
expect(mockComputeStepOutputSchema).toHaveBeenCalledWith({
|
||||
step: trigger,
|
||||
workflowVersionId: 'version-id',
|
||||
});
|
||||
expect(mockUpdateOneRecord).toHaveBeenCalledWith({
|
||||
idToUpdate: 'version-id',
|
||||
updateOneRecordInput: {
|
||||
|
||||
+1
@@ -28,6 +28,7 @@ export const useUpdateWorkflowVersionTrigger = () => {
|
||||
const outputSchema = (
|
||||
await computeStepOutputSchema({
|
||||
step: updatedTrigger,
|
||||
workflowVersionId,
|
||||
})
|
||||
)?.data?.computeStepOutputSchema;
|
||||
|
||||
|
||||
+5
-5
@@ -1,11 +1,11 @@
|
||||
import { type WorkflowManualTriggerAvailability } from '@/workflow/types/Workflow';
|
||||
import { COMMAND_MENU_DEFAULT_ICON } from '@/workflow/workflow-trigger/constants/CommandMenuDefaultIcon';
|
||||
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
|
||||
import { getManualTriggerDefaultSettings } from '../getManualTriggerDefaultSettings';
|
||||
import { getManualTriggerDefaultSettingsDeprecated } from '../getManualTriggerDefaultSettingsDeprecated';
|
||||
|
||||
it('returns settings for a manual trigger that can be activated from any where', () => {
|
||||
expect(
|
||||
getManualTriggerDefaultSettings({
|
||||
getManualTriggerDefaultSettingsDeprecated({
|
||||
availability: 'EVERYWHERE',
|
||||
activeNonSystemObjectMetadataItems: generatedMockObjectMetadataItems,
|
||||
}),
|
||||
@@ -19,7 +19,7 @@ it('returns settings for a manual trigger that can be activated from any where',
|
||||
|
||||
it('returns settings for a manual trigger that can be activated from any where', () => {
|
||||
expect(
|
||||
getManualTriggerDefaultSettings({
|
||||
getManualTriggerDefaultSettingsDeprecated({
|
||||
availability: 'WHEN_RECORD_SELECTED',
|
||||
activeNonSystemObjectMetadataItems: generatedMockObjectMetadataItems,
|
||||
icon: 'IconTest',
|
||||
@@ -34,7 +34,7 @@ it('returns settings for a manual trigger that can be activated from any where',
|
||||
|
||||
it('returns settings for WHEN_RECORD_SELECTED with default icon when no custom icon provided', () => {
|
||||
expect(
|
||||
getManualTriggerDefaultSettings({
|
||||
getManualTriggerDefaultSettingsDeprecated({
|
||||
availability: 'WHEN_RECORD_SELECTED',
|
||||
activeNonSystemObjectMetadataItems: generatedMockObjectMetadataItems,
|
||||
}),
|
||||
@@ -51,7 +51,7 @@ it('throws error for unsupported availability type', () => {
|
||||
'INVALID_AVAILABILITY' as WorkflowManualTriggerAvailability;
|
||||
|
||||
expect(() =>
|
||||
getManualTriggerDefaultSettings({
|
||||
getManualTriggerDefaultSettingsDeprecated({
|
||||
availability: invalidAvailability,
|
||||
activeNonSystemObjectMetadataItems: generatedMockObjectMetadataItems,
|
||||
}),
|
||||
+9
@@ -10,6 +10,7 @@ describe('getTriggerDefaultDefinition', () => {
|
||||
defaultLabel: DatabaseTriggerDefaultLabel.RECORD_IS_CREATED,
|
||||
type: 'DATABASE_EVENT',
|
||||
activeNonSystemObjectMetadataItems: [],
|
||||
isIteratorEnabled: false,
|
||||
});
|
||||
}).toThrow();
|
||||
});
|
||||
@@ -20,6 +21,7 @@ describe('getTriggerDefaultDefinition', () => {
|
||||
defaultLabel: DatabaseTriggerDefaultLabel.RECORD_IS_CREATED,
|
||||
type: 'DATABASE_EVENT',
|
||||
activeNonSystemObjectMetadataItems: generatedMockObjectMetadataItems,
|
||||
isIteratorEnabled: false,
|
||||
}),
|
||||
).toStrictEqual({
|
||||
type: 'DATABASE_EVENT',
|
||||
@@ -41,6 +43,7 @@ describe('getTriggerDefaultDefinition', () => {
|
||||
defaultLabel: DatabaseTriggerDefaultLabel.RECORD_IS_UPDATED,
|
||||
type: 'DATABASE_EVENT',
|
||||
activeNonSystemObjectMetadataItems: generatedMockObjectMetadataItems,
|
||||
isIteratorEnabled: false,
|
||||
}),
|
||||
).toStrictEqual({
|
||||
type: 'DATABASE_EVENT',
|
||||
@@ -62,6 +65,7 @@ describe('getTriggerDefaultDefinition', () => {
|
||||
defaultLabel: DatabaseTriggerDefaultLabel.RECORD_IS_DELETED,
|
||||
type: 'DATABASE_EVENT',
|
||||
activeNonSystemObjectMetadataItems: generatedMockObjectMetadataItems,
|
||||
isIteratorEnabled: false,
|
||||
}),
|
||||
).toStrictEqual({
|
||||
type: 'DATABASE_EVENT',
|
||||
@@ -83,6 +87,7 @@ describe('getTriggerDefaultDefinition', () => {
|
||||
defaultLabel: DatabaseTriggerDefaultLabel.RECORD_IS_CREATED,
|
||||
type: 'DATABASE_EVENT',
|
||||
activeNonSystemObjectMetadataItems: generatedMockObjectMetadataItems,
|
||||
isIteratorEnabled: false,
|
||||
}),
|
||||
).toStrictEqual({
|
||||
type: 'DATABASE_EVENT',
|
||||
@@ -104,6 +109,7 @@ describe('getTriggerDefaultDefinition', () => {
|
||||
defaultLabel: 'Launch manually',
|
||||
type: 'MANUAL',
|
||||
activeNonSystemObjectMetadataItems: generatedMockObjectMetadataItems,
|
||||
isIteratorEnabled: false,
|
||||
}),
|
||||
).toStrictEqual({
|
||||
type: 'MANUAL',
|
||||
@@ -127,6 +133,7 @@ describe('getTriggerDefaultDefinition', () => {
|
||||
defaultLabel: 'On a schedule',
|
||||
type: 'CRON',
|
||||
activeNonSystemObjectMetadataItems: generatedMockObjectMetadataItems,
|
||||
isIteratorEnabled: false,
|
||||
}),
|
||||
).toStrictEqual({
|
||||
type: 'CRON',
|
||||
@@ -149,6 +156,7 @@ describe('getTriggerDefaultDefinition', () => {
|
||||
defaultLabel: 'Webhook',
|
||||
type: 'WEBHOOK',
|
||||
activeNonSystemObjectMetadataItems: generatedMockObjectMetadataItems,
|
||||
isIteratorEnabled: false,
|
||||
}),
|
||||
).toStrictEqual({
|
||||
type: 'WEBHOOK',
|
||||
@@ -171,6 +179,7 @@ describe('getTriggerDefaultDefinition', () => {
|
||||
defaultLabel: DatabaseTriggerDefaultLabel.RECORD_IS_CREATED,
|
||||
type: 'unknown' as any,
|
||||
activeNonSystemObjectMetadataItems: generatedMockObjectMetadataItems,
|
||||
isIteratorEnabled: false,
|
||||
});
|
||||
}).toThrow('Unknown type: unknown');
|
||||
});
|
||||
|
||||
+30
-13
@@ -1,40 +1,57 @@
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import {
|
||||
type WorkflowManualTriggerAvailability,
|
||||
type WorkflowManualTriggerSettings,
|
||||
} from '@/workflow/types/Workflow';
|
||||
import { type WorkflowManualTriggerSettings } from '@/workflow/types/Workflow';
|
||||
import { COMMAND_MENU_DEFAULT_ICON } from '@/workflow/workflow-trigger/constants/CommandMenuDefaultIcon';
|
||||
import { assertUnreachable } from 'twenty-shared/utils';
|
||||
|
||||
export const getManualTriggerDefaultSettings = ({
|
||||
availability,
|
||||
availabilityType,
|
||||
activeNonSystemObjectMetadataItems,
|
||||
icon,
|
||||
isPinned,
|
||||
}: {
|
||||
availability: WorkflowManualTriggerAvailability;
|
||||
availabilityType: 'GLOBAL' | 'SINGLE_RECORD' | 'BULK_RECORDS';
|
||||
activeNonSystemObjectMetadataItems: ObjectMetadataItem[];
|
||||
icon?: string;
|
||||
isPinned?: boolean;
|
||||
}): WorkflowManualTriggerSettings => {
|
||||
switch (availability) {
|
||||
case 'EVERYWHERE': {
|
||||
switch (availabilityType) {
|
||||
case 'GLOBAL': {
|
||||
return {
|
||||
objectType: undefined,
|
||||
availability: {
|
||||
type: 'GLOBAL',
|
||||
locations: undefined,
|
||||
},
|
||||
outputSchema: {},
|
||||
icon: icon || COMMAND_MENU_DEFAULT_ICON,
|
||||
isPinned: isPinned || false,
|
||||
};
|
||||
}
|
||||
case 'WHEN_RECORD_SELECTED': {
|
||||
case 'SINGLE_RECORD': {
|
||||
return {
|
||||
objectType: activeNonSystemObjectMetadataItems[0].nameSingular,
|
||||
availability: {
|
||||
type: 'SINGLE_RECORD',
|
||||
objectNameSingular:
|
||||
activeNonSystemObjectMetadataItems[0].nameSingular,
|
||||
},
|
||||
outputSchema: {},
|
||||
icon: icon || COMMAND_MENU_DEFAULT_ICON,
|
||||
isPinned: isPinned || false,
|
||||
};
|
||||
}
|
||||
case 'BULK_RECORDS': {
|
||||
return {
|
||||
availability: {
|
||||
type: 'BULK_RECORDS',
|
||||
objectNameSingular:
|
||||
activeNonSystemObjectMetadataItems[0].nameSingular,
|
||||
},
|
||||
outputSchema: {},
|
||||
icon: icon || COMMAND_MENU_DEFAULT_ICON,
|
||||
isPinned: isPinned || false,
|
||||
};
|
||||
}
|
||||
default: {
|
||||
return assertUnreachable(availabilityType);
|
||||
}
|
||||
}
|
||||
|
||||
return assertUnreachable(availability);
|
||||
};
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import {
|
||||
type WorkflowManualTriggerAvailability,
|
||||
type WorkflowManualTriggerSettings,
|
||||
} from '@/workflow/types/Workflow';
|
||||
import { COMMAND_MENU_DEFAULT_ICON } from '@/workflow/workflow-trigger/constants/CommandMenuDefaultIcon';
|
||||
import { assertUnreachable } from 'twenty-shared/utils';
|
||||
|
||||
export const getManualTriggerDefaultSettingsDeprecated = ({
|
||||
availability,
|
||||
activeNonSystemObjectMetadataItems,
|
||||
icon,
|
||||
isPinned,
|
||||
}: {
|
||||
availability: WorkflowManualTriggerAvailability;
|
||||
activeNonSystemObjectMetadataItems: ObjectMetadataItem[];
|
||||
icon?: string;
|
||||
isPinned?: boolean;
|
||||
}): WorkflowManualTriggerSettings => {
|
||||
switch (availability) {
|
||||
case 'EVERYWHERE': {
|
||||
return {
|
||||
objectType: undefined,
|
||||
outputSchema: {},
|
||||
icon: icon || COMMAND_MENU_DEFAULT_ICON,
|
||||
isPinned: isPinned || false,
|
||||
};
|
||||
}
|
||||
case 'WHEN_RECORD_SELECTED': {
|
||||
return {
|
||||
objectType: activeNonSystemObjectMetadataItems[0].nameSingular,
|
||||
outputSchema: {},
|
||||
icon: icon || COMMAND_MENU_DEFAULT_ICON,
|
||||
isPinned: isPinned || false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return assertUnreachable(availability);
|
||||
};
|
||||
+14
-1
@@ -5,6 +5,7 @@ import {
|
||||
} from '@/workflow/types/Workflow';
|
||||
import { DATABASE_TRIGGER_TYPES } from '@/workflow/workflow-trigger/constants/DatabaseTriggerTypes';
|
||||
import { getManualTriggerDefaultSettings } from '@/workflow/workflow-trigger/utils/getManualTriggerDefaultSettings';
|
||||
import { getManualTriggerDefaultSettingsDeprecated } from '@/workflow/workflow-trigger/utils/getManualTriggerDefaultSettingsDeprecated';
|
||||
import { assertUnreachable } from 'twenty-shared/utils';
|
||||
|
||||
// TODO: This needs to be migrated to the server
|
||||
@@ -12,10 +13,12 @@ export const getTriggerDefaultDefinition = ({
|
||||
defaultLabel,
|
||||
type,
|
||||
activeNonSystemObjectMetadataItems,
|
||||
isIteratorEnabled,
|
||||
}: {
|
||||
defaultLabel: string;
|
||||
type: WorkflowTriggerType;
|
||||
activeNonSystemObjectMetadataItems: ObjectMetadataItem[];
|
||||
isIteratorEnabled: boolean;
|
||||
}): WorkflowTrigger => {
|
||||
if (activeNonSystemObjectMetadataItems.length === 0) {
|
||||
throw new Error(
|
||||
@@ -44,10 +47,20 @@ export const getTriggerDefaultDefinition = ({
|
||||
};
|
||||
}
|
||||
case 'MANUAL': {
|
||||
if (isIteratorEnabled) {
|
||||
return {
|
||||
...baseTriggerDefinition,
|
||||
type,
|
||||
settings: getManualTriggerDefaultSettings({
|
||||
availabilityType: 'GLOBAL',
|
||||
activeNonSystemObjectMetadataItems,
|
||||
}),
|
||||
};
|
||||
}
|
||||
return {
|
||||
...baseTriggerDefinition,
|
||||
type,
|
||||
settings: getManualTriggerDefaultSettings({
|
||||
settings: getManualTriggerDefaultSettingsDeprecated({
|
||||
availability: 'WHEN_RECORD_SELECTED',
|
||||
activeNonSystemObjectMetadataItems,
|
||||
}),
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
import { type BaseOutputSchemaV2 } from '@/workflow/workflow-variables/types/BaseOutputSchemaV2';
|
||||
import { type RecordOutputSchemaV2 } from '@/workflow/workflow-variables/types/RecordOutputSchemaV2';
|
||||
|
||||
export type ManualTriggerOutputSchema =
|
||||
| BaseOutputSchemaV2
|
||||
| RecordOutputSchemaV2;
|
||||
+3
-1
@@ -6,6 +6,7 @@ import { type BaseOutputSchemaV2 } from '@/workflow/workflow-variables/types/Bas
|
||||
import { type CodeOutputSchema } from '@/workflow/workflow-variables/types/CodeOutputSchema';
|
||||
import { type FindRecordsOutputSchema } from '@/workflow/workflow-variables/types/FindRecordsOutputSchema';
|
||||
import { type FormOutputSchema } from '@/workflow/workflow-variables/types/FormOutputSchema';
|
||||
import { type ManualTriggerOutputSchema } from '@/workflow/workflow-variables/types/ManualTriggerOutputSchema';
|
||||
import { type RecordOutputSchemaV2 } from '@/workflow/workflow-variables/types/RecordOutputSchemaV2';
|
||||
|
||||
export type OutputSchemaV2 =
|
||||
@@ -13,7 +14,8 @@ export type OutputSchemaV2 =
|
||||
| CodeOutputSchema
|
||||
| FindRecordsOutputSchema
|
||||
| FormOutputSchema
|
||||
| RecordOutputSchemaV2;
|
||||
| RecordOutputSchemaV2
|
||||
| ManualTriggerOutputSchema;
|
||||
|
||||
export type StepOutputSchemaV2 = {
|
||||
id: string;
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import {
|
||||
type WorkflowActionType,
|
||||
type WorkflowTriggerType,
|
||||
} from '@/workflow/types/Workflow';
|
||||
import { type ManualTriggerOutputSchema } from '@/workflow/workflow-variables/types/ManualTriggerOutputSchema';
|
||||
import { type OutputSchemaV2 } from '@/workflow/workflow-variables/types/StepOutputSchemaV2';
|
||||
|
||||
export const isManualTriggerOutputSchema = (
|
||||
stepType: WorkflowActionType | WorkflowTriggerType,
|
||||
schema: OutputSchemaV2,
|
||||
): schema is ManualTriggerOutputSchema => {
|
||||
return stepType === 'MANUAL';
|
||||
};
|
||||
+1
-6
@@ -5,12 +5,7 @@ import {
|
||||
import { type RecordActionOutputSchema } from '@/workflow/workflow-variables/types/RecordActionOutputSchema';
|
||||
import { type OutputSchemaV2 } from '@/workflow/workflow-variables/types/StepOutputSchemaV2';
|
||||
|
||||
const RECORD_STEP_TYPES = [
|
||||
'CREATE_RECORD',
|
||||
'UPDATE_RECORD',
|
||||
'DELETE_RECORD',
|
||||
'MANUAL',
|
||||
];
|
||||
const RECORD_STEP_TYPES = ['CREATE_RECORD', 'UPDATE_RECORD', 'DELETE_RECORD'];
|
||||
|
||||
export const isRecordStepOutputSchema = (
|
||||
stepType: WorkflowActionType | WorkflowTriggerType,
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { extractRawVariableNamePart } from '@/workflow/workflow-variables/utils/extractRawVariableNamePart';
|
||||
import { extractRawVariableNamePart } from 'twenty-shared/workflow';
|
||||
|
||||
describe('extractRawVariableNamePart', () => {
|
||||
it('returns the last part of a properly formatted variable', () => {
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { CAPTURE_ALL_VARIABLE_TAG_INNER_REGEX } from '@/workflow/workflow-variables/constants/CaptureAllVariableTagInnerRegex';
|
||||
import { CAPTURE_ALL_VARIABLE_TAG_INNER_REGEX } from 'twenty-shared/workflow';
|
||||
|
||||
export const removeVariablesFromJson = (json: string): string => {
|
||||
return json.replaceAll(CAPTURE_ALL_VARIABLE_TAG_INNER_REGEX, 'null');
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { CAPTURE_ALL_VARIABLE_TAG_INNER_REGEX } from '@/workflow/workflow-variables/constants/CaptureAllVariableTagInnerRegex';
|
||||
import { type VariableSearchResult } from '@/workflow/workflow-variables/hooks/useSearchVariable';
|
||||
import type { BaseOutputSchemaV2 } from '@/workflow/workflow-variables/types/BaseOutputSchemaV2';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { CAPTURE_ALL_VARIABLE_TAG_INNER_REGEX } from 'twenty-shared/workflow';
|
||||
|
||||
const parseVariableName = (rawVariableName: string) => {
|
||||
const variableWithoutBrackets = rawVariableName.replace(
|
||||
|
||||
+1
-1
@@ -1,9 +1,9 @@
|
||||
import { CAPTURE_ALL_VARIABLE_TAG_INNER_REGEX } from '@/workflow/workflow-variables/constants/CaptureAllVariableTagInnerRegex';
|
||||
import { type VariableSearchResult } from '@/workflow/workflow-variables/hooks/useSearchVariable';
|
||||
import type { FindRecordsOutputSchema } from '@/workflow/workflow-variables/types/FindRecordsOutputSchema';
|
||||
import { searchRecordOutputSchema as searchRecordOutputSchemaUtil } from '@/workflow/workflow-variables/utils/searchVariableThroughRecordOutputSchema';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { CAPTURE_ALL_VARIABLE_TAG_INNER_REGEX } from 'twenty-shared/workflow';
|
||||
|
||||
type SearchResultKey = 'first' | 'last' | 'totalCount';
|
||||
|
||||
|
||||
+1
-1
@@ -1,8 +1,8 @@
|
||||
import { CAPTURE_ALL_VARIABLE_TAG_INNER_REGEX } from '@/workflow/workflow-variables/constants/CaptureAllVariableTagInnerRegex';
|
||||
import { type VariableSearchResult } from '@/workflow/workflow-variables/hooks/useSearchVariable';
|
||||
import type { FormOutputSchema } from '@/workflow/workflow-variables/types/FormOutputSchema';
|
||||
import { searchRecordOutputSchema } from '@/workflow/workflow-variables/utils/searchVariableThroughRecordOutputSchema';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { CAPTURE_ALL_VARIABLE_TAG_INNER_REGEX } from 'twenty-shared/workflow';
|
||||
|
||||
/**
|
||||
* Parses a variable name to extract its components for Form outputs
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
import { isRecordOutputSchemaV2 } from '@/workflow/workflow-variables/types/guards/isRecordOutputSchemaV2';
|
||||
import { type ManualTriggerOutputSchema } from '@/workflow/workflow-variables/types/ManualTriggerOutputSchema';
|
||||
import { searchVariableThroughBaseOutputSchema } from '@/workflow/workflow-variables/utils/searchVariableThroughBaseOutputSchema';
|
||||
import { searchVariableThroughRecordOutputSchema } from '@/workflow/workflow-variables/utils/searchVariableThroughRecordOutputSchema';
|
||||
|
||||
export const searchVariableThroughManualTriggerOutputSchema = ({
|
||||
stepName,
|
||||
manualTriggerOutputSchema,
|
||||
rawVariableName,
|
||||
isFullRecord,
|
||||
}: {
|
||||
stepName: string;
|
||||
manualTriggerOutputSchema: ManualTriggerOutputSchema;
|
||||
rawVariableName: string;
|
||||
isFullRecord: boolean;
|
||||
}) => {
|
||||
if (isRecordOutputSchemaV2(manualTriggerOutputSchema)) {
|
||||
return searchVariableThroughRecordOutputSchema({
|
||||
stepName,
|
||||
recordOutputSchema: manualTriggerOutputSchema,
|
||||
rawVariableName,
|
||||
isFullRecord,
|
||||
});
|
||||
}
|
||||
|
||||
return searchVariableThroughBaseOutputSchema({
|
||||
stepName,
|
||||
baseOutputSchema: manualTriggerOutputSchema,
|
||||
rawVariableName,
|
||||
isFullRecord,
|
||||
});
|
||||
};
|
||||
+11
@@ -6,6 +6,7 @@ import { isCodeOutputSchema } from '@/workflow/workflow-variables/types/guards/i
|
||||
import { isDatabaseEventTriggerOutputSchema } from '@/workflow/workflow-variables/types/guards/isDatabaseEventTriggerOutputSchema';
|
||||
import { isFindRecordsOutputSchema } from '@/workflow/workflow-variables/types/guards/isFindRecordsOutputSchema';
|
||||
import { isFormOutputSchema } from '@/workflow/workflow-variables/types/guards/isFormOutputSchema';
|
||||
import { isManualTriggerOutputSchema } from '@/workflow/workflow-variables/types/guards/isManualTriggerOutputSchema';
|
||||
import { isRecordStepOutputSchema } from '@/workflow/workflow-variables/types/guards/isRecordStepOutputSchema';
|
||||
import { type StepOutputSchemaV2 } from '@/workflow/workflow-variables/types/StepOutputSchemaV2';
|
||||
|
||||
@@ -13,6 +14,7 @@ import { searchVariableThroughBaseOutputSchema } from '@/workflow/workflow-varia
|
||||
import { searchVariableThroughCodeOutputSchema } from '@/workflow/workflow-variables/utils/searchVariableThroughCodeOutputSchema';
|
||||
import { searchVariableThroughFindRecordsOutputSchema } from '@/workflow/workflow-variables/utils/searchVariableThroughFindRecordsOutputSchema';
|
||||
import { searchVariableThroughFormOutputSchema } from '@/workflow/workflow-variables/utils/searchVariableThroughFormOutputSchema';
|
||||
import { searchVariableThroughManualTriggerOutputSchema } from '@/workflow/workflow-variables/utils/searchVariableThroughManualTriggerOutputSchema';
|
||||
import { searchVariableThroughRecordEventOutputSchema } from '@/workflow/workflow-variables/utils/searchVariableThroughRecordEventOutputSchema';
|
||||
import { searchVariableThroughRecordOutputSchema } from '@/workflow/workflow-variables/utils/searchVariableThroughRecordOutputSchema';
|
||||
|
||||
@@ -36,6 +38,15 @@ export const searchVariableThroughOutputSchemaV2 = ({
|
||||
});
|
||||
}
|
||||
|
||||
if (isManualTriggerOutputSchema(stepType, stepOutputSchema.outputSchema)) {
|
||||
return searchVariableThroughManualTriggerOutputSchema({
|
||||
stepName: stepOutputSchema.name,
|
||||
manualTriggerOutputSchema: stepOutputSchema.outputSchema,
|
||||
rawVariableName,
|
||||
isFullRecord,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
isDatabaseEventTriggerOutputSchema(stepType, stepOutputSchema.outputSchema)
|
||||
) {
|
||||
|
||||
+1
-1
@@ -1,8 +1,8 @@
|
||||
import { CAPTURE_ALL_VARIABLE_TAG_INNER_REGEX } from '@/workflow/workflow-variables/constants/CaptureAllVariableTagInnerRegex';
|
||||
import { type VariableSearchResult } from '@/workflow/workflow-variables/hooks/useSearchVariable';
|
||||
import { type RecordOutputSchemaV2 } from '@/workflow/workflow-variables/types/RecordOutputSchemaV2';
|
||||
import { searchRecordOutputSchema } from '@/workflow/workflow-variables/utils/searchVariableThroughRecordOutputSchema';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { CAPTURE_ALL_VARIABLE_TAG_INNER_REGEX } from 'twenty-shared/workflow';
|
||||
|
||||
/**
|
||||
* Parses a variable name to extract its components
|
||||
|
||||
+1
-1
@@ -1,4 +1,3 @@
|
||||
import { CAPTURE_ALL_VARIABLE_TAG_INNER_REGEX } from '@/workflow/workflow-variables/constants/CaptureAllVariableTagInnerRegex';
|
||||
import { type VariableSearchResult } from '@/workflow/workflow-variables/hooks/useSearchVariable';
|
||||
import {
|
||||
type FieldOutputSchemaV2,
|
||||
@@ -7,6 +6,7 @@ import {
|
||||
} from '@/workflow/workflow-variables/types/RecordOutputSchemaV2';
|
||||
import { isRecordOutputSchemaV2 } from '@/workflow/workflow-variables/types/guards/isRecordOutputSchemaV2';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { CAPTURE_ALL_VARIABLE_TAG_INNER_REGEX } from 'twenty-shared/workflow';
|
||||
|
||||
const getRecordObjectLabel = (
|
||||
recordSchema: RecordOutputSchemaV2,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { WorkflowTextEditorVariableChip } from '@/workflow/workflow-variables/components/WorkflowTextEditorVariableChip';
|
||||
import { extractRawVariableNamePart } from '@/workflow/workflow-variables/utils/extractRawVariableNamePart';
|
||||
import { Node } from '@tiptap/core';
|
||||
import { mergeAttributes, ReactNodeViewRenderer } from '@tiptap/react';
|
||||
import { extractRawVariableNamePart } from 'twenty-shared/workflow';
|
||||
|
||||
declare module '@tiptap/core' {
|
||||
interface Commands<ReturnType> {
|
||||
|
||||
+7
@@ -2,6 +2,7 @@ import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
import graphqlTypeJson from 'graphql-type-json';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { type WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
import { type WorkflowTrigger } from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type';
|
||||
|
||||
@@ -12,4 +13,10 @@ export class ComputeStepOutputSchemaInput {
|
||||
nullable: false,
|
||||
})
|
||||
step: WorkflowTrigger | WorkflowAction;
|
||||
|
||||
@Field(() => UUIDScalarType, {
|
||||
description: 'Workflow version ID',
|
||||
nullable: false,
|
||||
})
|
||||
workflowVersionId: string;
|
||||
}
|
||||
|
||||
+2
-1
@@ -37,11 +37,12 @@ export class WorkflowBuilderResolver {
|
||||
@Mutation(() => graphqlTypeJson)
|
||||
async computeStepOutputSchema(
|
||||
@AuthWorkspace() { id: workspaceId }: Workspace,
|
||||
@Args('input') { step }: ComputeStepOutputSchemaInput,
|
||||
@Args('input') { step, workflowVersionId }: ComputeStepOutputSchemaInput,
|
||||
): Promise<OutputSchema> {
|
||||
return this.workflowSchemaWorkspaceService.computeStepOutputSchema({
|
||||
step,
|
||||
workspaceId,
|
||||
workflowVersionId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
type FakeValueTypes =
|
||||
export type FakeValueTypes =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
|
||||
-1
@@ -13,7 +13,6 @@ describe('computeWorkflowVersionStepChanges', () => {
|
||||
name: 'Test Manual Trigger',
|
||||
type: WorkflowTriggerType.MANUAL,
|
||||
settings: {
|
||||
input: {},
|
||||
outputSchema: {},
|
||||
},
|
||||
nextStepIds: ['step-1'],
|
||||
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
import { type Leaf } from 'src/modules/workflow/workflow-builder/workflow-schema/types/output-schema.type';
|
||||
|
||||
export const DEFAULT_ITERATOR_CURRENT_ITEM: Leaf = {
|
||||
label: 'Current Item',
|
||||
isLeaf: true,
|
||||
type: 'unknown',
|
||||
value: null,
|
||||
};
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import { DEFAULT_ITERATOR_CURRENT_ITEM } from 'src/modules/workflow/workflow-builder/workflow-schema/constants/default-iterator-current-item.const';
|
||||
import { generateFakeArrayItem } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-array-item';
|
||||
|
||||
describe('generateFakeArrayItem', () => {
|
||||
it('should return default iterator when input cannot be parsed', () => {
|
||||
const result = generateFakeArrayItem({ items: 'invalid json' });
|
||||
|
||||
expect(result).toEqual(DEFAULT_ITERATOR_CURRENT_ITEM);
|
||||
});
|
||||
|
||||
it('should handle string array input', () => {
|
||||
const result = generateFakeArrayItem({ items: '["test1", "test2"]' });
|
||||
|
||||
expect(result).toEqual({
|
||||
label: 'Current Item',
|
||||
isLeaf: true,
|
||||
type: 'string',
|
||||
value: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle regular array input', () => {
|
||||
const result = generateFakeArrayItem({ items: [1, 2, 3] });
|
||||
|
||||
expect(result).toEqual({
|
||||
label: 'Current Item',
|
||||
isLeaf: true,
|
||||
type: 'number',
|
||||
value: expect.any(Number),
|
||||
});
|
||||
});
|
||||
|
||||
it('should return default iterator when input is parsed but not an array', () => {
|
||||
const result = generateFakeArrayItem({ items: '{"key": "value"}' });
|
||||
|
||||
expect(result).toEqual(DEFAULT_ITERATOR_CURRENT_ITEM);
|
||||
});
|
||||
});
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
import { DEFAULT_ITERATOR_CURRENT_ITEM } from 'src/modules/workflow/workflow-builder/workflow-schema/constants/default-iterator-current-item.const';
|
||||
|
||||
import { generateFakeArrayItem } from './generate-fake-array-item';
|
||||
|
||||
describe('generateFakeArrayItem', () => {
|
||||
it('should return default iterator when input cannot be parsed', () => {
|
||||
const result = generateFakeArrayItem({ items: 'invalid json' });
|
||||
|
||||
expect(result).toEqual(DEFAULT_ITERATOR_CURRENT_ITEM);
|
||||
});
|
||||
|
||||
it('should handle string array input', () => {
|
||||
const result = generateFakeArrayItem({ items: '["test1", "test2"]' });
|
||||
|
||||
expect(result).toEqual({
|
||||
label: 'Current Item',
|
||||
isLeaf: true,
|
||||
type: 'string',
|
||||
value: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle regular array input', () => {
|
||||
const result = generateFakeArrayItem({ items: [1, 2, 3] });
|
||||
|
||||
expect(result).toEqual({
|
||||
label: 'Current Item',
|
||||
isLeaf: true,
|
||||
type: 'number',
|
||||
value: expect.any(Number),
|
||||
});
|
||||
});
|
||||
|
||||
it('should return default iterator when input is parsed but not an array', () => {
|
||||
const result = generateFakeArrayItem({ items: '{"key": "value"}' });
|
||||
|
||||
expect(result).toEqual(DEFAULT_ITERATOR_CURRENT_ITEM);
|
||||
});
|
||||
});
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import { isArray } from '@sniptt/guards';
|
||||
import { isString } from 'class-validator';
|
||||
|
||||
import { generateFakeValue } from 'src/engine/utils/generate-fake-value';
|
||||
import { DEFAULT_ITERATOR_CURRENT_ITEM } from 'src/modules/workflow/workflow-builder/workflow-schema/constants/default-iterator-current-item.const';
|
||||
import { type InputSchemaPropertyType } from 'src/modules/workflow/workflow-builder/workflow-schema/types/input-schema.type';
|
||||
import { type Leaf } from 'src/modules/workflow/workflow-builder/workflow-schema/types/output-schema.type';
|
||||
|
||||
export const generateFakeArrayItem = ({
|
||||
items,
|
||||
}: {
|
||||
items: unknown[] | string;
|
||||
}): Leaf => {
|
||||
let parsedItems: unknown[] | string;
|
||||
|
||||
try {
|
||||
parsedItems = isString(items) ? JSON.parse(items) : items;
|
||||
} catch {
|
||||
return DEFAULT_ITERATOR_CURRENT_ITEM;
|
||||
}
|
||||
|
||||
if (isArray(parsedItems) && parsedItems.length > 0) {
|
||||
const type = typeof parsedItems[0];
|
||||
|
||||
return {
|
||||
label: 'Current Item',
|
||||
isLeaf: true,
|
||||
type: type as InputSchemaPropertyType,
|
||||
value: generateFakeValue(type),
|
||||
};
|
||||
}
|
||||
|
||||
return DEFAULT_ITERATOR_CURRENT_ITEM;
|
||||
};
|
||||
+178
-15
@@ -1,10 +1,26 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { isString } from '@sniptt/guards';
|
||||
import { isDefined, isValidVariable } from 'twenty-shared/utils';
|
||||
import {
|
||||
BulkRecordsAvailability,
|
||||
extractRawVariableNamePart,
|
||||
GlobalAvailability,
|
||||
SingleRecordAvailability,
|
||||
TRIGGER_STEP_ID,
|
||||
} from 'twenty-shared/workflow';
|
||||
|
||||
import { type DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
||||
import { checkStringIsDatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/utils/check-string-is-database-event-action';
|
||||
import { generateFakeValue } from 'src/engine/utils/generate-fake-value';
|
||||
import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service';
|
||||
import { type OutputSchema } from 'src/modules/workflow/workflow-builder/workflow-schema/types/output-schema.type';
|
||||
import { DEFAULT_ITERATOR_CURRENT_ITEM } from 'src/modules/workflow/workflow-builder/workflow-schema/constants/default-iterator-current-item.const';
|
||||
import {
|
||||
Leaf,
|
||||
Node,
|
||||
type OutputSchema,
|
||||
} from 'src/modules/workflow/workflow-builder/workflow-schema/types/output-schema.type';
|
||||
import { generateFakeArrayItem } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-array-item';
|
||||
import { generateFakeFormResponse } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-form-response';
|
||||
import { generateFakeObjectRecord } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record';
|
||||
import { generateFakeObjectRecordEvent } from 'src/modules/workflow/workflow-builder/workflow-schema/utils/generate-fake-object-record-event';
|
||||
@@ -14,7 +30,7 @@ import {
|
||||
WorkflowActionType,
|
||||
} from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
import {
|
||||
type WorkflowTrigger,
|
||||
WorkflowTrigger,
|
||||
WorkflowTriggerType,
|
||||
} from 'src/modules/workflow/workflow-trigger/types/workflow-trigger.type';
|
||||
|
||||
@@ -27,9 +43,11 @@ export class WorkflowSchemaWorkspaceService {
|
||||
async computeStepOutputSchema({
|
||||
step,
|
||||
workspaceId,
|
||||
workflowVersionId,
|
||||
}: {
|
||||
step: WorkflowTrigger | WorkflowAction;
|
||||
workspaceId: string;
|
||||
workflowVersionId: string;
|
||||
}): Promise<OutputSchema> {
|
||||
const stepType = step.type;
|
||||
|
||||
@@ -41,16 +59,24 @@ export class WorkflowSchemaWorkspaceService {
|
||||
});
|
||||
}
|
||||
case WorkflowTriggerType.MANUAL: {
|
||||
const { objectType } = step.settings;
|
||||
const { objectType, availability } = step.settings;
|
||||
|
||||
if (!objectType) {
|
||||
return {};
|
||||
// TODO: to be deprecated once all triggers are migrated to the new availability type
|
||||
if (isDefined(objectType)) {
|
||||
return this.computeRecordOutputSchema({
|
||||
objectType,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
return this.computeRecordOutputSchema({
|
||||
objectType,
|
||||
workspaceId,
|
||||
});
|
||||
if (isDefined(availability)) {
|
||||
return this.computeTriggerOutputSchemaFromAvailability({
|
||||
availability,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
case WorkflowTriggerType.WEBHOOK:
|
||||
case WorkflowTriggerType.CRON: {
|
||||
@@ -77,13 +103,14 @@ export class WorkflowSchemaWorkspaceService {
|
||||
workspaceId,
|
||||
});
|
||||
case WorkflowActionType.ITERATOR: {
|
||||
const items = step.settings.input.items;
|
||||
|
||||
return {
|
||||
currentItem: {
|
||||
label: 'Current Item',
|
||||
isLeaf: true,
|
||||
type: 'unknown',
|
||||
value: generateFakeValue('unknown'),
|
||||
},
|
||||
currentItem: await this.computeLoopCurrentItemOutputSchema({
|
||||
items,
|
||||
workflowVersionId,
|
||||
workspaceId,
|
||||
}),
|
||||
currentItemIndex: {
|
||||
label: 'Current Item Index',
|
||||
isLeaf: true,
|
||||
@@ -107,9 +134,11 @@ export class WorkflowSchemaWorkspaceService {
|
||||
async enrichOutputSchema({
|
||||
step,
|
||||
workspaceId,
|
||||
workflowVersionId,
|
||||
}: {
|
||||
step: WorkflowAction;
|
||||
workspaceId: string;
|
||||
workflowVersionId: string;
|
||||
}): Promise<WorkflowAction> {
|
||||
// We don't enrich on the fly for code and HTTP request workflow actions.
|
||||
// For code actions, OutputSchema is computed and updated when testing the serverless function.
|
||||
@@ -128,6 +157,7 @@ export class WorkflowSchemaWorkspaceService {
|
||||
const outputSchema = await this.computeStepOutputSchema({
|
||||
step,
|
||||
workspaceId,
|
||||
workflowVersionId,
|
||||
});
|
||||
|
||||
result.settings = {
|
||||
@@ -231,4 +261,137 @@ export class WorkflowSchemaWorkspaceService {
|
||||
objectMetadataMaps,
|
||||
});
|
||||
}
|
||||
|
||||
private async computeTriggerOutputSchemaFromAvailability({
|
||||
availability,
|
||||
workspaceId,
|
||||
}: {
|
||||
availability:
|
||||
| GlobalAvailability
|
||||
| SingleRecordAvailability
|
||||
| BulkRecordsAvailability;
|
||||
workspaceId: string;
|
||||
}): Promise<OutputSchema> {
|
||||
if (availability.type === 'GLOBAL') {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (availability.type === 'SINGLE_RECORD') {
|
||||
return this.computeRecordOutputSchema({
|
||||
objectType: availability.objectNameSingular,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
if (availability.type === 'BULK_RECORDS') {
|
||||
const objectMetadataInfo =
|
||||
await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps(
|
||||
availability.objectNameSingular,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
return {
|
||||
[availability.objectNameSingular]: {
|
||||
label:
|
||||
objectMetadataInfo.objectMetadataItemWithFieldsMaps.labelPlural,
|
||||
isLeaf: true,
|
||||
type: 'array',
|
||||
value:
|
||||
'Array of ' +
|
||||
objectMetadataInfo.objectMetadataItemWithFieldsMaps.labelPlural,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
private async computeLoopCurrentItemOutputSchema({
|
||||
items,
|
||||
workflowVersionId,
|
||||
workspaceId,
|
||||
}: {
|
||||
items: string | undefined | unknown[];
|
||||
workflowVersionId: string;
|
||||
workspaceId: string;
|
||||
}): Promise<Leaf | Node> {
|
||||
if (!isDefined(items)) {
|
||||
return DEFAULT_ITERATOR_CURRENT_ITEM;
|
||||
}
|
||||
|
||||
if (isString(items) && isValidVariable(items)) {
|
||||
return this.computeIteratorCurrentItemFromVariable({
|
||||
items,
|
||||
workflowVersionId,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
return generateFakeArrayItem({ items });
|
||||
}
|
||||
|
||||
private async computeIteratorCurrentItemFromVariable({
|
||||
items,
|
||||
workflowVersionId,
|
||||
workspaceId,
|
||||
}: {
|
||||
items: string;
|
||||
workflowVersionId: string;
|
||||
workspaceId: string;
|
||||
}): Promise<Leaf | Node> {
|
||||
const workflowVersion =
|
||||
await this.workflowCommonWorkspaceService.getWorkflowVersionOrFail({
|
||||
workflowVersionId,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
const stepId = extractRawVariableNamePart({
|
||||
rawVariableName: items,
|
||||
part: 'stepId',
|
||||
});
|
||||
|
||||
if (stepId === TRIGGER_STEP_ID) {
|
||||
const trigger = workflowVersion.trigger;
|
||||
|
||||
if (!isDefined(trigger)) {
|
||||
return DEFAULT_ITERATOR_CURRENT_ITEM;
|
||||
}
|
||||
|
||||
switch (trigger.type) {
|
||||
case WorkflowTriggerType.MANUAL: {
|
||||
if (trigger.settings.availability?.type === 'BULK_RECORDS') {
|
||||
const objectMetadataInfo =
|
||||
await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps(
|
||||
trigger.settings.availability.objectNameSingular,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
return {
|
||||
label:
|
||||
'Current Item (' +
|
||||
objectMetadataInfo.objectMetadataItemWithFieldsMaps
|
||||
.labelSingular +
|
||||
')',
|
||||
isLeaf: false,
|
||||
type: 'object',
|
||||
value: await this.computeRecordOutputSchema({
|
||||
objectType: trigger.settings.availability.objectNameSingular,
|
||||
workspaceId,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
return DEFAULT_ITERATOR_CURRENT_ITEM;
|
||||
}
|
||||
// TODO(t.trompette): handle other trigger types
|
||||
default: {
|
||||
return DEFAULT_ITERATOR_CURRENT_ITEM;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(t.trompette): handle other step types
|
||||
|
||||
return DEFAULT_ITERATOR_CURRENT_ITEM;
|
||||
}
|
||||
}
|
||||
|
||||
+6
@@ -74,6 +74,7 @@ export class WorkflowVersionStepWorkspaceService {
|
||||
await this.workflowSchemaWorkspaceService.enrichOutputSchema({
|
||||
step: builtStep,
|
||||
workspaceId,
|
||||
workflowVersionId,
|
||||
});
|
||||
|
||||
const { updatedSteps, updatedTrigger } = insertStep({
|
||||
@@ -156,6 +157,7 @@ export class WorkflowVersionStepWorkspaceService {
|
||||
: await this.updateWorkflowVersionStepSettings({
|
||||
newStep: step,
|
||||
workspaceId,
|
||||
workflowVersionId,
|
||||
});
|
||||
|
||||
const updatedSteps = workflowVersion.steps.map((existingStep) => {
|
||||
@@ -378,19 +380,23 @@ export class WorkflowVersionStepWorkspaceService {
|
||||
position: existingStep.position,
|
||||
},
|
||||
workspaceId,
|
||||
workflowVersionId,
|
||||
});
|
||||
}
|
||||
|
||||
private async updateWorkflowVersionStepSettings({
|
||||
newStep,
|
||||
workspaceId,
|
||||
workflowVersionId,
|
||||
}: {
|
||||
newStep: WorkflowAction;
|
||||
workspaceId: string;
|
||||
workflowVersionId: string;
|
||||
}): Promise<WorkflowAction> {
|
||||
return this.workflowSchemaWorkspaceService.enrichOutputSchema({
|
||||
step: newStep,
|
||||
workspaceId,
|
||||
workflowVersionId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -96,6 +96,7 @@ export const computeStepOutputSchemaSchema = z.object({
|
||||
step: z
|
||||
.union([workflowTriggerSchema, workflowActionSchema])
|
||||
.describe('The workflow step configuration'),
|
||||
workflowVersionId: z.string().describe('The ID of the workflow version'),
|
||||
});
|
||||
|
||||
export const createCompleteWorkflowSchema = z.object({
|
||||
|
||||
+2
@@ -376,11 +376,13 @@ This is the most efficient way for AI to create workflows as it handles all the
|
||||
inputSchema: computeStepOutputSchemaSchema,
|
||||
execute: async (parameters: {
|
||||
step: WorkflowTrigger | WorkflowAction;
|
||||
workflowVersionId: string;
|
||||
}) => {
|
||||
try {
|
||||
return await this.workflowSchemaService.computeStepOutputSchema({
|
||||
step: parameters.step,
|
||||
workspaceId,
|
||||
workflowVersionId: parameters.workflowVersionId,
|
||||
});
|
||||
} catch (error) {
|
||||
return {
|
||||
|
||||
+10
-1
@@ -1,3 +1,9 @@
|
||||
import {
|
||||
type BulkRecordsAvailability,
|
||||
type GlobalAvailability,
|
||||
type SingleRecordAvailability,
|
||||
} from 'twenty-shared/workflow';
|
||||
|
||||
import { type OutputSchema } from 'src/modules/workflow/workflow-builder/workflow-schema/types/output-schema.type';
|
||||
|
||||
export enum WorkflowTriggerType {
|
||||
@@ -8,7 +14,6 @@ export enum WorkflowTriggerType {
|
||||
}
|
||||
|
||||
type BaseWorkflowTriggerSettings = {
|
||||
input?: object;
|
||||
outputSchema: OutputSchema;
|
||||
};
|
||||
|
||||
@@ -35,6 +40,10 @@ export type WorkflowManualTrigger = BaseTrigger & {
|
||||
settings: BaseWorkflowTriggerSettings & {
|
||||
objectType?: string;
|
||||
icon?: string;
|
||||
availability?:
|
||||
| GlobalAvailability
|
||||
| SingleRecordAvailability
|
||||
| BulkRecordsAvailability;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
* |___/
|
||||
*/
|
||||
|
||||
export { CAPTURE_ALL_VARIABLE_TAG_INNER_REGEX } from './constants/CaptureAllVariableTagInnerRegex';
|
||||
export { CONTENT_TYPE_VALUES_HTTP_REQUEST } from './constants/contentTypeValuesHttpRequest';
|
||||
export { TRIGGER_STEP_ID } from './constants/TriggerStepId';
|
||||
export { workflowAiAgentActionSchema } from './schemas/ai-agent-action-schema';
|
||||
@@ -56,5 +57,11 @@ export type {
|
||||
} from './types/WorkflowRunStateStepInfos';
|
||||
export { StepStatus } from './types/WorkflowRunStateStepInfos';
|
||||
export { canObjectBeManagedByWorkflow } from './utils/canObjectBeManagedByWorkflow';
|
||||
export { extractRawVariableNamePart } from './utils/extractRawVariableNameParts';
|
||||
export { getWorkflowRunContext } from './utils/getWorkflowRunContext';
|
||||
export { parseDataFromContentType } from './utils/parseDataFromContentType';
|
||||
export type {
|
||||
GlobalAvailability,
|
||||
SingleRecordAvailability,
|
||||
BulkRecordsAvailability,
|
||||
} from './workflow-trigger/types/workflow-trigger.type';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { z } from 'zod';
|
||||
import { baseTriggerSchema } from './base-trigger-schema';
|
||||
|
||||
export const workflowManualTriggerSchema = baseTriggerSchema
|
||||
export const workflowManualTriggerSchema = baseTriggerSchema
|
||||
.extend({
|
||||
type: z.literal('MANUAL'),
|
||||
settings: z.object({
|
||||
@@ -13,8 +13,25 @@ export const workflowManualTriggerSchema = baseTriggerSchema
|
||||
),
|
||||
icon: z.string().optional(),
|
||||
isPinned: z.boolean().optional(),
|
||||
availability: z
|
||||
.discriminatedUnion('type', [
|
||||
z.object({
|
||||
type: z.literal('GLOBAL'),
|
||||
locations: z.array(z.string()).optional(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('SINGLE_RECORD'),
|
||||
objectNameSingular: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('BULK_RECORDS'),
|
||||
objectNameSingular: z.string(),
|
||||
}),
|
||||
])
|
||||
.optional()
|
||||
.nullable(),
|
||||
}),
|
||||
})
|
||||
.describe(
|
||||
'Manual trigger that can be launched by the user. If a record is selected when launched, it is accessible via {{trigger.record.fieldName}}. If no record is selected, no data context is available.',
|
||||
);
|
||||
);
|
||||
+4
-4
@@ -1,5 +1,5 @@
|
||||
import { CAPTURE_ALL_VARIABLE_TAG_INNER_REGEX } from '@/workflow/workflow-variables/constants/CaptureAllVariableTagInnerRegex';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { isDefined } from '@/utils';
|
||||
import { CAPTURE_ALL_VARIABLE_TAG_INNER_REGEX } from '../constants/CaptureAllVariableTagInnerRegex';
|
||||
|
||||
export const extractRawVariableNamePart = ({
|
||||
rawVariableName,
|
||||
@@ -19,9 +19,9 @@ export const extractRawVariableNamePart = ({
|
||||
|
||||
const extractedPart =
|
||||
part === 'stepId'
|
||||
? parts.at(0)
|
||||
? parts[0]
|
||||
: part === 'selectedField'
|
||||
? parts.at(-1)
|
||||
? parts[parts.length - 1]
|
||||
: null;
|
||||
|
||||
if (!isDefined(extractedPart)) {
|
||||
@@ -0,0 +1,14 @@
|
||||
export type GlobalAvailability = {
|
||||
type: 'GLOBAL';
|
||||
locations?: string[];
|
||||
};
|
||||
|
||||
export type SingleRecordAvailability = {
|
||||
type: 'SINGLE_RECORD';
|
||||
objectNameSingular: string;
|
||||
};
|
||||
|
||||
export type BulkRecordsAvailability = {
|
||||
type: 'BULK_RECORDS';
|
||||
objectNameSingular: string;
|
||||
};
|
||||
Reference in New Issue
Block a user