@bpmnkit/plugins 0.0.18 → 0.0.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -24,13 +24,13 @@
24
24
  *
25
25
  * @packageDocumentation
26
26
  */
27
- import { zeebeExtensionsToXmlElements } from "@bpmnkit/core";
27
+ import { buildValidationDmn, findValidationStructure, insertValidationStructure, removeValidationStructure, validationDecisionId, zeebeExtensionsToXmlElements, } from "@bpmnkit/core";
28
28
  import { ELEMENT_TYPE_LABELS } from "@bpmnkit/editor";
29
29
  import { buildRegistrationFromTemplate } from "./template-engine.js";
30
30
  import { CAMUNDA_CONNECTOR_TEMPLATES } from "./templates/generated.js";
31
31
  export { CAMUNDA_CONNECTOR_TEMPLATES } from "./templates/generated.js";
32
32
  export { templateToServiceTaskOptions } from "./template-to-service-task.js";
33
- import { buildPropertiesWithExampleOutput, findFlowElement, findSequenceFlow, getExampleOutputJson, getIoInput, getTaskHeader, parseCalledElement, parseZeebeError, parseZeebeEscalation, parseZeebeExtensions, parseZeebeMessage, parseZeebeScript, parseZeebeSignal, updateFlowElement, updateSequenceFlow, xmlLocalName, } from "./util.js";
33
+ import { buildPropertiesWithExampleOutput, buildZeebeLoopCharacteristics, findFlowElement, findSequenceFlow, getExampleOutputJson, getIoInput, getTaskHeader, parseCalledElement, parseZeebeError, parseZeebeEscalation, parseZeebeExtensions, parseZeebeLoopCharacteristics, parseZeebeMessage, parseZeebeScript, parseZeebeSignal, updateFlowElement, updateSequenceFlow, xmlLocalName, } from "./util.js";
34
34
  /** Validates that a field value is valid JSON, or returns an error message. */
