Add variable path (#10720)
<img width="537" alt="Capture d’écran 2025-03-07 à 09 44 21" src="https://github.com/user-attachments/assets/52c4d292-01af-4389-aa66-551be2358dd7" /> - search through step output schema the variable - build the variable path - returns the variable label - display both
This commit is contained in:
@ -1,4 +1,6 @@
|
|||||||
import { extractVariableLabel } from '@/workflow/workflow-variables/utils/extractVariableLabel';
|
import { useStepsOutputSchema } from '@/workflow/hooks/useStepsOutputSchema';
|
||||||
|
import { extractRawVariableNamePart } from '@/workflow/workflow-variables/utils/extractRawVariableNamePart';
|
||||||
|
import { searchVariableThroughOutputSchema } from '@/workflow/workflow-variables/utils/searchVariableThroughOutputSchema';
|
||||||
import { css, useTheme } from '@emotion/react';
|
import { css, useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { isDefined } from 'twenty-shared';
|
import { isDefined } from 'twenty-shared';
|
||||||
@ -60,17 +62,48 @@ const StyledDelete = styled.button`
|
|||||||
type VariableChipProps = {
|
type VariableChipProps = {
|
||||||
rawVariableName: string;
|
rawVariableName: string;
|
||||||
onRemove?: () => void;
|
onRemove?: () => void;
|
||||||
|
isFullRecord?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const VariableChip = ({
|
export const VariableChip = ({
|
||||||
rawVariableName,
|
rawVariableName,
|
||||||
onRemove,
|
onRemove,
|
||||||
|
isFullRecord = false,
|
||||||
}: VariableChipProps) => {
|
}: VariableChipProps) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
const { getStepsOutputSchema } = useStepsOutputSchema({});
|
||||||
|
const stepId = extractRawVariableNamePart({
|
||||||
|
rawVariableName,
|
||||||
|
part: 'stepId',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isDefined(stepId)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stepOutputSchema = getStepsOutputSchema([stepId])?.[0];
|
||||||
|
|
||||||
|
if (!isDefined(stepOutputSchema)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { variableLabel, variablePathLabel } =
|
||||||
|
searchVariableThroughOutputSchema({
|
||||||
|
stepOutputSchema,
|
||||||
|
rawVariableName,
|
||||||
|
isFullRecord,
|
||||||
|
});
|
||||||
|
|
||||||
|
const label = isDefined(variableLabel)
|
||||||
|
? variableLabel
|
||||||
|
: extractRawVariableNamePart({
|
||||||
|
rawVariableName,
|
||||||
|
part: 'selectedField',
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledChip deletable={isDefined(onRemove)}>
|
<StyledChip deletable={isDefined(onRemove)}>
|
||||||
<StyledLabel>{extractVariableLabel(rawVariableName)}</StyledLabel>
|
<StyledLabel title={variablePathLabel}>{label}</StyledLabel>
|
||||||
|
|
||||||
{onRemove ? (
|
{onRemove ? (
|
||||||
<StyledDelete onClick={onRemove} aria-label="Remove variable">
|
<StyledDelete onClick={onRemove} aria-label="Remove variable">
|
||||||
|
|||||||
@ -10,15 +10,21 @@ const StyledContainer = styled.div`
|
|||||||
type VariableChipStandaloneProps = {
|
type VariableChipStandaloneProps = {
|
||||||
rawVariableName: string;
|
rawVariableName: string;
|
||||||
onRemove?: () => void;
|
onRemove?: () => void;
|
||||||
|
isFullRecord?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const VariableChipStandalone = ({
|
export const VariableChipStandalone = ({
|
||||||
rawVariableName,
|
rawVariableName,
|
||||||
onRemove,
|
onRemove,
|
||||||
|
isFullRecord,
|
||||||
}: VariableChipStandaloneProps) => {
|
}: VariableChipStandaloneProps) => {
|
||||||
return (
|
return (
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
<VariableChip rawVariableName={rawVariableName} onRemove={onRemove} />
|
<VariableChip
|
||||||
|
rawVariableName={rawVariableName}
|
||||||
|
onRemove={onRemove}
|
||||||
|
isFullRecord={isFullRecord}
|
||||||
|
/>
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import { Meta, StoryObj } from '@storybook/react';
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
import { expect, fn, userEvent, within } from '@storybook/test';
|
import { expect, fn, userEvent, within } from '@storybook/test';
|
||||||
|
import { WorkflowStepDecorator } from '~/testing/decorators/WorkflowStepDecorator';
|
||||||
|
import { MOCKED_STEP_ID } from '~/testing/mock-data/workflow';
|
||||||
import { FormAddressFieldInput } from '../FormAddressFieldInput';
|
import { FormAddressFieldInput } from '../FormAddressFieldInput';
|
||||||
|
|
||||||
const meta: Meta<typeof FormAddressFieldInput> = {
|
const meta: Meta<typeof FormAddressFieldInput> = {
|
||||||
@ -7,6 +9,7 @@ const meta: Meta<typeof FormAddressFieldInput> = {
|
|||||||
component: FormAddressFieldInput,
|
component: FormAddressFieldInput,
|
||||||
args: {},
|
args: {},
|
||||||
argTypes: {},
|
argTypes: {},
|
||||||
|
decorators: [WorkflowStepDecorator],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
@ -40,12 +43,12 @@ export const WithVariables: Story = {
|
|||||||
args: {
|
args: {
|
||||||
label: 'Address',
|
label: 'Address',
|
||||||
defaultValue: {
|
defaultValue: {
|
||||||
addressStreet1: '{{a.street1}}',
|
addressStreet1: `{{${MOCKED_STEP_ID}.address.street1}}`,
|
||||||
addressStreet2: '{{a.street2}}',
|
addressStreet2: `{{${MOCKED_STEP_ID}.address.street2}}`,
|
||||||
addressCity: '{{a.city}}',
|
addressCity: `{{${MOCKED_STEP_ID}.address.city}}`,
|
||||||
addressState: '{{a.state}}',
|
addressState: `{{${MOCKED_STEP_ID}.address.state}}`,
|
||||||
addressCountry: '{{a.country}}',
|
addressCountry: `{{${MOCKED_STEP_ID}.address.country}}`,
|
||||||
addressPostcode: '{{a.postcode}}',
|
addressPostcode: `{{${MOCKED_STEP_ID}.address.postcode}}`,
|
||||||
addressLat: 39.781721,
|
addressLat: 39.781721,
|
||||||
addressLng: -89.650148,
|
addressLng: -89.650148,
|
||||||
},
|
},
|
||||||
@ -58,14 +61,12 @@ export const WithVariables: Story = {
|
|||||||
const street2Variable = await canvas.findByText('street2');
|
const street2Variable = await canvas.findByText('street2');
|
||||||
const cityVariable = await canvas.findByText('city');
|
const cityVariable = await canvas.findByText('city');
|
||||||
const stateVariable = await canvas.findByText('state');
|
const stateVariable = await canvas.findByText('state');
|
||||||
const countryVariable = await canvas.findByText('country');
|
|
||||||
const postcodeVariable = await canvas.findByText('postcode');
|
const postcodeVariable = await canvas.findByText('postcode');
|
||||||
|
|
||||||
expect(street1Variable).toBeVisible();
|
expect(street1Variable).toBeVisible();
|
||||||
expect(street2Variable).toBeVisible();
|
expect(street2Variable).toBeVisible();
|
||||||
expect(cityVariable).toBeVisible();
|
expect(cityVariable).toBeVisible();
|
||||||
expect(stateVariable).toBeVisible();
|
expect(stateVariable).toBeVisible();
|
||||||
expect(countryVariable).toBeVisible();
|
|
||||||
expect(postcodeVariable).toBeVisible();
|
expect(postcodeVariable).toBeVisible();
|
||||||
|
|
||||||
const variablePickers = await canvas.findAllByText('VariablePicker');
|
const variablePickers = await canvas.findAllByText('VariablePicker');
|
||||||
|
|||||||
@ -2,6 +2,8 @@ import { CurrencyCode } from '@/object-record/record-field/types/CurrencyCode';
|
|||||||
import { FieldCurrencyValue } from '@/object-record/record-field/types/FieldMetadata';
|
import { FieldCurrencyValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
import { Meta, StoryObj } from '@storybook/react';
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
import { expect, within } from '@storybook/test';
|
import { expect, within } from '@storybook/test';
|
||||||
|
import { WorkflowStepDecorator } from '~/testing/decorators/WorkflowStepDecorator';
|
||||||
|
import { MOCKED_STEP_ID } from '~/testing/mock-data/workflow';
|
||||||
import { FormCurrencyFieldInput } from '../FormCurrencyFieldInput';
|
import { FormCurrencyFieldInput } from '../FormCurrencyFieldInput';
|
||||||
|
|
||||||
const meta: Meta<typeof FormCurrencyFieldInput> = {
|
const meta: Meta<typeof FormCurrencyFieldInput> = {
|
||||||
@ -9,6 +11,7 @@ const meta: Meta<typeof FormCurrencyFieldInput> = {
|
|||||||
component: FormCurrencyFieldInput,
|
component: FormCurrencyFieldInput,
|
||||||
args: {},
|
args: {},
|
||||||
argTypes: {},
|
argTypes: {},
|
||||||
|
decorators: [WorkflowStepDecorator],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
@ -36,18 +39,18 @@ export const WithVariable: Story = {
|
|||||||
args: {
|
args: {
|
||||||
label: 'Salary',
|
label: 'Salary',
|
||||||
defaultValue: {
|
defaultValue: {
|
||||||
currencyCode: CurrencyCode.USD,
|
currencyCode: `{{${MOCKED_STEP_ID}.amount.currencyCode}}` as CurrencyCode,
|
||||||
amountMicros: '{{a.b.c}}',
|
amountMicros: `{{${MOCKED_STEP_ID}.amount.amountMicros}}`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
play: async ({ canvasElement }) => {
|
play: async ({ canvasElement }) => {
|
||||||
const canvas = within(canvasElement);
|
const canvas = within(canvasElement);
|
||||||
|
|
||||||
const currency = await canvas.findByText(/USD/);
|
const amountMicros = await canvas.findByText('My Amount Micros');
|
||||||
expect(currency).toBeVisible();
|
const currencyCode = await canvas.findByText('My Currency Code');
|
||||||
|
|
||||||
const amountVariable = await canvas.findByText('c');
|
expect(amountMicros).toBeVisible();
|
||||||
expect(amountVariable).toBeVisible();
|
expect(currencyCode).toBeVisible();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -12,6 +12,8 @@ import {
|
|||||||
} from '@storybook/test';
|
} from '@storybook/test';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
|
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
|
||||||
|
import { WorkflowStepDecorator } from '~/testing/decorators/WorkflowStepDecorator';
|
||||||
|
import { MOCKED_STEP_ID } from '~/testing/mock-data/workflow';
|
||||||
import { FormDateFieldInput } from '../FormDateFieldInput';
|
import { FormDateFieldInput } from '../FormDateFieldInput';
|
||||||
|
|
||||||
const meta: Meta<typeof FormDateFieldInput> = {
|
const meta: Meta<typeof FormDateFieldInput> = {
|
||||||
@ -19,7 +21,7 @@ const meta: Meta<typeof FormDateFieldInput> = {
|
|||||||
component: FormDateFieldInput,
|
component: FormDateFieldInput,
|
||||||
args: {},
|
args: {},
|
||||||
argTypes: {},
|
argTypes: {},
|
||||||
decorators: [I18nFrontDecorator],
|
decorators: [I18nFrontDecorator, WorkflowStepDecorator],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
@ -308,7 +310,7 @@ export const SwitchesToStandaloneVariable: Story = {
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onVariableSelect('{{test}}');
|
onVariableSelect(`{{${MOCKED_STEP_ID}.createdAt}}`);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Add variable
|
Add variable
|
||||||
@ -322,7 +324,7 @@ export const SwitchesToStandaloneVariable: Story = {
|
|||||||
const addVariableButton = await canvas.findByText('Add variable');
|
const addVariableButton = await canvas.findByText('Add variable');
|
||||||
await userEvent.click(addVariableButton);
|
await userEvent.click(addVariableButton);
|
||||||
|
|
||||||
const variableTag = await canvas.findByText('test');
|
const variableTag = await canvas.findByText('Creation date');
|
||||||
expect(variableTag).toBeVisible();
|
expect(variableTag).toBeVisible();
|
||||||
|
|
||||||
const removeVariableButton = canvasElement.querySelector(
|
const removeVariableButton = canvasElement.querySelector(
|
||||||
@ -395,14 +397,14 @@ export const Disabled: Story = {
|
|||||||
export const DisabledWithVariable: Story = {
|
export const DisabledWithVariable: Story = {
|
||||||
args: {
|
args: {
|
||||||
label: 'Created At',
|
label: 'Created At',
|
||||||
defaultValue: `{{a.b.c}}`,
|
defaultValue: `{{${MOCKED_STEP_ID}.createdAt}}`,
|
||||||
onPersist: fn(),
|
onPersist: fn(),
|
||||||
readonly: true,
|
readonly: true,
|
||||||
VariablePicker: ({ onVariableSelect }) => {
|
VariablePicker: ({ onVariableSelect }) => {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onVariableSelect('{{test}}');
|
onVariableSelect(`{{${MOCKED_STEP_ID}.createdAt}}`);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Add variable
|
Add variable
|
||||||
@ -410,10 +412,11 @@ export const DisabledWithVariable: Story = {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
decorators: [WorkflowStepDecorator],
|
||||||
play: async ({ canvasElement }) => {
|
play: async ({ canvasElement }) => {
|
||||||
const canvas = within(canvasElement);
|
const canvas = within(canvasElement);
|
||||||
|
|
||||||
const variableChip = await canvas.findByText('c');
|
const variableChip = await canvas.findByText('Creation date');
|
||||||
expect(variableChip).toBeVisible();
|
expect(variableChip).toBeVisible();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -13,13 +13,15 @@ import {
|
|||||||
} from '@storybook/test';
|
} from '@storybook/test';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
|
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
|
||||||
|
import { WorkflowStepDecorator } from '~/testing/decorators/WorkflowStepDecorator';
|
||||||
|
import { MOCKED_STEP_ID } from '~/testing/mock-data/workflow';
|
||||||
|
|
||||||
const meta: Meta<typeof FormDateTimeFieldInput> = {
|
const meta: Meta<typeof FormDateTimeFieldInput> = {
|
||||||
title: 'UI/Data/Field/Form/Input/FormDateTimeFieldInput',
|
title: 'UI/Data/Field/Form/Input/FormDateTimeFieldInput',
|
||||||
component: FormDateTimeFieldInput,
|
component: FormDateTimeFieldInput,
|
||||||
args: {},
|
args: {},
|
||||||
argTypes: {},
|
argTypes: {},
|
||||||
decorators: [I18nFrontDecorator],
|
decorators: [I18nFrontDecorator, WorkflowStepDecorator],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
@ -337,7 +339,7 @@ export const SwitchesToStandaloneVariable: Story = {
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onVariableSelect('{{test}}');
|
onVariableSelect(`{{${MOCKED_STEP_ID}.createdAt}}`);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Add variable
|
Add variable
|
||||||
@ -351,7 +353,7 @@ export const SwitchesToStandaloneVariable: Story = {
|
|||||||
const addVariableButton = await canvas.findByText('Add variable');
|
const addVariableButton = await canvas.findByText('Add variable');
|
||||||
await userEvent.click(addVariableButton);
|
await userEvent.click(addVariableButton);
|
||||||
|
|
||||||
const variableTag = await canvas.findByText('test');
|
const variableTag = await canvas.findByText('Creation date');
|
||||||
expect(variableTag).toBeVisible();
|
expect(variableTag).toBeVisible();
|
||||||
|
|
||||||
const removeVariableButton = canvasElement.querySelector(
|
const removeVariableButton = canvasElement.querySelector(
|
||||||
@ -426,14 +428,14 @@ export const Disabled: Story = {
|
|||||||
export const DisabledWithVariable: Story = {
|
export const DisabledWithVariable: Story = {
|
||||||
args: {
|
args: {
|
||||||
label: 'Created At',
|
label: 'Created At',
|
||||||
defaultValue: `{{a.b.c}}`,
|
defaultValue: `{{${MOCKED_STEP_ID}.createdAt}}`,
|
||||||
onPersist: fn(),
|
onPersist: fn(),
|
||||||
readonly: true,
|
readonly: true,
|
||||||
VariablePicker: ({ onVariableSelect }) => {
|
VariablePicker: ({ onVariableSelect }) => {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onVariableSelect('{{test}}');
|
onVariableSelect(`{{${MOCKED_STEP_ID}.createdAt}}`);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Add variable
|
Add variable
|
||||||
@ -444,7 +446,7 @@ export const DisabledWithVariable: Story = {
|
|||||||
play: async ({ canvasElement }) => {
|
play: async ({ canvasElement }) => {
|
||||||
const canvas = within(canvasElement);
|
const canvas = within(canvasElement);
|
||||||
|
|
||||||
const variableChip = await canvas.findByText('c');
|
const variableChip = await canvas.findByText('Creation date');
|
||||||
expect(variableChip).toBeVisible();
|
expect(variableChip).toBeVisible();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import { Meta, StoryObj } from '@storybook/react';
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
import { expect, fn, userEvent, within } from '@storybook/test';
|
import { expect, fn, userEvent, within } from '@storybook/test';
|
||||||
|
import { WorkflowStepDecorator } from '~/testing/decorators/WorkflowStepDecorator';
|
||||||
|
import { MOCKED_STEP_ID } from '~/testing/mock-data/workflow';
|
||||||
import { FormEmailsFieldInput } from '../FormEmailsFieldInput';
|
import { FormEmailsFieldInput } from '../FormEmailsFieldInput';
|
||||||
|
|
||||||
const meta: Meta<typeof FormEmailsFieldInput> = {
|
const meta: Meta<typeof FormEmailsFieldInput> = {
|
||||||
@ -7,6 +9,7 @@ const meta: Meta<typeof FormEmailsFieldInput> = {
|
|||||||
component: FormEmailsFieldInput,
|
component: FormEmailsFieldInput,
|
||||||
args: {},
|
args: {},
|
||||||
argTypes: {},
|
argTypes: {},
|
||||||
|
decorators: [WorkflowStepDecorator],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
@ -34,7 +37,7 @@ export const WithVariable: Story = {
|
|||||||
args: {
|
args: {
|
||||||
label: 'Emails',
|
label: 'Emails',
|
||||||
defaultValue: {
|
defaultValue: {
|
||||||
primaryEmail: '{{a.b.c}}',
|
primaryEmail: `{{${MOCKED_STEP_ID}.name}}`,
|
||||||
additionalEmails: [],
|
additionalEmails: [],
|
||||||
},
|
},
|
||||||
VariablePicker: () => <div>VariablePicker</div>,
|
VariablePicker: () => <div>VariablePicker</div>,
|
||||||
@ -42,7 +45,7 @@ export const WithVariable: Story = {
|
|||||||
play: async ({ canvasElement }) => {
|
play: async ({ canvasElement }) => {
|
||||||
const canvas = within(canvasElement);
|
const canvas = within(canvasElement);
|
||||||
|
|
||||||
const primaryEmailVariable = await canvas.findByText('c');
|
const primaryEmailVariable = await canvas.findByText('Name');
|
||||||
expect(primaryEmailVariable).toBeVisible();
|
expect(primaryEmailVariable).toBeVisible();
|
||||||
|
|
||||||
const variablePicker = await canvas.findByText('VariablePicker');
|
const variablePicker = await canvas.findByText('VariablePicker');
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import { Meta, StoryObj } from '@storybook/react';
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
import { expect, fn, userEvent, within } from '@storybook/test';
|
import { expect, fn, userEvent, within } from '@storybook/test';
|
||||||
|
import { WorkflowStepDecorator } from '~/testing/decorators/WorkflowStepDecorator';
|
||||||
|
import { MOCKED_STEP_ID } from '~/testing/mock-data/workflow';
|
||||||
import { FormFullNameFieldInput } from '../FormFullNameFieldInput';
|
import { FormFullNameFieldInput } from '../FormFullNameFieldInput';
|
||||||
|
|
||||||
const meta: Meta<typeof FormFullNameFieldInput> = {
|
const meta: Meta<typeof FormFullNameFieldInput> = {
|
||||||
@ -7,6 +9,7 @@ const meta: Meta<typeof FormFullNameFieldInput> = {
|
|||||||
component: FormFullNameFieldInput,
|
component: FormFullNameFieldInput,
|
||||||
args: {},
|
args: {},
|
||||||
argTypes: {},
|
argTypes: {},
|
||||||
|
decorators: [WorkflowStepDecorator],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
@ -34,18 +37,18 @@ export const WithVariable: Story = {
|
|||||||
args: {
|
args: {
|
||||||
label: 'Name',
|
label: 'Name',
|
||||||
defaultValue: {
|
defaultValue: {
|
||||||
firstName: '{{a.firstName}}',
|
firstName: `{{${MOCKED_STEP_ID}.fullName.firstName}}`,
|
||||||
lastName: '{{a.lastName}}',
|
lastName: `{{${MOCKED_STEP_ID}.fullName.lastName}}`,
|
||||||
},
|
},
|
||||||
VariablePicker: () => <div>VariablePicker</div>,
|
VariablePicker: () => <div>VariablePicker</div>,
|
||||||
},
|
},
|
||||||
play: async ({ canvasElement }) => {
|
play: async ({ canvasElement }) => {
|
||||||
const canvas = within(canvasElement);
|
const canvas = within(canvasElement);
|
||||||
|
|
||||||
const firstNameVariable = await canvas.findByText('firstName');
|
const firstNameVariable = await canvas.findByText('Full Name First Name');
|
||||||
expect(firstNameVariable).toBeVisible();
|
expect(firstNameVariable).toBeVisible();
|
||||||
|
|
||||||
const lastNameVariable = await canvas.findByText('lastName');
|
const lastNameVariable = await canvas.findByText('Full Name Last Name');
|
||||||
expect(lastNameVariable).toBeVisible();
|
expect(lastNameVariable).toBeVisible();
|
||||||
|
|
||||||
const variablePickers = await canvas.findAllByText('VariablePicker');
|
const variablePickers = await canvas.findAllByText('VariablePicker');
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { expect } from '@storybook/jest';
|
import { expect } from '@storybook/jest';
|
||||||
import { Meta, StoryObj } from '@storybook/react';
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
import { fn, userEvent, within } from '@storybook/test';
|
import { fn, userEvent, within } from '@storybook/test';
|
||||||
|
import { WorkflowStepDecorator } from '~/testing/decorators/WorkflowStepDecorator';
|
||||||
import { FormLinksFieldInput } from '../FormLinksFieldInput';
|
import { FormLinksFieldInput } from '../FormLinksFieldInput';
|
||||||
|
|
||||||
const meta: Meta<typeof FormLinksFieldInput> = {
|
const meta: Meta<typeof FormLinksFieldInput> = {
|
||||||
@ -8,6 +9,7 @@ const meta: Meta<typeof FormLinksFieldInput> = {
|
|||||||
component: FormLinksFieldInput,
|
component: FormLinksFieldInput,
|
||||||
args: {},
|
args: {},
|
||||||
argTypes: {},
|
argTypes: {},
|
||||||
|
decorators: [WorkflowStepDecorator],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
@ -35,18 +37,18 @@ export const WithVariables: Story = {
|
|||||||
args: {
|
args: {
|
||||||
label: 'Domain Name',
|
label: 'Domain Name',
|
||||||
defaultValue: {
|
defaultValue: {
|
||||||
primaryLinkLabel: '{{a.label}}',
|
primaryLinkLabel: '{{04d5f3bf-9714-400d-ba27-644006a5fb1b.name}}',
|
||||||
primaryLinkUrl: '{{a.url}}',
|
primaryLinkUrl: '{{04d5f3bf-9714-400d-ba27-644006a5fb1b.stage}}',
|
||||||
},
|
},
|
||||||
VariablePicker: () => <div>VariablePicker</div>,
|
VariablePicker: () => <div>VariablePicker</div>,
|
||||||
},
|
},
|
||||||
play: async ({ canvasElement }) => {
|
play: async ({ canvasElement }) => {
|
||||||
const canvas = within(canvasElement);
|
const canvas = within(canvasElement);
|
||||||
|
|
||||||
const primaryLinkLabelVariable = await canvas.findByText('label');
|
const primaryLinkLabelVariable = await canvas.findByText('Name');
|
||||||
expect(primaryLinkLabelVariable).toBeVisible();
|
expect(primaryLinkLabelVariable).toBeVisible();
|
||||||
|
|
||||||
const primaryLinkUrlVariable = await canvas.findByText('url');
|
const primaryLinkUrlVariable = await canvas.findByText('Stage');
|
||||||
expect(primaryLinkUrlVariable).toBeVisible();
|
expect(primaryLinkUrlVariable).toBeVisible();
|
||||||
|
|
||||||
const variablePickers = await canvas.findAllByText('VariablePicker');
|
const variablePickers = await canvas.findAllByText('VariablePicker');
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import { expect } from '@storybook/jest';
|
import { expect } from '@storybook/jest';
|
||||||
import { Meta, StoryObj } from '@storybook/react';
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
import { fn, userEvent, within } from '@storybook/test';
|
import { fn, userEvent, within } from '@storybook/test';
|
||||||
|
import { WorkflowStepDecorator } from '~/testing/decorators/WorkflowStepDecorator';
|
||||||
|
import { MOCKED_STEP_ID } from '~/testing/mock-data/workflow';
|
||||||
import { FormMultiSelectFieldInput } from '../FormMultiSelectFieldInput';
|
import { FormMultiSelectFieldInput } from '../FormMultiSelectFieldInput';
|
||||||
|
|
||||||
const meta: Meta<typeof FormMultiSelectFieldInput> = {
|
const meta: Meta<typeof FormMultiSelectFieldInput> = {
|
||||||
@ -8,6 +10,7 @@ const meta: Meta<typeof FormMultiSelectFieldInput> = {
|
|||||||
component: FormMultiSelectFieldInput,
|
component: FormMultiSelectFieldInput,
|
||||||
args: {},
|
args: {},
|
||||||
argTypes: {},
|
argTypes: {},
|
||||||
|
decorators: [WorkflowStepDecorator],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
@ -117,14 +120,14 @@ export const Disabled: Story = {
|
|||||||
export const DisabledWithVariable: Story = {
|
export const DisabledWithVariable: Story = {
|
||||||
args: {
|
args: {
|
||||||
label: 'Created At',
|
label: 'Created At',
|
||||||
defaultValue: `{{a.b.c}}`,
|
defaultValue: `{{${MOCKED_STEP_ID}.stage}}`,
|
||||||
onPersist: fn(),
|
onPersist: fn(),
|
||||||
readonly: true,
|
readonly: true,
|
||||||
},
|
},
|
||||||
play: async ({ canvasElement }) => {
|
play: async ({ canvasElement }) => {
|
||||||
const canvas = within(canvasElement);
|
const canvas = within(canvasElement);
|
||||||
|
|
||||||
const variableChip = await canvas.findByText('c');
|
const variableChip = await canvas.findByText('Stage');
|
||||||
expect(variableChip).toBeVisible();
|
expect(variableChip).toBeVisible();
|
||||||
|
|
||||||
await userEvent.click(variableChip);
|
await userEvent.click(variableChip);
|
||||||
|
|||||||
@ -2,6 +2,8 @@ import { Meta, StoryObj } from '@storybook/react';
|
|||||||
import { expect, fn, userEvent, waitFor, within } from '@storybook/test';
|
import { expect, fn, userEvent, waitFor, within } from '@storybook/test';
|
||||||
|
|
||||||
import { FieldPhonesValue } from '@/object-record/record-field/types/FieldMetadata';
|
import { FieldPhonesValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
|
import { WorkflowStepDecorator } from '~/testing/decorators/WorkflowStepDecorator';
|
||||||
|
import { MOCKED_STEP_ID } from '~/testing/mock-data/workflow';
|
||||||
import { FormPhoneFieldInput } from '../FormPhoneFieldInput';
|
import { FormPhoneFieldInput } from '../FormPhoneFieldInput';
|
||||||
|
|
||||||
const meta: Meta<typeof FormPhoneFieldInput> = {
|
const meta: Meta<typeof FormPhoneFieldInput> = {
|
||||||
@ -9,6 +11,7 @@ const meta: Meta<typeof FormPhoneFieldInput> = {
|
|||||||
component: FormPhoneFieldInput,
|
component: FormPhoneFieldInput,
|
||||||
args: {},
|
args: {},
|
||||||
argTypes: {},
|
argTypes: {},
|
||||||
|
decorators: [WorkflowStepDecorator],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
@ -108,20 +111,17 @@ export const WithVariablesAsDefaultValues: Story = {
|
|||||||
args: {
|
args: {
|
||||||
label: 'Phone',
|
label: 'Phone',
|
||||||
defaultValue: {
|
defaultValue: {
|
||||||
primaryPhoneCountryCode: '{{a.countryCode}}',
|
primaryPhoneCountryCode: `{{${MOCKED_STEP_ID}.name}}`,
|
||||||
primaryPhoneNumber: '{{a.phoneNumber}}',
|
primaryPhoneNumber: `{{${MOCKED_STEP_ID}.amount.amountMicros}}`,
|
||||||
},
|
},
|
||||||
VariablePicker: () => <div>VariablePicker</div>,
|
VariablePicker: () => <div>VariablePicker</div>,
|
||||||
},
|
},
|
||||||
play: async ({ canvasElement }) => {
|
play: async ({ canvasElement }) => {
|
||||||
const canvas = within(canvasElement);
|
const canvas = within(canvasElement);
|
||||||
|
|
||||||
const countryCodeVariable = await canvas.findByText('countryCode');
|
const countryCodeVariable = await canvas.findByText('Name');
|
||||||
expect(countryCodeVariable).toBeVisible();
|
expect(countryCodeVariable).toBeVisible();
|
||||||
|
|
||||||
const phoneNumberVariable = await canvas.findByText('phoneNumber');
|
|
||||||
expect(phoneNumberVariable).toBeVisible();
|
|
||||||
|
|
||||||
const variablePickers = await canvas.findAllByText('VariablePicker');
|
const variablePickers = await canvas.findAllByText('VariablePicker');
|
||||||
|
|
||||||
expect(variablePickers).toHaveLength(1);
|
expect(variablePickers).toHaveLength(1);
|
||||||
@ -139,7 +139,7 @@ export const SelectingVariables: Story = {
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onVariableSelect('{{test}}');
|
onVariableSelect(`{{${MOCKED_STEP_ID}.phoneNumber}}`);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Add variable
|
Add variable
|
||||||
@ -162,12 +162,12 @@ export const SelectingVariables: Story = {
|
|||||||
|
|
||||||
await userEvent.click(phoneNumberVariablePicker);
|
await userEvent.click(phoneNumberVariablePicker);
|
||||||
|
|
||||||
const phoneNumberVariable = await canvas.findByText('test');
|
const phoneNumberVariable = await canvas.findByText('phoneNumber');
|
||||||
expect(phoneNumberVariable).toBeVisible();
|
expect(phoneNumberVariable).toBeVisible();
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(args.onPersist).toHaveBeenCalledWith({
|
expect(args.onPersist).toHaveBeenCalledWith({
|
||||||
primaryPhoneNumber: '{{test}}',
|
primaryPhoneNumber: `{{${MOCKED_STEP_ID}.phoneNumber}}`,
|
||||||
primaryPhoneCountryCode: '',
|
primaryPhoneCountryCode: '',
|
||||||
primaryPhoneCallingCode: '',
|
primaryPhoneCallingCode: '',
|
||||||
});
|
});
|
||||||
|
|||||||
@ -2,6 +2,8 @@ import { expect } from '@storybook/jest';
|
|||||||
import { Meta, StoryObj } from '@storybook/react';
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
import { fn, userEvent, waitFor, within } from '@storybook/test';
|
import { fn, userEvent, waitFor, within } from '@storybook/test';
|
||||||
import { getUserDevice } from 'twenty-ui';
|
import { getUserDevice } from 'twenty-ui';
|
||||||
|
import { WorkflowStepDecorator } from '~/testing/decorators/WorkflowStepDecorator';
|
||||||
|
import { MOCKED_STEP_ID } from '~/testing/mock-data/workflow';
|
||||||
import { FormRawJsonFieldInput } from '../FormRawJsonFieldInput';
|
import { FormRawJsonFieldInput } from '../FormRawJsonFieldInput';
|
||||||
|
|
||||||
const meta: Meta<typeof FormRawJsonFieldInput> = {
|
const meta: Meta<typeof FormRawJsonFieldInput> = {
|
||||||
@ -9,6 +11,7 @@ const meta: Meta<typeof FormRawJsonFieldInput> = {
|
|||||||
component: FormRawJsonFieldInput,
|
component: FormRawJsonFieldInput,
|
||||||
args: {},
|
args: {},
|
||||||
argTypes: {},
|
argTypes: {},
|
||||||
|
decorators: [WorkflowStepDecorator],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
@ -37,7 +40,7 @@ export const Readonly: Story = {
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onVariableSelect('{{test}}');
|
onVariableSelect(`{{${MOCKED_STEP_ID}.name}}`);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Add variable
|
Add variable
|
||||||
@ -142,7 +145,7 @@ export const DoesNotIgnoreInvalidJson: Story = {
|
|||||||
export const DisplayDefaultValueWithVariablesProperly: Story = {
|
export const DisplayDefaultValueWithVariablesProperly: Story = {
|
||||||
args: {
|
args: {
|
||||||
placeholder: 'Enter valid json',
|
placeholder: 'Enter valid json',
|
||||||
defaultValue: '{ "a": { "b" : {{var.test}} } }',
|
defaultValue: `{ "a": { "b" : {{${MOCKED_STEP_ID}.name}} } }`,
|
||||||
onPersist: fn(),
|
onPersist: fn(),
|
||||||
},
|
},
|
||||||
play: async ({ canvasElement }) => {
|
play: async ({ canvasElement }) => {
|
||||||
@ -150,7 +153,7 @@ export const DisplayDefaultValueWithVariablesProperly: Story = {
|
|||||||
|
|
||||||
await canvas.findByText(/{ "a": { "b" : /);
|
await canvas.findByText(/{ "a": { "b" : /);
|
||||||
|
|
||||||
const variableTag = await canvas.findByText('test');
|
const variableTag = await canvas.findByText('Name');
|
||||||
await expect(variableTag).toBeVisible();
|
await expect(variableTag).toBeVisible();
|
||||||
|
|
||||||
await canvas.findByText(/ } }/);
|
await canvas.findByText(/ } }/);
|
||||||
@ -304,7 +307,7 @@ export const HasHistory: Story = {
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onVariableSelect('{{test}}');
|
onVariableSelect(`{{${MOCKED_STEP_ID}.name}}`);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Add variable
|
Add variable
|
||||||
@ -331,7 +334,9 @@ export const HasHistory: Story = {
|
|||||||
|
|
||||||
await userEvent.type(editor, ' }');
|
await userEvent.type(editor, ' }');
|
||||||
|
|
||||||
expect(args.onPersist).toHaveBeenLastCalledWith('{ "a": {{test}} }');
|
expect(args.onPersist).toHaveBeenLastCalledWith(
|
||||||
|
`{ "a": {{${MOCKED_STEP_ID}.name}} }`,
|
||||||
|
);
|
||||||
|
|
||||||
await userEvent.type(editor, `{${controlKey}>}z{/${controlKey}}`);
|
await userEvent.type(editor, `{${controlKey}>}z{/${controlKey}}`);
|
||||||
|
|
||||||
@ -343,7 +348,9 @@ export const HasHistory: Story = {
|
|||||||
`{Shift>}{${controlKey}>}z{/${controlKey}}{/Shift}`,
|
`{Shift>}{${controlKey}>}z{/${controlKey}}{/Shift}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(editor).toHaveTextContent('{ "a": test }');
|
expect(editor).toHaveTextContent('{ "a": Name }');
|
||||||
expect(args.onPersist).toHaveBeenLastCalledWith('{ "a": {{test}} }');
|
expect(args.onPersist).toHaveBeenLastCalledWith(
|
||||||
|
`{ "a": {{${MOCKED_STEP_ID}.name}} }`,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import { expect } from '@storybook/jest';
|
import { expect } from '@storybook/jest';
|
||||||
import { Meta, StoryObj } from '@storybook/react';
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
import { fn, userEvent, within } from '@storybook/test';
|
import { fn, userEvent, within } from '@storybook/test';
|
||||||
|
import { WorkflowStepDecorator } from '~/testing/decorators/WorkflowStepDecorator';
|
||||||
|
import { MOCKED_STEP_ID } from '~/testing/mock-data/workflow';
|
||||||
import { FormSelectFieldInput } from '../FormSelectFieldInput';
|
import { FormSelectFieldInput } from '../FormSelectFieldInput';
|
||||||
|
|
||||||
const meta: Meta<typeof FormSelectFieldInput> = {
|
const meta: Meta<typeof FormSelectFieldInput> = {
|
||||||
@ -8,6 +10,7 @@ const meta: Meta<typeof FormSelectFieldInput> = {
|
|||||||
component: FormSelectFieldInput,
|
component: FormSelectFieldInput,
|
||||||
args: {},
|
args: {},
|
||||||
argTypes: {},
|
argTypes: {},
|
||||||
|
decorators: [WorkflowStepDecorator],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
@ -117,7 +120,7 @@ export const Disabled: Story = {
|
|||||||
export const DisabledWithVariable: Story = {
|
export const DisabledWithVariable: Story = {
|
||||||
args: {
|
args: {
|
||||||
label: 'Created At',
|
label: 'Created At',
|
||||||
defaultValue: `{{a.b.c}}`,
|
defaultValue: `{{${MOCKED_STEP_ID}.createdAt}}`,
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
label: 'Work Policy 1',
|
label: 'Work Policy 1',
|
||||||
@ -146,7 +149,7 @@ export const DisabledWithVariable: Story = {
|
|||||||
play: async ({ canvasElement }) => {
|
play: async ({ canvasElement }) => {
|
||||||
const canvas = within(canvasElement);
|
const canvas = within(canvasElement);
|
||||||
|
|
||||||
const variableChip = await canvas.findByText('c');
|
const variableChip = await canvas.findByText('Creation date');
|
||||||
expect(variableChip).toBeVisible();
|
expect(variableChip).toBeVisible();
|
||||||
|
|
||||||
await userEvent.click(variableChip);
|
await userEvent.click(variableChip);
|
||||||
|
|||||||
@ -8,6 +8,8 @@ import {
|
|||||||
within,
|
within,
|
||||||
} from '@storybook/test';
|
} from '@storybook/test';
|
||||||
import { getUserDevice } from 'twenty-ui';
|
import { getUserDevice } from 'twenty-ui';
|
||||||
|
import { WorkflowStepDecorator } from '~/testing/decorators/WorkflowStepDecorator';
|
||||||
|
import { MOCKED_STEP_ID } from '~/testing/mock-data/workflow';
|
||||||
import { FormTextFieldInput } from '../FormTextFieldInput';
|
import { FormTextFieldInput } from '../FormTextFieldInput';
|
||||||
|
|
||||||
const meta: Meta<typeof FormTextFieldInput> = {
|
const meta: Meta<typeof FormTextFieldInput> = {
|
||||||
@ -15,6 +17,7 @@ const meta: Meta<typeof FormTextFieldInput> = {
|
|||||||
component: FormTextFieldInput,
|
component: FormTextFieldInput,
|
||||||
args: {},
|
args: {},
|
||||||
argTypes: {},
|
argTypes: {},
|
||||||
|
decorators: [WorkflowStepDecorator],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
@ -78,7 +81,7 @@ export const WithVariable: Story = {
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onVariableSelect('{{test}}');
|
onVariableSelect(`{{${MOCKED_STEP_ID}.name}}`);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Add variable
|
Add variable
|
||||||
@ -96,11 +99,11 @@ export const WithVariable: Story = {
|
|||||||
|
|
||||||
await userEvent.click(addVariableButton);
|
await userEvent.click(addVariableButton);
|
||||||
|
|
||||||
const variable = await canvas.findByText('test');
|
const variable = await canvas.findByText('Name');
|
||||||
expect(variable).toBeVisible();
|
expect(variable).toBeVisible();
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(args.onPersist).toHaveBeenCalledWith('{{test}}');
|
expect(args.onPersist).toHaveBeenCalledWith(`{{${MOCKED_STEP_ID}.name}}`);
|
||||||
});
|
});
|
||||||
expect(args.onPersist).toHaveBeenCalledTimes(1);
|
expect(args.onPersist).toHaveBeenCalledTimes(1);
|
||||||
},
|
},
|
||||||
@ -110,7 +113,7 @@ export const WithDeletableVariable: Story = {
|
|||||||
args: {
|
args: {
|
||||||
label: 'Text',
|
label: 'Text',
|
||||||
placeholder: 'Text field...',
|
placeholder: 'Text field...',
|
||||||
defaultValue: 'test {{a.b.variable}} test',
|
defaultValue: `test {{${MOCKED_STEP_ID}.name}} test`,
|
||||||
onPersist: fn(),
|
onPersist: fn(),
|
||||||
},
|
},
|
||||||
play: async ({ canvasElement, args }) => {
|
play: async ({ canvasElement, args }) => {
|
||||||
@ -119,7 +122,7 @@ export const WithDeletableVariable: Story = {
|
|||||||
const editor = canvasElement.querySelector('.ProseMirror > p');
|
const editor = canvasElement.querySelector('.ProseMirror > p');
|
||||||
expect(editor).toBeVisible();
|
expect(editor).toBeVisible();
|
||||||
|
|
||||||
const variable = await canvas.findByText('variable');
|
const variable = await canvas.findByText('Name');
|
||||||
expect(variable).toBeVisible();
|
expect(variable).toBeVisible();
|
||||||
|
|
||||||
const deleteVariableButton = await canvas.findByRole('button', {
|
const deleteVariableButton = await canvas.findByRole('button', {
|
||||||
@ -173,7 +176,7 @@ export const Disabled: Story = {
|
|||||||
export const DisabledWithVariable: Story = {
|
export const DisabledWithVariable: Story = {
|
||||||
args: {
|
args: {
|
||||||
label: 'Text',
|
label: 'Text',
|
||||||
defaultValue: 'test {{a.b.variable}} test',
|
defaultValue: `test {{${MOCKED_STEP_ID}.name}} test`,
|
||||||
readonly: true,
|
readonly: true,
|
||||||
},
|
},
|
||||||
play: async ({ canvasElement }) => {
|
play: async ({ canvasElement }) => {
|
||||||
@ -182,7 +185,7 @@ export const DisabledWithVariable: Story = {
|
|||||||
expect(editor).toBeVisible();
|
expect(editor).toBeVisible();
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(editor).toHaveTextContent('test variable test');
|
expect(editor).toHaveTextContent('test Name test');
|
||||||
});
|
});
|
||||||
|
|
||||||
const deleteVariableButton = within(editor as HTMLElement).queryByRole(
|
const deleteVariableButton = within(editor as HTMLElement).queryByRole(
|
||||||
@ -200,7 +203,7 @@ export const HasHistory: Story = {
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onVariableSelect('{{test}}');
|
onVariableSelect(`{{${MOCKED_STEP_ID}.name}}`);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Add variable
|
Add variable
|
||||||
@ -225,7 +228,9 @@ export const HasHistory: Story = {
|
|||||||
|
|
||||||
await userEvent.click(addVariableButton);
|
await userEvent.click(addVariableButton);
|
||||||
|
|
||||||
expect(args.onPersist).toHaveBeenLastCalledWith('Hello World {{test}}');
|
expect(args.onPersist).toHaveBeenLastCalledWith(
|
||||||
|
`Hello World {{${MOCKED_STEP_ID}.name}}`,
|
||||||
|
);
|
||||||
|
|
||||||
await userEvent.type(editor, `{${controlKey}>}z{/${controlKey}}`);
|
await userEvent.type(editor, `{${controlKey}>}z{/${controlKey}}`);
|
||||||
|
|
||||||
@ -237,7 +242,9 @@ export const HasHistory: Story = {
|
|||||||
`{Shift>}{${controlKey}>}z{/${controlKey}}{/Shift}`,
|
`{Shift>}{${controlKey}>}z{/${controlKey}}{/Shift}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(editor).toHaveTextContent('Hello World test');
|
expect(editor).toHaveTextContent(`Hello World Name`);
|
||||||
expect(args.onPersist).toHaveBeenLastCalledWith('Hello World {{test}}');
|
expect(args.onPersist).toHaveBeenLastCalledWith(
|
||||||
|
`Hello World {{${MOCKED_STEP_ID}.name}}`,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,12 +1,8 @@
|
|||||||
|
import { WorkflowVersionComponentInstanceContext } from '@/workflow/states/context/WorkflowVersionComponentInstanceContext';
|
||||||
import { expect } from '@storybook/jest';
|
import { expect } from '@storybook/jest';
|
||||||
import { Meta, StoryObj } from '@storybook/react';
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
import {
|
import { fn, userEvent, waitFor, within } from '@storybook/test';
|
||||||
fn,
|
import { MOCKED_STEP_ID } from '~/testing/mock-data/workflow';
|
||||||
userEvent,
|
|
||||||
waitFor,
|
|
||||||
waitForElementToBeRemoved,
|
|
||||||
within,
|
|
||||||
} from '@storybook/test';
|
|
||||||
import { FormUuidFieldInput } from '../FormUuidFieldInput';
|
import { FormUuidFieldInput } from '../FormUuidFieldInput';
|
||||||
|
|
||||||
const meta: Meta<typeof FormUuidFieldInput> = {
|
const meta: Meta<typeof FormUuidFieldInput> = {
|
||||||
@ -14,8 +10,16 @@ const meta: Meta<typeof FormUuidFieldInput> = {
|
|||||||
component: FormUuidFieldInput,
|
component: FormUuidFieldInput,
|
||||||
args: {},
|
args: {},
|
||||||
argTypes: {},
|
argTypes: {},
|
||||||
|
decorators: [
|
||||||
|
(Story) => (
|
||||||
|
<WorkflowVersionComponentInstanceContext.Provider
|
||||||
|
value={{ instanceId: 'workflow-version-id' }}
|
||||||
|
>
|
||||||
|
<Story />
|
||||||
|
</WorkflowVersionComponentInstanceContext.Provider>
|
||||||
|
),
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
|
|
||||||
type Story = StoryObj<typeof FormUuidFieldInput>;
|
type Story = StoryObj<typeof FormUuidFieldInput>;
|
||||||
@ -150,69 +154,6 @@ export const ClearField: Story = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ReplaceStaticValueWithVariable: Story = {
|
|
||||||
args: {
|
|
||||||
label: 'UUID field',
|
|
||||||
placeholder: 'Enter UUID',
|
|
||||||
onPersist: fn(),
|
|
||||||
VariablePicker: ({ onVariableSelect }) => {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
onVariableSelect('{{test}}');
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Add variable
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
play: async ({ canvasElement, args }) => {
|
|
||||||
const canvas = within(canvasElement);
|
|
||||||
|
|
||||||
const input = await canvas.findByPlaceholderText('Enter UUID');
|
|
||||||
|
|
||||||
expect(input).toBeVisible();
|
|
||||||
expect(input).toHaveDisplayValue('');
|
|
||||||
|
|
||||||
const addVariableButton = await canvas.findByRole('button', {
|
|
||||||
name: 'Add variable',
|
|
||||||
});
|
|
||||||
|
|
||||||
const [, , variableTag] = await Promise.all([
|
|
||||||
userEvent.click(addVariableButton),
|
|
||||||
|
|
||||||
waitForElementToBeRemoved(input),
|
|
||||||
waitFor(() => {
|
|
||||||
const variableTag = canvas.getByText('test');
|
|
||||||
expect(variableTag).toBeVisible();
|
|
||||||
|
|
||||||
return variableTag;
|
|
||||||
}),
|
|
||||||
waitFor(() => {
|
|
||||||
expect(args.onPersist).toHaveBeenCalledWith('{{test}}');
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const removeVariableButton = canvasElement.querySelector(
|
|
||||||
'button .tabler-icon-x',
|
|
||||||
);
|
|
||||||
|
|
||||||
await Promise.all([
|
|
||||||
userEvent.click(removeVariableButton),
|
|
||||||
|
|
||||||
waitForElementToBeRemoved(variableTag),
|
|
||||||
waitFor(() => {
|
|
||||||
const input = canvas.getByPlaceholderText('Enter UUID');
|
|
||||||
expect(input).toBeVisible();
|
|
||||||
}),
|
|
||||||
waitFor(() => {
|
|
||||||
expect(args.onPersist).toHaveBeenCalledWith(null);
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Disabled: Story = {
|
export const Disabled: Story = {
|
||||||
args: {
|
args: {
|
||||||
label: 'UUID field',
|
label: 'UUID field',
|
||||||
@ -222,7 +163,7 @@ export const Disabled: Story = {
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onVariableSelect('{{test}}');
|
onVariableSelect(`{{${MOCKED_STEP_ID}.name}}`);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Add variable
|
Add variable
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import { useSetRecoilState } from 'recoil';
|
|||||||
import { ComponentDecorator, RouterDecorator } from 'twenty-ui';
|
import { ComponentDecorator, RouterDecorator } from 'twenty-ui';
|
||||||
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
|
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
|
||||||
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
|
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
|
||||||
|
import { WorkflowStepDecorator } from '~/testing/decorators/WorkflowStepDecorator';
|
||||||
import { WorkspaceDecorator } from '~/testing/decorators/WorkspaceDecorator';
|
import { WorkspaceDecorator } from '~/testing/decorators/WorkspaceDecorator';
|
||||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||||
import { oneFailedWorkflowRunQueryResult } from '~/testing/mock-data/workflow-run';
|
import { oneFailedWorkflowRunQueryResult } from '~/testing/mock-data/workflow-run';
|
||||||
@ -50,6 +51,7 @@ const meta: Meta<typeof RightDrawerWorkflowRunViewStep> = {
|
|||||||
RouterDecorator,
|
RouterDecorator,
|
||||||
ObjectMetadataItemsDecorator,
|
ObjectMetadataItemsDecorator,
|
||||||
WorkspaceDecorator,
|
WorkspaceDecorator,
|
||||||
|
WorkflowStepDecorator,
|
||||||
],
|
],
|
||||||
parameters: {
|
parameters: {
|
||||||
msw: {
|
msw: {
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
|
||||||
import { RecordChip } from '@/object-record/components/RecordChip';
|
import { RecordChip } from '@/object-record/components/RecordChip';
|
||||||
import { VariableChipStandalone } from '@/object-record/record-field/form-types/components/VariableChipStandalone';
|
import { VariableChipStandalone } from '@/object-record/record-field/form-types/components/VariableChipStandalone';
|
||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
@ -42,8 +41,6 @@ export const WorkflowSingleRecordFieldChip = ({
|
|||||||
onRemove,
|
onRemove,
|
||||||
disabled,
|
disabled,
|
||||||
}: WorkflowSingleRecordFieldChipProps) => {
|
}: WorkflowSingleRecordFieldChipProps) => {
|
||||||
const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular });
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!!draftValue &&
|
!!draftValue &&
|
||||||
draftValue.type === 'variable' &&
|
draftValue.type === 'variable' &&
|
||||||
@ -51,8 +48,9 @@ export const WorkflowSingleRecordFieldChip = ({
|
|||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<VariableChipStandalone
|
<VariableChipStandalone
|
||||||
rawVariableName={objectMetadataItem.labelSingular}
|
rawVariableName={draftValue.value}
|
||||||
onRemove={disabled ? undefined : onRemove}
|
onRemove={disabled ? undefined : onRemove}
|
||||||
|
isFullRecord
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -193,11 +193,5 @@ export const DisabledWithDefaultVariableValues: Story = {
|
|||||||
).queryByRole('button');
|
).queryByRole('button');
|
||||||
|
|
||||||
expect(openRecordSelectButton).not.toBeInTheDocument();
|
expect(openRecordSelectButton).not.toBeInTheDocument();
|
||||||
|
|
||||||
const recordVariableToDelete = await within(
|
|
||||||
canvas.getByTestId('workflow-edit-action-record-delete-object-record-id'),
|
|
||||||
).findByText('Person');
|
|
||||||
|
|
||||||
expect(recordVariableToDelete).toBeVisible();
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
export const CAPTURE_ALL_VARIABLE_TAG_INNER_REGEX = /{{([^{}]+)}}/g;
|
||||||
@ -33,9 +33,9 @@ export const useAvailableVariablesInWorkflowStep = ({
|
|||||||
const availableStepsOutputSchema: StepOutputSchema[] =
|
const availableStepsOutputSchema: StepOutputSchema[] =
|
||||||
getStepsOutputSchema(previousStepIds).filter(isDefined);
|
getStepsOutputSchema(previousStepIds).filter(isDefined);
|
||||||
|
|
||||||
const triggersOutputSchema: StepOutputSchema[] = getStepsOutputSchema([
|
const triggersOutputSchema: StepOutputSchema[] = isDefined(flow.trigger)
|
||||||
TRIGGER_STEP_ID,
|
? getStepsOutputSchema([TRIGGER_STEP_ID]).filter(isDefined)
|
||||||
]).filter(isDefined);
|
: [];
|
||||||
|
|
||||||
const availableVariablesInWorkflowStep = [
|
const availableVariablesInWorkflowStep = [
|
||||||
...availableStepsOutputSchema,
|
...availableStepsOutputSchema,
|
||||||
|
|||||||
@ -1,13 +1,36 @@
|
|||||||
import { extractVariableLabel } from '../extractVariableLabel';
|
import { extractRawVariableNamePart } from '@/workflow/workflow-variables/utils/extractRawVariableNamePart';
|
||||||
|
|
||||||
it('returns the last part of a properly formatted variable', () => {
|
describe('extractRawVariableNamePart', () => {
|
||||||
const rawVariable = '{{a.b.c}}';
|
it('returns the last part of a properly formatted variable', () => {
|
||||||
|
const rawVariable = '{{a.b.c}}';
|
||||||
|
|
||||||
expect(extractVariableLabel(rawVariable)).toBe('c');
|
expect(
|
||||||
});
|
extractRawVariableNamePart({
|
||||||
|
rawVariableName: rawVariable,
|
||||||
it('stops on unclosed variables', () => {
|
part: 'selectedField',
|
||||||
const rawVariable = '{{ test {{a.b.c}}';
|
}),
|
||||||
|
).toBe('c');
|
||||||
expect(extractVariableLabel(rawVariable)).toBe('c');
|
});
|
||||||
|
|
||||||
|
it('returns the first part of a properly formatted variable', () => {
|
||||||
|
const rawVariable = '{{a.b.c}}';
|
||||||
|
|
||||||
|
expect(
|
||||||
|
extractRawVariableNamePart({
|
||||||
|
rawVariableName: rawVariable,
|
||||||
|
part: 'stepId',
|
||||||
|
}),
|
||||||
|
).toBe('a');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('stops on unclosed variables', () => {
|
||||||
|
const rawVariable = '{{ test {{a.b.c}}';
|
||||||
|
|
||||||
|
expect(
|
||||||
|
extractRawVariableNamePart({
|
||||||
|
rawVariableName: rawVariable,
|
||||||
|
part: 'selectedField',
|
||||||
|
}),
|
||||||
|
).toBe('c');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -0,0 +1,195 @@
|
|||||||
|
import { StepOutputSchema } from '@/workflow/workflow-variables/types/StepOutputSchema';
|
||||||
|
import { searchVariableThroughOutputSchema } from '@/workflow/workflow-variables/utils/searchVariableThroughOutputSchema';
|
||||||
|
|
||||||
|
const mockStep = {
|
||||||
|
id: 'step-1',
|
||||||
|
name: 'Step 1',
|
||||||
|
outputSchema: {
|
||||||
|
company: {
|
||||||
|
isLeaf: false,
|
||||||
|
icon: 'company',
|
||||||
|
label: 'Company',
|
||||||
|
value: {
|
||||||
|
object: {
|
||||||
|
nameSingular: 'company',
|
||||||
|
fieldIdName: 'id',
|
||||||
|
label: 'Company',
|
||||||
|
value: 'John',
|
||||||
|
isLeaf: true,
|
||||||
|
},
|
||||||
|
fields: {
|
||||||
|
name: { label: 'Name', value: 'Twenty', isLeaf: true },
|
||||||
|
address: { label: 'Address', value: '123 Main St', isLeaf: true },
|
||||||
|
},
|
||||||
|
_outputSchemaType: 'RECORD',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
person: {
|
||||||
|
isLeaf: false,
|
||||||
|
icon: 'person',
|
||||||
|
label: 'Person',
|
||||||
|
value: {
|
||||||
|
object: {
|
||||||
|
nameSingular: 'person',
|
||||||
|
fieldIdName: 'id',
|
||||||
|
label: 'Person',
|
||||||
|
value: 'Jane',
|
||||||
|
isLeaf: true,
|
||||||
|
},
|
||||||
|
fields: {
|
||||||
|
firstName: { label: 'First Name', value: 'Jane', isLeaf: true },
|
||||||
|
lastName: { label: 'Last Name', value: 'Doe', isLeaf: true },
|
||||||
|
email: { label: 'Email', value: 'jane@example.com', isLeaf: true },
|
||||||
|
},
|
||||||
|
_outputSchemaType: 'RECORD',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
simpleData: {
|
||||||
|
isLeaf: true,
|
||||||
|
label: 'Simple Data',
|
||||||
|
value: 'Simple value',
|
||||||
|
},
|
||||||
|
nestedData: {
|
||||||
|
isLeaf: false,
|
||||||
|
label: 'Nested Data',
|
||||||
|
value: {
|
||||||
|
field1: { label: 'Field 1', value: 'Value 1', isLeaf: true },
|
||||||
|
field2: { label: 'Field 2', value: 'Value 2', isLeaf: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} satisfies StepOutputSchema;
|
||||||
|
|
||||||
|
describe('searchVariableThroughOutputSchema', () => {
|
||||||
|
it('should find a company field variable', () => {
|
||||||
|
const result = searchVariableThroughOutputSchema({
|
||||||
|
stepOutputSchema: mockStep,
|
||||||
|
rawVariableName: '{{step-1.company.name}}',
|
||||||
|
isFullRecord: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
variableLabel: 'Name',
|
||||||
|
variablePathLabel: 'Step 1 > Company > Name',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should find a person field variable', () => {
|
||||||
|
const result = searchVariableThroughOutputSchema({
|
||||||
|
stepOutputSchema: mockStep,
|
||||||
|
rawVariableName: '{{step-1.person.email}}',
|
||||||
|
isFullRecord: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
variableLabel: 'Email',
|
||||||
|
variablePathLabel: 'Step 1 > Person > Email',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should find a company object variable', () => {
|
||||||
|
const result = searchVariableThroughOutputSchema({
|
||||||
|
stepOutputSchema: mockStep,
|
||||||
|
rawVariableName: '{{step-1.company.id}}',
|
||||||
|
isFullRecord: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
variableLabel: 'Company',
|
||||||
|
variablePathLabel: 'Step 1 > Company > Company',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should find a person object variable', () => {
|
||||||
|
const result = searchVariableThroughOutputSchema({
|
||||||
|
stepOutputSchema: mockStep,
|
||||||
|
rawVariableName: '{{step-1.person.id}}',
|
||||||
|
isFullRecord: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
variableLabel: 'Person',
|
||||||
|
variablePathLabel: 'Step 1 > Person > Person',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle simple data fields', () => {
|
||||||
|
const result = searchVariableThroughOutputSchema({
|
||||||
|
stepOutputSchema: mockStep,
|
||||||
|
rawVariableName: '{{step-1.simpleData}}',
|
||||||
|
isFullRecord: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
variableLabel: 'Simple Data',
|
||||||
|
variablePathLabel: 'Step 1 > Simple Data',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle nested data fields', () => {
|
||||||
|
const result = searchVariableThroughOutputSchema({
|
||||||
|
stepOutputSchema: mockStep,
|
||||||
|
rawVariableName: '{{step-1.nestedData.field1}}',
|
||||||
|
isFullRecord: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
variableLabel: 'Field 1',
|
||||||
|
variablePathLabel: 'Step 1 > Nested Data > Field 1',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle invalid variable names', () => {
|
||||||
|
const result = searchVariableThroughOutputSchema({
|
||||||
|
stepOutputSchema: mockStep,
|
||||||
|
rawVariableName: '{{invalid}}',
|
||||||
|
isFullRecord: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
variableLabel: undefined,
|
||||||
|
variablePathLabel: 'Step 1 > undefined',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle non-existent paths', () => {
|
||||||
|
const result = searchVariableThroughOutputSchema({
|
||||||
|
stepOutputSchema: mockStep,
|
||||||
|
rawVariableName: '{{step-1.nonExistent.field}}',
|
||||||
|
isFullRecord: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
variableLabel: undefined,
|
||||||
|
variablePathLabel: 'Step 1 > undefined',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle the case where the path has dots in field names', () => {
|
||||||
|
const mockStepWithDotInField = {
|
||||||
|
id: 'step-1',
|
||||||
|
name: 'Step 1',
|
||||||
|
outputSchema: {
|
||||||
|
'complex.field': {
|
||||||
|
isLeaf: false,
|
||||||
|
label: 'Complex Field',
|
||||||
|
value: {
|
||||||
|
field1: { label: 'Field 1', value: 'Value 1', isLeaf: true },
|
||||||
|
field2: { label: 'Field 2', value: 'Value 2', isLeaf: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} satisfies StepOutputSchema;
|
||||||
|
|
||||||
|
const result = searchVariableThroughOutputSchema({
|
||||||
|
stepOutputSchema: mockStepWithDotInField,
|
||||||
|
rawVariableName: '{{step-1.complex.field.field1}}',
|
||||||
|
isFullRecord: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
variableLabel: 'Field 1',
|
||||||
|
variablePathLabel: 'Step 1 > Complex Field > Field 1',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
import { CAPTURE_ALL_VARIABLE_TAG_INNER_REGEX } from '@/workflow/workflow-variables/constants/CaptureAllVariableTagInnerRegex';
|
||||||
|
import { isDefined } from 'twenty-shared';
|
||||||
|
|
||||||
|
export const extractRawVariableNamePart = ({
|
||||||
|
rawVariableName,
|
||||||
|
part,
|
||||||
|
}: {
|
||||||
|
rawVariableName: string;
|
||||||
|
part: 'stepId' | 'selectedField';
|
||||||
|
}) => {
|
||||||
|
const variableWithoutBrackets = rawVariableName.replace(
|
||||||
|
CAPTURE_ALL_VARIABLE_TAG_INNER_REGEX,
|
||||||
|
(_, variableName) => {
|
||||||
|
return variableName;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const parts = variableWithoutBrackets.split('.');
|
||||||
|
|
||||||
|
const extractedPart =
|
||||||
|
part === 'stepId'
|
||||||
|
? parts.at(0)
|
||||||
|
: part === 'selectedField'
|
||||||
|
? parts.at(-1)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (!isDefined(extractedPart)) {
|
||||||
|
throw new Error('Expected to find at least one splitted chunk.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return extractedPart;
|
||||||
|
};
|
||||||
@ -1,21 +0,0 @@
|
|||||||
import { isDefined } from 'twenty-shared';
|
|
||||||
|
|
||||||
const CAPTURE_ALL_VARIABLE_TAG_INNER_REGEX = /{{([^{}]+)}}/g;
|
|
||||||
|
|
||||||
export const extractVariableLabel = (rawVariableName: string) => {
|
|
||||||
const variableWithoutBrackets = rawVariableName.replace(
|
|
||||||
CAPTURE_ALL_VARIABLE_TAG_INNER_REGEX,
|
|
||||||
(_, variableName) => {
|
|
||||||
return variableName;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const parts = variableWithoutBrackets.split('.');
|
|
||||||
const displayText = parts.at(-1);
|
|
||||||
|
|
||||||
if (!isDefined(displayText)) {
|
|
||||||
throw new Error('Expected to find at least one splitted chunk.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return displayText;
|
|
||||||
};
|
|
||||||
@ -0,0 +1,128 @@
|
|||||||
|
import { CAPTURE_ALL_VARIABLE_TAG_INNER_REGEX } from '@/workflow/workflow-variables/constants/CaptureAllVariableTagInnerRegex';
|
||||||
|
import {
|
||||||
|
OutputSchema,
|
||||||
|
StepOutputSchema,
|
||||||
|
} from '@/workflow/workflow-variables/types/StepOutputSchema';
|
||||||
|
import { isBaseOutputSchema } from '@/workflow/workflow-variables/utils/isBaseOutputSchema';
|
||||||
|
import { isRecordOutputSchema } from '@/workflow/workflow-variables/utils/isRecordOutputSchema';
|
||||||
|
import { isDefined } from 'twenty-shared';
|
||||||
|
|
||||||
|
const getDisplayedSubStepObjectLabel = (outputSchema: OutputSchema) => {
|
||||||
|
if (!isRecordOutputSchema(outputSchema)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return outputSchema.object.label;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDisplayedSubStepFieldLabel = (
|
||||||
|
key: string,
|
||||||
|
outputSchema: OutputSchema,
|
||||||
|
) => {
|
||||||
|
if (isBaseOutputSchema(outputSchema)) {
|
||||||
|
return outputSchema[key]?.label;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRecordOutputSchema(outputSchema)) {
|
||||||
|
return outputSchema.fields[key]?.label;
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
const searchCurrentStepOutputSchema = ({
|
||||||
|
stepOutputSchema,
|
||||||
|
path,
|
||||||
|
isFullRecord,
|
||||||
|
selectedField,
|
||||||
|
}: {
|
||||||
|
stepOutputSchema: StepOutputSchema;
|
||||||
|
path: string[];
|
||||||
|
isFullRecord: boolean;
|
||||||
|
selectedField: string;
|
||||||
|
}) => {
|
||||||
|
let currentSubStep = stepOutputSchema.outputSchema;
|
||||||
|
let nextKeyIndex = 0;
|
||||||
|
let nextKey = path[nextKeyIndex];
|
||||||
|
let variablePathLabel = stepOutputSchema.name;
|
||||||
|
|
||||||
|
while (nextKeyIndex < path.length) {
|
||||||
|
if (isRecordOutputSchema(currentSubStep)) {
|
||||||
|
const currentField = currentSubStep.fields[nextKey];
|
||||||
|
currentSubStep = currentField?.value;
|
||||||
|
nextKey = path[nextKeyIndex + 1];
|
||||||
|
variablePathLabel = `${variablePathLabel} > ${currentField?.label}`;
|
||||||
|
} else if (isBaseOutputSchema(currentSubStep)) {
|
||||||
|
if (isDefined(currentSubStep[nextKey])) {
|
||||||
|
const currentField = currentSubStep[nextKey];
|
||||||
|
currentSubStep = currentField?.value;
|
||||||
|
nextKey = path[nextKeyIndex + 1];
|
||||||
|
variablePathLabel = `${variablePathLabel} > ${currentField?.label}`;
|
||||||
|
} else {
|
||||||
|
// If the key is not found in the step, we handle the case where the path has been wrongly split
|
||||||
|
// For example, if there is a dot in the field name
|
||||||
|
if (nextKeyIndex + 1 < path.length) {
|
||||||
|
nextKey = `${nextKey}.${path[nextKeyIndex + 1]}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nextKeyIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isDefined(currentSubStep)) {
|
||||||
|
return {
|
||||||
|
variableLabel: undefined,
|
||||||
|
variablePathLabel: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
variableLabel: isFullRecord
|
||||||
|
? getDisplayedSubStepObjectLabel(currentSubStep)
|
||||||
|
: getDisplayedSubStepFieldLabel(selectedField, currentSubStep),
|
||||||
|
variablePathLabel,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const searchVariableThroughOutputSchema = ({
|
||||||
|
stepOutputSchema,
|
||||||
|
rawVariableName,
|
||||||
|
isFullRecord = false,
|
||||||
|
}: {
|
||||||
|
stepOutputSchema: StepOutputSchema;
|
||||||
|
rawVariableName: string;
|
||||||
|
isFullRecord?: boolean;
|
||||||
|
}) => {
|
||||||
|
const variableWithoutBrackets = rawVariableName.replace(
|
||||||
|
CAPTURE_ALL_VARIABLE_TAG_INNER_REGEX,
|
||||||
|
(_, variableName) => {
|
||||||
|
return variableName;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const parts = variableWithoutBrackets.split('.');
|
||||||
|
|
||||||
|
const stepId = parts.at(0);
|
||||||
|
const selectedField = parts.at(-1);
|
||||||
|
// path is the remaining parts of the variable name
|
||||||
|
const path = parts.slice(1, -1);
|
||||||
|
|
||||||
|
if (!isDefined(stepId) || !isDefined(selectedField)) {
|
||||||
|
return {
|
||||||
|
variableLabel: undefined,
|
||||||
|
variablePathLabel: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const { variableLabel, variablePathLabel } = searchCurrentStepOutputSchema({
|
||||||
|
stepOutputSchema,
|
||||||
|
path,
|
||||||
|
isFullRecord,
|
||||||
|
selectedField,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
variableLabel,
|
||||||
|
variablePathLabel: `${variablePathLabel} > ${variableLabel}`,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { WorkflowTextEditorVariableChip } from '@/workflow/workflow-variables/components/WorkflowTextEditorVariableChip';
|
import { WorkflowTextEditorVariableChip } from '@/workflow/workflow-variables/components/WorkflowTextEditorVariableChip';
|
||||||
import { extractVariableLabel } from '@/workflow/workflow-variables/utils/extractVariableLabel';
|
import { extractRawVariableNamePart } from '@/workflow/workflow-variables/utils/extractRawVariableNamePart';
|
||||||
import { Node } from '@tiptap/core';
|
import { Node } from '@tiptap/core';
|
||||||
import { mergeAttributes, ReactNodeViewRenderer } from '@tiptap/react';
|
import { mergeAttributes, ReactNodeViewRenderer } from '@tiptap/react';
|
||||||
|
|
||||||
@ -38,7 +38,10 @@ export const VariableTag = Node.create({
|
|||||||
'data-type': 'variableTag',
|
'data-type': 'variableTag',
|
||||||
class: 'variable-tag',
|
class: 'variable-tag',
|
||||||
}),
|
}),
|
||||||
extractVariableLabel(variable),
|
extractRawVariableNamePart({
|
||||||
|
rawVariableName: variable,
|
||||||
|
part: 'selectedField',
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@ -63,7 +63,7 @@ export const DateTimeSettingsTimezone: Story = {
|
|||||||
await canvas.findByText('Date and time');
|
await canvas.findByText('Date and time');
|
||||||
|
|
||||||
const timezoneSelect = await canvas.findByText(
|
const timezoneSelect = await canvas.findByText(
|
||||||
'(GMT-05:00) Eastern Standard Time - New York',
|
'(GMT-04:00) Eastern Daylight Time - New York',
|
||||||
);
|
);
|
||||||
|
|
||||||
userEvent.click(timezoneSelect);
|
userEvent.click(timezoneSelect);
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
|
import { usePopulateStepsOutputSchema } from '@/workflow/hooks/usePopulateStepsOutputSchema';
|
||||||
|
import { WorkflowVersionComponentInstanceContext } from '@/workflow/states/context/WorkflowVersionComponentInstanceContext';
|
||||||
import { workflowIdState } from '@/workflow/states/workflowIdState';
|
import { workflowIdState } from '@/workflow/states/workflowIdState';
|
||||||
|
import { WorkflowVersion } from '@/workflow/types/Workflow';
|
||||||
import { workflowSelectedNodeState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeState';
|
import { workflowSelectedNodeState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeState';
|
||||||
import { Decorator } from '@storybook/react';
|
import { Decorator } from '@storybook/react';
|
||||||
import { useEffect } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useSetRecoilState } from 'recoil';
|
import { useSetRecoilState } from 'recoil';
|
||||||
import {
|
import {
|
||||||
getWorkflowMock,
|
getWorkflowMock,
|
||||||
@ -11,11 +14,30 @@ import {
|
|||||||
export const WorkflowStepDecorator: Decorator = (Story) => {
|
export const WorkflowStepDecorator: Decorator = (Story) => {
|
||||||
const setWorkflowId = useSetRecoilState(workflowIdState);
|
const setWorkflowId = useSetRecoilState(workflowIdState);
|
||||||
const setWorkflowSelectedNode = useSetRecoilState(workflowSelectedNodeState);
|
const setWorkflowSelectedNode = useSetRecoilState(workflowSelectedNodeState);
|
||||||
|
const workflowVersion = getWorkflowMock().versions.edges[0]
|
||||||
|
.node as WorkflowVersion;
|
||||||
|
const { populateStepsOutputSchema } = usePopulateStepsOutputSchema({
|
||||||
|
workflowVersionId: workflowVersion.id,
|
||||||
|
});
|
||||||
|
const [ready, setReady] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setWorkflowId(getWorkflowMock().id);
|
setWorkflowId(getWorkflowMock().id);
|
||||||
setWorkflowSelectedNode(getWorkflowNodeIdMock());
|
setWorkflowSelectedNode(getWorkflowNodeIdMock());
|
||||||
}, [setWorkflowId, setWorkflowSelectedNode]);
|
populateStepsOutputSchema(workflowVersion);
|
||||||
|
setReady(true);
|
||||||
|
}, [
|
||||||
|
setWorkflowId,
|
||||||
|
setWorkflowSelectedNode,
|
||||||
|
populateStepsOutputSchema,
|
||||||
|
workflowVersion,
|
||||||
|
]);
|
||||||
|
|
||||||
return <Story />;
|
return (
|
||||||
|
<WorkflowVersionComponentInstanceContext.Provider
|
||||||
|
value={{ instanceId: workflowVersion.id }}
|
||||||
|
>
|
||||||
|
{ready && <Story />}
|
||||||
|
</WorkflowVersionComponentInstanceContext.Provider>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -25,6 +25,8 @@ export const getWorkflowNodeIdMock = () => {
|
|||||||
return getWorkflowMock().versions.edges[0].node.steps[0].id;
|
return getWorkflowMock().versions.edges[0].node.steps[0].id;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const MOCKED_STEP_ID = '04d5f3bf-9714-400d-ba27-644006a5fb1b';
|
||||||
|
|
||||||
export const workflowQueryResult = {
|
export const workflowQueryResult = {
|
||||||
workflows: {
|
workflows: {
|
||||||
__typename: 'WorkflowConnection',
|
__typename: 'WorkflowConnection',
|
||||||
@ -76,7 +78,7 @@ export const workflowQueryResult = {
|
|||||||
status: 'DRAFT',
|
status: 'DRAFT',
|
||||||
steps: [
|
steps: [
|
||||||
{
|
{
|
||||||
id: '04d5f3bf-9714-400d-ba27-644006a5fb1b',
|
id: MOCKED_STEP_ID,
|
||||||
name: 'Create Record',
|
name: 'Create Record',
|
||||||
type: 'CREATE_RECORD',
|
type: 'CREATE_RECORD',
|
||||||
valid: false,
|
valid: false,
|
||||||
@ -107,6 +109,28 @@ export const workflowQueryResult = {
|
|||||||
value: 'My text',
|
value: 'My text',
|
||||||
isLeaf: true,
|
isLeaf: true,
|
||||||
},
|
},
|
||||||
|
fullName: {
|
||||||
|
icon: 'IconTargetArrow',
|
||||||
|
type: 'TEXT',
|
||||||
|
label: 'Full Name',
|
||||||
|
isLeaf: false,
|
||||||
|
value: {
|
||||||
|
firstName: {
|
||||||
|
icon: 'IconTargetArrow',
|
||||||
|
type: 'TEXT',
|
||||||
|
label: 'Full Name First Name',
|
||||||
|
value: 'John',
|
||||||
|
isLeaf: true,
|
||||||
|
},
|
||||||
|
lastName: {
|
||||||
|
icon: 'IconTargetArrow',
|
||||||
|
type: 'TEXT',
|
||||||
|
label: 'Full Name Last Name',
|
||||||
|
value: 'Doe',
|
||||||
|
isLeaf: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
stage: {
|
stage: {
|
||||||
icon: 'IconProgressCheck',
|
icon: 'IconProgressCheck',
|
||||||
type: 'SELECT',
|
type: 'SELECT',
|
||||||
@ -120,13 +144,13 @@ export const workflowQueryResult = {
|
|||||||
value: {
|
value: {
|
||||||
amountMicros: {
|
amountMicros: {
|
||||||
type: 'NUMERIC',
|
type: 'NUMERIC',
|
||||||
label: ' Amount Micros',
|
label: 'My Amount Micros',
|
||||||
value: null,
|
value: null,
|
||||||
isLeaf: true,
|
isLeaf: true,
|
||||||
},
|
},
|
||||||
currencyCode: {
|
currencyCode: {
|
||||||
type: 'TEXT',
|
type: 'TEXT',
|
||||||
label: ' Currency Code',
|
label: 'My Currency Code',
|
||||||
value: 'My text',
|
value: 'My text',
|
||||||
isLeaf: true,
|
isLeaf: true,
|
||||||
},
|
},
|
||||||
@ -186,6 +210,13 @@ export const workflowQueryResult = {
|
|||||||
value: '01/23/2025 15:16',
|
value: '01/23/2025 15:16',
|
||||||
isLeaf: true,
|
isLeaf: true,
|
||||||
},
|
},
|
||||||
|
salary: {
|
||||||
|
icon: 'IconMoneybag',
|
||||||
|
type: 'NUMBER',
|
||||||
|
label: 'Salary',
|
||||||
|
value: 1000000000,
|
||||||
|
isLeaf: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
object: {
|
object: {
|
||||||
icon: 'IconTargetArrow',
|
icon: 'IconTargetArrow',
|
||||||
|
|||||||
Reference in New Issue
Block a user