This is an automated email from the ASF dual-hosted git repository.
jomarko pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-kie-tools.git
The following commit(s) were added to refs/heads/main by this push:
new c5f0c9aae04 [incubator-kie-tools#4034] BPMN Editor removes reserved
HumanTask Assignments without a warning (#4035)
c5f0c9aae04 is described below
commit c5f0c9aae04b8f0566d4312e61009ae76578d949
Author: Jozef Marko <[email protected]>
AuthorDate: Fri Oct 30 08:03:05 2026 +0100
[incubator-kie-tools#4034] BPMN Editor removes reserved HumanTask
Assignments without a warning (#4035)
---
.../customTasks/RestServiceTaskPropertiesPanel.tsx | 15 +--
packages/bpmn-editor/src/i18n/BpmnEditorI18n.ts | 1 +
packages/bpmn-editor/src/i18n/locales/en.ts | 1 +
.../dataMapping/DataMappingFormSection.tsx | 110 ++++++++++++++-------
.../features/CustomTasks/CustomTasks.stories.tsx | 4 +-
.../CustomTasks/other/OtherCustomTasks.tsx | 80 +++++++++++++++
.../tests-e2e/__fixtures__/customTasks.ts | 9 +-
.../tests-e2e/flowElements/addCustomTasks.spec.ts | 102 +++++++++++++++++++
8 files changed, 278 insertions(+), 44 deletions(-)
diff --git
a/packages/bpmn-editor-envelope/src/customTasks/RestServiceTaskPropertiesPanel.tsx
b/packages/bpmn-editor-envelope/src/customTasks/RestServiceTaskPropertiesPanel.tsx
index eaa2d1d3c5a..8bf80d9950b 100644
---
a/packages/bpmn-editor-envelope/src/customTasks/RestServiceTaskPropertiesPanel.tsx
+++
b/packages/bpmn-editor-envelope/src/customTasks/RestServiceTaskPropertiesPanel.tsx
@@ -30,10 +30,7 @@ import { Select, SelectList, SelectOption } from
"@patternfly/react-core/dist/js
import { MenuToggle, MenuToggleElement } from
"@patternfly/react-core/dist/js/components/MenuToggle";
import { List, ListItem } from
"@patternfly/react-core/dist/js/components/List";
import { Table, Thead, Tr, Th, Tbody, Td } from
"@patternfly/react-table/dist/js/components/Table";
-import {
- BidirectionalDataMappingFormSection,
- useDataMapping,
-} from
"@kie-tools/bpmn-editor/dist/propertiesPanel/dataMapping/DataMappingFormSection";
+import { BidirectionalDataMappingFormSection } from
"@kie-tools/bpmn-editor/dist/propertiesPanel/dataMapping/DataMappingFormSection";
import { OnEntryAndExitScriptsFormSection } from
"@kie-tools/bpmn-editor/dist/propertiesPanel/onEntryAndExitScripts/OnEntryAndExitScriptsFormSection";
import { FormGroup, FormSection, ActionGroup, FormHelperText } from
"@patternfly/react-core/dist/js/components/Form";
import { TextInput } from
"@patternfly/react-core/dist/js/components/TextInput";
@@ -51,7 +48,11 @@ import { TimesIcon } from
"@patternfly/react-icons/dist/js/icons/times-icon";
import HelpIcon from "@patternfly/react-icons/dist/esm/icons/help-icon";
import { bpmnEditorEnvelopeI18nDefaults, bpmnEditorEnvelopeI18nDictionaries }
from "../i18n";
import { I18n } from "@kie-tools-core/i18n/dist/core";
-import { DataMapping, setDataMappingForElement } from
"@kie-tools/bpmn-editor/dist/mutations/_dataMapping";
+import {
+ DataMapping,
+ getDataMapping,
+ setDataMappingForElement,
+} from "@kie-tools/bpmn-editor/dist/mutations/_dataMapping";
import { DEFAULT_DATA_TYPES } from
"@kie-tools/bpmn-editor/dist/mutations/addOrGetItemDefinitions";
import { BpmnEditorChannelApi } from "../BpmnEditorChannelApi";
import {
@@ -89,7 +90,9 @@ export type ContentDataVariable = {
export const RestServiceTaskPropertiesPanel:
CustomTask["propertiesPanelComponent"] = ({ task }) => {
const i18n = new I18n(bpmnEditorEnvelopeI18nDefaults,
bpmnEditorEnvelopeI18nDictionaries).getCurrent();
const bpmnEditorStoreApi = useBpmnEditorStoreApi();
- const { inputDataMapping, outputDataMapping } = useDataMapping(task, () =>
{});
+ // Read the full data mapping straight from the model. `useDataMapping`
strips the reserved inputs
+ // (Url, Host, ...) that this panel is responsible for editing.
+ const { inputDataMapping, outputDataMapping } = React.useMemo(() =>
getDataMapping(task), [task]);
const { channelApi } =
useKogitoEditorEnvelopeContext<KogitoEditorEnvelopeApi, BpmnEditorChannelApi>();
const [localUrl, setLocalUrl] = React.useState<string>("");
diff --git a/packages/bpmn-editor/src/i18n/BpmnEditorI18n.ts
b/packages/bpmn-editor/src/i18n/BpmnEditorI18n.ts
index 7a08210bb49..f08845d45a9 100644
--- a/packages/bpmn-editor/src/i18n/BpmnEditorI18n.ts
+++ b/packages/bpmn-editor/src/i18n/BpmnEditorI18n.ts
@@ -112,6 +112,7 @@ interface BpmnEditorDictionary extends ReferenceDictionary<{
noDataMappingsYet: (entryTitle: string) => string;
addDataMapping: (entryTitle: string) => string;
messageInfoIconHelperText: string;
+ reservedInputName: string;
};
eventDefinitionProperties: {
message: string;
diff --git a/packages/bpmn-editor/src/i18n/locales/en.ts
b/packages/bpmn-editor/src/i18n/locales/en.ts
index c5a1cabcff9..fd5b54b67b8 100644
--- a/packages/bpmn-editor/src/i18n/locales/en.ts
+++ b/packages/bpmn-editor/src/i18n/locales/en.ts
@@ -118,6 +118,7 @@ export const en: BpmnEditorI18n = {
addDataMapping: (entryTitle: string): string => `Add ${entryTitle} data
mapping`,
messageInfoIconHelperText:
"For message events, the data type is determined by the messages type.
To change it, update the message type in the Properties Manager(Messages tab).",
+ reservedInputName: "This input should be entered as a property for the
task.",
},
eventDefinitionProperties: {
message: "Message",
diff --git
a/packages/bpmn-editor/src/propertiesPanel/dataMapping/DataMappingFormSection.tsx
b/packages/bpmn-editor/src/propertiesPanel/dataMapping/DataMappingFormSection.tsx
index c8d5ed07d7b..a2be83ac59e 100644
---
a/packages/bpmn-editor/src/propertiesPanel/dataMapping/DataMappingFormSection.tsx
+++
b/packages/bpmn-editor/src/propertiesPanel/dataMapping/DataMappingFormSection.tsx
@@ -19,7 +19,8 @@
import * as React from "react";
import { useBpmnEditorStore, useBpmnEditorStoreApi } from
"../../store/StoreContext";
-import { ActionGroup, Form, FormSection } from
"@patternfly/react-core/dist/js/components/Form";
+import { ActionGroup, Form, FormHelperText, FormSection } from
"@patternfly/react-core/dist/js/components/Form";
+import { HelperText, HelperTextItem } from
"@patternfly/react-core/dist/js/components/HelperText";
import { SectionHeader } from
"@kie-tools/xyflow-react-kie-diagram/dist/propertiesPanel/SectionHeader";
import { Button, ButtonVariant } from
"@patternfly/react-core/dist/js/components/Button";
import { EditIcon } from "@patternfly/react-icons/dist/js/icons/edit-icon";
@@ -110,38 +111,50 @@ export function DataMappingFormSection({
);
}
+function isDataInputReserved(element: WithDataMapping, dataInputName: string |
undefined, customTasks: CustomTask[]) {
+ return (
+ (DATA_INPUT_RESERVED_NAMES.get(element.__$$element) ?? new
Set()).has(dataInputName!) ||
+ (element.__$$element === "task" &&
+ !!customTasks.find((c) => {
+ if (!c.matches(element)) {
+ return false;
+ }
+ return !!c.dataInputReservedNames.find((n) => {
+ if (n.startsWith("*")) {
+ const suffix = n.slice(1);
+ return dataInputName?.endsWith(suffix);
+ } else if (n.endsWith("*")) {
+ const prefix = n.slice(0, -1);
+ return dataInputName?.startsWith(prefix);
+ } else {
+ return n === dataInputName;
+ }
+ });
+ }))
+ );
+}
+
function flaggedDataInputs<T extends { name?: string; "@_name"?: string }>(
element: WithDataMapping,
array: undefined | T[],
customTasks: CustomTask[]
-): undefined | { dataMapping: T; hide: boolean }[] {
- return array?.map((dataInput) => ({
- dataMapping: dataInput,
- hide:
+): undefined | { dataMapping: T; hide: boolean; reserved: boolean }[] {
+ return array?.map((dataInput) => {
+ const dataInputName = dataInput["@_name"] ?? dataInput.name;
+
+ const isMultiInstanceHidden =
(element.loopCharacteristics?.__$$element ===
"multiInstanceLoopCharacteristics" &&
- element.loopCharacteristics.inputDataItem?.["@_name"] ===
(dataInput["@_name"] ?? dataInput.name)) ||
- (dataInput["@_name"] ?? dataInput.name) ===
-
MULTI_INSTANCE_TASK_IO_SPECIFICATION_DATA_INPUTS_CONSTANTS.IN_COLLECTION ||
- (DATA_INPUT_RESERVED_NAMES.get(element.__$$element) ?? new
Set()).has((dataInput["@_name"] ?? dataInput.name)!) ||
- (element.__$$element === "task" &&
- !!customTasks.find((c) => {
- if (!c.matches(element)) {
- return false;
- }
- const dataInputName = dataInput["@_name"] ?? dataInput.name;
- return !!c.dataInputReservedNames.find((n) => {
- if (n.startsWith("*")) {
- const suffix = n.slice(1);
- return dataInputName?.endsWith(suffix);
- } else if (n.endsWith("*")) {
- const prefix = n.slice(0, -1);
- return dataInputName?.startsWith(prefix);
- } else {
- return n === dataInputName;
- }
- });
- })),
- }));
+ element.loopCharacteristics.inputDataItem?.["@_name"] ===
dataInputName) ||
+ dataInputName ===
MULTI_INSTANCE_TASK_IO_SPECIFICATION_DATA_INPUTS_CONSTANTS.IN_COLLECTION;
+
+ const isReserved = isDataInputReserved(element, dataInputName,
customTasks);
+
+ return {
+ dataMapping: dataInput,
+ hide: isMultiInstanceHidden,
+ reserved: isReserved,
+ };
+ });
}
function flaggedDataOutputs<T extends { name?: string; "@_name"?: string }>(
@@ -170,7 +183,7 @@ export function BidirectionalDataMappingFormSection({
element }: { element: With
const { customTasks } = useCustomTasks();
const isReadOnly = useBpmnEditorStore((s) => s.settings.isReadOnly);
const inputCount = flaggedDataInputs(element,
element.ioSpecification?.dataInput, customTasks ?? [])?.filter(
- (s) => !s.hide
+ (s) => !s.hide && !s.reserved
).length;
const outputCount = flaggedDataOutputs(element,
element.ioSpecification?.dataOutput, customTasks ?? [])?.filter(
(s) => !s.hide
@@ -461,7 +474,7 @@ export function DataMappingsList({
</div>
{section === "input" &&
flaggedDataInputs(flowElement as any, inputDataMapping,
customTasks ?? [])?.flatMap(
- ({ hide, dataMapping: entry }, i) =>
+ ({ hide, reserved, dataMapping: entry }, i) =>
hide ? (
[]
) : (
@@ -483,6 +496,7 @@ export function DataMappingsList({
isRequired={true}
placeholder={i18n.dataMapping.namePlaceholder}
value={entry.name}
+ validated={reserved ? "error" : "default"}
onChange={(e, value) => handleInputChange(i, "name",
value, "input", { isExpression: false })}
/>
</GridItem>
@@ -530,6 +544,15 @@ export function DataMappingsList({
)}
</GridItem>
</Grid>
+ {reserved && (
+ <FormHelperText style={{ padding: "0 8px 4px" }}>
+ <HelperText>
+ <HelperTextItem variant="error"
icon={<InfoCircleIcon />}>
+ {i18n.dataMapping.reservedInputName}
+ </HelperTextItem>
+ </HelperText>
+ </FormHelperText>
+ )}
</div>
)
)}
@@ -645,6 +668,7 @@ export function useDataMapping(
setShowDataMappingModal: React.Dispatch<React.SetStateAction<boolean>>
) {
const bpmnEditorStoreApi = useBpmnEditorStoreApi();
+ const { customTasks } = useCustomTasks();
const [inputDataMapping, setInputDataMapping] = useState<DataMapping[]>([]);
const [outputDataMapping, setOutputDataMapping] =
useState<DataMapping[]>([]);
@@ -761,7 +785,8 @@ export function useDataMapping(
[inputDataMapping, outputDataMapping]
);
- // populates intermediary data mapping state from the model
+ // populates intermediary data mapping state from the model,
+ // stripping reserved entries so they never appear in the editable list.
useEffect(() => {
if (!element) {
return;
@@ -769,9 +794,11 @@ export function useDataMapping(
const { inputDataMapping, outputDataMapping } = getDataMapping(element);
- setInputDataMapping(inputDataMapping);
+ setInputDataMapping(
+ inputDataMapping.filter((d) => !isDataInputReserved(element as any,
d.name, customTasks ?? []))
+ );
setOutputDataMapping(outputDataMapping);
- }, [element]);
+ }, [element, customTasks]);
const handleSubmit = useCallback(
(event: React.FormEvent<HTMLFormElement>) => {
@@ -782,9 +809,22 @@ export function useDataMapping(
}
bpmnEditorStoreApi.setState((s) => {
+ // Re-read the reserved entries that were stripped from the editable
state
+ // so they are preserved through the save cycle.
+ const { inputDataMapping: modelInputDataMapping } =
getDataMapping(element);
+ const reservedEntries = modelInputDataMapping.filter((d) =>
+ isDataInputReserved(element as any, d.name, customTasks ?? [])
+ );
+
+ // Exclude user-typed entries whose names match reserved names — they
would
+ // collide with internal task inputs and must not be saved as user
data mappings.
+ const userEntries = inputDataMapping.filter(
+ (d) => !isDataInputReserved(element as any, d.name, customTasks ??
[])
+ );
+
setDataMappingForElement({
definitions: s.bpmn.model.definitions,
- inputDataMapping,
+ inputDataMapping: [...reservedEntries, ...userEntries],
outputDataMapping,
elementId: element["@_id"],
element: element.__$$element,
@@ -793,7 +833,7 @@ export function useDataMapping(
setShowDataMappingModal(false);
},
- [bpmnEditorStoreApi, element, inputDataMapping, outputDataMapping,
setShowDataMappingModal]
+ [bpmnEditorStoreApi, element, inputDataMapping, outputDataMapping,
setShowDataMappingModal, customTasks]
);
return {
diff --git
a/packages/bpmn-editor/stories/features/CustomTasks/CustomTasks.stories.tsx
b/packages/bpmn-editor/stories/features/CustomTasks/CustomTasks.stories.tsx
index a21a8250936..dbfa8001570 100644
--- a/packages/bpmn-editor/stories/features/CustomTasks/CustomTasks.stories.tsx
+++ b/packages/bpmn-editor/stories/features/CustomTasks/CustomTasks.stories.tsx
@@ -23,7 +23,7 @@ import { getMarshaller } from "@kie-tools/bpmn-marshaller";
import { generateUuid } from "@kie-tools/boxed-expression-component/dist/api";
import { BpmnEditorWrapper, StorybookBpmnEditorProps } from
"../../bpmnEditorStoriesWrapper";
import { BpmnEditor, BpmnEditorProps } from "../../../src/BpmnEditor";
-import { GRPC_API_CALL_TASK, REST_API_CALL_TASK } from
"./other/OtherCustomTasks";
+import { GRPC_API_CALL_TASK, REST_API_CALL_TASK, RESERVED_INPUTS_TASK,
WID_LIKE_TASK } from "./other/OtherCustomTasks";
export const generateEmptyBpmn20 = () => `<?xml version="1.0"
encoding="UTF-8"?>
<definitions
@@ -90,6 +90,6 @@ export const CustomTasks: Story = {
issueTrackerHref: "",
xml: marshaller.builder.build(model),
//
- customTasks: [REST_API_CALL_TASK, GRPC_API_CALL_TASK],
+ customTasks: [REST_API_CALL_TASK, GRPC_API_CALL_TASK,
RESERVED_INPUTS_TASK, WID_LIKE_TASK],
},
};
diff --git
a/packages/bpmn-editor/stories/features/CustomTasks/other/OtherCustomTasks.tsx
b/packages/bpmn-editor/stories/features/CustomTasks/other/OtherCustomTasks.tsx
index ef87881511e..26212acac90 100644
---
a/packages/bpmn-editor/stories/features/CustomTasks/other/OtherCustomTasks.tsx
+++
b/packages/bpmn-editor/stories/features/CustomTasks/other/OtherCustomTasks.tsx
@@ -20,6 +20,9 @@
import * as React from "react";
import { generateUuid } from "@kie-tools/boxed-expression-component/dist/api";
import { CustomTask } from "../../../../src/BpmnEditor";
+import { BidirectionalDataMappingFormSection } from
"../../../../src/propertiesPanel/dataMapping/DataMappingFormSection";
+import { DataMapping, setInputAndOutputDataMapping } from
"../../../../src/mutations/_dataMapping";
+import { DEFAULT_DATA_TYPES } from
"../../../../src/mutations/addOrGetItemDefinitions";
import "@kie-tools/bpmn-marshaller/dist/drools-extension";
export const PropertiesPanelComponent: CustomTask["propertiesPanelComponent"]
= ({ task }) => (
@@ -95,3 +98,80 @@ export const GRPC_API_CALL_TASK: CustomTask = {
"@_name": "gRPC API call Task",
}),
};
+
+export const RESERVED_INPUTS_TASK_NAME = "reserved-inputs-task";
+
+/**
+ * Simulates a built-in Custom Task (like Rest or Milestone) where specific
Data Mapping inputs
+ * are reserved/owned by the task's custom properties panel ("Url",
"HEADER_*"),
+ * and one regular unreserved input ("Message") is meant to be edited in the
Data Mapping modal.
+ */
+export const RESERVED_INPUTS_TASK: CustomTask = {
+ id: RESERVED_INPUTS_TASK_NAME,
+ displayGroup: "Other",
+ displayName: "Reserved Inputs Task",
+ displayDescription: "",
+ dataInputReservedNames: ["Url", "HEADER_*"],
+ dataOutputReservedNames: [],
+ iconSvgElement: (
+ <svg width="30" height="30" viewBox="0 0 30 30"
xmlns="http://www.w3.org/2000/svg" fill="none">
+ <text x="15" y="25" textAnchor="middle" fontSize="24" fontFamily="Arial"
fontWeight={"light"}>
+ 🔒
+ </text>
+ </svg>
+ ),
+ propertiesPanelComponent: ({ task }) => <BidirectionalDataMappingFormSection
element={task} />,
+ matches: (task) => task["@_drools:taskName"] === RESERVED_INPUTS_TASK_NAME,
+ produce: () => ({
+ __$$element: "task",
+ "@_id": generateUuid(),
+ "@_drools:taskName": RESERVED_INPUTS_TASK_NAME,
+ "@_name": "Reserved Inputs Task",
+ }),
+ onAdded: (_state, task) => {
+ const inputs: DataMapping[] = [
+ { dtype: DEFAULT_DATA_TYPES.STRING, name: "Url", isExpression: true,
value: "http://localhost" },
+ { dtype: DEFAULT_DATA_TYPES.STRING, name: "HEADER_Accept", isExpression:
true, value: "text/plain" },
+ { dtype: DEFAULT_DATA_TYPES.STRING, name: "Message", isExpression:
false, variableRef: undefined },
+ ];
+ setInputAndOutputDataMapping(new Map(), inputs, [], task);
+ },
+};
+
+export const WID_LIKE_TASK_NAME = "wid-like-task";
+
+/**
+ * Simulates a Custom Task generated from a WID file where no inputs are
reserved
+ * (`dataInputReservedNames: []`), so all WID parameters must be visible and
editable
+ * in the Data Mapping modal.
+ */
+export const WID_LIKE_TASK: CustomTask = {
+ id: WID_LIKE_TASK_NAME,
+ displayGroup: "Other",
+ displayName: "WID Custom Task",
+ displayDescription: "",
+ dataInputReservedNames: [],
+ dataOutputReservedNames: [],
+ iconSvgElement: (
+ <svg width="30" height="30" viewBox="0 0 30 30"
xmlns="http://www.w3.org/2000/svg" fill="none">
+ <text x="15" y="25" textAnchor="middle" fontSize="24" fontFamily="Arial"
fontWeight={"light"}>
+ 📋
+ </text>
+ </svg>
+ ),
+ propertiesPanelComponent: ({ task }) => <BidirectionalDataMappingFormSection
element={task} />,
+ matches: (task) => task["@_drools:taskName"] === WID_LIKE_TASK_NAME,
+ produce: () => ({
+ __$$element: "task",
+ "@_id": generateUuid(),
+ "@_drools:taskName": WID_LIKE_TASK_NAME,
+ "@_name": "WID Custom Task",
+ }),
+ onAdded: (_state, task) => {
+ const inputs: DataMapping[] = [
+ { dtype: DEFAULT_DATA_TYPES.STRING, name: "Param1", isExpression: false,
variableRef: undefined },
+ { dtype: DEFAULT_DATA_TYPES.STRING, name: "Param2", isExpression: false,
variableRef: undefined },
+ ];
+ setInputAndOutputDataMapping(new Map(), inputs, [], task);
+ },
+};
diff --git a/packages/bpmn-editor/tests-e2e/__fixtures__/customTasks.ts
b/packages/bpmn-editor/tests-e2e/__fixtures__/customTasks.ts
index 051c4d3f6b0..b18633b7542 100644
--- a/packages/bpmn-editor/tests-e2e/__fixtures__/customTasks.ts
+++ b/packages/bpmn-editor/tests-e2e/__fixtures__/customTasks.ts
@@ -17,7 +17,7 @@
* under the License.
*/
-import { Page } from "@playwright/test";
+import { expect, Page } from "@playwright/test";
import { Diagram } from "./diagram";
import { Nodes, NodeType } from "./nodes";
@@ -65,6 +65,13 @@ export class CustomTasks {
needsSelection: false,
});
}
+
+ // The popover stays open after the drop and overlays the top-left of the
canvas,
+ // intercepting hover/click events on nodes placed beneath it.
+ if (await popover.isVisible()) {
+ await this.closePalette();
+ await expect(popover).not.toBeVisible();
+ }
}
public async getAvailableCustomTasks(): Promise<string[]> {
diff --git a/packages/bpmn-editor/tests-e2e/flowElements/addCustomTasks.spec.ts
b/packages/bpmn-editor/tests-e2e/flowElements/addCustomTasks.spec.ts
index 328ac479222..0a4f635d681 100644
--- a/packages/bpmn-editor/tests-e2e/flowElements/addCustomTasks.spec.ts
+++ b/packages/bpmn-editor/tests-e2e/flowElements/addCustomTasks.spec.ts
@@ -125,6 +125,108 @@ test.describe("Add Custom Tasks", () => {
});
});
+ test.describe("Custom Task Data Mapping inputs", () => {
+ test.describe("Built-in style Custom Task with reserved inputs (e.g. Rest,
Milestone)", () => {
+ test.beforeEach(async ({ customTasks, nodes, taskPropertiesPanel }) => {
+ await customTasks.dragCustomTask({
+ customTaskId: "reserved-inputs-task",
+ targetPosition: { x: 300, y: 300 },
+ thenRenameTo: "Reserved Task",
+ });
+ await expect(nodes.get({ name: "Reserved Task" })).toBeAttached();
+ await nodes.select({ name: "Reserved Task", position:
NodePosition.CENTER });
+ await taskPropertiesPanel.open();
+ });
+
+ test("should hide reserved inputs from the Data Mapping modal and
preserve them in the BPMN model", async ({
+ taskPropertiesPanel,
+ page,
+ jsonModel,
+ }) => {
+ const getModelInputNames = () =>
+ jsonModel.getTasks().then((t) =>
t[0].ioSpecification?.dataInput?.map((i) => i["@_name"]));
+
+ // Initial task state contains reserved inputs ("Url",
"HEADER_Accept") and regular input ("Message")
+ expect(await getModelInputNames()).toEqual(["Url", "HEADER_Accept",
"Message"]);
+
+ await taskPropertiesPanel.openDataMappingModal();
+
+ const modal = page.getByRole("dialog");
+ const visibleNameInputs = modal.getByLabel("name");
+
+ // Only the unreserved input ("Message") is shown in the modal
+ await expect(visibleNameInputs).toHaveCount(1);
+ await expect(visibleNameInputs.first()).toHaveValue("Message");
+
+ // Reserved inputs ("Url", "HEADER_Accept") are not listed: the only
name input is "Message" (asserted above)
+ await expect(page.getByText("This input should be entered as a
property for the task.")).toHaveCount(0);
+
+ // Saving and closing the modal must keep reserved inputs untouched in
the BPMN model
+ await taskPropertiesPanel.closeDataMappingModal();
+ expect((await getModelInputNames())?.sort()).toEqual(["HEADER_Accept",
"Message", "Url"]);
+ });
+
+ test("should not save user-typed inputs whose names match reserved
names/patterns", async ({
+ taskPropertiesPanel,
+ jsonModel,
+ page,
+ }) => {
+ await taskPropertiesPanel.openDataMappingModal();
+ await taskPropertiesPanel.addDataInputInModal({ name: "HEADER_Other"
});
+
+ // The reserved-name validation error is shown while the user types a
reserved name/pattern
+ await expect(page.getByText("This input should be entered as a
property for the task.")).toBeVisible();
+
+ await taskPropertiesPanel.closeDataMappingModal();
+
+ const names = (await
jsonModel.getTasks())[0].ioSpecification?.dataInput?.map((i) => i["@_name"]);
+ expect(names?.filter((n) => n === "HEADER_Other")).toEqual([]);
+ expect(names).toEqual(expect.arrayContaining(["Url", "HEADER_Accept",
"Message"]));
+ });
+ });
+
+ test.describe("WID-style Custom Task with unreserved inputs", () => {
+ test("should display all parameters in the Data Mapping modal when no
inputs are reserved", async ({
+ customTasks,
+ nodes,
+ taskPropertiesPanel,
+ page,
+ jsonModel,
+ }) => {
+ await customTasks.dragCustomTask({
+ customTaskId: "wid-like-task",
+ targetPosition: { x: 300, y: 300 },
+ thenRenameTo: "WID Task",
+ });
+ await expect(nodes.get({ name: "WID Task" })).toBeAttached();
+ await nodes.select({ name: "WID Task", position: NodePosition.CENTER
});
+ await taskPropertiesPanel.open();
+
+ const getModelInputNames = () =>
+ jsonModel.getTasks().then((t) =>
t[0].ioSpecification?.dataInput?.map((i) => i["@_name"]));
+
+ // Initial task model has all WID parameters
+ expect(await getModelInputNames()).toEqual(["Param1", "Param2"]);
+
+ await taskPropertiesPanel.openDataMappingModal();
+
+ const modal = page.getByRole("dialog");
+ const visibleNameInputs = modal.getByLabel("name");
+
+ // All parameters are visible and editable in the modal
+ await expect(visibleNameInputs).toHaveCount(2);
+ await expect(visibleNameInputs.nth(0)).toHaveValue("Param1");
+ await expect(visibleNameInputs.nth(1)).toHaveValue("Param2");
+
+ // Adding an extra input in the modal should also succeed
+ await taskPropertiesPanel.addDataInputInModal({ name: "Param3" });
+ await taskPropertiesPanel.closeDataMappingModal();
+
+ expect((await getModelInputNames())?.sort()).toEqual(["Param1",
"Param2", "Param3"]);
+ });
+ });
+ });
+
test("should rename custom task", async ({ customTasks, nodes }) => {
await customTasks.dragCustomTask({
customTaskId: "rest-api-call-task",
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]