35
35
  function validateJson(value) {
36
36
  if (typeof value !== "string" || value.trim() === "")
@@ -358,11 +358,124 @@ const GENERAL_TYPES = [
358
358
  "inclusiveGateway",
359
359
  "eventBasedGateway",
360
360
  "complexGateway",
361
- "subProcess",
362
361
  "transaction",
363
362
  "manualTask",
364
363
  "task",
365
364
  ];
365
+ // ── Sub-process schema (general + multi-instance) ─────────────────────────────
366
+ const SUB_PROCESS_SCHEMA = {
367
+ compact: [{ key: "name", label: "Name", type: "text", placeholder: "Sub-process name" }],
368
+ groups: [
369
+ {
370
+ id: "general",
371
+ label: "General",
372
+ fields: [
373
+ { key: "name", label: "Name", type: "text", placeholder: "Sub-process name" },
374
+ {
375
+ key: "documentation",
376
+ label: "Documentation",
377
+ type: "textarea",
378
+ placeholder: "Add notes or documentation for this element…",
379
+ },
380
+ ],
381
+ },
382
+ {
383
+ id: "multi-instance",
384
+ label: "Multi-instance",
385
+ fields: [
386
+ // Guided setup button — visible only when no loop is configured
387
+ {
388
+ key: "_setupForEach",
389
+ label: "Process each item in a list",
390
+ type: "action",
391
+ hint: "Configure this sub-process to run once for each item in a collection.",
392
+ condition: (v) => v.multiInstanceMode === "none",
393
+ onClick: (_values, setValue) => {
394
+ setValue("multiInstanceMode", "parallel");
395
+ setValue("elementVariable", "item");
396
+ },
397
+ },
398
+ {
399
+ key: "multiInstanceMode",
400
+ label: "Loop type",
401
+ type: "select",
402
+ options: [
403
+ { value: "none", label: "None" },
404
+ { value: "parallel", label: "Parallel (for each, all at once)" },
405
+ { value: "sequential", label: "Sequential (for each, one at a time)" },
406
+ ],
407
+ condition: (v) => v.multiInstanceMode !== "none",
408
+ },
409
+ {
410
+ key: "collection",
411
+ label: "Collection",
412
+ type: "feel-expression",
413
+ placeholder: "= emails",
414
+ hint: "FEEL expression that returns the array to iterate over.",
415
+ condition: (v) => v.multiInstanceMode !== "none",
416
+ },
417
+ {
418
+ key: "elementVariable",
419
+ label: "Element variable",
420
+ type: "text",
421
+ placeholder: "item",
422
+ hint: "Variable name for the current iteration item. Available inside the sub-process as a process variable.",
423
+ condition: (v) => v.multiInstanceMode !== "none",
424
+ },
425
+ ],
426
+ },
427
+ ],
428
+ };
429
+ const SUB_PROCESS_ADAPTER = {
430
+ read(defs, id) {
431
+ const el = findFlowElement(defs, id);
432
+ if (!el)
433
+ return {};
434
+ const lc = "loopCharacteristics" in el ? el.loopCharacteristics : undefined;
435
+ const loop = parseZeebeLoopCharacteristics(lc?.extensionElements ?? []);
436
+ let multiInstanceMode = "none";
437
+ if (lc) {
438
+ multiInstanceMode = lc.isSequential ? "sequential" : "parallel";
439
+ }
440
+ return {
441
+ name: el.name ?? "",
442
+ documentation: el.documentation ?? "",
443
+ multiInstanceMode,
444
+ collection: loop?.inputCollection ?? "",
445
+ elementVariable: loop?.inputElement ?? "",
446
+ };
447
+ },
448
+ write(defs, id, values) {
449
+ return updateFlowElement(defs, id, (el) => {
450
+ const mode = typeof values.multiInstanceMode === "string" ? values.multiInstanceMode : "none";
451
+ const collection = typeof values.collection === "string" ? values.collection : "";
452
+ const elementVariable = typeof values.elementVariable === "string" ? values.elementVariable : "";
453
+ let loopCharacteristics;
454
+ if (mode !== "none") {
455
+ const extEls = collection
456
+ ? [
457
+ buildZeebeLoopCharacteristics({
458
+ inputCollection: collection,
459
+ inputElement: elementVariable,
460
+ }),
461
+ ]
462
+ : [];
463
+ loopCharacteristics = {
464
+ isSequential: mode === "sequential" ? true : undefined,
465
+ extensionElements: extEls,
466
+ };
467
+ }
468
+ return {
469
+ ...el,
470
+ name: typeof values.name === "string" ? values.name : el.name,
471
+ documentation: typeof values.documentation === "string"
472
+ ? values.documentation || undefined
473
+ : el.documentation,
474
+ loopCharacteristics,
475
+ };
476
+ });
477
+ },
478
+ };
366
479
  // ── User task schema (formId) ─────────────────────────────────────────────────
367
480
  function makeUserTaskSchema() {
368
481
  return {
@@ -436,7 +549,7 @@ const USER_TASK_ADAPTER = {
436
549
  },
437
550
  };
438
551
  // ── Business rule task schema (decisionId + resultVariable) ──────────────────
439
- function makeBusinessRuleTaskSchema() {
552
+ function makeBusinessRuleTaskSchema(onOpenDmn) {
440
553
  return {
441
554
  compact: [{ key: "name", label: "Name", type: "text", placeholder: "Task name" }],
442
555
  groups: [
@@ -452,6 +565,18 @@ function makeBusinessRuleTaskSchema() {
452
565
  placeholder: "e.g. Decision_1m0rvzp",
453
566
  hint: "ID of the DMN decision to evaluate.",
454
567
  },
568
+ {
569
+ key: "_openDmn",
570
+ label: "Open DMN",
571
+ type: "action",
572
+ hint: "Open the referenced DMN decision in the editor.",
573
+ condition: (values) => typeof values.decisionId === "string" && values.decisionId !== "",
574
+ onClick: (values) => {
575
+ const decId = values.decisionId;
576
+ if (decId)
577
+ onOpenDmn?.(decId);
578
+ },
579
+ },
455
580
  {
456
581
  key: "resultVariable",
457
582
  label: "Result variable",
@@ -1192,19 +1317,6 @@ function eventDefToRegistration(defType) {
1192
1317
  return null;
1193
1318
  }
1194
1319
  }
1195
- const START_EVENT_ADAPTER = {
1196
- read: GENERAL_ADAPTER.read,
1197
- write: GENERAL_ADAPTER.write,
1198
- resolve(defs, id) {
1199
- const el = findFlowElement(defs, id);
1200
- if (!el || el.type !== "startEvent")
1201
- return null;
1202
- const defType = el.eventDefinitions[0]?.type;
1203
- if (!defType)
1204
- return null;
1205
- return eventDefToRegistration(defType);
1206
- },
1207
- };
1208
1320
  const END_EVENT_ADAPTER = {
1209
1321
  read: GENERAL_ADAPTER.read,
1210
1322
  write: GENERAL_ADAPTER.write,
@@ -1257,6 +1369,400 @@ const BOUNDARY_EVENT_ADAPTER = {
1257
1369
  return eventDefToRegistration(defType);
1258
1370
  },
1259
1371
  };
1372
+ // ── Input validation wizard modal ─────────────────────────────────────────────
1373
+ const VALIDATION_MODAL_CSS = `
1374
+ .bpmnkit-val-overlay {
1375
+ position: fixed; inset: 0; background: rgba(0,0,0,0.6); backdrop-filter: blur(4px);
1376
+ display: flex; align-items: center; justify-content: center; z-index: 99999;
1377
+ }
1378
+ .bpmnkit-val-dialog {
1379
+ background: var(--bpmnkit-surface, #161626);
1380
+ border: 1px solid var(--bpmnkit-border, #2a2a42);
1381
+ border-radius: 10px; padding: 20px; width: 680px; max-width: 95vw; max-height: 85vh;
1382
+ display: flex; flex-direction: column; gap: 14px;
1383
+ font-family: var(--bpmnkit-font, system-ui, sans-serif);
1384
+ font-size: 13px; color: var(--bpmnkit-fg, #cdd6f4);
1385
+ box-shadow: 0 24px 64px rgba(0,0,0,0.7);
1386
+ }
1387
+ .bpmnkit-val-dialog h2 {
1388
+ font-size: 14px; font-weight: 600; margin: 0;
1389
+ color: var(--bpmnkit-fg, #cdd6f4);
1390
+ }
1391
+ .bpmnkit-val-dialog p.hint {
1392
+ font-size: 12px; color: var(--bpmnkit-fg-muted, #8888a8); margin: 0;
1393
+ }
1394
+ .bpmnkit-val-table { overflow-y: auto; flex: 1; }
1395
+ .bpmnkit-val-table table {
1396
+ width: 100%; border-collapse: collapse; font-size: 12px;
1397
+ }
1398
+ .bpmnkit-val-table th {
1399
+ text-align: left; padding: 6px 8px; font-weight: 500;
1400
+ color: var(--bpmnkit-fg-muted, #8888a8); font-size: 11px; text-transform: uppercase;
1401
+ border-bottom: 1px solid var(--bpmnkit-border, #2a2a42);
1402
+ }
1403
+ .bpmnkit-val-table td { padding: 4px 4px; vertical-align: middle; }
1404
+ .bpmnkit-val-table input[type=text], .bpmnkit-val-table input[type=number], .bpmnkit-val-table select {
1405
+ background: var(--bpmnkit-surface-2, #1e1e2e);
1406
+ border: 1px solid var(--bpmnkit-border, #2a2a42);
1407
+ border-radius: 4px; padding: 4px 7px; font-size: 12px;
1408
+ color: var(--bpmnkit-fg, #cdd6f4); width: 100%; box-sizing: border-box;
1409
+ }
1410
+ .bpmnkit-val-table input[type=text]:focus, .bpmnkit-val-table input[type=number]:focus,
1411
+ .bpmnkit-val-table select:focus {
1412
+ outline: none; border-color: var(--bpmnkit-accent, #6b9df7);
1413
+ }
1414
+ .bpmnkit-val-table input:disabled, .bpmnkit-val-table select:disabled {
1415
+ opacity: 0.35; cursor: not-allowed;
1416
+ }
1417
+ .bpmnkit-val-chk { display: flex; align-items: center; justify-content: center; }
1418
+ .bpmnkit-val-chk input[type=checkbox] { width: 14px; height: 14px; cursor: pointer; }
1419
+ .bpmnkit-val-del {
1420
+ background: none; border: none; cursor: pointer; padding: 2px 6px; border-radius: 4px;
1421
+ color: var(--bpmnkit-fg-muted, #8888a8); font-size: 14px; line-height: 1;
1422
+ }
1423
+ .bpmnkit-val-del:hover { color: var(--bpmnkit-danger, #f87171); background: var(--bpmnkit-surface-2, #1e1e2e); }
1424
+ .bpmnkit-val-add {
1425
+ background: none; border: 1px dashed var(--bpmnkit-border, #2a2a42); border-radius: 6px;
1426
+ padding: 6px 12px; font-size: 12px; cursor: pointer; width: 100%;
1427
+ color: var(--bpmnkit-fg-muted, #8888a8); margin-top: 4px;
1428
+ }
1429
+ .bpmnkit-val-add:hover { border-color: var(--bpmnkit-accent, #6b9df7); color: var(--bpmnkit-accent, #6b9df7); }
1430
+ .bpmnkit-val-actions { display: flex; justify-content: flex-end; gap: 8px; }
1431
+ .bpmnkit-val-btn {
1432
+ padding: 6px 16px; border-radius: 6px; font-size: 12px; font-weight: 500;
1433
+ cursor: pointer; border: 1px solid var(--bpmnkit-border, #2a2a42);
1434
+ background: var(--bpmnkit-surface-2, #1e1e2e); color: var(--bpmnkit-fg, #cdd6f4);
1435
+ }
1436
+ .bpmnkit-val-btn:hover { background: var(--bpmnkit-accent-subtle, rgba(107,157,247,0.15)); }
1437
+ .bpmnkit-val-btn--primary {
1438
+ background: var(--bpmnkit-accent, #6b9df7); color: #fff; border-color: var(--bpmnkit-accent, #6b9df7);
1439
+ }
1440
+ .bpmnkit-val-btn--primary:hover { filter: brightness(1.1); }
1441
+ .bpmnkit-val-summary {
1442
+ background: var(--bpmnkit-surface-2, #1e1e2e); border-radius: 6px; padding: 8px 10px;
1443
+ font-size: 12px; display: flex; flex-direction: column; gap: 3px;
1444
+ }
1445
+ .bpmnkit-val-summary-row { display: flex; gap: 6px; align-items: baseline; }
1446
+ .bpmnkit-val-summary-name { color: var(--bpmnkit-accent-bright, #89b4fa); font-weight: 500; }
1447
+ .bpmnkit-val-summary-meta { color: var(--bpmnkit-fg-muted, #8888a8); }
1448
+ .bpmnkit-val-summary-badge {
1449
+ font-size: 10px; padding: 1px 5px; border-radius: 3px; font-weight: 500;
1450
+ background: var(--bpmnkit-accent-subtle, rgba(107,157,247,0.15));
1451
+ color: var(--bpmnkit-accent-bright, #89b4fa);
1452
+ }
1453
+ .bpmnkit-val-summary-badge--req {
1454
+ background: rgba(249,115,22,0.15); color: #fb923c;
1455
+ }
1456
+ `;
1457
+ function injectValidationModalCss() {
1458
+ const id = "bpmnkit-validation-modal-css";
1459
+ if (document.getElementById(id))
1460
+ return;
1461
+ const style = document.createElement("style");
1462
+ style.id = id;
1463
+ style.textContent = VALIDATION_MODAL_CSS;
1464
+ document.head.appendChild(style);
1465
+ }
1466
+ /**
1467
+ * Opens the input validation wizard modal.
1468
+ * Resolves with the variable definitions on confirm, or null on cancel.
1469
+ */
1470
+ function openValidationWizard() {
1471
+ injectValidationModalCss();
1472
+ return new Promise((resolve) => {
1473
+ const rows = [
1474
+ {
1475
+ name: "",
1476
+ type: "string",
1477
+ required: true,
1478
+ min: "",
1479
+ max: "",
1480
+ minLength: "",
1481
+ maxLength: "",
1482
+ pattern: "",
1483
+ },
1484
+ ];
1485
+ const overlay = document.createElement("div");
1486
+ overlay.className = "bpmnkit-val-overlay";
1487
+ const dialog = document.createElement("div");
1488
+ dialog.className = "bpmnkit-val-dialog";
1489
+ overlay.appendChild(dialog);
1490
+ const title = document.createElement("h2");
1491
+ title.textContent = "Add Input Validation";
1492
+ dialog.appendChild(title);
1493
+ const hint = document.createElement("p");
1494
+ hint.className = "hint";
1495
+ hint.textContent =
1496
+ "Define the variables this process expects. A validation DMN table and wiring will be inserted after the start event.";
1497
+ dialog.appendChild(hint);
1498
+ const tableWrap = document.createElement("div");
1499
+ tableWrap.className = "bpmnkit-val-table";
1500
+ dialog.appendChild(tableWrap);
1501
+ function renderTable() {
1502
+ tableWrap.innerHTML = "";
1503
+ const table = document.createElement("table");
1504
+ const thead = document.createElement("thead");
1505
+ thead.innerHTML = `<tr>
1506
+ <th style="width:22%">Name</th>
1507
+ <th style="width:14%">Type</th>
1508
+ <th style="width:8%;text-align:center">Req.</th>
1509
+ <th style="width:10%">Min</th>
1510
+ <th style="width:10%">Max</th>
1511
+ <th style="width:10%">MinLen</th>
1512
+ <th style="width:10%">MaxLen</th>
1513
+ <th style="width:10%">Pattern</th>
1514
+ <th style="width:6%"></th>
1515
+ </tr>`;
1516
+ table.appendChild(thead);
1517
+ const tbody = document.createElement("tbody");
1518
+ for (let i = 0; i < rows.length; i++) {
1519
+ const row = rows[i];
1520
+ if (!row)
1521
+ continue;
1522
+ const tr = document.createElement("tr");
1523
+ const isNum = row.type === "number";
1524
+ const isStr = row.type === "string";
1525
+ tr.innerHTML = `
1526
+ <td><input type="text" class="v-name" value="${escHtml(row.name)}" placeholder="variableName"/></td>
1527
+ <td><select class="v-type">
1528
+ <option value="string"${row.type === "string" ? " selected" : ""}>string</option>
1529
+ <option value="number"${row.type === "number" ? " selected" : ""}>number</option>
1530
+ <option value="boolean"${row.type === "boolean" ? " selected" : ""}>boolean</option>
1531
+ <option value="context"${row.type === "context" ? " selected" : ""}>context</option>
1532
+ <option value="list"${row.type === "list" ? " selected" : ""}>list</option>
1533
+ <option value="any"${row.type === "any" ? " selected" : ""}>any</option>
1534
+ </select></td>
1535
+ <td class="bpmnkit-val-chk"><input type="checkbox" class="v-req"${row.required ? " checked" : ""}/></td>
1536
+ <td><input type="number" class="v-min" value="${escHtml(row.min)}" placeholder="—"${!isNum ? " disabled" : ""}/></td>
1537
+ <td><input type="number" class="v-max" value="${escHtml(row.max)}" placeholder="—"${!isNum ? " disabled" : ""}/></td>
1538
+ <td><input type="number" class="v-minlen" value="${escHtml(row.minLength)}" placeholder="—"${!isStr ? " disabled" : ""}/></td>
1539
+ <td><input type="number" class="v-maxlen" value="${escHtml(row.maxLength)}" placeholder="—"${!isStr ? " disabled" : ""}/></td>
1540
+ <td><input type="text" class="v-pattern" value="${escHtml(row.pattern)}" placeholder="regex"${!isStr ? " disabled" : ""}/></td>
1541
+ <td><button class="bpmnkit-val-del v-del" title="Remove">✕</button></td>
1542
+ `;
1543
+ const readRow = (idx) => {
1544
+ const r = rows[idx];
1545
+ if (!r)
1546
+ return;
1547
+ r.name = (tr.querySelector(".v-name")?.value ?? "").trim();
1548
+ r.type = (tr.querySelector(".v-type")?.value ??
1549
+ "string");
1550
+ r.required = tr.querySelector(".v-req")?.checked ?? false;
1551
+ r.min = tr.querySelector(".v-min")?.value ?? "";
1552
+ r.max = tr.querySelector(".v-max")?.value ?? "";
1553
+ r.minLength = tr.querySelector(".v-minlen")?.value ?? "";
1554
+ r.maxLength = tr.querySelector(".v-maxlen")?.value ?? "";
1555
+ r.pattern = tr.querySelector(".v-pattern")?.value ?? "";
1556
+ };
1557
+ tr.querySelector(".v-type")?.addEventListener("change", () => {
1558
+ readRow(i);
1559
+ renderTable();
1560
+ });
1561
+ tr.querySelector(".v-name")?.addEventListener("input", () => readRow(i));
1562
+ tr.querySelector(".v-req")?.addEventListener("change", () => readRow(i));
1563
+ tr.querySelector(".v-min")?.addEventListener("input", () => readRow(i));
1564
+ tr.querySelector(".v-max")?.addEventListener("input", () => readRow(i));
1565
+ tr.querySelector(".v-minlen")?.addEventListener("input", () => readRow(i));
1566
+ tr.querySelector(".v-maxlen")?.addEventListener("input", () => readRow(i));
1567
+ tr.querySelector(".v-pattern")?.addEventListener("input", () => readRow(i));
1568
+ tr.querySelector(".v-del")?.addEventListener("click", () => {
1569
+ readRow(i);
1570
+ rows.splice(i, 1);
1571
+ renderTable();
1572
+ });
1573
+ tbody.appendChild(tr);
1574
+ }
1575
+ table.appendChild(tbody);
1576
+ tableWrap.appendChild(table);
1577
+ const addBtn = document.createElement("button");
1578
+ addBtn.className = "bpmnkit-val-add";
1579
+ addBtn.textContent = "+ Add variable";
1580
+ addBtn.addEventListener("click", () => {
1581
+ rows.push({
1582
+ name: "",
1583
+ type: "string",
1584
+ required: true,
1585
+ min: "",
1586
+ max: "",
1587
+ minLength: "",
1588
+ maxLength: "",
1589
+ pattern: "",
1590
+ });
1591
+ renderTable();
1592
+ const inputs = tableWrap.querySelectorAll(".v-name");
1593
+ inputs[inputs.length - 1]?.focus();
1594
+ });
1595
+ tableWrap.appendChild(addBtn);
1596
+ }
1597
+ renderTable();
1598
+ const actions = document.createElement("div");
1599
+ actions.className = "bpmnkit-val-actions";
1600
+ const cancelBtn = document.createElement("button");
1601
+ cancelBtn.className = "bpmnkit-val-btn";
1602
+ cancelBtn.textContent = "Cancel";
1603
+ cancelBtn.addEventListener("click", () => {
1604
+ overlay.remove();
1605
+ resolve(null);
1606
+ });
1607
+ const generateBtn = document.createElement("button");
1608
+ generateBtn.className = "bpmnkit-val-btn bpmnkit-val-btn--primary";
1609
+ generateBtn.textContent = "Generate Validation";
1610
+ generateBtn.addEventListener("click", () => {
1611
+ const defs = rows
1612
+ .filter((r) => r.name)
1613
+ .map((r) => ({
1614
+ name: r.name,
1615
+ type: r.type,
1616
+ required: r.required,
1617
+ ...(r.type === "number" && r.min !== "" ? { min: Number(r.min) } : {}),
1618
+ ...(r.type === "number" && r.max !== "" ? { max: Number(r.max) } : {}),
1619
+ ...(r.type === "string" && r.minLength !== ""
1620
+ ? { minLength: Number(r.minLength) }
1621
+ : {}),
1622
+ ...(r.type === "string" && r.maxLength !== ""
1623
+ ? { maxLength: Number(r.maxLength) }
1624
+ : {}),
1625
+ ...(r.type === "string" && r.pattern ? { pattern: r.pattern } : {}),
1626
+ }));
1627
+ overlay.remove();
1628
+ resolve(defs.length > 0 ? defs : null);
1629
+ });
1630
+ actions.appendChild(cancelBtn);
1631
+ actions.appendChild(generateBtn);
1632
+ dialog.appendChild(actions);
1633
+ overlay.addEventListener("click", (e) => {
1634
+ if (e.target === overlay) {
1635
+ overlay.remove();
1636
+ resolve(null);
1637
+ }
1638
+ });
1639
+ document.body.appendChild(overlay);
1640
+ });
1641
+ }
1642
+ function escHtml(s) {
1643
+ return s
1644
+ .replace(/&/g, "&amp;")
1645
+ .replace(/"/g, "&quot;")
1646
+ .replace(/</g, "&lt;")
1647
+ .replace(/>/g, "&gt;");
1648
+ }
1649
+ function makeStartEventSchema(callbacks) {
1650
+ return {
1651
+ compact: [{ key: "name", label: "Name", type: "text", placeholder: "Event name" }],
1652
+ groups: [
1653
+ {
1654
+ id: "general",
1655
+ label: "General",
1656
+ fields: [
1657
+ { key: "name", label: "Name", type: "text", placeholder: "Event name" },
1658
+ {
1659
+ key: "documentation",
1660
+ label: "Documentation",
1661
+ type: "textarea",
1662
+ placeholder: "Add notes or documentation for this element…",
1663
+ },
1664
+ ],
1665
+ },
1666
+ {
1667
+ id: "input-validation",
1668
+ label: "Input Validation",
1669
+ fields: [
1670
+ // Shown when no validation is configured
1671
+ {
1672
+ key: "_addValidation",
1673
+ label: "Add Input Validation",
1674
+ type: "action",
1675
+ hint: "Generate a DMN validation table and error path after this start event.",
1676
+ condition: (values) => values._hasValidation !== true,
1677
+ onClick: (_values, setValue) => {
1678
+ void openValidationWizard().then(async (vars) => {
1679
+ if (!vars || vars.length === 0)
1680
+ return;
1681
+ const startEventId = _values._elementId;
1682
+ if (!startEventId)
1683
+ return;
1684
+ const decId = validationDecisionId(startEventId);
1685
+ const dmnXml = buildValidationDmn(startEventId, vars);
1686
+ const fileName = `${startEventId}_validation.dmn`;
1687
+ callbacks.onCreateValidationDmn?.(dmnXml, fileName, decId);
1688
+ callbacks.applyChange?.((defs) => insertValidationStructure(defs, startEventId, decId));
1689
+ setValue("_hasValidation", true);
1690
+ setValue("_decisionId", decId);
1691
+ });
1692
+ },
1693
+ },
1694
+ // Shown when validation is already configured
1695
+ {
1696
+ key: "_decisionId",
1697
+ label: "Decision ID",
1698
+ type: "text",
1699
+ condition: (values) => values._hasValidation === true,
1700
+ },
1701
+ {
1702
+ key: "_editValidation",
1703
+ label: "Edit Validation DMN",
1704
+ type: "action",
1705
+ hint: "Open the validation decision table in the DMN editor.",
1706
+ condition: (values) => values._hasValidation === true,
1707
+ onClick: (values) => {
1708
+ const decId = values._decisionId;
1709
+ if (decId)
1710
+ callbacks.onEditValidationDmn?.(decId);
1711
+ },
1712
+ },
1713
+ {
1714
+ key: "_removeValidation",
1715
+ label: "Remove Validation",
1716
+ type: "action",
1717
+ hint: "Delete the validation Business Rule Task, gateway, and error end event.",
1718
+ condition: (values) => values._hasValidation === true,
1719
+ onClick: (values, setValue) => {
1720
+ const startEventId = values._elementId;
1721
+ if (!startEventId)
1722
+ return;
1723
+ callbacks.applyChange?.((defs) => removeValidationStructure(defs, startEventId));
1724
+ setValue("_hasValidation", false);
1725
+ setValue("_decisionId", "");
1726
+ },
1727
+ },
1728
+ ],
1729
+ },
1730
+ ],
1731
+ };
1732
+ }
1733
+ function makeStartEventAdapter(callbacks) {
1734
+ return {
1735
+ read(defs, id) {
1736
+ const el = findFlowElement(defs, id);
1737
+ const structure = findValidationStructure(defs, id);
1738
+ return {
1739
+ name: el?.name ?? "",
1740
+ documentation: el?.documentation ?? "",
1741
+ _elementId: id,
1742
+ _hasValidation: structure !== null,
1743
+ _decisionId: structure?.decisionId ?? "",
1744
+ };
1745
+ },
1746
+ write(defs, id, values) {
1747
+ return updateFlowElement(defs, id, (el) => ({
1748
+ ...el,
1749
+ name: typeof values.name === "string" ? values.name : el.name,
1750
+ documentation: typeof values.documentation === "string"
1751
+ ? values.documentation || undefined
1752
+ : el.documentation,
1753
+ }));
1754
+ },
1755
+ resolve(defs, id) {
1756
+ const el = findFlowElement(defs, id);
1757
+ if (!el || el.type !== "startEvent")
1758
+ return null;
1759
+ const defType = el.eventDefinitions[0]?.type;
1760
+ if (!defType)
1761
+ return null;
1762
+ return eventDefToRegistration(defType);
1763
+ },
1764
+ };
1765
+ }
1260
1766
  // ── Factory ───────────────────────────────────────────────────────────────────
1261
1767
  /**
1262
1768
  * Creates the BPMN config panel extension plugin.
@@ -1272,10 +1778,17 @@ const BOUNDARY_EVENT_ADAPTER = {
1272
1778
  */
1273
1779
  export function createConfigPanelBpmnPlugin(configPanel, options = {}) {
1274
1780
  const userTaskSchema = makeUserTaskSchema();
1275
- const businessRuleTaskSchema = makeBusinessRuleTaskSchema();
1781
+ const businessRuleTaskSchema = makeBusinessRuleTaskSchema(options.onEditValidationDmn);
1276
1782
  const callActivitySchema = makeCallActivitySchema();
1277
1783
  const scriptTaskSchema = makeScriptTaskSchema(options.openFeelPlayground);
1278
1784
  const sequenceFlowSchema = makeSequenceFlowSchema(options.openFeelPlayground);
1785
+ const validationCallbacks = {
1786
+ applyChange: options.applyChange,
1787
+ onCreateValidationDmn: options.onCreateValidationDmn,
1788
+ onEditValidationDmn: options.onEditValidationDmn,
1789
+ };
1790
+ const startEventSchema = makeStartEventSchema(validationCallbacks);
1791
+ const startEventAdapter = makeStartEventAdapter(validationCallbacks);
1279
1792
  return {
1280
1793
  name: "config-panel-bpmn",
1281
1794
  install() {
@@ -1284,7 +1797,7 @@ export function createConfigPanelBpmnPlugin(configPanel, options = {}) {
1284
1797
  configPanel.registerSchema(type, GENERAL_SCHEMA, GENERAL_ADAPTER);
1285
1798
  }
1286
1799
  // Events: dispatcher adapters resolve to event-definition-specific schemas
1287
- configPanel.registerSchema("startEvent", GENERAL_SCHEMA, START_EVENT_ADAPTER);
1800
+ configPanel.registerSchema("startEvent", startEventSchema, startEventAdapter);
1288
1801
  configPanel.registerSchema("endEvent", GENERAL_SCHEMA, END_EVENT_ADAPTER);
1289
1802
  configPanel.registerSchema("intermediateCatchEvent", GENERAL_SCHEMA, CATCH_EVENT_ADAPTER);
1290
1803
  configPanel.registerSchema("intermediateThrowEvent", GENERAL_SCHEMA, THROW_EVENT_ADAPTER);
@@ -1297,6 +1810,8 @@ export function createConfigPanelBpmnPlugin(configPanel, options = {}) {
1297
1810
  configPanel.registerSchema("businessRuleTask", businessRuleTaskSchema, BUSINESS_RULE_TASK_ADAPTER);
1298
1811
  // Service task: template-aware adapter
1299
1812
  configPanel.registerSchema("serviceTask", GENERIC_SERVICE_TASK_SCHEMA, SERVICE_TASK_ADAPTER);
1813
+ // Sub-process: general fields + multi-instance configuration
1814
+ configPanel.registerSchema("subProcess", SUB_PROCESS_SCHEMA, SUB_PROCESS_ADAPTER);
1300
1815
  // Ad-hoc subprocess: template-aware adapter (AI Agent pattern)
1301
1816
  configPanel.registerSchema("adHocSubProcess", GENERIC_ADHOC_SCHEMA, ADHOC_SUBPROCESS_ADAPTER);
1302
1817
  // Script task: FEEL expression + result variable
@@ -5,6 +5,20 @@ export declare function xmlLocalName(qname: string): string;
5
5
  export declare function findFlowElement(defs: BpmnDefinitions, id: string): BpmnFlowElement | undefined;
6
6
  export declare function updateFlowElement(defs: BpmnDefinitions, id: string, fn: (el: BpmnFlowElement) => BpmnFlowElement): BpmnDefinitions;
7
7
  export declare function parseZeebeExtensions(extensionElements: XmlElement[]): ZeebeExtensions;
8
+ /** Parsed `zeebe:loopCharacteristics` extension element from a multi-instance loop. */
9
+ export interface ZeebeLoopCharacteristics {
10
+ inputCollection: string;
11
+ inputElement: string;
12
+ outputCollection?: string;
13
+ outputElement?: string;
14
+ }
15
+ /** Read `zeebe:loopCharacteristics` from a `multiInstanceLoopCharacteristics` extensionElements list. */
16
+ export declare function parseZeebeLoopCharacteristics(extensionElements: XmlElement[]): ZeebeLoopCharacteristics | undefined;
17
+ /** Build a `zeebe:loopCharacteristics` XmlElement from collection/element options. */
18
+ export declare function buildZeebeLoopCharacteristics(opts: {
19
+ inputCollection: string;
20
+ inputElement: string;
21
+ }): XmlElement;
8
22
  /** Read the example output JSON string from parsed zeebe extensions. */
9
23
  export declare function getExampleOutputJson(ext: ZeebeExtensions): string;
10
24
  /**
@@ -101,6 +101,27 @@ export function parseZeebeExtensions(extensionElements) {
101
101
  }
102
102
  return ext;
103
103
  }
104
+ /** Read `zeebe:loopCharacteristics` from a `multiInstanceLoopCharacteristics` extensionElements list. */
105
+ export function parseZeebeLoopCharacteristics(extensionElements) {
106
+ const loopEl = extensionElements.find((e) => xmlLocalName(e.name) === "loopCharacteristics");
107
+ if (!loopEl)
108
+ return undefined;
109
+ return {
110
+ inputCollection: loopEl.attributes.inputCollection ?? "",
111
+ inputElement: loopEl.attributes.inputElement ?? "",
112
+ outputCollection: loopEl.attributes.outputCollection,
113
+ outputElement: loopEl.attributes.outputElement,
114
+ };
115
+ }
116
+ /** Build a `zeebe:loopCharacteristics` XmlElement from collection/element options. */
117
+ export function buildZeebeLoopCharacteristics(opts) {
118
+ const attrs = {};
119
+ if (opts.inputCollection)
120
+ attrs.inputCollection = opts.inputCollection;
121
+ if (opts.inputElement)
122
+ attrs.inputElement = opts.inputElement;
123
+ return { name: "zeebe:loopCharacteristics", attributes: attrs, children: [] };
124
+ }
104
125
  const EXAMPLE_OUTPUT_JSON_KEY = "camundaModeler:exampleOutputJson";
105
126
  /** Read the example output JSON string from parsed zeebe extensions. */
106
127
  export function getExampleOutputJson(ext) {