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]

Reply via email to