@masterteam/flowplus-workflow 0.0.2 → 0.0.4

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.
@@ -27,7 +27,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
27
27
  import { DateField } from '@masterteam/components/date-field';
28
28
  import { ToastService } from '@masterteam/components/toast';
29
29
  import * as i1$2 from '@foblex/flow';
30
- import { FExternalItem, FExternalItemPreview, FFlowModule, EFMarkerType, provideFFlow, withReflowOnResize, EFReflowDeltaSource, EFReflowCollision } from '@foblex/flow';
30
+ import { FExternalItem, FExternalItemPreview, EFResizeHandleType, FFlowModule, EFMarkerType, EFCanvasLayer, provideFFlow, withReflowOnResize, EFReflowDeltaSource, EFReflowCollision } from '@foblex/flow';
31
31
  import { Avatar } from '@masterteam/components/avatar';
32
32
  import { RadioCardsField } from '@masterteam/components/radio-cards-field';
33
33
  import { DynamicForm } from '@masterteam/forms/dynamic-form';
@@ -1244,16 +1244,16 @@ function defaultApprovalDecisionValues(routeOutputKeys) {
1244
1244
  function dynamicRouteOutputKeysForConfig$2(nodeType, config) {
1245
1245
  if (nodeType === 'ParallelStart') {
1246
1246
  return uniqueStrings$2(readArrayRecords$3(config['branches'])
1247
- .map((branch) => readString$d(branch, 'key'))
1247
+ .map((branch) => readString$e(branch, 'key'))
1248
1248
  .filter((key) => !!key?.trim()));
1249
1249
  }
1250
1250
  if (nodeType === 'Switch') {
1251
1251
  const caseKeys = readArrayRecords$3(config['cases'])
1252
- .map((item) => readString$d(item, 'key'))
1252
+ .map((item) => readString$e(item, 'key'))
1253
1253
  .filter((key) => !!key?.trim())
1254
1254
  .map((key) => switchCaseRouteKey$3(key));
1255
- const defaultKey = readString$d(config, 'defaultOutputKey') ??
1256
- readString$d(config, 'defaultCaseKey') ??
1255
+ const defaultKey = readString$e(config, 'defaultOutputKey') ??
1256
+ readString$e(config, 'defaultCaseKey') ??
1257
1257
  'default';
1258
1258
  return uniqueStrings$2([
1259
1259
  ...caseKeys,
@@ -1397,7 +1397,7 @@ function readRecord$3(value) {
1397
1397
  ? value
1398
1398
  : null;
1399
1399
  }
1400
- function readString$d(record, key) {
1400
+ function readString$e(record, key) {
1401
1401
  const value = record?.[key];
1402
1402
  return typeof value === 'string' ? value : undefined;
1403
1403
  }
@@ -1593,19 +1593,19 @@ function automationBuilderCatalogToWorkflowCatalog(catalog) {
1593
1593
  const triggerTypes = (catalog.triggerTypes ?? []).map((item) => {
1594
1594
  const raw = asRecord$7(item);
1595
1595
  const type = catalogTriggerType$2(item);
1596
- const key = readString$c(raw, 'key') ?? type;
1596
+ const key = readString$d(raw, 'key') ?? type;
1597
1597
  return {
1598
1598
  key,
1599
1599
  value: type,
1600
1600
  label: primaryText(item.displayName) ?? type,
1601
1601
  displayName: item.displayName ?? type,
1602
1602
  description: item.description ?? null,
1603
- icon: readString$c(raw, 'icon') ?? readString$c(raw, 'iconKey') ?? null,
1603
+ icon: readString$d(raw, 'icon') ?? readString$d(raw, 'iconKey') ?? null,
1604
1604
  metadata: {
1605
1605
  automationTriggerType: true,
1606
1606
  type,
1607
1607
  triggerType: type,
1608
- category: readString$c(raw, 'category') ?? null,
1608
+ category: readString$d(raw, 'category') ?? null,
1609
1609
  configSchema: item.configSchema ?? null,
1610
1610
  authPolicySchema: item.authPolicySchema ?? null,
1611
1611
  payloadSchema: item.payloadSchema ?? null,
@@ -1776,8 +1776,8 @@ function workflowMetadataRequestToAutomationMetadata(request) {
1776
1776
  name: primaryText(request.name) ?? 'Untitled automation',
1777
1777
  };
1778
1778
  const description = primaryText(request.description);
1779
- const ownerId = readString$c(metadata, 'ownerId');
1780
- const projectId = readString$c(metadata, 'projectId');
1779
+ const ownerId = readString$d(metadata, 'ownerId');
1780
+ const projectId = readString$d(metadata, 'projectId');
1781
1781
  if (description)
1782
1782
  payload.description = description;
1783
1783
  if (ownerId)
@@ -1793,17 +1793,17 @@ function workflowStepToAutomationNodeRequest(step, existing) {
1793
1793
  };
1794
1794
  const key = step.key ??
1795
1795
  existing?.key ??
1796
- readString$c(metadata, 'nodeKey') ??
1796
+ readString$d(metadata, 'nodeKey') ??
1797
1797
  keyFromText(step.name ?? existing?.name ?? 'node');
1798
1798
  const type = String(step.type ?? existing?.type ?? 'SetFields');
1799
1799
  const mergedConfig = {
1800
1800
  ...readConfigFromStep(existing),
1801
1801
  ...readConfigFromStep(step),
1802
1802
  };
1803
- const configJson = readString$c(metadata, 'configJson') ?? stringifyJson$1(mergedConfig) ?? '{}';
1803
+ const configJson = readString$d(metadata, 'configJson') ?? stringifyJson$1(mergedConfig) ?? '{}';
1804
1804
  const positionJson = layoutToJson(step.layout) ??
1805
1805
  layoutToJson(existing?.layout) ??
1806
- readString$c(metadata, 'positionJson') ??
1806
+ readString$d(metadata, 'positionJson') ??
1807
1807
  stringifyJson$1(readRecord$2(metadata, 'position')) ??
1808
1808
  '{}';
1809
1809
  const nodeType = String(step.type ?? existing?.type ?? 'SetFields');
@@ -1817,7 +1817,7 @@ function workflowStepToAutomationNodeRequest(step, existing) {
1817
1817
  timeoutPolicyJson: jsonField(metadata, 'timeoutPolicyJson', '{}'),
1818
1818
  retryPolicyJson: jsonField(metadata, 'retryPolicyJson', '{}'),
1819
1819
  errorPolicyJson: jsonField(metadata, 'errorPolicyJson', '{}'),
1820
- sideEffectPolicy: readString$c(metadata, 'sideEffectPolicy') ??
1820
+ sideEffectPolicy: readString$d(metadata, 'sideEffectPolicy') ??
1821
1821
  defaultSideEffectPolicyForNodeType(nodeType),
1822
1822
  };
1823
1823
  }
@@ -1850,7 +1850,7 @@ function workflowTriggerToAutomationTriggerRequest(trigger, existing) {
1850
1850
  ...(asRecord$7(trigger.metadata) ?? {}),
1851
1851
  };
1852
1852
  const type = String(trigger.type ?? existing?.type ?? 'ManualTrigger');
1853
- const key = readString$c(metadata, 'triggerKey') ??
1853
+ const key = readString$d(metadata, 'triggerKey') ??
1854
1854
  trigger.webhookKey ??
1855
1855
  existing?.webhookKey ??
1856
1856
  keyFromText(`${type}_trigger`);
@@ -1860,7 +1860,20 @@ function workflowTriggerToAutomationTriggerRequest(trigger, existing) {
1860
1860
  ...parseJsonObject$4(trigger.configJson),
1861
1861
  ...readRecord$2(metadata, 'config'),
1862
1862
  };
1863
- const startNodeKey = readString$c(metadata, 'startNodeKey') ?? readString$c(config, 'startNodeKey');
1863
+ const layout = readRecord$2(metadata, 'layout');
1864
+ const layoutX = readNumber$5(layout, 'x');
1865
+ const layoutY = readNumber$5(layout, 'y');
1866
+ if (layoutX != null && layoutY != null) {
1867
+ const ui = { ...readRecord$2(config['ui']) };
1868
+ ui['layout'] = {
1869
+ x: layoutX,
1870
+ y: layoutY,
1871
+ width: readNumber$5(layout, 'width') ?? null,
1872
+ height: readNumber$5(layout, 'height') ?? null,
1873
+ };
1874
+ config['ui'] = ui;
1875
+ }
1876
+ const startNodeKey = readString$d(metadata, 'startNodeKey') ?? readString$d(config, 'startNodeKey');
1864
1877
  if (startNodeKey)
1865
1878
  config['startNodeKey'] = startNodeKey;
1866
1879
  return {
@@ -1872,7 +1885,7 @@ function workflowTriggerToAutomationTriggerRequest(trigger, existing) {
1872
1885
  existing?.isEnabled ??
1873
1886
  existing?.enabled ??
1874
1887
  true,
1875
- configJson: trigger.configJson ?? stringifyJson$1(config) ?? '{}',
1888
+ configJson: stringifyJson$1(config) ?? '{}',
1876
1889
  schemaJson: trigger.schemaJson ??
1877
1890
  existing?.schemaJson ??
1878
1891
  stringifyJson$1(trigger.payloadSchema) ??
@@ -1897,37 +1910,37 @@ function workflowConnectionToAutomationRouteRequest(connection, index, existing)
1897
1910
  const targetStepId = readNumber$5(connectionRecord, 'targetStepId') ??
1898
1911
  existing?.targetStepId ??
1899
1912
  0;
1900
- const sourceNodeKey = readString$c(metadata, 'sourceNodeKey') ??
1913
+ const sourceNodeKey = readString$d(metadata, 'sourceNodeKey') ??
1901
1914
  index.nodeKeyByStepId.get(sourceStepId) ??
1902
1915
  existing?.sourceStepKey ??
1903
1916
  '';
1904
- const targetNodeKey = readString$c(metadata, 'targetNodeKey') ??
1917
+ const targetNodeKey = readString$d(metadata, 'targetNodeKey') ??
1905
1918
  index.nodeKeyByStepId.get(targetStepId) ??
1906
1919
  existing?.targetStepKey ??
1907
1920
  '';
1908
- const sourcePortKey = readString$c(connectionRecord, 'sourcePortKey');
1921
+ const sourcePortKey = readString$d(connectionRecord, 'sourcePortKey');
1909
1922
  const sourceOutputChanged = sourcePortKey != null &&
1910
1923
  existing?.sourcePortKey != null &&
1911
1924
  normalizeRouteKey(sourcePortKey) !== normalizeRouteKey(existing.sourcePortKey);
1912
- const sourceOutputKey = readString$c(connectionMetadata, 'sourceOutputKey') ??
1925
+ const sourceOutputKey = readString$d(connectionMetadata, 'sourceOutputKey') ??
1913
1926
  sourcePortKey ??
1914
1927
  existing?.sourcePortKey ??
1915
1928
  readSelectedActionKey(connection) ??
1916
1929
  '';
1917
- const explicitConditionJson = readString$c(connectionMetadata, 'conditionJson') ??
1930
+ const explicitConditionJson = readString$d(connectionMetadata, 'conditionJson') ??
1918
1931
  readExpressionText(connection);
1919
1932
  const preservedConditionJson = sourceOutputChanged
1920
1933
  ? null
1921
- : (readString$c(existingMetadata, 'conditionJson') ??
1934
+ : (readString$d(existingMetadata, 'conditionJson') ??
1922
1935
  existing?.expressionText ??
1923
1936
  null);
1924
1937
  const conditionJson = explicitConditionJson ??
1925
1938
  preservedConditionJson ??
1926
1939
  defaultConditionJsonForRoute(sourceOutputKey);
1927
- const routeType = normalizeAutomationRouteType(readString$c(connectionMetadata, 'routeType') ??
1928
- (sourceOutputChanged ? null : readString$c(existingMetadata, 'routeType')), sourceOutputKey, conditionJson);
1940
+ const routeType = normalizeAutomationRouteType(readString$d(connectionMetadata, 'routeType') ??
1941
+ (sourceOutputChanged ? null : readString$d(existingMetadata, 'routeType')), sourceOutputKey, conditionJson);
1929
1942
  return {
1930
- routeId: readString$c(metadata, 'routeId') ??
1943
+ routeId: readString$d(metadata, 'routeId') ??
1931
1944
  index.routeIdByConnectionId.get(readNumber$5(connectionRecord, 'id') ?? 0) ??
1932
1945
  null,
1933
1946
  sourceNodeKey,
@@ -1942,11 +1955,11 @@ function findWorkflowStepByNodeKey(builder, nodeKey) {
1942
1955
  return builder.steps.find((step) => step.key === nodeKey) ?? null;
1943
1956
  }
1944
1957
  function findWorkflowTriggerByKey(builder, triggerKey) {
1945
- return (builder.triggers.find((trigger) => readString$c(asRecord$7(trigger.metadata), 'triggerKey') === triggerKey) ?? null);
1958
+ return (builder.triggers.find((trigger) => readString$d(asRecord$7(trigger.metadata), 'triggerKey') === triggerKey) ?? null);
1946
1959
  }
1947
1960
  function findWorkflowConnectionForRoute(builder, route, routeId) {
1948
1961
  if (routeId != null) {
1949
- const exact = builder.connections.find((connection) => readString$c(asRecord$7(connection.metadata), 'routeId') ===
1962
+ const exact = builder.connections.find((connection) => readString$d(asRecord$7(connection.metadata), 'routeId') ===
1950
1963
  String(routeId));
1951
1964
  if (exact)
1952
1965
  return exact;
@@ -1958,7 +1971,7 @@ function findWorkflowConnectionForRoute(builder, route, routeId) {
1958
1971
  function automationNodeToWorkflowStep(node, workflowId, index, triggerStartKeys, routeOutputsByType) {
1959
1972
  const raw = asRecord$7(node);
1960
1973
  const nodeKey = node.key || keyFromText(node.name ?? `node_${index}`);
1961
- const nodeType = readString$c(raw, 'type') ?? readString$c(raw, 'nodeType') ?? 'SetFields';
1974
+ const nodeType = readString$d(raw, 'type') ?? readString$d(raw, 'nodeType') ?? 'SetFields';
1962
1975
  const id = readNumber$5(raw, 'nodeId') ??
1963
1976
  readNumber$5(raw, 'id') ??
1964
1977
  toWorkflowEntityId(nodeKey, `node:${workflowId}`);
@@ -2013,11 +2026,11 @@ function automationNodeToWorkflowStep(node, workflowId, index, triggerStartKeys,
2013
2026
  }
2014
2027
  function automationTriggerToWorkflowTrigger(trigger, workflowId, index) {
2015
2028
  const raw = asRecord$7(trigger);
2016
- const triggerType = readString$c(raw, 'type') ??
2017
- readString$c(raw, 'triggerType') ??
2029
+ const triggerType = readString$d(raw, 'type') ??
2030
+ readString$d(raw, 'triggerType') ??
2018
2031
  'ManualTrigger';
2019
2032
  const triggerKey = trigger.key ||
2020
- readString$c(raw, 'key') ||
2033
+ readString$d(raw, 'key') ||
2021
2034
  keyFromText(`${triggerType}_trigger`);
2022
2035
  const id = readNumber$5(raw, 'triggerId') ??
2023
2036
  readNumber$5(raw, 'id') ??
@@ -2040,7 +2053,7 @@ function automationTriggerToWorkflowTrigger(trigger, workflowId, index) {
2040
2053
  name: toTranslatableText$1(trigger.name || triggerKey),
2041
2054
  enabled,
2042
2055
  isEnabled: enabled,
2043
- webhookKey: readString$c(config, 'webhookKey') ?? triggerKey,
2056
+ webhookKey: readString$d(config, 'webhookKey') ?? triggerKey,
2044
2057
  workflowKey: triggerKey,
2045
2058
  triggerId: id,
2046
2059
  payloadSchema: parseJsonValue$2(trigger.schemaJson ?? null),
@@ -2058,7 +2071,7 @@ function automationTriggerToWorkflowTrigger(trigger, workflowId, index) {
2058
2071
  layout,
2059
2072
  schemaJson: trigger.schemaJson ?? null,
2060
2073
  authenticationPolicyJson: trigger.authenticationPolicyJson ?? null,
2061
- startNodeKey: readString$c(config, 'startNodeKey') ?? null,
2074
+ startNodeKey: readString$d(config, 'startNodeKey') ?? null,
2062
2075
  },
2063
2076
  };
2064
2077
  }
@@ -2218,7 +2231,7 @@ function automationDetailToWorkflowLayout(workflowId, steps, connections, trigge
2218
2231
  function automationNodeTypeToWorkflowStepType(item) {
2219
2232
  const raw = asRecord$7(item);
2220
2233
  const type = catalogNodeType$1(item);
2221
- const key = readString$c(raw, 'key') ?? type;
2234
+ const key = readString$d(raw, 'key') ?? type;
2222
2235
  const routeOutputKeys = readStringArray$7(raw, 'routeOutputKeys');
2223
2236
  const supportsRetry = ['HTTP', 'FlowPlusCommit', 'CallAutomation'].includes(type) ||
2224
2237
  !!item.defaultRetryPolicy;
@@ -2244,9 +2257,9 @@ function automationNodeTypeToWorkflowStepType(item) {
2244
2257
  typeKey: type,
2245
2258
  displayName: item.displayName ?? type,
2246
2259
  description: item.description ?? null,
2247
- category: readString$c(raw, 'category') ?? categoryForNodeType(type),
2248
- icon: readString$c(raw, 'icon') ??
2249
- readString$c(raw, 'iconKey') ??
2260
+ category: readString$d(raw, 'category') ?? categoryForNodeType(type),
2261
+ icon: readString$d(raw, 'icon') ??
2262
+ readString$d(raw, 'iconKey') ??
2250
2263
  iconForNodeType(type),
2251
2264
  colorToken: item.colorToken ?? type,
2252
2265
  tags: [type],
@@ -2322,7 +2335,7 @@ function routeOutputKeysByNodeType(catalog) {
2322
2335
  for (const item of catalog?.nodeTypes ?? []) {
2323
2336
  const raw = asRecord$7(item);
2324
2337
  const type = catalogNodeType$1(item);
2325
- const key = readString$c(raw, 'key') ?? type;
2338
+ const key = readString$d(raw, 'key') ?? type;
2326
2339
  const keys = readStringArray$7(raw, 'routeOutputKeys');
2327
2340
  out.set(type, keys);
2328
2341
  out.set(key, keys);
@@ -2349,17 +2362,17 @@ function dynamicRouteOutputKeysForConfig$1(nodeType, config) {
2349
2362
  if (nodeType === 'ParallelStart') {
2350
2363
  const branches = arrayRecords(config['branches']);
2351
2364
  return uniqueStrings$1(branches
2352
- .map((branch) => readString$c(branch, 'key'))
2365
+ .map((branch) => readString$d(branch, 'key'))
2353
2366
  .filter((key) => !!key?.trim()));
2354
2367
  }
2355
2368
  if (nodeType === 'Switch') {
2356
2369
  const cases = arrayRecords(config['cases']);
2357
2370
  const caseKeys = cases
2358
- .map((item) => readString$c(item, 'key'))
2371
+ .map((item) => readString$d(item, 'key'))
2359
2372
  .filter((key) => !!key?.trim())
2360
2373
  .map((key) => switchCaseRouteKey$2(key));
2361
- const defaultKey = readString$c(config, 'defaultOutputKey') ??
2362
- readString$c(config, 'defaultCaseKey') ??
2374
+ const defaultKey = readString$d(config, 'defaultOutputKey') ??
2375
+ readString$d(config, 'defaultCaseKey') ??
2363
2376
  'default';
2364
2377
  return uniqueStrings$1([
2365
2378
  ...caseKeys,
@@ -2400,7 +2413,7 @@ function readConfigFromStep(step) {
2400
2413
  return {};
2401
2414
  const metadata = asRecord$7(step.metadata);
2402
2415
  return stripUndefined({
2403
- ...parseJsonObject$4(readString$c(metadata, 'configJson')),
2416
+ ...parseJsonObject$4(readString$d(metadata, 'configJson')),
2404
2417
  ...readRecord$2(metadata, 'config'),
2405
2418
  automated: step.automated ?? undefined,
2406
2419
  plugin: step.plugin ?? undefined,
@@ -2465,7 +2478,7 @@ function readTriggerStartNodeKey(trigger) {
2465
2478
  ...parseJsonObject$4(trigger.configJson),
2466
2479
  ...readRecord$2(asRecord$7(trigger), 'config'),
2467
2480
  };
2468
- return readString$c(config, 'startNodeKey') ?? null;
2481
+ return readString$d(config, 'startNodeKey') ?? null;
2469
2482
  }
2470
2483
  function positionToLayout(workflowId, stepId, stepKey, position, index) {
2471
2484
  return {
@@ -2520,7 +2533,7 @@ function isWorkflowNodeLayout(layout) {
2520
2533
  return !!layout;
2521
2534
  }
2522
2535
  function jsonField(record, jsonKey, fallback) {
2523
- const existing = readString$c(record, jsonKey);
2536
+ const existing = readString$d(record, jsonKey);
2524
2537
  if (existing != null)
2525
2538
  return existing;
2526
2539
  const valueKey = jsonKey.replace(/Json$/, '');
@@ -2531,14 +2544,14 @@ function readSelectedActionKey(value) {
2531
2544
  const selected = raw['selectedActions'];
2532
2545
  if (!Array.isArray(selected) || selected.length === 0)
2533
2546
  return null;
2534
- return readString$c(asRecord$7(selected[0]), 'actionKey') ?? null;
2547
+ return readString$d(asRecord$7(selected[0]), 'actionKey') ?? null;
2535
2548
  }
2536
2549
  function readExpressionText(value) {
2537
2550
  const raw = asRecord$7(value);
2538
- return (readString$c(raw, 'expressionText') ??
2539
- readString$c(raw, 'expression') ??
2540
- readString$c(raw, 'formulaRaw') ??
2541
- readString$c(asRecord$7(raw['formula']), 'expression') ??
2551
+ return (readString$d(raw, 'expressionText') ??
2552
+ readString$d(raw, 'expression') ??
2553
+ readString$d(raw, 'formulaRaw') ??
2554
+ readString$d(asRecord$7(raw['formula']), 'expression') ??
2542
2555
  null);
2543
2556
  }
2544
2557
  function categoryForNodeType(type) {
@@ -2599,23 +2612,23 @@ function defaultSideEffectPolicyForNodeType(type) {
2599
2612
  }
2600
2613
  function catalogNodeType$1(item) {
2601
2614
  const raw = asRecord$7(item);
2602
- return (readString$c(raw, 'type') ??
2603
- readString$c(raw, 'nodeType') ??
2604
- readString$c(raw, 'key') ??
2615
+ return (readString$d(raw, 'type') ??
2616
+ readString$d(raw, 'nodeType') ??
2617
+ readString$d(raw, 'key') ??
2605
2618
  'SetFields');
2606
2619
  }
2607
2620
  function catalogTriggerType$2(item) {
2608
2621
  const raw = asRecord$7(item);
2609
- return (readString$c(raw, 'type') ??
2610
- readString$c(raw, 'triggerType') ??
2611
- readString$c(raw, 'key') ??
2622
+ return (readString$d(raw, 'type') ??
2623
+ readString$d(raw, 'triggerType') ??
2624
+ readString$d(raw, 'key') ??
2612
2625
  'ManualTrigger');
2613
2626
  }
2614
2627
  function namespaceKey(namespace) {
2615
2628
  const raw = asRecord$7(namespace);
2616
- return (readString$c(raw, 'key') ??
2617
- readString$c(raw, 'name') ??
2618
- readString$c(raw, 'label') ??
2629
+ return (readString$d(raw, 'key') ??
2630
+ readString$d(raw, 'name') ??
2631
+ readString$d(raw, 'label') ??
2619
2632
  '$json');
2620
2633
  }
2621
2634
  function namespacePaths(namespace) {
@@ -2647,9 +2660,9 @@ function primaryText(value) {
2647
2660
  return value || null;
2648
2661
  if (value && typeof value === 'object') {
2649
2662
  const raw = value;
2650
- return (readString$c(raw, 'en') ??
2651
- readString$c(raw, 'display') ??
2652
- readString$c(raw, 'ar') ??
2663
+ return (readString$d(raw, 'en') ??
2664
+ readString$d(raw, 'display') ??
2665
+ readString$d(raw, 'ar') ??
2653
2666
  null);
2654
2667
  }
2655
2668
  return null;
@@ -2657,9 +2670,9 @@ function primaryText(value) {
2657
2670
  function toTranslatableText$1(value) {
2658
2671
  if (value && typeof value === 'object' && !Array.isArray(value)) {
2659
2672
  const raw = value;
2660
- const en = readString$c(raw, 'en');
2661
- const ar = readString$c(raw, 'ar');
2662
- const display = readString$c(raw, 'display');
2673
+ const en = readString$d(raw, 'en');
2674
+ const ar = readString$d(raw, 'ar');
2675
+ const display = readString$d(raw, 'display');
2663
2676
  return {
2664
2677
  ...raw,
2665
2678
  en: en ?? ar ?? display ?? '',
@@ -2738,7 +2751,7 @@ function asRecord$7(value) {
2738
2751
  return {};
2739
2752
  return value;
2740
2753
  }
2741
- function readString$c(record, key) {
2754
+ function readString$d(record, key) {
2742
2755
  const value = record?.[key];
2743
2756
  return typeof value === 'string' ? value : undefined;
2744
2757
  }
@@ -2752,31 +2765,31 @@ function readBoolean$6(record, key) {
2752
2765
 
2753
2766
  function normalizeWorkflowDefinitionSummaryDto(value) {
2754
2767
  const raw = asRecord$6(value);
2755
- const moduleType = readString$b(raw, 'moduleType');
2756
- const primaryModuleKey = readString$b(raw, 'primaryModuleKey');
2768
+ const moduleType = readString$c(raw, 'moduleType');
2769
+ const primaryModuleKey = readString$c(raw, 'primaryModuleKey');
2757
2770
  const isPublished = readBoolean$5(raw, 'isPublished');
2758
2771
  const isValid = readBoolean$5(raw, 'isValid');
2759
2772
  return {
2760
2773
  ...spreadAs(raw),
2761
2774
  id: readNumber$4(raw, 'id'),
2762
- key: readString$b(raw, 'key'),
2775
+ key: readString$c(raw, 'key'),
2763
2776
  name: toTranslatableText(raw['name']),
2764
2777
  description: toTranslatableText(raw['description']),
2765
- status: readString$b(raw, 'status') ?? (isPublished ? 'Published' : 'Draft'),
2778
+ status: readString$c(raw, 'status') ?? (isPublished ? 'Published' : 'Draft'),
2766
2779
  isPublished,
2767
2780
  isValid,
2768
2781
  primaryModuleId: readNullableNumber(raw, 'primaryModuleId'),
2769
2782
  primaryModuleKey,
2770
2783
  moduleType,
2771
- primaryModuleType: readString$b(raw, 'primaryModuleType') ?? moduleType ?? primaryModuleKey,
2772
- operationType: readString$b(raw, 'operationType'),
2773
- commandName: readString$b(raw, 'commandName'),
2784
+ primaryModuleType: readString$c(raw, 'primaryModuleType') ?? moduleType ?? primaryModuleKey,
2785
+ operationType: readString$c(raw, 'operationType'),
2786
+ commandName: readString$c(raw, 'commandName'),
2774
2787
  hasUnpublishedChanges: readNullableBoolean(raw, 'hasUnpublishedChanges'),
2775
2788
  version: readNullableNumber(raw, 'version'),
2776
2789
  draftVersion: readNullableNumber(raw, 'draftVersion'),
2777
2790
  publishedVersion: readNullableNumber(raw, 'publishedVersion'),
2778
- rowVersion: readString$b(raw, 'rowVersion'),
2779
- triggerType: readString$b(raw, 'triggerType'),
2791
+ rowVersion: readString$c(raw, 'rowVersion'),
2792
+ triggerType: readString$c(raw, 'triggerType'),
2780
2793
  };
2781
2794
  }
2782
2795
  function normalizeWorkflowDefinitionDto(value) {
@@ -2784,9 +2797,9 @@ function normalizeWorkflowDefinitionDto(value) {
2784
2797
  }
2785
2798
  function normalizeWorkflowTriggerDto(value) {
2786
2799
  const raw = asRecord$6(value);
2787
- const payloadSchemaJson = readString$b(raw, 'payloadSchemaJson') ?? readString$b(raw, 'payloadSchema');
2800
+ const payloadSchemaJson = readString$c(raw, 'payloadSchemaJson') ?? readString$c(raw, 'payloadSchema');
2788
2801
  const metadata = readObject$3(raw, 'metadata') ?? {};
2789
- const parsedConfig = parseJsonObject$3(readString$b(raw, 'configJson'));
2802
+ const parsedConfig = parseJsonObject$3(readString$c(raw, 'configJson'));
2790
2803
  if (parsedConfig && !metadata['config'])
2791
2804
  metadata['config'] = parsedConfig;
2792
2805
  return {
@@ -2794,8 +2807,8 @@ function normalizeWorkflowTriggerDto(value) {
2794
2807
  id: readNumber$4(raw, 'id'),
2795
2808
  workflowId: readNumber$4(raw, 'workflowId', null) ?? readNumber$4(raw, 'requestSchemaId'),
2796
2809
  requestSchemaId: readNullableNumber(raw, 'requestSchemaId'),
2797
- type: (readString$b(raw, 'type') ?? 'Manual'),
2798
- typeKey: readString$b(raw, 'typeKey'),
2810
+ type: (readString$c(raw, 'type') ?? 'Manual'),
2811
+ typeKey: readString$c(raw, 'typeKey'),
2799
2812
  name: toTranslatableText(raw['name']),
2800
2813
  enabled: readNullableBoolean(raw, 'enabled') ??
2801
2814
  readNullableBoolean(raw, 'isEnabled') ??
@@ -2806,24 +2819,24 @@ function normalizeWorkflowTriggerDto(value) {
2806
2819
  payloadSchema: parseJsonSchema(payloadSchemaJson) ??
2807
2820
  payloadSchemaJson,
2808
2821
  payloadSchemaJson,
2809
- initiationPropertyMappings: readString$b(raw, 'initiationPropertyMappings'),
2822
+ initiationPropertyMappings: readString$c(raw, 'initiationPropertyMappings'),
2810
2823
  targetProcessSchemaId: readNullableNumber(raw, 'targetProcessSchemaId'),
2811
2824
  targetProcessSchemaName: toTranslatableText(raw['targetProcessSchemaName']),
2812
- expectedHttpMethod: readString$b(raw, 'expectedHttpMethod'),
2813
- workflowKey: readString$b(raw, 'workflowKey'),
2825
+ expectedHttpMethod: readString$c(raw, 'expectedHttpMethod'),
2826
+ workflowKey: readString$c(raw, 'workflowKey'),
2814
2827
  triggerId: readNullableNumber(raw, 'triggerId') ?? readNullableNumber(raw, 'id'),
2815
2828
  allowedContentTypes: readStringArray$6(raw, 'allowedContentTypes'),
2816
2829
  examplePayload: raw['examplePayload'],
2817
- exampleCurl: readString$b(raw, 'exampleCurl'),
2830
+ exampleCurl: readString$c(raw, 'exampleCurl'),
2818
2831
  secretConfigured: readNullableBoolean(raw, 'secretConfigured') ??
2819
2832
  readNullableBoolean(raw, 'hasSecret') ??
2820
2833
  false,
2821
2834
  hasSecret: readNullableBoolean(raw, 'hasSecret') ??
2822
2835
  readNullableBoolean(raw, 'secretConfigured') ??
2823
2836
  false,
2824
- configJson: readString$b(raw, 'configJson'),
2825
- schemaJson: readString$b(raw, 'schemaJson'),
2826
- authenticationPolicyJson: readString$b(raw, 'authenticationPolicyJson'),
2837
+ configJson: readString$c(raw, 'configJson'),
2838
+ schemaJson: readString$c(raw, 'schemaJson'),
2839
+ authenticationPolicyJson: readString$c(raw, 'authenticationPolicyJson'),
2827
2840
  metadata,
2828
2841
  };
2829
2842
  }
@@ -2836,18 +2849,18 @@ function normalizeWorkflowStepDto(value) {
2836
2849
  id: readNumber$4(raw, 'id'),
2837
2850
  requestSchemaId: readNumber$4(raw, 'requestSchemaId', null) ??
2838
2851
  readNumber$4(raw, 'requestSchemaId'),
2839
- key: readString$b(raw, 'key') ?? `step_${readNumber$4(raw, 'id')}`,
2852
+ key: readString$c(raw, 'key') ?? `step_${readNumber$4(raw, 'id')}`,
2840
2853
  name: toTranslatableText(raw['name']),
2841
2854
  description: toTranslatableText(raw['description']),
2842
- type: (readString$b(raw, 'type') ?? 'UserInput'),
2843
- typeKey: readString$b(raw, 'typeKey'),
2855
+ type: (readString$c(raw, 'type') ?? 'UserInput'),
2856
+ typeKey: readString$c(raw, 'typeKey'),
2844
2857
  isInitial: readBoolean$5(raw, 'isInitial'),
2845
2858
  isSystem: readBoolean$5(raw, 'isSystem'),
2846
2859
  isLocked: readBoolean$5(raw, 'isLocked'),
2847
- systemKind: readString$b(raw, 'systemKind'),
2848
- targetType: readString$b(raw, 'targetType'),
2849
- targetValue: readString$b(raw, 'targetValue'),
2850
- groupSelection: readString$b(raw, 'groupSelection'),
2860
+ systemKind: readString$c(raw, 'systemKind'),
2861
+ targetType: readString$c(raw, 'targetType'),
2862
+ targetValue: readString$c(raw, 'targetValue'),
2863
+ groupSelection: readString$c(raw, 'groupSelection'),
2851
2864
  sla: readNullableNumber(raw, 'sla') ?? 0,
2852
2865
  slaHours: readNullableNumber(raw, 'slaHours'),
2853
2866
  metadata: readObject$3(raw, 'metadata'),
@@ -2862,7 +2875,7 @@ function normalizeWorkflowStepDto(value) {
2862
2875
  joinParallel: normalizeJoinParallelConfig(raw['joinParallel']),
2863
2876
  layout: normalizeStepLayout(raw['layout']),
2864
2877
  validationState: raw['validationState'] ?? null,
2865
- formBindingMode: readString$b(raw, 'formBindingMode'),
2878
+ formBindingMode: readString$c(raw, 'formBindingMode'),
2866
2879
  formId: readNullableNumber(raw, 'formId'),
2867
2880
  tags: Array.isArray(raw['tags']) ? raw['tags'] : undefined,
2868
2881
  };
@@ -2873,9 +2886,9 @@ function normalizeWorkflowConnectionDto(value, steps = [], fallbackSelectedActio
2873
2886
  const targetStepId = readNumber$4(raw, 'targetStepId', null) ?? readNumber$4(raw, 'target');
2874
2887
  const sourceStep = steps.find((step) => step.id === sourceStepId);
2875
2888
  const selectedActions = normalizeSelectedActions(raw['selectedActions'], sourceStep, fallbackSelectedActions);
2876
- const expressionText = readString$b(raw, 'expressionText') ??
2877
- readString$b(raw, 'expression') ??
2878
- readString$b(raw, 'formulaRaw');
2889
+ const expressionText = readString$c(raw, 'expressionText') ??
2890
+ readString$c(raw, 'expression') ??
2891
+ readString$c(raw, 'formulaRaw');
2879
2892
  return {
2880
2893
  ...spreadAs(raw),
2881
2894
  id: readNumber$4(raw, 'id'),
@@ -2885,19 +2898,19 @@ function normalizeWorkflowConnectionDto(value, steps = [], fallbackSelectedActio
2885
2898
  targetStepId,
2886
2899
  source: readNullableNumber(raw, 'source'),
2887
2900
  target: readNullableNumber(raw, 'target'),
2888
- sourceStepKey: readString$b(raw, 'sourceStepKey'),
2889
- targetStepKey: readString$b(raw, 'targetStepKey'),
2890
- sourcePortKey: readString$b(raw, 'sourcePortKey') ??
2901
+ sourceStepKey: readString$c(raw, 'sourceStepKey'),
2902
+ targetStepKey: readString$c(raw, 'targetStepKey'),
2903
+ sourcePortKey: readString$c(raw, 'sourcePortKey') ??
2891
2904
  selectedActions[0]?.actionKey ??
2892
2905
  undefined,
2893
- targetPortKey: readString$b(raw, 'targetPortKey') ?? 'in',
2906
+ targetPortKey: readString$c(raw, 'targetPortKey') ?? 'in',
2894
2907
  priority: readNullableNumber(raw, 'priority') ?? 0,
2895
2908
  label: toTranslatableText(raw['label']),
2896
2909
  formula: expressionText
2897
2910
  ? { expression: expressionText }
2898
2911
  : null,
2899
- formulaRaw: readString$b(raw, 'formulaRaw'),
2900
- expression: readString$b(raw, 'expression'),
2912
+ formulaRaw: readString$c(raw, 'formulaRaw'),
2913
+ expression: readString$c(raw, 'expression'),
2901
2914
  expressionText,
2902
2915
  selectedActions,
2903
2916
  metadata: readObject$3(raw, 'metadata'),
@@ -2939,24 +2952,24 @@ function normalizeWorkflowBuilderDto(value) {
2939
2952
  supports: Array.isArray(raw['supports'])
2940
2953
  ? raw['supports']
2941
2954
  : undefined,
2942
- rowVersion: readString$b(raw, 'rowVersion') ?? definition.rowVersion ?? null,
2955
+ rowVersion: readString$c(raw, 'rowVersion') ?? definition.rowVersion ?? null,
2943
2956
  };
2944
2957
  }
2945
2958
  function normalizeWorkflowPluginDescriptorDto(value) {
2946
2959
  const raw = asRecord$6(value);
2947
- const inputSchema = parseJsonSchema(readString$b(raw, 'inputSchemaJson')) ??
2960
+ const inputSchema = parseJsonSchema(readString$c(raw, 'inputSchemaJson')) ??
2948
2961
  schemaFromPluginFields(raw['inputFields']);
2949
- const outputSchema = parseJsonSchema(readString$b(raw, 'outputSchemaJson')) ??
2962
+ const outputSchema = parseJsonSchema(readString$c(raw, 'outputSchemaJson')) ??
2950
2963
  schemaFromPluginFields(raw['outputFields']);
2951
2964
  return {
2952
2965
  ...spreadAs(raw),
2953
- pluginId: readString$b(raw, 'pluginId') ?? '',
2966
+ pluginId: readString$c(raw, 'pluginId') ?? '',
2954
2967
  displayName: toTranslatableText(raw['displayName']),
2955
2968
  description: toTranslatableText(raw['description']),
2956
2969
  inputSchema,
2957
2970
  outputSchema,
2958
- inputSchemaJson: readString$b(raw, 'inputSchemaJson'),
2959
- outputSchemaJson: readString$b(raw, 'outputSchemaJson'),
2971
+ inputSchemaJson: readString$c(raw, 'inputSchemaJson'),
2972
+ outputSchemaJson: readString$c(raw, 'outputSchemaJson'),
2960
2973
  inputFields: Array.isArray(raw['inputFields'])
2961
2974
  ? raw['inputFields']
2962
2975
  : [],
@@ -2964,14 +2977,14 @@ function normalizeWorkflowPluginDescriptorDto(value) {
2964
2977
  ? raw['outputFields']
2965
2978
  : [],
2966
2979
  isAvailable: readNullableBoolean(raw, 'isAvailable'),
2967
- errorMessage: readString$b(raw, 'errorMessage'),
2980
+ errorMessage: readString$c(raw, 'errorMessage'),
2968
2981
  };
2969
2982
  }
2970
2983
  function normalizeWorkflowAppDescriptorDto(value) {
2971
2984
  const raw = asRecord$6(value);
2972
2985
  return {
2973
2986
  ...spreadAs(raw),
2974
- appCode: readString$b(raw, 'appCode') ?? '',
2987
+ appCode: readString$c(raw, 'appCode') ?? '',
2975
2988
  displayName: toTranslatableText(raw['displayName']),
2976
2989
  description: toTranslatableText(raw['description']),
2977
2990
  actionCount: readNullableNumber(raw, 'actionCount'),
@@ -2982,16 +2995,16 @@ function normalizeWorkflowAppActionDescriptorDto(value) {
2982
2995
  const inputSchema = schemaFromAppInputSchema(raw['inputSchema']);
2983
2996
  const outputSchema = schemaFromAppOutputSchema(raw['outputSchema']);
2984
2997
  const configSchema = readObject$3(raw, 'configSchema')?.['schema'] ??
2985
- parseJsonSchema(readString$b(raw, 'configSchemaJson'));
2998
+ parseJsonSchema(readString$c(raw, 'configSchemaJson'));
2986
2999
  return {
2987
3000
  ...spreadAs(raw),
2988
- appCode: readString$b(raw, 'appCode'),
2989
- actionKey: readString$b(raw, 'actionKey') ?? '',
3001
+ appCode: readString$c(raw, 'appCode'),
3002
+ actionKey: readString$c(raw, 'actionKey') ?? '',
2990
3003
  displayName: toTranslatableText(raw['displayName']),
2991
3004
  description: toTranslatableText(raw['description']),
2992
- category: readString$b(raw, 'category'),
3005
+ category: readString$c(raw, 'category'),
2993
3006
  configSchema: configSchema ?? null,
2994
- configSchemaJson: readString$b(raw, 'configSchemaJson'),
3007
+ configSchemaJson: readString$c(raw, 'configSchemaJson'),
2995
3008
  runtimeInputsSchema: inputSchema,
2996
3009
  outputsSchema: outputSchema,
2997
3010
  inputSchema: Array.isArray(raw['inputSchema'])
@@ -3028,9 +3041,9 @@ function normalizeWorkflowTestRunResultDto(value) {
3028
3041
  return {
3029
3042
  ...spreadAs(raw),
3030
3043
  workflowId: readNullableNumber(raw, 'workflowId'),
3031
- testRunId: readString$b(raw, 'testRunId') ??
3044
+ testRunId: readString$c(raw, 'testRunId') ??
3032
3045
  `workflow_${readNullableNumber(raw, 'workflowId') ?? 'test'}`,
3033
- status: readString$b(raw, 'status') ??
3046
+ status: readString$c(raw, 'status') ??
3034
3047
  (readNullableBoolean(raw, 'success') === false ? 'Failed' : 'Succeeded'),
3035
3048
  success: readNullableBoolean(raw, 'success'),
3036
3049
  canStart: readNullableBoolean(raw, 'canStart'),
@@ -3248,10 +3261,10 @@ function normalizeWorkflowStepActions(value) {
3248
3261
  return [];
3249
3262
  return value.map((item) => {
3250
3263
  const raw = asRecord$6(item);
3251
- const key = readString$b(raw, 'key') ??
3252
- readString$b(raw, 'typeKey') ??
3253
- readString$b(raw, 'type') ??
3254
- readString$b(raw, 'statusKey') ??
3264
+ const key = readString$c(raw, 'key') ??
3265
+ readString$c(raw, 'typeKey') ??
3266
+ readString$c(raw, 'type') ??
3267
+ readString$c(raw, 'statusKey') ??
3255
3268
  '';
3256
3269
  return {
3257
3270
  ...spreadAs(raw),
@@ -3262,25 +3275,25 @@ function normalizeWorkflowStepActions(value) {
3262
3275
  stepSchemaId: readNullableNumber(raw, 'stepSchemaId') ?? undefined,
3263
3276
  order: readNullableNumber(raw, 'order') ?? undefined,
3264
3277
  key,
3265
- type: readString$b(raw, 'type'),
3266
- typeKey: readString$b(raw, 'typeKey'),
3278
+ type: readString$c(raw, 'type'),
3279
+ typeKey: readString$c(raw, 'typeKey'),
3267
3280
  label: toTranslatableText(raw['label'] ?? raw['name'] ?? key),
3268
3281
  name: toTranslatableText(raw['name'] ?? raw['label'] ?? key),
3269
- statusKey: readString$b(raw, 'statusKey'),
3270
- color: readString$b(raw, 'color'),
3271
- icon: readString$b(raw, 'icon'),
3282
+ statusKey: readString$c(raw, 'statusKey'),
3283
+ color: readString$c(raw, 'color'),
3284
+ icon: readString$c(raw, 'icon'),
3272
3285
  requireConfirmation: readNullableBoolean(raw, 'requireConfirmation') ?? undefined,
3273
3286
  confirmationMessage: toTranslatableText(raw['confirmationMessage']),
3274
3287
  requireSignature: readNullableBoolean(raw, 'requireSignature') ?? undefined,
3275
- routingBehavior: readString$b(raw, 'routingBehavior'),
3288
+ routingBehavior: readString$c(raw, 'routingBehavior'),
3276
3289
  routesThroughSelectedActionConnection: readNullableBoolean(raw, 'routesThroughSelectedActionConnection') ??
3277
3290
  undefined,
3278
3291
  isTerminal: readNullableBoolean(raw, 'isTerminal') ??
3279
- readString$b(raw, 'routingBehavior') === 'Terminal',
3292
+ readString$c(raw, 'routingBehavior') === 'Terminal',
3280
3293
  isRouteable: readNullableBoolean(raw, 'isRouteable') ??
3281
3294
  readNullableBoolean(raw, 'canRoute') ??
3282
3295
  readNullableBoolean(raw, 'routesThroughSelectedActionConnection') ??
3283
- readString$b(raw, 'routingBehavior') === 'SelectedActionConnection',
3296
+ readString$c(raw, 'routingBehavior') === 'SelectedActionConnection',
3284
3297
  canRoute: readNullableBoolean(raw, 'canRoute') ??
3285
3298
  readNullableBoolean(raw, 'isRouteable') ??
3286
3299
  undefined,
@@ -3300,9 +3313,9 @@ function normalizeWorkflowStepProperties(value) {
3300
3313
  readNullableNumber(property, 'id') ??
3301
3314
  0,
3302
3315
  refId: readNullableNumber(raw, 'refId') ?? undefined,
3303
- refType: readString$b(raw, 'refType'),
3304
- type: readString$b(raw, 'type'),
3305
- propertyKey: readString$b(raw, 'propertyKey') ?? readString$b(property, 'key'),
3316
+ refType: readString$c(raw, 'refType'),
3317
+ type: readString$c(raw, 'type'),
3318
+ propertyKey: readString$c(raw, 'propertyKey') ?? readString$c(property, 'key'),
3306
3319
  read: readNullableBoolean(raw, 'read') ??
3307
3320
  readNullableBoolean(raw, 'isRead') ??
3308
3321
  false,
@@ -3314,9 +3327,9 @@ function normalizeWorkflowStepProperties(value) {
3314
3327
  property: property
3315
3328
  ? {
3316
3329
  id: readNullableNumber(property, 'id'),
3317
- key: readString$b(property, 'key'),
3330
+ key: readString$c(property, 'key'),
3318
3331
  name: toTranslatableText(property['name']),
3319
- viewType: readString$b(property, 'viewType'),
3332
+ viewType: readString$c(property, 'viewType'),
3320
3333
  isRequired: readNullableBoolean(property, 'isRequired') ?? undefined,
3321
3334
  isTranslatable: readNullableBoolean(property, 'isTranslatable') ?? undefined,
3322
3335
  }
@@ -3330,25 +3343,25 @@ function normalizeAutomatedConfig(value) {
3330
3343
  const raw = asRecord$6(value);
3331
3344
  return {
3332
3345
  ...spreadAs(raw),
3333
- method: readString$b(raw, 'method') ?? 'POST',
3334
- serviceUrl: readString$b(raw, 'serviceUrl') ?? readString$b(raw, 'url'),
3335
- url: readString$b(raw, 'url') ?? readString$b(raw, 'serviceUrl'),
3346
+ method: readString$c(raw, 'method') ?? 'POST',
3347
+ serviceUrl: readString$c(raw, 'serviceUrl') ?? readString$c(raw, 'url'),
3348
+ url: readString$c(raw, 'url') ?? readString$c(raw, 'serviceUrl'),
3336
3349
  timeoutSeconds: readNullableNumber(raw, 'timeoutSeconds'),
3337
3350
  retryCount: readNullableNumber(raw, 'retryCount'),
3338
3351
  retryDelaySeconds: readNullableNumber(raw, 'retryDelaySeconds'),
3339
- onFailureBehavior: readString$b(raw, 'onFailureBehavior') ??
3340
- readString$b(raw, 'failureBehavior'),
3341
- failureBehavior: readString$b(raw, 'failureBehavior') ??
3342
- readString$b(raw, 'onFailureBehavior'),
3352
+ onFailureBehavior: readString$c(raw, 'onFailureBehavior') ??
3353
+ readString$c(raw, 'failureBehavior'),
3354
+ failureBehavior: readString$c(raw, 'failureBehavior') ??
3355
+ readString$c(raw, 'onFailureBehavior'),
3343
3356
  headers: pairsFromUnknown(raw['headers']),
3344
3357
  headersMap: readStringRecord(raw['headers']),
3345
3358
  query: pairsFromUnknown(raw['query'] ?? raw['queryParameters'] ?? raw['queryParams']),
3346
3359
  queryParameters: readStringRecord(raw['queryParameters'] ?? raw['queryParams']),
3347
3360
  queryParams: readStringRecord(raw['queryParams'] ?? raw['queryParameters']),
3348
- jsonBody: readString$b(raw, 'jsonBody') ?? readString$b(raw, 'body'),
3349
- body: readString$b(raw, 'body') ?? readString$b(raw, 'jsonBody'),
3350
- bodyType: readString$b(raw, 'bodyType'),
3351
- bodyKind: readString$b(raw, 'bodyKind'),
3361
+ jsonBody: readString$c(raw, 'jsonBody') ?? readString$c(raw, 'body'),
3362
+ body: readString$c(raw, 'body') ?? readString$c(raw, 'jsonBody'),
3363
+ bodyType: readString$c(raw, 'bodyType'),
3364
+ bodyKind: readString$c(raw, 'bodyKind'),
3352
3365
  outputs: normalizeAutomatedOutputMappings(raw['outputs'] ?? raw['outputMappings']),
3353
3366
  outputMappings: normalizeAutomatedOutputMappings(raw['outputMappings'] ?? raw['outputs']),
3354
3367
  placeholders: readStringRecord(raw['placeholders']),
@@ -3367,17 +3380,17 @@ function normalizePluginConfig(value) {
3367
3380
  : [];
3368
3381
  return {
3369
3382
  ...spreadAs(raw),
3370
- pluginId: readString$b(raw, 'pluginId'),
3371
- inputSchemaJson: readString$b(raw, 'inputSchemaJson'),
3372
- outputSchemaJson: readString$b(raw, 'outputSchemaJson'),
3383
+ pluginId: readString$c(raw, 'pluginId'),
3384
+ inputSchemaJson: readString$c(raw, 'inputSchemaJson'),
3385
+ outputSchemaJson: readString$c(raw, 'outputSchemaJson'),
3373
3386
  waitForCompletion: readNullableBoolean(raw, 'waitForCompletion') ?? true,
3374
3387
  timeoutSeconds: readNullableNumber(raw, 'timeoutSeconds'),
3375
3388
  retryCount: readNullableNumber(raw, 'retryCount'),
3376
3389
  retryDelaySeconds: readNullableNumber(raw, 'retryDelaySeconds'),
3377
- onFailureBehavior: readString$b(raw, 'onFailureBehavior') ??
3378
- readString$b(raw, 'failureBehavior'),
3379
- failureBehavior: readString$b(raw, 'failureBehavior') ??
3380
- readString$b(raw, 'onFailureBehavior'),
3390
+ onFailureBehavior: readString$c(raw, 'onFailureBehavior') ??
3391
+ readString$c(raw, 'failureBehavior'),
3392
+ failureBehavior: readString$c(raw, 'failureBehavior') ??
3393
+ readString$c(raw, 'onFailureBehavior'),
3381
3394
  skipInputValidation: readNullableBoolean(raw, 'skipInputValidation') ?? undefined,
3382
3395
  inputs: Array.isArray(raw['inputs']) && raw['inputs'].length > 0
3383
3396
  ? raw['inputs']
@@ -3404,10 +3417,10 @@ function normalizeAppActionConfig(value) {
3404
3417
  : [];
3405
3418
  return {
3406
3419
  ...spreadAs(raw),
3407
- appCode: readString$b(raw, 'appCode'),
3408
- actionKey: readString$b(raw, 'actionKey') ?? '',
3409
- configJson: readString$b(raw, 'configJson'),
3410
- config: parseJsonObject$3(readString$b(raw, 'configJson')) ??
3420
+ appCode: readString$c(raw, 'appCode'),
3421
+ actionKey: readString$c(raw, 'actionKey') ?? '',
3422
+ configJson: readString$c(raw, 'configJson'),
3423
+ config: parseJsonObject$3(readString$c(raw, 'configJson')) ??
3411
3424
  readObject$3(raw, 'config') ??
3412
3425
  {},
3413
3426
  configMappings,
@@ -3419,7 +3432,7 @@ function normalizeAppActionConfig(value) {
3419
3432
  ? raw['outputs']
3420
3433
  : outputMappings.map((mapping) => normalizeAppOutput(mapping)),
3421
3434
  outputMappings,
3422
- failureBehavior: readString$b(raw, 'failureBehavior'),
3435
+ failureBehavior: readString$c(raw, 'failureBehavior'),
3423
3436
  timeoutSeconds: readNullableNumber(raw, 'timeoutSeconds'),
3424
3437
  };
3425
3438
  }
@@ -3470,7 +3483,7 @@ function normalizeJoinParallelConfig(value) {
3470
3483
  const anyBranchApprove = readNullableBoolean(raw, 'anyBranchApprove') ?? false;
3471
3484
  return {
3472
3485
  ...spreadAs(raw),
3473
- policy: readString$b(raw, 'policy') ?? (anyBranchApprove ? 'Any' : 'All'),
3486
+ policy: readString$c(raw, 'policy') ?? (anyBranchApprove ? 'Any' : 'All'),
3474
3487
  anyBranchApprove,
3475
3488
  threshold: readNullableNumber(raw, 'threshold'),
3476
3489
  customCondition: raw['customCondition'] ??
@@ -3501,12 +3514,12 @@ function normalizeSelectedActions(value, sourceStep, fallbackSelectedActions = [
3501
3514
  return {
3502
3515
  stepActionId: stepActionId ?? undefined,
3503
3516
  actionId: stepActionId ?? undefined,
3504
- actionKey: readString$b(raw, 'actionKey') ??
3505
- readString$b(raw, 'key') ??
3506
- readString$b(raw, 'typeKey') ??
3507
- readString$b(raw, 'statusKey') ??
3517
+ actionKey: readString$c(raw, 'actionKey') ??
3518
+ readString$c(raw, 'key') ??
3519
+ readString$c(raw, 'typeKey') ??
3520
+ readString$c(raw, 'statusKey') ??
3508
3521
  '',
3509
- key: readString$b(raw, 'key'),
3522
+ key: readString$c(raw, 'key'),
3510
3523
  };
3511
3524
  })
3512
3525
  .filter((item) => item.actionKey);
@@ -3564,12 +3577,12 @@ function normalizeAutomatedOutputMappings(value) {
3564
3577
  return value.map((item) => {
3565
3578
  const raw = asRecord$6(item);
3566
3579
  return {
3567
- source: readString$b(raw, 'source') ?? readString$b(raw, 'jsonPath') ?? 'response',
3568
- targetPath: readString$b(raw, 'targetPath') ?? readString$b(raw, 'contextPath') ?? '',
3569
- jsonPath: readString$b(raw, 'jsonPath'),
3570
- contextPath: readString$b(raw, 'contextPath'),
3571
- writePolicy: readString$b(raw, 'writePolicy'),
3572
- valueType: readString$b(raw, 'valueType'),
3580
+ source: readString$c(raw, 'source') ?? readString$c(raw, 'jsonPath') ?? 'response',
3581
+ targetPath: readString$c(raw, 'targetPath') ?? readString$c(raw, 'contextPath') ?? '',
3582
+ jsonPath: readString$c(raw, 'jsonPath'),
3583
+ contextPath: readString$c(raw, 'contextPath'),
3584
+ writePolicy: readString$c(raw, 'writePolicy'),
3585
+ valueType: readString$c(raw, 'valueType'),
3573
3586
  isSecret: readNullableBoolean(raw, 'isSecret') ?? undefined,
3574
3587
  };
3575
3588
  });
@@ -3578,27 +3591,27 @@ function normalizePluginInput(value) {
3578
3591
  const raw = asRecord$6(value);
3579
3592
  if (readNullableNumber(raw, 'requestPropertyId') != null) {
3580
3593
  return {
3581
- field: readString$b(raw, 'pluginInputFieldName') ?? '',
3594
+ field: readString$c(raw, 'pluginInputFieldName') ?? '',
3582
3595
  source: 'Property',
3583
3596
  value: readNullableNumber(raw, 'requestPropertyId'),
3584
3597
  };
3585
3598
  }
3586
- if (readString$b(raw, 'contextPath')) {
3599
+ if (readString$c(raw, 'contextPath')) {
3587
3600
  return {
3588
- field: readString$b(raw, 'pluginInputFieldName') ?? '',
3601
+ field: readString$c(raw, 'pluginInputFieldName') ?? '',
3589
3602
  source: 'Context',
3590
- value: readString$b(raw, 'contextPath') ?? '',
3603
+ value: readString$c(raw, 'contextPath') ?? '',
3591
3604
  };
3592
3605
  }
3593
- if (readString$b(raw, 'transformExpression')) {
3606
+ if (readString$c(raw, 'transformExpression')) {
3594
3607
  return {
3595
- field: readString$b(raw, 'pluginInputFieldName') ?? '',
3608
+ field: readString$c(raw, 'pluginInputFieldName') ?? '',
3596
3609
  source: 'Expression',
3597
- value: readString$b(raw, 'transformExpression') ?? '',
3610
+ value: readString$c(raw, 'transformExpression') ?? '',
3598
3611
  };
3599
3612
  }
3600
3613
  return {
3601
- field: readString$b(raw, 'pluginInputFieldName') ?? '',
3614
+ field: readString$c(raw, 'pluginInputFieldName') ?? '',
3602
3615
  source: 'Static',
3603
3616
  value: raw['staticValue'],
3604
3617
  };
@@ -3606,35 +3619,35 @@ function normalizePluginInput(value) {
3606
3619
  function normalizePluginOutput(value) {
3607
3620
  const raw = asRecord$6(value);
3608
3621
  return {
3609
- field: readString$b(raw, 'pluginOutputFieldName') ?? '',
3610
- targetPath: readString$b(raw, 'contextPath') ?? readString$b(raw, 'targetPath') ?? '',
3622
+ field: readString$c(raw, 'pluginOutputFieldName') ?? '',
3623
+ targetPath: readString$c(raw, 'contextPath') ?? readString$c(raw, 'targetPath') ?? '',
3611
3624
  };
3612
3625
  }
3613
3626
  function normalizeAppRuntimeInput(value) {
3614
3627
  const raw = asRecord$6(value);
3615
3628
  if (readNullableNumber(raw, 'requestPropertyId') != null) {
3616
3629
  return {
3617
- field: readString$b(raw, 'fieldKey') ?? '',
3630
+ field: readString$c(raw, 'fieldKey') ?? '',
3618
3631
  source: 'Property',
3619
3632
  value: readNullableNumber(raw, 'requestPropertyId'),
3620
3633
  };
3621
3634
  }
3622
- if (readString$b(raw, 'contextPath')) {
3635
+ if (readString$c(raw, 'contextPath')) {
3623
3636
  return {
3624
- field: readString$b(raw, 'fieldKey') ?? '',
3637
+ field: readString$c(raw, 'fieldKey') ?? '',
3625
3638
  source: 'Context',
3626
- value: readString$b(raw, 'contextPath') ?? '',
3639
+ value: readString$c(raw, 'contextPath') ?? '',
3627
3640
  };
3628
3641
  }
3629
- if (readString$b(raw, 'transformExpression')) {
3642
+ if (readString$c(raw, 'transformExpression')) {
3630
3643
  return {
3631
- field: readString$b(raw, 'fieldKey') ?? '',
3644
+ field: readString$c(raw, 'fieldKey') ?? '',
3632
3645
  source: 'Expression',
3633
- value: readString$b(raw, 'transformExpression') ?? '',
3646
+ value: readString$c(raw, 'transformExpression') ?? '',
3634
3647
  };
3635
3648
  }
3636
3649
  return {
3637
- field: readString$b(raw, 'fieldKey') ?? '',
3650
+ field: readString$c(raw, 'fieldKey') ?? '',
3638
3651
  source: 'Static',
3639
3652
  value: raw['staticValue'],
3640
3653
  };
@@ -3642,8 +3655,8 @@ function normalizeAppRuntimeInput(value) {
3642
3655
  function normalizeAppOutput(value) {
3643
3656
  const raw = asRecord$6(value);
3644
3657
  return {
3645
- field: readString$b(raw, 'fieldKey') ?? '',
3646
- targetPath: readString$b(raw, 'contextPath') ?? readString$b(raw, 'targetPath') ?? '',
3658
+ field: readString$c(raw, 'fieldKey') ?? '',
3659
+ targetPath: readString$c(raw, 'contextPath') ?? readString$c(raw, 'targetPath') ?? '',
3647
3660
  };
3648
3661
  }
3649
3662
  function normalizeSubprocessPropertyMapping(value) {
@@ -3654,23 +3667,23 @@ function normalizeSubprocessPropertyMapping(value) {
3654
3667
  parentRequestPropertyId: readNullableNumber(raw, 'parentRequestPropertyId') ?? undefined,
3655
3668
  subRequestPropertyId: readNullableNumber(raw, 'subRequestPropertyId') ?? undefined,
3656
3669
  reverseMapping: readNullableBoolean(raw, 'reverseMapping') ?? undefined,
3657
- parentPath: readString$b(raw, 'parentPath') ??
3670
+ parentPath: readString$c(raw, 'parentPath') ??
3658
3671
  String(readNullableNumber(raw, 'parentRequestPropertyId') ?? ''),
3659
- childPath: readString$b(raw, 'childPath') ??
3672
+ childPath: readString$c(raw, 'childPath') ??
3660
3673
  String(readNullableNumber(raw, 'subRequestPropertyId') ?? ''),
3661
- direction: readString$b(raw, 'direction') ??
3674
+ direction: readString$c(raw, 'direction') ??
3662
3675
  (readNullableBoolean(raw, 'reverseMapping') ? 'Both' : 'In'),
3663
3676
  required: readNullableBoolean(raw, 'required') ?? undefined,
3664
- transform: readString$b(raw, 'transform'),
3677
+ transform: readString$c(raw, 'transform'),
3665
3678
  };
3666
3679
  }
3667
3680
  function normalizeSubprocessContextMapping(value) {
3668
3681
  const raw = asRecord$6(value);
3669
3682
  return {
3670
- parentPath: readString$b(raw, 'parentPath') ?? '',
3671
- childPath: readString$b(raw, 'childPath') ?? '',
3672
- direction: readString$b(raw, 'direction') ?? 'In',
3673
- transform: readString$b(raw, 'transform'),
3683
+ parentPath: readString$c(raw, 'parentPath') ?? '',
3684
+ childPath: readString$c(raw, 'childPath') ?? '',
3685
+ direction: readString$c(raw, 'direction') ?? 'In',
3686
+ transform: readString$c(raw, 'transform'),
3674
3687
  };
3675
3688
  }
3676
3689
  function normalizeActionsForPayload(actions) {
@@ -3721,14 +3734,14 @@ function normalizePluginInputsForPayload(mappings) {
3721
3734
  if ('pluginInputFieldName' in raw || 'contextPath' in raw) {
3722
3735
  return { ...raw };
3723
3736
  }
3724
- const source = readString$b(raw, 'source') ?? 'Context';
3737
+ const source = readString$c(raw, 'source') ?? 'Context';
3725
3738
  return {
3726
3739
  id: 0,
3727
- pluginInputFieldName: readString$b(raw, 'field') ?? '',
3740
+ pluginInputFieldName: readString$c(raw, 'field') ?? '',
3728
3741
  requestPropertyId: source === 'Property' ? readNullableNumber(raw, 'value') : undefined,
3729
- contextPath: source === 'Context' ? (readString$b(raw, 'value') ?? '') : undefined,
3742
+ contextPath: source === 'Context' ? (readString$c(raw, 'value') ?? '') : undefined,
3730
3743
  staticValue: source === 'Static' ? raw['value'] : undefined,
3731
- transformExpression: source === 'Expression' ? (readString$b(raw, 'value') ?? '') : undefined,
3744
+ transformExpression: source === 'Expression' ? (readString$c(raw, 'value') ?? '') : undefined,
3732
3745
  };
3733
3746
  });
3734
3747
  }
@@ -3742,9 +3755,9 @@ function normalizePluginOutputsForPayload(mappings) {
3742
3755
  }
3743
3756
  return {
3744
3757
  id: 0,
3745
- pluginOutputFieldName: readString$b(raw, 'field') ?? '',
3746
- contextPath: readString$b(raw, 'targetPath') ?? readString$b(raw, 'contextPath') ?? '',
3747
- transformExpression: readString$b(raw, 'transformExpression') ?? undefined,
3758
+ pluginOutputFieldName: readString$c(raw, 'field') ?? '',
3759
+ contextPath: readString$c(raw, 'targetPath') ?? readString$c(raw, 'contextPath') ?? '',
3760
+ transformExpression: readString$c(raw, 'transformExpression') ?? undefined,
3748
3761
  };
3749
3762
  });
3750
3763
  }
@@ -3760,17 +3773,17 @@ function normalizeAppActionMappingsForPayload(mappings, kind) {
3760
3773
  }
3761
3774
  if (kind === 'output') {
3762
3775
  return {
3763
- fieldKey: readString$b(raw, 'field') ?? '',
3764
- contextPath: readString$b(raw, 'targetPath') ?? readString$b(raw, 'contextPath') ?? '',
3776
+ fieldKey: readString$c(raw, 'field') ?? '',
3777
+ contextPath: readString$c(raw, 'targetPath') ?? readString$c(raw, 'contextPath') ?? '',
3765
3778
  };
3766
3779
  }
3767
- const source = readString$b(raw, 'source') ?? 'Context';
3780
+ const source = readString$c(raw, 'source') ?? 'Context';
3768
3781
  return {
3769
- fieldKey: readString$b(raw, 'field') ?? '',
3782
+ fieldKey: readString$c(raw, 'field') ?? '',
3770
3783
  requestPropertyId: source === 'Property' ? readNullableNumber(raw, 'value') : undefined,
3771
- contextPath: source === 'Context' ? (readString$b(raw, 'value') ?? '') : undefined,
3784
+ contextPath: source === 'Context' ? (readString$c(raw, 'value') ?? '') : undefined,
3772
3785
  staticValue: source === 'Static' ? raw['value'] : undefined,
3773
- transformExpression: source === 'Expression' ? (readString$b(raw, 'value') ?? '') : undefined,
3786
+ transformExpression: source === 'Expression' ? (readString$c(raw, 'value') ?? '') : undefined,
3774
3787
  };
3775
3788
  });
3776
3789
  }
@@ -3808,7 +3821,7 @@ function normalizeContextSnapshot(value) {
3808
3821
  const snapshot = {};
3809
3822
  for (const item of value) {
3810
3823
  const raw = asRecord$6(item);
3811
- const path = readString$b(raw, 'path');
3824
+ const path = readString$c(raw, 'path');
3812
3825
  if (!path)
3813
3826
  continue;
3814
3827
  snapshot[path] = raw['value'];
@@ -3822,12 +3835,12 @@ function schemaFromPluginFields(value) {
3822
3835
  const required = [];
3823
3836
  for (const item of value) {
3824
3837
  const raw = asRecord$6(item);
3825
- const key = readString$b(raw, 'name');
3838
+ const key = readString$c(raw, 'name');
3826
3839
  if (!key)
3827
3840
  continue;
3828
3841
  properties[key] = {
3829
- type: readString$b(raw, 'type') ?? 'string',
3830
- description: readString$b(raw, 'description') ?? '',
3842
+ type: readString$c(raw, 'type') ?? 'string',
3843
+ description: readString$c(raw, 'description') ?? '',
3831
3844
  };
3832
3845
  if (readNullableBoolean(raw, 'required'))
3833
3846
  required.push(key);
@@ -3841,12 +3854,12 @@ function schemaFromAppInputSchema(value) {
3841
3854
  const required = [];
3842
3855
  for (const item of value) {
3843
3856
  const raw = asRecord$6(item);
3844
- const key = readString$b(raw, 'key');
3857
+ const key = readString$c(raw, 'key');
3845
3858
  if (!key)
3846
3859
  continue;
3847
3860
  properties[key] = {
3848
3861
  type: 'string',
3849
- description: readString$b(raw, 'source') ?? '',
3862
+ description: readString$c(raw, 'source') ?? '',
3850
3863
  };
3851
3864
  if (readNullableBoolean(raw, 'required'))
3852
3865
  required.push(key);
@@ -3859,12 +3872,12 @@ function schemaFromAppOutputSchema(value) {
3859
3872
  const properties = {};
3860
3873
  for (const item of value) {
3861
3874
  const raw = asRecord$6(item);
3862
- const key = readString$b(raw, 'key');
3875
+ const key = readString$c(raw, 'key');
3863
3876
  if (!key)
3864
3877
  continue;
3865
3878
  properties[key] = {
3866
- type: readString$b(raw, 'type') ?? 'object',
3867
- description: readString$b(raw, 'description') ?? '',
3879
+ type: readString$c(raw, 'type') ?? 'object',
3880
+ description: readString$c(raw, 'description') ?? '',
3868
3881
  };
3869
3882
  }
3870
3883
  return { type: 'object', properties };
@@ -3872,9 +3885,9 @@ function schemaFromAppOutputSchema(value) {
3872
3885
  function toTranslatableText(value) {
3873
3886
  if (value && typeof value === 'object') {
3874
3887
  const raw = value;
3875
- const en = readString$b(raw, 'en');
3876
- const ar = readString$b(raw, 'ar');
3877
- const display = readString$b(raw, 'display');
3888
+ const en = readString$c(raw, 'en');
3889
+ const ar = readString$c(raw, 'ar');
3890
+ const display = readString$c(raw, 'display');
3878
3891
  return {
3879
3892
  en: en ?? ar ?? display ?? '',
3880
3893
  ar: ar ?? en ?? display ?? '',
@@ -3889,9 +3902,9 @@ function toPrimaryText(value) {
3889
3902
  return value || undefined;
3890
3903
  if (value && typeof value === 'object') {
3891
3904
  const raw = value;
3892
- return (readString$b(raw, 'en') ??
3893
- readString$b(raw, 'display') ??
3894
- readString$b(raw, 'ar') ??
3905
+ return (readString$c(raw, 'en') ??
3906
+ readString$c(raw, 'display') ??
3907
+ readString$c(raw, 'ar') ??
3895
3908
  undefined);
3896
3909
  }
3897
3910
  return undefined;
@@ -3913,8 +3926,8 @@ function pairsFromUnknown(value) {
3913
3926
  return value
3914
3927
  .map((item) => asRecord$6(item))
3915
3928
  .map((item) => ({
3916
- key: readString$b(item, 'key') ?? '',
3917
- value: readString$b(item, 'value') ?? '',
3929
+ key: readString$c(item, 'key') ?? '',
3930
+ value: readString$c(item, 'value') ?? '',
3918
3931
  }));
3919
3932
  }
3920
3933
  return Object.entries(readStringRecord(value) ?? {}).map(([key, recordValue]) => ({
@@ -3981,7 +3994,7 @@ function readObject$3(record, key) {
3981
3994
  return null;
3982
3995
  return value;
3983
3996
  }
3984
- function readString$b(record, key) {
3997
+ function readString$c(record, key) {
3985
3998
  const value = record[key];
3986
3999
  return typeof value === 'string' ? value : undefined;
3987
4000
  }
@@ -4918,7 +4931,7 @@ function normalizePathValidation(paths, response) {
4918
4931
  }
4919
4932
  return {
4920
4933
  results: raw.map((item, index) => {
4921
- const path = readString$a(item, 'path') ?? readString$a(item, 'expression');
4934
+ const path = readString$b(item, 'path') ?? readString$b(item, 'expression');
4922
4935
  const valid = readBoolean$4(item, 'isValid') ??
4923
4936
  readBoolean$4(item, 'valid') ??
4924
4937
  readBoolean$4(item, 'success') ??
@@ -4930,8 +4943,8 @@ function normalizePathValidation(paths, response) {
4930
4943
  path: path ?? paths[index] ?? '',
4931
4944
  isValid: valid,
4932
4945
  isAvailable: available,
4933
- type: readString$a(item, 'type'),
4934
- message: readString$a(item, 'message') ?? readString$a(item, 'error'),
4946
+ type: readString$b(item, 'type'),
4947
+ message: readString$b(item, 'message') ?? readString$b(item, 'error'),
4935
4948
  };
4936
4949
  }),
4937
4950
  };
@@ -4950,7 +4963,7 @@ function asRecordArray(value) {
4950
4963
  return null;
4951
4964
  return value.filter((item) => !!item && typeof item === 'object' && !Array.isArray(item));
4952
4965
  }
4953
- function readString$a(source, key) {
4966
+ function readString$b(source, key) {
4954
4967
  const value = source[key];
4955
4968
  return typeof value === 'string' ? value : null;
4956
4969
  }
@@ -5101,8 +5114,8 @@ function normalizeExecutionListResult(result) {
5101
5114
  ...result,
5102
5115
  items,
5103
5116
  hasMore: readBoolean$3(raw, 'hasMore') ?? result.hasMore,
5104
- nextCursor: readString$9(raw, 'nextCursor') ??
5105
- buildCursor(readString$9(raw, 'nextCursorStartedAtUtc'), readId$3(raw, 'nextCursorExecutionId')),
5117
+ nextCursor: readString$a(raw, 'nextCursor') ??
5118
+ buildCursor(readString$a(raw, 'nextCursorStartedAtUtc'), readId$3(raw, 'nextCursorExecutionId')),
5106
5119
  };
5107
5120
  }
5108
5121
  function normalizeExecutionDetail(detail) {
@@ -5116,11 +5129,11 @@ function normalizeExecutionDetail(detail) {
5116
5129
  ? readArray$1(raw, 'events')
5117
5130
  : readArray$1(raw, 'timeline')).map(normalizeExecutionEvent);
5118
5131
  const triggerSummary = raw['triggerSummary'] ??
5119
- parseJsonValue$1(readString$9(raw, 'triggerSummaryJson'));
5132
+ parseJsonValue$1(readString$a(raw, 'triggerSummaryJson'));
5120
5133
  return {
5121
5134
  ...detail,
5122
5135
  ...summary,
5123
- triggerSummaryJson: readString$9(raw, 'triggerSummaryJson') ?? null,
5136
+ triggerSummaryJson: readString$a(raw, 'triggerSummaryJson') ?? null,
5124
5137
  triggerSummary,
5125
5138
  nodeRuns,
5126
5139
  waits,
@@ -5131,7 +5144,7 @@ function normalizeExecutionDetail(detail) {
5131
5144
  flowPlusCommitAudits: readArray$1(raw, 'flowPlusCommitAudits'),
5132
5145
  approvalDecisions: readArray$1(raw, 'approvalDecisions'),
5133
5146
  timelineHasMore: readBoolean$3(raw, 'timelineHasMore') ?? false,
5134
- timelineNextAfterUtc: readString$9(raw, 'timelineNextAfterUtc') ?? null,
5147
+ timelineNextAfterUtc: readString$a(raw, 'timelineNextAfterUtc') ?? null,
5135
5148
  };
5136
5149
  }
5137
5150
  function normalizeExecutionSummary(value) {
@@ -5143,18 +5156,18 @@ function normalizeExecutionSummary(value) {
5143
5156
  automationId: readId$3(raw, 'automationId') ?? '',
5144
5157
  revisionId,
5145
5158
  automationRevisionId: readId$3(raw, 'automationRevisionId') ?? revisionId,
5146
- automationName: readString$9(raw, 'automationName') ?? null,
5159
+ automationName: readString$a(raw, 'automationName') ?? null,
5147
5160
  revisionNumber: readNumber$2(raw, 'revisionNumber') ?? null,
5148
- status: String(readString$9(raw, 'status') ?? ''),
5149
- triggerType: readString$9(raw, 'triggerType') ?? null,
5150
- triggerKey: readString$9(raw, 'triggerKey') ?? null,
5151
- createdAtUtc: readString$9(raw, 'createdAtUtc') ?? null,
5152
- startedAtUtc: readString$9(raw, 'startedAtUtc') ??
5153
- readString$9(raw, 'createdAtUtc') ??
5161
+ status: String(readString$a(raw, 'status') ?? ''),
5162
+ triggerType: readString$a(raw, 'triggerType') ?? null,
5163
+ triggerKey: readString$a(raw, 'triggerKey') ?? null,
5164
+ createdAtUtc: readString$a(raw, 'createdAtUtc') ?? null,
5165
+ startedAtUtc: readString$a(raw, 'startedAtUtc') ??
5166
+ readString$a(raw, 'createdAtUtc') ??
5154
5167
  null,
5155
- completedAtUtc: readString$9(raw, 'completedAtUtc') ?? null,
5168
+ completedAtUtc: readString$a(raw, 'completedAtUtc') ?? null,
5156
5169
  durationMs: readNumber$2(raw, 'durationMs') ?? null,
5157
- correlationId: readString$9(raw, 'correlationId') ?? null,
5170
+ correlationId: readString$a(raw, 'correlationId') ?? null,
5158
5171
  };
5159
5172
  }
5160
5173
  function normalizeNodeRun(value) {
@@ -5162,9 +5175,9 @@ function normalizeNodeRun(value) {
5162
5175
  return {
5163
5176
  ...raw,
5164
5177
  nodeRunId: readId$3(raw, 'nodeRunId') ?? '',
5165
- nodeKey: readString$9(raw, 'nodeKey') ?? '',
5166
- nodeType: String(readString$9(raw, 'nodeType') ?? ''),
5167
- status: String(readString$9(raw, 'status') ?? ''),
5178
+ nodeKey: readString$a(raw, 'nodeKey') ?? '',
5179
+ nodeType: String(readString$a(raw, 'nodeType') ?? ''),
5180
+ status: String(readString$a(raw, 'status') ?? ''),
5168
5181
  attempts: readArray$1(raw, 'attempts').map((attempt) => {
5169
5182
  const attemptRaw = asRecord$5(attempt);
5170
5183
  return {
@@ -5173,9 +5186,9 @@ function normalizeNodeRun(value) {
5173
5186
  readId$3(attemptRaw, 'nodeAttemptId') ??
5174
5187
  '',
5175
5188
  nodeAttemptId: readId$3(attemptRaw, 'nodeAttemptId'),
5176
- status: String(readString$9(attemptRaw, 'status') ?? ''),
5177
- errorMessage: readString$9(attemptRaw, 'errorMessage') ??
5178
- readString$9(attemptRaw, 'errorJson') ??
5189
+ status: String(readString$a(attemptRaw, 'status') ?? ''),
5190
+ errorMessage: readString$a(attemptRaw, 'errorMessage') ??
5191
+ readString$a(attemptRaw, 'errorJson') ??
5179
5192
  null,
5180
5193
  };
5181
5194
  }),
@@ -5187,9 +5200,9 @@ function normalizeRuntimeWait(value) {
5187
5200
  ...raw,
5188
5201
  runtimeWaitId: readId$3(raw, 'runtimeWaitId') ?? '',
5189
5202
  nodeRunId: readId$3(raw, 'nodeRunId') ?? null,
5190
- nodeKey: readString$9(raw, 'nodeKey') ?? null,
5191
- waitType: readString$9(raw, 'waitType') ?? '',
5192
- status: readString$9(raw, 'status') ?? '',
5203
+ nodeKey: readString$a(raw, 'nodeKey') ?? null,
5204
+ waitType: readString$a(raw, 'waitType') ?? '',
5205
+ status: readString$a(raw, 'status') ?? '',
5193
5206
  };
5194
5207
  }
5195
5208
  function normalizeExecutionEvent(value) {
@@ -5197,27 +5210,27 @@ function normalizeExecutionEvent(value) {
5197
5210
  return {
5198
5211
  ...raw,
5199
5212
  eventId: readId$3(raw, 'eventId') ?? '',
5200
- eventType: readString$9(raw, 'eventType') ?? '',
5201
- severity: readString$9(raw, 'severity') ?? 'Info',
5202
- occurredAtUtc: readString$9(raw, 'occurredAtUtc') ?? '',
5213
+ eventType: readString$a(raw, 'eventType') ?? '',
5214
+ severity: readString$a(raw, 'severity') ?? 'Info',
5215
+ occurredAtUtc: readString$a(raw, 'occurredAtUtc') ?? '',
5203
5216
  nodeRunId: readId$3(raw, 'nodeRunId') ?? null,
5204
5217
  nodeAttemptId: readId$3(raw, 'nodeAttemptId') ?? null,
5205
- correlationId: readString$9(raw, 'correlationId') ?? null,
5206
- data: raw['data'] ?? parseJsonValue$1(readString$9(raw, 'dataJson')),
5207
- dataJson: readString$9(raw, 'dataJson') ?? null,
5218
+ correlationId: readString$a(raw, 'correlationId') ?? null,
5219
+ data: raw['data'] ?? parseJsonValue$1(readString$a(raw, 'dataJson')),
5220
+ dataJson: readString$a(raw, 'dataJson') ?? null,
5208
5221
  };
5209
5222
  }
5210
5223
  function normalizeNodeDataDetail(detail) {
5211
5224
  const raw = asRecord$5(detail);
5212
5225
  const input = normalizeDataBlob(raw['input']);
5213
5226
  const output = normalizeDataBlob(raw['output']);
5214
- const error = parseJsonValue$1(readString$9(raw, 'errorJson'));
5227
+ const error = parseJsonValue$1(readString$a(raw, 'errorJson'));
5215
5228
  const summary = raw['summary'] ?? {
5216
5229
  input,
5217
5230
  output,
5218
5231
  error,
5219
5232
  replayAvailable: readBoolean$3(raw, 'replayAvailable') ?? false,
5220
- replayAvailabilityReason: readString$9(raw, 'replayAvailabilityReason') ?? null,
5233
+ replayAvailabilityReason: readString$a(raw, 'replayAvailabilityReason') ?? null,
5221
5234
  };
5222
5235
  return {
5223
5236
  ...detail,
@@ -5225,7 +5238,7 @@ function normalizeNodeDataDetail(detail) {
5225
5238
  summary,
5226
5239
  input: raw['input'],
5227
5240
  output: raw['output'],
5228
- errorJson: readString$9(raw, 'errorJson') ?? null,
5241
+ errorJson: readString$a(raw, 'errorJson') ?? null,
5229
5242
  attempts: readArray$1(raw, 'attempts').map((attempt) => {
5230
5243
  const attemptRaw = asRecord$5(attempt);
5231
5244
  return {
@@ -5233,7 +5246,7 @@ function normalizeNodeDataDetail(detail) {
5233
5246
  attemptId: readId$3(attemptRaw, 'attemptId') ??
5234
5247
  readId$3(attemptRaw, 'nodeAttemptId') ??
5235
5248
  '',
5236
- status: String(readString$9(attemptRaw, 'status') ?? ''),
5249
+ status: String(readString$a(attemptRaw, 'status') ?? ''),
5237
5250
  };
5238
5251
  }),
5239
5252
  logs: readArray$1(raw, 'logs').map(normalizeExecutionEvent),
@@ -5252,8 +5265,8 @@ function normalizeDataBlob(value) {
5252
5265
  return {
5253
5266
  ...raw,
5254
5267
  content: raw['content'] ??
5255
- parseJsonValue$1(readString$9(raw, 'contentJson')) ??
5256
- readString$9(raw, 'contentJson') ??
5268
+ parseJsonValue$1(readString$a(raw, 'contentJson')) ??
5269
+ readString$a(raw, 'contentJson') ??
5257
5270
  null,
5258
5271
  };
5259
5272
  }
@@ -5281,7 +5294,7 @@ function readArray$1(record, key) {
5281
5294
  const value = record[key];
5282
5295
  return Array.isArray(value) ? value : [];
5283
5296
  }
5284
- function readString$9(record, key) {
5297
+ function readString$a(record, key) {
5285
5298
  const value = record[key];
5286
5299
  return typeof value === 'string' ? value : undefined;
5287
5300
  }
@@ -6720,6 +6733,43 @@ class SetLayoutAutosavePaused {
6720
6733
  this.paused = paused;
6721
6734
  }
6722
6735
  }
6736
+ class AddCanvasNote {
6737
+ note;
6738
+ static type = '[FlowplusWorkflow] Add Canvas Note';
6739
+ constructor(note) {
6740
+ this.note = note;
6741
+ }
6742
+ }
6743
+ class UpdateCanvasNote {
6744
+ noteId;
6745
+ patch;
6746
+ static type = '[FlowplusWorkflow] Update Canvas Note';
6747
+ constructor(noteId, patch) {
6748
+ this.noteId = noteId;
6749
+ this.patch = patch;
6750
+ }
6751
+ }
6752
+ class DeleteCanvasNote {
6753
+ noteId;
6754
+ static type = '[FlowplusWorkflow] Delete Canvas Note';
6755
+ constructor(noteId) {
6756
+ this.noteId = noteId;
6757
+ }
6758
+ }
6759
+ class DuplicateCanvasNote {
6760
+ noteId;
6761
+ static type = '[FlowplusWorkflow] Duplicate Canvas Note';
6762
+ constructor(noteId) {
6763
+ this.noteId = noteId;
6764
+ }
6765
+ }
6766
+ class SelectCanvasNote {
6767
+ noteId;
6768
+ static type = '[FlowplusWorkflow] Select Canvas Note';
6769
+ constructor(noteId) {
6770
+ this.noteId = noteId;
6771
+ }
6772
+ }
6723
6773
  /* ============================================================
6724
6774
  * Validation / publish
6725
6775
  * ============================================================ */
@@ -6762,10 +6812,12 @@ class ClearSelection {
6762
6812
  class SetSelectionFromCanvas {
6763
6813
  stepIds;
6764
6814
  connectionIds;
6815
+ canvasNoteIds;
6765
6816
  static type = '[FlowplusWorkflow] Set Selection From Canvas';
6766
- constructor(stepIds, connectionIds) {
6817
+ constructor(stepIds, connectionIds, canvasNoteIds = []) {
6767
6818
  this.stepIds = stepIds;
6768
6819
  this.connectionIds = connectionIds;
6820
+ this.canvasNoteIds = canvasNoteIds;
6769
6821
  }
6770
6822
  }
6771
6823
  class SetActiveInspectorTab {
@@ -6950,6 +7002,188 @@ class RunWorkflowTest {
6950
7002
  }
6951
7003
  }
6952
7004
 
7005
+ const CANVAS_NOTE_METADATA_KEY = 'canvasNotes';
7006
+ const CANVAS_NOTE_GROUP_PREFIX = 'note:';
7007
+ const CANVAS_NOTE_DEFAULT_WIDTH = 360;
7008
+ const CANVAS_NOTE_DEFAULT_HEIGHT = 220;
7009
+ const CANVAS_NOTE_MIN_WIDTH = 220;
7010
+ const CANVAS_NOTE_MIN_HEIGHT = 140;
7011
+ const CANVAS_NOTE_DEFAULT_TEXT = "## I'm a note\nDouble click to edit. **Guide**";
7012
+ const CANVAS_NOTE_COLORS = [
7013
+ {
7014
+ key: 'amber',
7015
+ label: 'Amber',
7016
+ background: '#733e0a',
7017
+ border: '#a16207',
7018
+ text: '#fff7ed',
7019
+ },
7020
+ {
7021
+ key: 'slate',
7022
+ label: 'Slate',
7023
+ background: '#1f2937',
7024
+ border: '#475569',
7025
+ text: '#f8fafc',
7026
+ },
7027
+ {
7028
+ key: 'rose',
7029
+ label: 'Rose',
7030
+ background: '#881337',
7031
+ border: '#be123c',
7032
+ text: '#fff1f2',
7033
+ },
7034
+ {
7035
+ key: 'emerald',
7036
+ label: 'Emerald',
7037
+ background: '#064e3b',
7038
+ border: '#059669',
7039
+ text: '#ecfdf5',
7040
+ },
7041
+ {
7042
+ key: 'blue',
7043
+ label: 'Blue',
7044
+ background: '#1e3a8a',
7045
+ border: '#2563eb',
7046
+ text: '#eff6ff',
7047
+ },
7048
+ {
7049
+ key: 'violet',
7050
+ label: 'Violet',
7051
+ background: '#4c1d95',
7052
+ border: '#7c3aed',
7053
+ text: '#f5f3ff',
7054
+ },
7055
+ {
7056
+ key: 'neutral',
7057
+ label: 'Neutral',
7058
+ background: '#262626',
7059
+ border: '#737373',
7060
+ text: '#fafafa',
7061
+ },
7062
+ ];
7063
+ const DEFAULT_COLOR = CANVAS_NOTE_COLORS[0];
7064
+ function canvasNoteGroupId(noteId) {
7065
+ return `${CANVAS_NOTE_GROUP_PREFIX}${noteId}`;
7066
+ }
7067
+ function parseCanvasNoteGroupId(value) {
7068
+ if (!value?.startsWith(CANVAS_NOTE_GROUP_PREFIX))
7069
+ return null;
7070
+ const id = value.slice(CANVAS_NOTE_GROUP_PREFIX.length).trim();
7071
+ return id || null;
7072
+ }
7073
+ function resolveCanvasNoteColor(color) {
7074
+ return (CANVAS_NOTE_COLORS.find((item) => item.key === color) ?? DEFAULT_COLOR);
7075
+ }
7076
+ function createCanvasNote(position, now = new Date().toISOString()) {
7077
+ return {
7078
+ id: createCanvasNoteId(),
7079
+ text: CANVAS_NOTE_DEFAULT_TEXT,
7080
+ color: DEFAULT_COLOR.key,
7081
+ x: Math.round(position.x),
7082
+ y: Math.round(position.y),
7083
+ width: CANVAS_NOTE_DEFAULT_WIDTH,
7084
+ height: CANVAS_NOTE_DEFAULT_HEIGHT,
7085
+ createdAt: now,
7086
+ updatedAt: now,
7087
+ };
7088
+ }
7089
+ function readCanvasNotesFromLayout(layout) {
7090
+ return normalizeCanvasNotes(layout?.metadata?.[CANVAS_NOTE_METADATA_KEY]);
7091
+ }
7092
+ function writeCanvasNotesToLayout(layout, workflowId, notes) {
7093
+ const metadata = { ...(layout?.metadata ?? {}) };
7094
+ metadata[CANVAS_NOTE_METADATA_KEY] = notes.map((note) => ({ ...note }));
7095
+ return {
7096
+ workflowId,
7097
+ version: layout?.version ?? null,
7098
+ nodes: layout?.nodes ?? [],
7099
+ connections: layout?.connections ?? [],
7100
+ viewport: layout?.viewport ?? null,
7101
+ metadata,
7102
+ };
7103
+ }
7104
+ function upsertCanvasNote(notes, note) {
7105
+ const index = notes.findIndex((item) => item.id === note.id);
7106
+ if (index < 0)
7107
+ return [...notes, note];
7108
+ const next = notes.slice();
7109
+ next[index] = note;
7110
+ return next;
7111
+ }
7112
+ function patchCanvasNote(note, patch, now = new Date().toISOString()) {
7113
+ return {
7114
+ ...note,
7115
+ text: patch.text ?? note.text,
7116
+ color: resolveCanvasNoteColor(patch.color ?? note.color).key,
7117
+ x: normalizeNumber(patch.x, note.x),
7118
+ y: normalizeNumber(patch.y, note.y),
7119
+ width: Math.max(CANVAS_NOTE_MIN_WIDTH, normalizeNumber(patch.width, note.width)),
7120
+ height: Math.max(CANVAS_NOTE_MIN_HEIGHT, normalizeNumber(patch.height, note.height)),
7121
+ updatedAt: now,
7122
+ };
7123
+ }
7124
+ function duplicateCanvasNote(note, now = new Date().toISOString()) {
7125
+ return {
7126
+ ...note,
7127
+ id: createCanvasNoteId(),
7128
+ x: note.x + 32,
7129
+ y: note.y + 32,
7130
+ createdAt: now,
7131
+ updatedAt: now,
7132
+ };
7133
+ }
7134
+ function normalizeCanvasNotes(value) {
7135
+ if (!Array.isArray(value))
7136
+ return [];
7137
+ return value
7138
+ .map((item) => normalizeCanvasNote(item))
7139
+ .filter((item) => item != null);
7140
+ }
7141
+ function normalizeCanvasNote(value) {
7142
+ if (!isRecord(value))
7143
+ return null;
7144
+ const id = readString$9(value, 'id');
7145
+ if (!id)
7146
+ return null;
7147
+ return {
7148
+ id,
7149
+ text: readString$9(value, 'text') ?? CANVAS_NOTE_DEFAULT_TEXT,
7150
+ color: resolveCanvasNoteColor(readString$9(value, 'color')).key,
7151
+ x: normalizeNumber(value['x'], 120),
7152
+ y: normalizeNumber(value['y'], 120),
7153
+ width: Math.max(CANVAS_NOTE_MIN_WIDTH, normalizeNumber(value['width'], CANVAS_NOTE_DEFAULT_WIDTH)),
7154
+ height: Math.max(CANVAS_NOTE_MIN_HEIGHT, normalizeNumber(value['height'], CANVAS_NOTE_DEFAULT_HEIGHT)),
7155
+ createdAt: readString$9(value, 'createdAt'),
7156
+ updatedAt: readString$9(value, 'updatedAt'),
7157
+ };
7158
+ }
7159
+ function createCanvasNoteId() {
7160
+ const cryptoApi = globalThis.crypto;
7161
+ if (cryptoApi && typeof cryptoApi.randomUUID === 'function') {
7162
+ return cryptoApi.randomUUID();
7163
+ }
7164
+ return `note_${Date.now().toString(36)}_${Math.random()
7165
+ .toString(36)
7166
+ .slice(2, 8)}`;
7167
+ }
7168
+ function normalizeNumber(value, fallback) {
7169
+ if (typeof value === 'number' && Number.isFinite(value)) {
7170
+ return Math.round(value);
7171
+ }
7172
+ if (typeof value === 'string' && value.trim()) {
7173
+ const parsed = Number(value);
7174
+ if (Number.isFinite(parsed))
7175
+ return Math.round(parsed);
7176
+ }
7177
+ return fallback;
7178
+ }
7179
+ function readString$9(record, key) {
7180
+ const value = record[key];
7181
+ return typeof value === 'string' && value.trim() ? value : null;
7182
+ }
7183
+ function isRecord(value) {
7184
+ return value != null && typeof value === 'object' && !Array.isArray(value);
7185
+ }
7186
+
6953
7187
  /**
6954
7188
  * NGXS state model for the FlowPlus design-time workflow builder.
6955
7189
  *
@@ -7031,6 +7265,7 @@ const FLOWPLUS_WORKFLOW_DEFAULT_STATE = {
7031
7265
  selection: {
7032
7266
  stepIds: [],
7033
7267
  connectionIds: [],
7268
+ canvasNoteIds: [],
7034
7269
  triggerId: null,
7035
7270
  isWorkflowSelected: true,
7036
7271
  activeTab: 'overview',
@@ -7841,6 +8076,7 @@ let FlowplusWorkflowState = class FlowplusWorkflowState {
7841
8076
  ? Array.from(new Set([...cur.stepIds, action.stepId]))
7842
8077
  : [action.stepId],
7843
8078
  connectionIds: action.append ? cur.connectionIds : [],
8079
+ canvasNoteIds: [],
7844
8080
  triggerId: null,
7845
8081
  isWorkflowSelected: false,
7846
8082
  activeTab: cur.activeTab || 'overview',
@@ -8044,6 +8280,7 @@ let FlowplusWorkflowState = class FlowplusWorkflowState {
8044
8280
  selection: {
8045
8281
  stepIds: [],
8046
8282
  connectionIds: [action.connectionId],
8283
+ canvasNoteIds: [],
8047
8284
  triggerId: null,
8048
8285
  isWorkflowSelected: false,
8049
8286
  activeTab: 'configure',
@@ -8059,6 +8296,7 @@ let FlowplusWorkflowState = class FlowplusWorkflowState {
8059
8296
  selection: {
8060
8297
  stepIds: [],
8061
8298
  connectionIds: [],
8299
+ canvasNoteIds: [],
8062
8300
  triggerId: action.triggerId,
8063
8301
  isWorkflowSelected: false,
8064
8302
  activeTab: 'configure',
@@ -8257,6 +8495,7 @@ let FlowplusWorkflowState = class FlowplusWorkflowState {
8257
8495
  }).pipe(catchError(() => EMPTY));
8258
8496
  }
8259
8497
  triggerTimers = new Map();
8498
+ triggerCommitSeq = new Map();
8260
8499
  updateTrigger(ctx, action) {
8261
8500
  const state = ctx.getState();
8262
8501
  ctx.patchState({
@@ -8287,18 +8526,24 @@ let FlowplusWorkflowState = class FlowplusWorkflowState {
8287
8526
  }
8288
8527
  const patch = ctxState.patch;
8289
8528
  this.triggerTimers.delete(triggerId);
8529
+ const seq = (this.triggerCommitSeq.get(triggerId) ?? 0) + 1;
8530
+ this.triggerCommitSeq.set(triggerId, seq);
8290
8531
  handleApiRequest({
8291
8532
  ctx,
8292
8533
  key: FlowplusWorkflowActionKey.UpdateTrigger,
8293
8534
  request$: this.defApi.updateTrigger(triggerId, patch),
8294
8535
  onSuccess: (server, s) => {
8536
+ const isLatest = this.triggerCommitSeq.get(triggerId) === seq;
8295
8537
  const stillEditing = this.triggerTimers.has(triggerId);
8296
8538
  const hasPending = hasOtherPending(s.builder.pendingOperations);
8297
8539
  const hasQueuedTriggerUpdates = this.triggerTimers.size > 0;
8540
+ const applyServer = isLatest && !stillEditing;
8298
8541
  return {
8299
8542
  builder: {
8300
8543
  ...s.builder,
8301
- triggers: s.builder.triggers.map((trigger) => trigger.id === triggerId && !stillEditing ? server : trigger),
8544
+ triggers: s.builder.triggers.map((trigger) => trigger.id === triggerId && applyServer
8545
+ ? { ...trigger, ...server, ...patch }
8546
+ : trigger),
8302
8547
  dirty: {
8303
8548
  ...s.builder.dirty,
8304
8549
  triggers: hasPending || hasQueuedTriggerUpdates,
@@ -8370,6 +8615,95 @@ let FlowplusWorkflowState = class FlowplusWorkflowState {
8370
8615
  }
8371
8616
  return undefined;
8372
8617
  }
8618
+ addCanvasNote(ctx, action) {
8619
+ const state = ctx.getState();
8620
+ const workflowId = state.builder.workflowId;
8621
+ if (!workflowId)
8622
+ return;
8623
+ const notes = upsertCanvasNote(readCanvasNotesFromLayout(state.builder.layout), action.note);
8624
+ this.patchCanvasNotes(ctx, workflowId, notes, [action.note.id]);
8625
+ return ctx.dispatch(new MarkLayoutDirty());
8626
+ }
8627
+ updateCanvasNote(ctx, action) {
8628
+ const state = ctx.getState();
8629
+ const workflowId = state.builder.workflowId;
8630
+ if (!workflowId)
8631
+ return;
8632
+ const notes = readCanvasNotesFromLayout(state.builder.layout);
8633
+ const next = notes.map((note) => note.id === action.noteId ? patchCanvasNote(note, action.patch) : note);
8634
+ if (!next.some((note) => note.id === action.noteId))
8635
+ return;
8636
+ this.patchCanvasNotes(ctx, workflowId, next);
8637
+ return ctx.dispatch(new MarkLayoutDirty());
8638
+ }
8639
+ deleteCanvasNote(ctx, action) {
8640
+ const state = ctx.getState();
8641
+ const workflowId = state.builder.workflowId;
8642
+ if (!workflowId)
8643
+ return;
8644
+ const notes = readCanvasNotesFromLayout(state.builder.layout);
8645
+ const next = notes.filter((note) => note.id !== action.noteId);
8646
+ if (next.length === notes.length)
8647
+ return;
8648
+ const selected = state.builder.selection.canvasNoteIds.includes(action.noteId)
8649
+ ? []
8650
+ : state.builder.selection.canvasNoteIds;
8651
+ this.patchCanvasNotes(ctx, workflowId, next, selected);
8652
+ return ctx.dispatch(new MarkLayoutDirty());
8653
+ }
8654
+ duplicateCanvasNote(ctx, action) {
8655
+ const state = ctx.getState();
8656
+ const workflowId = state.builder.workflowId;
8657
+ if (!workflowId)
8658
+ return;
8659
+ const notes = readCanvasNotesFromLayout(state.builder.layout);
8660
+ const note = notes.find((item) => item.id === action.noteId);
8661
+ if (!note)
8662
+ return;
8663
+ const copy = duplicateCanvasNote(note);
8664
+ this.patchCanvasNotes(ctx, workflowId, [...notes, copy], [copy.id]);
8665
+ return ctx.dispatch(new MarkLayoutDirty());
8666
+ }
8667
+ selectCanvasNote(ctx, action) {
8668
+ const state = ctx.getState();
8669
+ ctx.patchState({
8670
+ builder: {
8671
+ ...state.builder,
8672
+ selection: {
8673
+ stepIds: [],
8674
+ connectionIds: [],
8675
+ canvasNoteIds: action.noteId ? [action.noteId] : [],
8676
+ triggerId: null,
8677
+ isWorkflowSelected: false,
8678
+ activeTab: 'overview',
8679
+ },
8680
+ },
8681
+ ui: { ...state.ui, isInspectorOpen: false },
8682
+ });
8683
+ }
8684
+ patchCanvasNotes(ctx, workflowId, notes, selectedNoteIds) {
8685
+ const state = ctx.getState();
8686
+ const selection = selectedNoteIds === undefined
8687
+ ? state.builder.selection
8688
+ : {
8689
+ stepIds: [],
8690
+ connectionIds: [],
8691
+ canvasNoteIds: selectedNoteIds,
8692
+ triggerId: null,
8693
+ isWorkflowSelected: false,
8694
+ activeTab: selectedNoteIds.length > 1 ? 'multi' : 'overview',
8695
+ };
8696
+ ctx.patchState({
8697
+ builder: {
8698
+ ...state.builder,
8699
+ layout: writeCanvasNotesToLayout(state.builder.layout, workflowId, notes),
8700
+ selection,
8701
+ layoutSaveStatus: 'dirty',
8702
+ dirty: { ...state.builder.dirty, layout: true },
8703
+ },
8704
+ ui: { ...state.ui, isInspectorOpen: false },
8705
+ });
8706
+ }
8373
8707
  saveLayout(ctx) {
8374
8708
  if (this.layoutTimer) {
8375
8709
  clearTimeout(this.layoutTimer);
@@ -8483,8 +8817,13 @@ let FlowplusWorkflowState = class FlowplusWorkflowState {
8483
8817
  setSelectionFromCanvas(ctx, action) {
8484
8818
  const state = ctx.getState();
8485
8819
  const cur = state.builder.selection;
8486
- const nothing = action.stepIds.length === 0 && action.connectionIds.length === 0;
8487
- const single = action.stepIds.length + action.connectionIds.length === 1;
8820
+ const nothing = action.stepIds.length === 0 &&
8821
+ action.connectionIds.length === 0 &&
8822
+ action.canvasNoteIds.length === 0;
8823
+ const single = action.stepIds.length +
8824
+ action.connectionIds.length +
8825
+ action.canvasNoteIds.length ===
8826
+ 1;
8488
8827
  let activeTab = cur.activeTab;
8489
8828
  if (nothing)
8490
8829
  activeTab = 'overview';
@@ -8497,12 +8836,16 @@ let FlowplusWorkflowState = class FlowplusWorkflowState {
8497
8836
  else if (action.connectionIds.length === 1) {
8498
8837
  activeTab = 'configure';
8499
8838
  }
8839
+ else if (action.canvasNoteIds.length === 1) {
8840
+ activeTab = 'overview';
8841
+ }
8500
8842
  ctx.patchState({
8501
8843
  builder: {
8502
8844
  ...state.builder,
8503
8845
  selection: {
8504
8846
  stepIds: action.stepIds,
8505
8847
  connectionIds: action.connectionIds,
8848
+ canvasNoteIds: action.canvasNoteIds,
8506
8849
  triggerId: null,
8507
8850
  // Clicking empty canvas no longer auto-selects the workflow root —
8508
8851
  // the canvas-first shell expects the inspector to close on empty
@@ -8519,7 +8862,9 @@ let FlowplusWorkflowState = class FlowplusWorkflowState {
8519
8862
  // empty canvas still closes whatever was open.
8520
8863
  ui: {
8521
8864
  ...state.ui,
8522
- isInspectorOpen: nothing ? false : state.ui.isInspectorOpen,
8865
+ isInspectorOpen: nothing || action.canvasNoteIds.length > 0
8866
+ ? false
8867
+ : state.ui.isInspectorOpen,
8523
8868
  },
8524
8869
  });
8525
8870
  }
@@ -9226,6 +9571,21 @@ __decorate([
9226
9571
  __decorate([
9227
9572
  Action(SetLayoutAutosavePaused)
9228
9573
  ], FlowplusWorkflowState.prototype, "setLayoutAutosavePaused", null);
9574
+ __decorate([
9575
+ Action(AddCanvasNote)
9576
+ ], FlowplusWorkflowState.prototype, "addCanvasNote", null);
9577
+ __decorate([
9578
+ Action(UpdateCanvasNote)
9579
+ ], FlowplusWorkflowState.prototype, "updateCanvasNote", null);
9580
+ __decorate([
9581
+ Action(DeleteCanvasNote)
9582
+ ], FlowplusWorkflowState.prototype, "deleteCanvasNote", null);
9583
+ __decorate([
9584
+ Action(DuplicateCanvasNote)
9585
+ ], FlowplusWorkflowState.prototype, "duplicateCanvasNote", null);
9586
+ __decorate([
9587
+ Action(SelectCanvasNote)
9588
+ ], FlowplusWorkflowState.prototype, "selectCanvasNote", null);
9229
9589
  __decorate([
9230
9590
  Action(SaveLayout)
9231
9591
  ], FlowplusWorkflowState.prototype, "saveLayout", null);
@@ -9438,7 +9798,7 @@ FlowplusWorkflowState = FlowplusWorkflowState_1 = __decorate([
9438
9798
  ], FlowplusWorkflowState);
9439
9799
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: FlowplusWorkflowState, decorators: [{
9440
9800
  type: Injectable
9441
- }], propDecorators: { loadWorkflowList: [], setStudioFilter: [], createWorkflow: [], duplicateWorkflow: [], deleteWorkflow: [], loadWorkflowBuilder: [], applyBuilderSnapshot: [], reloadWorkflowBuilder: [], clearWorkflowBuilder: [], loadWorkflowCatalog: [], setWorkflowCatalog: [], loadContextCatalog: [], loadContextCatalogForStep: [], setContextCatalog: [], updateWorkflowMetadata: [], commitWorkflowMetadata: [], setWorkflowDefinition: [], updateWorkflowResources: [], updateWorkflowVariables: [], selectStep: [], createStep: [], updateStep: [], deleteStep: [], moveStep: [], selectConnection: [], selectTrigger: [], createConnection: [], updateConnection: [], deleteConnection: [], loadTriggers: [], createTrigger: [], updateTrigger: [], deleteTrigger: [], loadLayout: [], markLayoutDirty: [], setLayoutAutosavePaused: [], saveLayout: [], validateWorkflow: [], setValidation: [], setLayout: [], publishWorkflow: [], unpublishWorkflow: [], setSelection: [], setSelectionFromCanvas: [], setActiveInspectorTab: [], setBottomPanelTab: [], setBottomPanelOpen: [], setMinimap: [], setPaletteOpen: [], setInspectorOpen: [], setCreateDialogOpen: [], setPaletteSearch: [], setCanvasViewport: [], setReadonly: [], setPendingOperation: [], clearPendingOperation: [], clearConflict: [], undoBuilderCommand: [], redoBuilderCommand: [], clearCommandHistory: [], setSelectedRuntimeTrigger: [], runAutomationTrigger: [], pollAutomationExecution: [], loadLatestAutomationExecution: [], loadAutomationExecution: [], applyAutomationExecutionDetail: [], clearAutomationRuntimeState: [], selectRuntimeNodeRun: [], runWorkflowTest: [] } });
9801
+ }], propDecorators: { loadWorkflowList: [], setStudioFilter: [], createWorkflow: [], duplicateWorkflow: [], deleteWorkflow: [], loadWorkflowBuilder: [], applyBuilderSnapshot: [], reloadWorkflowBuilder: [], clearWorkflowBuilder: [], loadWorkflowCatalog: [], setWorkflowCatalog: [], loadContextCatalog: [], loadContextCatalogForStep: [], setContextCatalog: [], updateWorkflowMetadata: [], commitWorkflowMetadata: [], setWorkflowDefinition: [], updateWorkflowResources: [], updateWorkflowVariables: [], selectStep: [], createStep: [], updateStep: [], deleteStep: [], moveStep: [], selectConnection: [], selectTrigger: [], createConnection: [], updateConnection: [], deleteConnection: [], loadTriggers: [], createTrigger: [], updateTrigger: [], deleteTrigger: [], loadLayout: [], markLayoutDirty: [], setLayoutAutosavePaused: [], addCanvasNote: [], updateCanvasNote: [], deleteCanvasNote: [], duplicateCanvasNote: [], selectCanvasNote: [], saveLayout: [], validateWorkflow: [], setValidation: [], setLayout: [], publishWorkflow: [], unpublishWorkflow: [], setSelection: [], setSelectionFromCanvas: [], setActiveInspectorTab: [], setBottomPanelTab: [], setBottomPanelOpen: [], setMinimap: [], setPaletteOpen: [], setInspectorOpen: [], setCreateDialogOpen: [], setPaletteSearch: [], setCanvasViewport: [], setReadonly: [], setPendingOperation: [], clearPendingOperation: [], clearConflict: [], undoBuilderCommand: [], redoBuilderCommand: [], clearCommandHistory: [], setSelectedRuntimeTrigger: [], runAutomationTrigger: [], pollAutomationExecution: [], loadLatestAutomationExecution: [], loadAutomationExecution: [], applyAutomationExecutionDetail: [], clearAutomationRuntimeState: [], selectRuntimeNodeRun: [], runWorkflowTest: [] } });
9442
9802
  function collectRuntimeFacts(detail) {
9443
9803
  const nodeKeyByRunId = new Map((detail.nodeRuns ?? []).map((nodeRun) => [
9444
9804
  String(nodeRun.nodeRunId),
@@ -9831,6 +10191,8 @@ class FlowplusWorkflowFacade {
9831
10191
  selection = select(FlowplusWorkflowState.selection);
9832
10192
  viewport = select(FlowplusWorkflowState.viewport);
9833
10193
  layout = select(FlowplusWorkflowState.layout);
10194
+ canvasNotes = computed(() => readCanvasNotesFromLayout(this.layout()), ...(ngDevMode ? [{ debugName: "canvasNotes" }] : /* istanbul ignore next */ []));
10195
+ selectedCanvasNoteIds = computed(() => this.selection().canvasNoteIds, ...(ngDevMode ? [{ debugName: "selectedCanvasNoteIds" }] : /* istanbul ignore next */ []));
9834
10196
  saveStatus = select(FlowplusWorkflowState.saveStatus);
9835
10197
  layoutSaveStatus = select(FlowplusWorkflowState.layoutSaveStatus);
9836
10198
  dirtyFlags = select(FlowplusWorkflowState.dirtyFlags);
@@ -10244,20 +10606,24 @@ class FlowplusWorkflowFacade {
10244
10606
  return this.store.dispatch(new SetSelection({
10245
10607
  stepIds: [],
10246
10608
  connectionIds: [],
10609
+ canvasNoteIds: [],
10247
10610
  triggerId: null,
10248
10611
  isWorkflowSelected: true,
10249
10612
  activeTab: 'overview',
10250
10613
  }));
10251
10614
  }
10252
10615
  /** Set selection from a Foblex `fSelectionChange` payload. */
10253
- setSelectionFromCanvas(stepIds, connectionIds) {
10254
- return this.store.dispatch(new SetSelectionFromCanvas(stepIds, connectionIds));
10616
+ setSelectionFromCanvas(stepIds, connectionIds, canvasNoteIds = []) {
10617
+ return this.store.dispatch(new SetSelectionFromCanvas(stepIds, connectionIds, canvasNoteIds));
10255
10618
  }
10256
10619
  /** Select every step (Ctrl/Cmd+A). */
10257
10620
  selectAll() {
10258
10621
  const ids = this.steps().map((s) => s.id);
10259
10622
  return this.store.dispatch(new SetSelectionFromCanvas(ids, []));
10260
10623
  }
10624
+ selectCanvasNote(noteId) {
10625
+ return this.store.dispatch(new SelectCanvasNote(noteId));
10626
+ }
10261
10627
  setInspectorTab(tab) {
10262
10628
  return this.store.dispatch(new SetActiveInspectorTab(tab));
10263
10629
  }
@@ -10301,6 +10667,21 @@ class FlowplusWorkflowFacade {
10301
10667
  deleteTrigger(triggerId) {
10302
10668
  return this.store.dispatch(new DeleteTrigger(triggerId));
10303
10669
  }
10670
+ /* -------- canvas notes -------- */
10671
+ addCanvasNote(position) {
10672
+ const note = createCanvasNote(position);
10673
+ this.store.dispatch(new AddCanvasNote(note));
10674
+ return note;
10675
+ }
10676
+ updateCanvasNote(noteId, patch) {
10677
+ return this.store.dispatch(new UpdateCanvasNote(noteId, patch));
10678
+ }
10679
+ deleteCanvasNote(noteId) {
10680
+ return this.store.dispatch(new DeleteCanvasNote(noteId));
10681
+ }
10682
+ duplicateCanvasNote(noteId) {
10683
+ return this.store.dispatch(new DuplicateCanvasNote(noteId));
10684
+ }
10304
10685
  /* -------- layout -------- */
10305
10686
  loadLayout() {
10306
10687
  return this.store.dispatch(new LoadLayout());
@@ -10346,6 +10727,28 @@ class FlowplusWorkflowFacade {
10346
10727
  y: trigger.y,
10347
10728
  };
10348
10729
  }
10730
+ let layoutForNotes = state.layout;
10731
+ if ((args.notes?.length ?? 0) > 0) {
10732
+ const notes = readCanvasNotesFromLayout(state.layout);
10733
+ let notesChanged = false;
10734
+ const nextNotes = notes.map((note) => {
10735
+ const update = args.notes?.find((item) => item.noteId === note.id);
10736
+ if (!update)
10737
+ return note;
10738
+ const next = patchCanvasNote(note, update);
10739
+ if (next.x !== note.x ||
10740
+ next.y !== note.y ||
10741
+ next.width !== note.width ||
10742
+ next.height !== note.height) {
10743
+ notesChanged = true;
10744
+ }
10745
+ return next;
10746
+ });
10747
+ if (notesChanged) {
10748
+ changed = true;
10749
+ layoutForNotes = writeCanvasNotesToLayout(state.layout, state.workflowId, nextNotes);
10750
+ }
10751
+ }
10349
10752
  if (!changed)
10350
10753
  return false;
10351
10754
  if ((args.triggers?.length ?? 0) > 0) {
@@ -10353,11 +10756,17 @@ class FlowplusWorkflowFacade {
10353
10756
  }
10354
10757
  this.store.dispatch(new SetLayout({
10355
10758
  workflowId: state.workflowId,
10356
- version: state.layout?.version ?? null,
10759
+ version: layoutForNotes?.version ?? null,
10357
10760
  nodes: Array.from(nodesByStepId.values()),
10358
- connections: state.layout?.connections ?? [],
10359
- viewport: state.layout?.viewport ?? null,
10360
- metadata,
10761
+ connections: layoutForNotes?.connections ?? [],
10762
+ viewport: layoutForNotes?.viewport ?? null,
10763
+ metadata: {
10764
+ ...metadata,
10765
+ ...(layoutForNotes?.metadata ?? {}),
10766
+ ...((args.triggers?.length ?? 0) > 0
10767
+ ? { triggerPositions }
10768
+ : {}),
10769
+ },
10361
10770
  }));
10362
10771
  if (options.scheduleSave !== false) {
10363
10772
  this.store.dispatch(new MarkLayoutDirty());
@@ -12468,6 +12877,19 @@ function toPolicy(main, advanced, advancedEmptyState) {
12468
12877
  };
12469
12878
  }
12470
12879
 
12880
+ const SCHEDULE_MODE_EXCLUSIVE_KEYS = [
12881
+ 'cron',
12882
+ 'cronExpression',
12883
+ 'intervalSeconds',
12884
+ 'everySeconds',
12885
+ 'seconds',
12886
+ 'intervalMinutes',
12887
+ 'everyMinutes',
12888
+ 'runAt',
12889
+ 'runAtUtc',
12890
+ 'oneTimeAt',
12891
+ 'at',
12892
+ ];
12471
12893
  class AutomationSmartEditorComponent {
12472
12894
  step = input(null, ...(ngDevMode ? [{ debugName: "step" }] : /* istanbul ignore next */ []));
12473
12895
  trigger = input(null, ...(ngDevMode ? [{ debugName: "trigger" }] : /* istanbul ignore next */ []));
@@ -12628,10 +13050,26 @@ class AutomationSmartEditorComponent {
12628
13050
  { value: 'hmac', label: 'HMAC' },
12629
13051
  { value: 'sharedSecret', label: 'Shared secret' },
12630
13052
  ];
12631
- scheduleModeOptions = [
12632
- { value: 'cron', label: 'Cron' },
12633
- { value: 'interval', label: 'Interval' },
12634
- ];
13053
+ scheduleModeOptions = computed(() => {
13054
+ const supportedModes = readStringArray$2(asRecord$4(this.scheduleOptions())['supportedModes']);
13055
+ const modes = supportedModes.length > 0
13056
+ ? supportedModes
13057
+ : ['cron', 'interval', 'once'];
13058
+ const seen = new Set();
13059
+ return modes
13060
+ .map((mode) => String(mode).trim())
13061
+ .filter((mode) => {
13062
+ const normalized = mode.toLowerCase();
13063
+ if (!normalized || seen.has(normalized))
13064
+ return false;
13065
+ seen.add(normalized);
13066
+ return true;
13067
+ })
13068
+ .map((mode) => ({
13069
+ value: mode,
13070
+ label: scheduleModeLabel(mode),
13071
+ }));
13072
+ }, ...(ngDevMode ? [{ debugName: "scheduleModeOptions" }] : /* istanbul ignore next */ []));
12635
13073
  ifOperatorOptions = [
12636
13074
  { value: 'equals', label: 'Equals' },
12637
13075
  { value: 'notEquals', label: 'Not equals' },
@@ -12716,6 +13154,11 @@ class AutomationSmartEditorComponent {
12716
13154
  label: humanize$1(policy),
12717
13155
  })),
12718
13156
  ], ...(ngDevMode ? [{ debugName: "misfirePolicyOptions" }] : /* istanbul ignore next */ []));
13157
+ scheduleMode = computed(() => normalizeScheduleMode(this.config()['mode']), ...(ngDevMode ? [{ debugName: "scheduleMode" }] : /* istanbul ignore next */ []));
13158
+ showScheduleCron = computed(() => this.scheduleMode() === 'cron', ...(ngDevMode ? [{ debugName: "showScheduleCron" }] : /* istanbul ignore next */ []));
13159
+ showScheduleInterval = computed(() => this.scheduleMode() === 'interval', ...(ngDevMode ? [{ debugName: "showScheduleInterval" }] : /* istanbul ignore next */ []));
13160
+ showScheduleOnce = computed(() => this.scheduleMode() === 'once', ...(ngDevMode ? [{ debugName: "showScheduleOnce" }] : /* istanbul ignore next */ []));
13161
+ showScheduleStartDate = computed(() => !this.showScheduleOnce(), ...(ngDevMode ? [{ debugName: "showScheduleStartDate" }] : /* istanbul ignore next */ []));
12719
13162
  formOptions = computed(() => [
12720
13163
  { value: '', label: 'Select backend form' },
12721
13164
  ...this.forms().map((form) => ({
@@ -13002,6 +13445,14 @@ class AutomationSmartEditorComponent {
13002
13445
  onConfigFieldChange(key, value) {
13003
13446
  this.patchConfig({ ...this.config(), [key]: coerceFieldValue(value) });
13004
13447
  }
13448
+ onScheduleModeChange(value) {
13449
+ const mode = normalizeScheduleMode(selectScalarValue(value));
13450
+ const next = { ...this.config(), mode };
13451
+ for (const key of SCHEDULE_MODE_EXCLUSIVE_KEYS) {
13452
+ delete next[key];
13453
+ }
13454
+ this.patchConfig(next);
13455
+ }
13005
13456
  onSubworkflowTargetChange(automationId) {
13006
13457
  const automation = this.subworkflowAutomations().find((item) => String(item.automationId) === String(automationId));
13007
13458
  this.patchConfig({
@@ -13807,7 +14258,7 @@ class AutomationSmartEditorComponent {
13807
14258
  .subscribe((result) => this.subworkflowAutomations.set(result.items ?? []));
13808
14259
  }
13809
14260
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: AutomationSmartEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
13810
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: AutomationSmartEditorComponent, isStandalone: true, selector: "fp-automation-smart-editor", inputs: { step: { classPropertyName: "step", publicName: "step", isSignal: true, isRequired: false, transformFunction: null }, trigger: { classPropertyName: "trigger", publicName: "trigger", isSignal: true, isRequired: false, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, view: { classPropertyName: "view", publicName: "view", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "block h-full min-h-0" }, ngImport: i0, template: "<div\n class=\"fp-scroll flex h-full min-h-0 flex-col overflow-y-auto\"\n fpDropData\n [fpDropAutoInsert]=\"false\"\n (dataDrop)=\"insertExpression($event)\"\n>\n <div class=\"space-y-4 px-5 py-5\">\n @if (helperError()) {\n <div\n class=\"rounded-lg border border-[rgb(var(--fp-warning))]/30 bg-[rgb(var(--fp-warning))]/10 px-3 py-2 text-[12px] leading-5 text-(--p-text-color)\"\n >\n {{ helperError() }}\n </div>\n }\n\n @if (sectionInMain(\"startConnection\") && trigger()) {\n <section\n class=\"flex flex-col gap-0 overflow-hidden rounded-md border border-surface-200 bg-surface-0\"\n >\n <h3\n class=\"m-0 border-b border-surface-200 bg-surface-50 px-4 py-3 text-lg font-semibold text-color\"\n >\n Start connection\n </h3>\n <div class=\"space-y-3 p-4\">\n @if (startConnection().key) {\n @if (startConnection().step) {\n <div class=\"flex flex-wrap items-start justify-between gap-3\">\n <div class=\"min-w-0 space-y-1\">\n <div class=\"text-[12px] font-semibold text-(--p-text-muted-color)\">\n First node connected\n </div>\n <div class=\"truncate text-[14px] font-semibold text-(--p-text-color)\">\n {{ startConnection().label }}\n </div>\n <div class=\"flex flex-wrap items-center gap-2 text-[12px] text-(--p-text-muted-color)\">\n <span>Managed on canvas</span>\n <span\n class=\"rounded-md bg-(--p-surface-100) px-2 py-1 font-mono text-[11px]\"\n >\n {{ startConnection().key }}\n </span>\n </div>\n </div>\n <div class=\"flex shrink-0 flex-wrap gap-2\">\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n severity=\"secondary\"\n label=\"Focus connected node\"\n (onClick)=\"focusStartConnection()\"\n />\n </div>\n </div>\n } @else {\n <div class=\"space-y-2\">\n <div class=\"text-[14px] font-semibold text-(--p-text-color)\">\n No first node connected\n </div>\n <p class=\"m-0 text-[12px] leading-5 text-(--p-text-muted-color)\">\n The saved start connection points to a node key that is not on\n the canvas. Connect this trigger to the first node on the\n canvas.\n </p>\n <div class=\"flex flex-wrap items-center gap-2 text-[12px] text-(--p-text-muted-color)\">\n <span>Technical key</span>\n <span\n class=\"rounded-md bg-(--p-surface-100) px-2 py-1 font-mono text-[11px]\"\n >\n {{ startConnection().key }}\n </span>\n <span>Managed on canvas</span>\n </div>\n </div>\n }\n } @else {\n <div class=\"rounded-lg border border-dashed border-(--p-content-border-color) bg-(--p-surface-50) p-3\">\n <div class=\"text-[14px] font-semibold text-(--p-text-color)\">\n No first node connected\n </div>\n <p class=\"m-0 mt-1 text-[12px] leading-5 text-(--p-text-muted-color)\">\n Connect this trigger to the first node on the canvas.\n </p>\n </div>\n }\n </div>\n </section>\n }\n\n @switch (editorType()) {\n @case (\"ManualTrigger\") {\n @if (sectionInMain(\"manualTrigger\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Manual run input</div>\n @if (hasTriggerPayloadSchema() && triggerPayloadSchema(); as schema) {\n <div class=\"rounded-md border border-surface-200 bg-surface-0 p-3\">\n <div class=\"fp-ae-label mb-2\">Payload schema</div>\n <pre class=\"fp-ae-code\">{{ schemaText(schema) }}</pre>\n </div>\n }\n @if (triggerPayloadSample(); as sample) {\n <div class=\"mt-3 rounded-md border border-surface-200 bg-surface-0 p-3\">\n <div class=\"fp-ae-label mb-2\">Sample payload</div>\n <pre class=\"fp-ae-code\">{{ schemaText(sample) }}</pre>\n </div>\n }\n @if (!hasTriggerPayloadSchema() && !triggerPayloadSample()) {\n <p class=\"fp-ae-copy\">\n This manual trigger has no backend-provided input schema. It can still be connected to the first step on the canvas.\n </p>\n }\n </section>\n }\n }\n @case (\"WebhookTrigger\") {\n @if (sectionInMain(\"webhookSetup\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Webhook setup</div>\n @if (webhookSetup(); as setup) {\n <div class=\"flex gap-2\">\n <mt-text-field\n class=\"flex-1 font-mono\"\n [ngModel]=\"setup.webhookUrl ?? ''\"\n [readonly]=\"true\"\n label=\"Webhook URL\"\n hint=\"Backend-generated endpoint clients should call to start this trigger.\"\n />\n <mt-button class=\"self-end\" size=\"small\" variant=\"outlined\" label=\"Copy\" (onClick)=\"copyWebhookUrl()\" />\n </div>\n <div class=\"grid gap-2 md:grid-cols-2\">\n <div class=\"fp-ae-kv\">\n <span>Auth mode</span>\n <strong>{{ setup.authMode ?? \"Backend default\" }}</strong>\n </div>\n <div class=\"fp-ae-kv\">\n <span>Required headers</span>\n <strong>{{ (setup.requiredHeaders ?? []).join(\", \") || \"-\" }}</strong>\n </div>\n </div>\n @if (setup.hmacSigning) {\n <p class=\"fp-ae-copy\">{{ setup.hmacSigning }}</p>\n }\n @if (setup.sampleRequest) {\n <details class=\"mt-3 rounded-lg border border-(--p-content-border-color)\">\n <summary class=\"cursor-pointer px-3 py-2 text-[12px] font-semibold\">\n Sample request\n </summary>\n <pre class=\"fp-ae-code\">{{ schemaText(setup.sampleRequest) }}</pre>\n </details>\n }\n } @else {\n <p class=\"fp-ae-copy\">\n Webhook setup is not available for this draft yet.\n </p>\n }\n </section>\n }\n @if (sectionInMain(\"authPolicy\") && hasAuthPolicy()) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Authentication policy</div>\n <div class=\"grid gap-3 md:grid-cols-3\">\n <mt-select-field\n [ngModel]=\"authPolicy()['mode'] ?? ''\"\n (ngModelChange)=\"onAuthFieldChange('mode', $event)\"\n label=\"Mode\"\n hint=\"Authentication mode required by the backend webhook policy.\"\n [options]=\"authModeOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-text-field\n [ngModel]=\"authPolicy()['signatureHeaderName'] ?? ''\"\n (ngModelChange)=\"onAuthFieldChange('signatureHeaderName', $event)\"\n label=\"Signature header\"\n hint=\"Header name that carries the webhook signature when the policy requires signed requests.\"\n />\n <mt-text-field\n [ngModel]=\"authPolicy()['timestampHeaderName'] ?? ''\"\n (ngModelChange)=\"onAuthFieldChange('timestampHeaderName', $event)\"\n label=\"Timestamp header\"\n hint=\"Header name that carries the request timestamp for replay protection.\"\n />\n </div>\n </section>\n }\n }\n @case (\"FormSubmitTrigger\") {\n @if (sectionInMain(\"formBinding\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Form binding</div>\n <div class=\"grid gap-3 md:grid-cols-2\">\n <mt-select-field\n [ngModel]=\"selectedFormId() || formBinding()?.formId || ''\"\n (ngModelChange)=\"onFormChange($event)\"\n label=\"Form\"\n hint=\"Choose a backend form that will submit data into this trigger.\"\n [options]=\"formOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-select-field\n [ngModel]=\"selectedFormVersionId() || formBinding()?.formVersionId || ''\"\n (ngModelChange)=\"onFormVersionChange($event)\"\n label=\"Form version\"\n hint=\"Persist the exact backend formVersionId. Do not type or generate IDs manually.\"\n [options]=\"formVersionOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n </div>\n <div class=\"mt-3 flex flex-wrap items-center gap-2\">\n <mt-button size=\"small\" severity=\"primary\" label=\"Save binding\" (onClick)=\"saveFormBinding()\" />\n @if (formBinding()) {\n <span class=\"rounded-lg bg-(--p-surface-100) px-2 py-1 font-mono text-[11px] text-(--p-text-muted-color)\">\n {{ formBinding()!.formVersionId }}\n </span>\n }\n </div>\n @if (formSchema(); as schema) {\n <details class=\"mt-3 rounded-lg border border-(--p-content-border-color)\">\n <summary class=\"cursor-pointer px-3 py-2 text-[12px] font-semibold\">\n Form schema preview\n </summary>\n <pre class=\"fp-ae-code\">{{ schemaText(schema.schema) }}</pre>\n </details>\n }\n </section>\n }\n }\n @case (\"ScheduleTrigger\") {\n @if (sectionInMain(\"schedule\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Schedule</div>\n <div class=\"grid gap-3 md:grid-cols-2\">\n <mt-select-field\n [ngModel]=\"config()['mode'] ?? 'cron'\"\n (ngModelChange)=\"onConfigFieldChange('mode', $event)\"\n label=\"Mode\"\n hint=\"Pick one schedule mode: cron, interval, or once. Backend validation requires exactly one mode.\"\n [options]=\"scheduleModeOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-select-field\n [ngModel]=\"config()['timezone'] ?? 'UTC'\"\n (ngModelChange)=\"onConfigFieldChange('timezone', $event)\"\n label=\"Timezone\"\n hint=\"Timezone used to calculate the next fire time.\"\n [options]=\"timeZoneOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"config()['cron'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('cron', $event)\"\n label=\"Cron\"\n hint=\"Cron expression used only when mode is cron, for example 0 9 * * *.\"\n />\n <mt-number-field\n [ngModel]=\"numberValue(config()['intervalSeconds'])\"\n (ngModelChange)=\"onConfigFieldChange('intervalSeconds', $event)\"\n label=\"Interval seconds\"\n hint=\"Repeat interval in seconds used only when mode is interval.\"\n [min]=\"0\"\n />\n <mt-date-field\n [ngModel]=\"config()['startDate'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('startDate', $event)\"\n label=\"Start date UTC\"\n hint=\"UTC date/time used for once schedules or as the first allowed run time.\"\n [showTime]=\"true\"\n [showClear]=\"true\"\n [pInputs]=\"dateTimePickerInputs\"\n />\n <mt-select-field\n [ngModel]=\"config()['misfirePolicy'] ?? 'SkipMissed'\"\n (ngModelChange)=\"onConfigFieldChange('misfirePolicy', $event)\"\n label=\"Misfire policy\"\n hint=\"Backend behavior when a scheduled run is missed while the automation is unavailable.\"\n [options]=\"misfirePolicyOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n </div>\n <div class=\"mt-3 flex gap-2\">\n <mt-button size=\"small\" variant=\"outlined\" label=\"Validate\" (onClick)=\"validateSchedule()\" />\n <mt-button size=\"small\" severity=\"primary\" label=\"Preview next fire\" (onClick)=\"previewSchedule()\" />\n </div>\n @if (schedulePreview(); as preview) {\n <div class=\"mt-3 rounded-lg bg-(--p-surface-100) px-3 py-2 text-[12px]\">\n <strong class=\"text-emerald-600\">Valid</strong>\n <span class=\"ms-2\">Next fire: {{ preview.nextFireAtUtc ?? \"-\" }}</span>\n </div>\n }\n </section>\n }\n }\n @case (\"SetFields\") {\n @if (sectionInMain(\"setFields\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Set fields</div>\n <ng-container *ngTemplateOutlet=\"mapEditor; context: { objectKey: 'fields', rows: fieldsRows(), keyLabel: 'Field', valueLabel: 'Value', addLabel: 'Add field' }\" />\n </section>\n }\n }\n @case (\"If\") {\n @if (sectionInMain(\"condition\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Condition</div>\n <div class=\"grid gap-3 md:grid-cols-[1fr_180px_1fr]\">\n <mt-text-field\n [ngModel]=\"fieldText('left')\"\n (focusin)=\"setExpressionTarget('config:left')\"\n (ngModelChange)=\"onConfigFieldChange('left', $event)\"\n label=\"Left\"\n hint=\"Left-side value or expression to compare.\"\n />\n <mt-select-field\n [ngModel]=\"config()['operator'] ?? 'equals'\"\n (ngModelChange)=\"onConfigFieldChange('operator', $event)\"\n label=\"Operator\"\n hint=\"Comparison operator used by the backend condition evaluator.\"\n [options]=\"ifOperatorOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-text-field\n [ngModel]=\"fieldText('right')\"\n (focusin)=\"setExpressionTarget('config:right')\"\n (ngModelChange)=\"onConfigFieldChange('right', $event)\"\n label=\"Right\"\n hint=\"Right-side value or expression to compare against.\"\n />\n </div>\n <div class=\"mt-3 rounded-md border border-surface-200 bg-surface-0 p-3\">\n <div class=\"fp-ae-label mb-2\">Route outputs</div>\n <div class=\"flex flex-wrap gap-1.5\">\n @for (key of routeOutputKeys(); track key) {\n <span class=\"rounded-md bg-(--p-surface-100) px-2 py-1 font-mono text-[11px]\">{{ key }}</span>\n }\n </div>\n </div>\n </section>\n }\n }\n @case (\"HTTP\") {\n @if (sectionInMain(\"httpRequest\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">HTTP request</div>\n <div class=\"grid gap-3 md:grid-cols-[150px_1fr]\">\n <mt-select-field\n [ngModel]=\"config()['method'] ?? 'GET'\"\n (ngModelChange)=\"onConfigFieldChange('method', $event)\"\n label=\"Method\"\n hint=\"HTTP method used for the outbound request.\"\n [options]=\"httpMethodOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"config()['url'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:url')\"\n (ngModelChange)=\"onConfigFieldChange('url', $event)\"\n label=\"URL\"\n hint=\"Target URL. Expressions are supported for dynamic hosts, paths, and query values.\"\n />\n </div>\n <div class=\"mt-3\">\n <ng-container *ngTemplateOutlet=\"mapEditor; context: { objectKey: 'headers', rows: headerRows(), keyLabel: 'Header', valueLabel: 'Value', addLabel: 'Add header' }\" />\n </div>\n <div class=\"mt-3\">\n <ng-container *ngTemplateOutlet=\"mapEditor; context: { objectKey: 'query', rows: queryRows(), keyLabel: 'Query param', valueLabel: 'Value', addLabel: 'Add query param' }\" />\n </div>\n @if (supportsConfigKey('bodyMode')) {\n <mt-select-field\n class=\"mt-3\"\n [ngModel]=\"config()['bodyMode'] ?? 'json'\"\n (ngModelChange)=\"onConfigFieldChange('bodyMode', $event)\"\n label=\"Body mode\"\n hint=\"Backend-supported request body serialization mode.\"\n [options]=\"httpBodyModeOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n }\n <mt-textarea-field\n class=\"mt-3 w-full font-mono\"\n [ngModel]=\"valueText(config()['body'] ?? '')\"\n (focusin)=\"setExpressionTarget('config:body')\"\n (ngModelChange)=\"onConfigFieldChange('body', $event)\"\n label=\"Body\"\n hint=\"Request body sent by the HTTP node. Use JSON or expressions when the backend schema allows it.\"\n rows=\"6\"\n />\n @if (supportsConfigKey('timeoutSeconds') || supportsConfigKey('responseHandling')) {\n <div class=\"mt-3 grid gap-3 md:grid-cols-2\">\n @if (supportsConfigKey('timeoutSeconds')) {\n <mt-number-field\n [ngModel]=\"numberValue(config()['timeoutSeconds'])\"\n (ngModelChange)=\"onConfigFieldChange('timeoutSeconds', $event)\"\n label=\"Timeout seconds\"\n hint=\"Request timeout when exposed by the backend schema.\"\n [min]=\"0\"\n />\n }\n @if (supportsConfigKey('responseHandling')) {\n <mt-text-field\n [ngModel]=\"config()['responseHandling'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('responseHandling', $event)\"\n label=\"Response handling\"\n hint=\"Backend-supported response handling mode or expression.\"\n />\n }\n </div>\n }\n @if (sectionInMain(\"credentials\")) {\n <div class=\"mt-3 rounded-md border border-surface-200 bg-surface-0 p-3\">\n <div class=\"fp-ae-label mb-2\">Credential</div>\n <ng-container *ngTemplateOutlet=\"credentialSelector\" />\n </div>\n }\n </section>\n }\n }\n @case (\"Wait\") {\n @if (sectionInMain(\"wait\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Wait</div>\n <div class=\"grid gap-3 md:grid-cols-3\">\n <mt-select-field\n [ngModel]=\"config()['mode'] ?? 'duration'\"\n (ngModelChange)=\"onConfigFieldChange('mode', $event)\"\n label=\"Mode\"\n hint=\"Choose whether this wait uses a duration or a specific date/time.\"\n [options]=\"waitModeOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-number-field\n [ngModel]=\"numberValue(config()['durationSeconds'])\"\n (ngModelChange)=\"onConfigFieldChange('durationSeconds', $event)\"\n label=\"Duration seconds\"\n hint=\"How long execution should wait when mode is duration.\"\n [min]=\"0\"\n />\n <mt-date-field\n [ngModel]=\"config()['waitUntil'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('waitUntil', $event)\"\n label=\"Wait until\"\n hint=\"Date/time that resolves to when execution should resume.\"\n [showTime]=\"true\"\n [showClear]=\"true\"\n [pInputs]=\"dateTimePickerInputs\"\n />\n </div>\n @if (supportsConfigKey('resumePayloadSchema') || supportsConfigKey('resumePayload')) {\n <mt-textarea-field\n class=\"mt-3 w-full font-mono\"\n [ngModel]=\"valueText(config()['resumePayloadSchema'] ?? config()['resumePayload'] ?? '')\"\n (focusin)=\"setExpressionTarget('config:resumePayload')\"\n (ngModelChange)=\"onConfigFieldChange('resumePayload', $event)\"\n label=\"Resume payload\"\n hint=\"Expected payload when the backend supports manual or external resume data.\"\n rows=\"5\"\n />\n }\n </section>\n }\n }\n @case (\"HumanApproval\") {\n @if (sectionInMain(\"approvalTask\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Approval task</div>\n @if (assignmentOptions()?.providerStatus && assignmentOptions()?.providerStatus !== \"Available\") {\n <p class=\"fp-ae-copy\">\n Assignment provider status: {{ assignmentOptions()?.providerStatus }}.\n The backend provider is the source of truth for available assignees.\n </p>\n }\n <div class=\"grid gap-3 xl:grid-cols-3\">\n <mt-text-field\n [ngModel]=\"config()['title'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:title')\"\n (ngModelChange)=\"onConfigFieldChange('title', $event)\"\n label=\"Approval title\"\n hint=\"Approval title shown to the assigned human approver.\"\n />\n <mt-select-field\n [ngModel]=\"selectedAssignmentKey()\"\n (ngModelChange)=\"onAssignmentOptionChange($event)\"\n label=\"Assignment\"\n hint=\"Backend-provided assignee, role, or group that can decide this approval.\"\n [options]=\"assignmentSelectOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n @if (humanApprovalSupportsConfig('priority')) {\n <mt-text-field\n [ngModel]=\"config()['priority'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('priority', $event)\"\n label=\"Priority\"\n hint=\"Approval priority when supported by the backend schema.\"\n />\n }\n </div>\n <mt-textarea-field\n class=\"mt-3 w-full\"\n [ngModel]=\"config()['message'] ?? config()['instructions'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:message')\"\n (ngModelChange)=\"onConfigFieldChange('message', $event)\"\n label=\"Approval message\"\n hint=\"Decision instructions shown to the approver.\"\n rows=\"4\"\n />\n @if (humanApprovalSupportsConfig('dueDate') || humanApprovalSupportsConfig('dueInSeconds') || humanApprovalSupportsConfig('timeoutSeconds') || humanApprovalSupportsConfig('expiresAt') || humanApprovalSupportsConfig('commentsRequired') || humanApprovalSupportsConfig('allowReturn')) {\n <div class=\"mt-3 grid gap-3 xl:grid-cols-4\">\n @if (humanApprovalSupportsConfig('dueDate')) {\n <mt-date-field\n [ngModel]=\"config()['dueDate'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('dueDate', $event)\"\n label=\"Due date\"\n hint=\"Backend-supported approval due date.\"\n [showTime]=\"true\"\n [showClear]=\"true\"\n [pInputs]=\"dateTimePickerInputs\"\n />\n }\n @if (humanApprovalSupportsConfig('dueInSeconds')) {\n <mt-number-field\n [ngModel]=\"numberValue(config()['dueInSeconds'])\"\n (ngModelChange)=\"onConfigFieldChange('dueInSeconds', $event)\"\n label=\"Due in seconds\"\n hint=\"Relative approval due duration when supported by the backend schema.\"\n [min]=\"0\"\n />\n }\n @if (humanApprovalSupportsConfig('timeoutSeconds')) {\n <mt-number-field\n [ngModel]=\"numberValue(config()['timeoutSeconds'])\"\n (ngModelChange)=\"onConfigFieldChange('timeoutSeconds', $event)\"\n label=\"Timeout seconds\"\n hint=\"Approval timeout emitted according to backend approval runtime support.\"\n [min]=\"0\"\n />\n }\n @if (humanApprovalSupportsConfig('expiresAt')) {\n <mt-date-field\n [ngModel]=\"config()['expiresAt'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('expiresAt', $event)\"\n label=\"Expires at\"\n hint=\"Backend-supported approval expiry timestamp.\"\n [showTime]=\"true\"\n [showClear]=\"true\"\n [pInputs]=\"dateTimePickerInputs\"\n />\n }\n @if (humanApprovalSupportsConfig('commentsRequired')) {\n <div class=\"flex min-h-[4.25rem] items-end rounded-md border border-surface-200 bg-surface-0 px-3 py-2\">\n <mt-toggle-field\n size=\"small\"\n label=\"Comments required\"\n labelPosition=\"end\"\n [ngModel]=\"config()['commentsRequired'] === true\"\n (ngModelChange)=\"onConfigFieldChange('commentsRequired', $event === true)\"\n hint=\"Require approver comments when the backend supports this flag.\"\n />\n </div>\n }\n @if (humanApprovalSupportsConfig('allowReturn')) {\n <div class=\"flex min-h-[4.25rem] items-end rounded-md border border-surface-200 bg-surface-0 px-3 py-2\">\n <mt-toggle-field\n size=\"small\"\n label=\"Allow return for changes\"\n labelPosition=\"end\"\n [ngModel]=\"config()['allowReturn'] !== false\"\n (ngModelChange)=\"onConfigFieldChange('allowReturn', $event === true)\"\n hint=\"Allow ReturnForChanges when the backend schema exposes this option.\"\n />\n </div>\n }\n </div>\n }\n <div class=\"mt-3 space-y-3\">\n <div class=\"fp-ae-section-title\">Decision options</div>\n <div class=\"overflow-hidden rounded-md border border-surface-200 bg-surface-0\">\n @if (selectedApprovalDecisionRows().length > 0) {\n <div class=\"hidden border-b border-surface-200 bg-surface-50 px-3 py-2 text-[12px] font-semibold text-(--p-text-muted-color) xl:grid xl:grid-cols-[1.1fr_1fr_1.2fr_80px_44px] xl:gap-3\">\n <span>Decision label</span>\n <span>Canonical value</span>\n <span>Route output key</span>\n <span>Routes</span>\n <span>Action</span>\n </div>\n @for (decision of selectedApprovalDecisionRows(); track decision.value) {\n <div class=\"grid gap-2 border-b border-surface-100 px-3 py-3 last:border-b-0 xl:grid-cols-[1.1fr_1fr_1.2fr_80px_44px] xl:items-center xl:gap-3\">\n <div class=\"min-w-0\">\n <div class=\"text-[11px] font-semibold text-(--p-text-muted-color) xl:hidden\">Decision label</div>\n <div class=\"truncate text-[13px] font-semibold text-(--p-text-color)\">{{ decision.label }}</div>\n </div>\n <div class=\"min-w-0\">\n <div class=\"text-[11px] font-semibold text-(--p-text-muted-color) xl:hidden\">Canonical value</div>\n <div class=\"truncate font-mono text-[12px] text-(--p-text-color)\">{{ decision.value }}</div>\n </div>\n <div class=\"min-w-0\">\n <div class=\"text-[11px] font-semibold text-(--p-text-muted-color) xl:hidden\">Route output key</div>\n <div class=\"truncate font-mono text-[12px] text-(--p-text-muted-color)\">{{ decision.routeOutputKey }}</div>\n </div>\n <div>\n <div class=\"text-[11px] font-semibold text-(--p-text-muted-color) xl:hidden\">Routes</div>\n <div class=\"text-[13px] text-(--p-text-color)\">{{ decision.routeCount }}</div>\n </div>\n <mt-button\n class=\"justify-self-start xl:justify-self-end\"\n size=\"small\"\n variant=\"outlined\"\n severity=\"danger\"\n icon=\"general.trash-01\"\n tooltip=\"Remove decision option\"\n (onClick)=\"removeApprovalDecision(decision.value)\"\n />\n </div>\n }\n } @else {\n <div class=\"px-3 py-4 text-[12px] text-(--p-text-muted-color)\">\n Add at least one backend-supported approval decision.\n </div>\n }\n </div>\n @if (approvalDecisionIssues().length > 0) {\n <div class=\"rounded-lg border border-[rgb(var(--fp-error))]/30 bg-[rgb(var(--fp-error))]/10 px-3 py-2 text-[12px] leading-5 text-[rgb(var(--fp-error))]\">\n @for (issue of approvalDecisionIssues(); track issue) {\n <div>{{ issue }}</div>\n }\n </div>\n }\n <div class=\"grid gap-2 md:grid-cols-[minmax(0,1fr)_auto]\">\n <mt-select-field\n [ngModel]=\"approvalDecisionToAdd()\"\n (ngModelChange)=\"approvalDecisionToAdd.set($event)\"\n label=\"Decision to add\"\n hint=\"Only backend-supported decisions with matching route outputs are available.\"\n [options]=\"addableApprovalDecisionOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-button\n class=\"self-end\"\n size=\"small\"\n variant=\"outlined\"\n icon=\"general.plus\"\n label=\"Add decision option\"\n [disabled]=\"!approvalDecisionToAdd()\"\n (onClick)=\"addApprovalDecision()\"\n />\n </div>\n </div>\n @if (humanApprovalSupportsConfig('payload') || humanApprovalSupportsConfig('context') || humanApprovalSupportsConfig('metadata')) {\n <div class=\"mt-3 grid gap-3\">\n @if (humanApprovalSupportsConfig('payload')) {\n <mt-textarea-field\n class=\"w-full font-mono\"\n [ngModel]=\"valueText(config()['payload'] ?? '')\"\n (focusin)=\"setExpressionTarget('config:payload')\"\n (ngModelChange)=\"onConfigFieldChange('payload', $event)\"\n label=\"Payload\"\n hint=\"Approval task payload or context fields supported by backend schema.\"\n rows=\"5\"\n />\n }\n @if (humanApprovalSupportsConfig('context')) {\n <mt-textarea-field\n class=\"w-full font-mono\"\n [ngModel]=\"valueText(config()['context'] ?? '')\"\n (focusin)=\"setExpressionTarget('config:context')\"\n (ngModelChange)=\"onConfigFieldChange('context', $event)\"\n label=\"Context\"\n hint=\"Additional approval context supported by backend schema.\"\n rows=\"5\"\n />\n }\n @if (humanApprovalSupportsConfig('metadata')) {\n <mt-textarea-field\n class=\"w-full font-mono\"\n [ngModel]=\"valueText(config()['metadata'] ?? '')\"\n (focusin)=\"setExpressionTarget('config:metadata')\"\n (ngModelChange)=\"onConfigFieldChange('metadata', $event)\"\n label=\"Metadata\"\n hint=\"Additional approval metadata supported by the backend schema.\"\n rows=\"4\"\n />\n }\n </div>\n }\n <div class=\"mt-3 flex gap-2\">\n <mt-button size=\"small\" variant=\"outlined\" label=\"Validate assignment\" (onClick)=\"validateAssignment()\" />\n </div>\n @if (assignmentValidation(); as result) {\n <div class=\"mt-3 rounded-lg bg-(--p-surface-100) px-3 py-2 text-[12px]\">\n {{ result.isValid === false ? \"Assignment invalid\" : \"Assignment accepted by helper\" }}\n @for (issue of resultIssues(result); track issue) {\n <div class=\"mt-1 text-[11px] text-(--p-text-muted-color)\">{{ issue }}</div>\n }\n </div>\n }\n @if (sectionInMain(\"approvalOutput\")) {\n <div class=\"mt-3 rounded-md border border-surface-200 bg-surface-0 p-3\">\n <div class=\"fp-ae-label mb-2\">Output data</div>\n <div class=\"flex flex-wrap gap-1.5\">\n @for (field of approvalOutputFieldLabels; track field) {\n <span class=\"rounded-md bg-(--p-surface-100) px-2 py-1 font-mono text-[11px]\">{{ field }}</span>\n }\n </div>\n <div class=\"mt-3 fp-ae-label mb-2\">Approval routes</div>\n <div class=\"flex flex-wrap gap-1.5\">\n @for (key of routeOutputKeys(); track key) {\n <span class=\"rounded-md bg-(--p-surface-100) px-2 py-1 font-mono text-[11px]\">{{ key }}</span>\n }\n </div>\n </div>\n }\n </section>\n }\n }\n @case (\"FlowPlusCommit\") {\n @if (sectionInMain(\"flowplusCommit\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">FlowPlus commit</div>\n <p class=\"fp-ae-copy\">\n This node is the explicit module-data write boundary. Approval does\n not commit data unless this node is reached.\n </p>\n <div class=\"grid gap-3 md:grid-cols-2\">\n <mt-select-field\n [ngModel]=\"config()['targetModule'] ?? ''\"\n (ngModelChange)=\"onModuleChange($event)\"\n label=\"Module\"\n hint=\"Module whose records will be written by this explicit FlowPlus commit.\"\n [options]=\"moduleOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-select-field\n [ngModel]=\"config()['operation'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('operation', $event)\"\n label=\"Operation\"\n hint=\"Write operation supported by the selected backend module schema.\"\n [options]=\"operationOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n </div>\n <mt-text-field\n class=\"mt-3 font-mono\"\n [ngModel]=\"config()['idempotencyKey'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:idempotencyKey')\"\n (ngModelChange)=\"onConfigFieldChange('idempotencyKey', $event)\"\n label=\"Idempotency key\"\n hint=\"Optional stable key used by the backend to prevent duplicate writes.\"\n />\n @if (selectedModuleFields().length) {\n <div class=\"mt-3 space-y-2\">\n <div class=\"fp-ae-section-title\">Module schema</div>\n <div class=\"grid gap-2 md:grid-cols-2\">\n @for (field of selectedModuleFields(); track field.key) {\n <div class=\"fp-ae-kv\">\n <span>{{ field.displayName ?? field.key }}</span>\n <strong>{{ field.viewType ?? \"Value\" }} @if (field.required) { * }</strong>\n </div>\n }\n </div>\n </div>\n }\n <div class=\"mt-3\">\n <ng-container *ngTemplateOutlet=\"mapEditor; context: { objectKey: 'mapping', rows: mappingRows(), keyLabel: 'Module field', valueLabel: 'Expression / value', addLabel: 'Add module field' }\" />\n </div>\n <div class=\"mt-3 flex gap-2\">\n <mt-button size=\"small\" severity=\"primary\" label=\"Validate mapping\" (onClick)=\"validateCommitMapping()\" />\n </div>\n @if (commitValidation(); as result) {\n <div class=\"mt-3 rounded-lg bg-(--p-surface-100) px-3 py-2 text-[12px]\">\n {{ result.isValid === false ? \"Mapping invalid\" : \"Mapping accepted by helper\" }}\n @for (issue of resultIssues(result); track issue) {\n <div class=\"mt-1 text-[11px] text-(--p-text-muted-color)\">{{ issue }}</div>\n }\n </div>\n }\n @if (sectionInMain(\"credentials\")) {\n <div class=\"mt-3 rounded-md border border-surface-200 bg-surface-0 p-3\">\n <div class=\"fp-ae-label mb-2\">Credential</div>\n <ng-container *ngTemplateOutlet=\"credentialSelector\" />\n </div>\n }\n </section>\n }\n }\n @case (\"WebhookResponse\") {\n @if (sectionInMain(\"webhookResponse\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Webhook response</div>\n <div class=\"grid gap-3 md:grid-cols-2\">\n <mt-number-field\n [ngModel]=\"numberValue(config()['statusCode']) ?? 200\"\n (ngModelChange)=\"onConfigFieldChange('statusCode', $event)\"\n label=\"Status code\"\n hint=\"HTTP status code sent back by the webhook response node.\"\n [min]=\"100\"\n [max]=\"599\"\n />\n <mt-select-field\n [ngModel]=\"config()['responseMode'] ?? 'json'\"\n (ngModelChange)=\"onConfigFieldChange('responseMode', $event)\"\n label=\"Response mode\"\n hint=\"How the webhook response body should be serialized.\"\n [options]=\"responseModeOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n </div>\n <div class=\"mt-3\">\n <ng-container *ngTemplateOutlet=\"mapEditor; context: { objectKey: 'headers', rows: headerRows(), keyLabel: 'Header', valueLabel: 'Value', addLabel: 'Add header' }\" />\n </div>\n <mt-textarea-field\n class=\"mt-3 w-full font-mono\"\n [ngModel]=\"valueText(config()['body'] ?? '')\"\n (focusin)=\"setExpressionTarget('config:body')\"\n (ngModelChange)=\"onConfigFieldChange('body', $event)\"\n label=\"Body\"\n hint=\"Body returned to the webhook caller. Expressions can use current execution data.\"\n rows=\"6\"\n />\n </section>\n }\n }\n @case (\"CallAutomation\") {\n @if (sectionInMain(\"callAutomation\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Subworkflow</div>\n <div class=\"grid gap-3 md:grid-cols-2\">\n <mt-select-field\n [ngModel]=\"config()['targetAutomationId'] ?? ''\"\n (ngModelChange)=\"onSubworkflowTargetChange($event)\"\n label=\"Target automation\"\n hint=\"Backend-provided automation candidate. Tenant and recursion validation stay on the backend.\"\n [options]=\"subworkflowAutomationOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-select-field\n [ngModel]=\"config()['revisionMode'] ?? 'Active'\"\n (ngModelChange)=\"onConfigFieldChange('revisionMode', $event)\"\n label=\"Revision mode\"\n hint=\"Choose which published or active child revision to call.\"\n [options]=\"revisionModeOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n @if (config()['revisionMode'] === \"SpecificRevision\") {\n <mt-text-field\n [ngModel]=\"config()['specificRevisionId'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('specificRevisionId', $event)\"\n label=\"Specific revision id\"\n hint=\"Required only when revision mode is Specific revision.\"\n />\n }\n <mt-toggle-field\n size=\"small\"\n labelPosition=\"end\"\n [ngModel]=\"config()['waitForCompletion'] !== false\"\n (ngModelChange)=\"onConfigFieldChange('waitForCompletion', $event === true)\"\n label=\"Wait for completion\"\n hint=\"When disabled, the parent continues after dispatch and backend records the child linkage.\"\n />\n </div>\n <div class=\"mt-3 grid gap-3 md:grid-cols-2\">\n <mt-textarea-field\n class=\"w-full font-mono md:col-span-2\"\n [ngModel]=\"valueText(config()['inputMappingJson'] ?? '{}')\"\n (focusin)=\"setExpressionTarget('config:inputMappingJson')\"\n (ngModelChange)=\"onConfigFieldChange('inputMappingJson', $event)\"\n label=\"Input mapping JSON\"\n hint=\"JSON object or expression map sent into the child automation input.\"\n rows=\"7\"\n />\n <mt-textarea-field\n class=\"w-full font-mono md:col-span-2\"\n [ngModel]=\"valueText(config()['outputMappingJson'] ?? '{}')\"\n (focusin)=\"setExpressionTarget('config:outputMappingJson')\"\n (ngModelChange)=\"onConfigFieldChange('outputMappingJson', $event)\"\n label=\"Output mapping JSON\"\n hint=\"JSON object or expression map for child output/status returned to the parent.\"\n rows=\"7\"\n />\n </div>\n </section>\n }\n }\n @case (\"Subworkflow\") {\n @if (sectionInMain(\"callAutomation\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Subworkflow</div>\n <div class=\"grid gap-3 md:grid-cols-2\">\n <mt-select-field\n [ngModel]=\"config()['targetAutomationId'] ?? ''\"\n (ngModelChange)=\"onSubworkflowTargetChange($event)\"\n label=\"Target automation\"\n hint=\"Backend-provided automation candidate. Tenant and recursion validation stay on the backend.\"\n [options]=\"subworkflowAutomationOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-select-field\n [ngModel]=\"config()['revisionMode'] ?? 'Active'\"\n (ngModelChange)=\"onConfigFieldChange('revisionMode', $event)\"\n label=\"Revision mode\"\n hint=\"Choose which published or active child revision to call.\"\n [options]=\"revisionModeOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n @if (config()['revisionMode'] === \"SpecificRevision\") {\n <mt-text-field\n [ngModel]=\"config()['specificRevisionId'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('specificRevisionId', $event)\"\n label=\"Specific revision id\"\n hint=\"Required only when revision mode is Specific revision.\"\n />\n }\n <mt-toggle-field\n size=\"small\"\n labelPosition=\"end\"\n [ngModel]=\"config()['waitForCompletion'] !== false\"\n (ngModelChange)=\"onConfigFieldChange('waitForCompletion', $event === true)\"\n label=\"Wait for completion\"\n hint=\"When disabled, the parent continues after dispatch and backend records the child linkage.\"\n />\n </div>\n <div class=\"mt-3 grid gap-3 md:grid-cols-2\">\n <mt-textarea-field\n class=\"w-full font-mono md:col-span-2\"\n [ngModel]=\"valueText(config()['inputMappingJson'] ?? '{}')\"\n (focusin)=\"setExpressionTarget('config:inputMappingJson')\"\n (ngModelChange)=\"onConfigFieldChange('inputMappingJson', $event)\"\n label=\"Input mapping JSON\"\n hint=\"JSON object or expression map sent into the child automation input.\"\n rows=\"7\"\n />\n <mt-textarea-field\n class=\"w-full font-mono md:col-span-2\"\n [ngModel]=\"valueText(config()['outputMappingJson'] ?? '{}')\"\n (focusin)=\"setExpressionTarget('config:outputMappingJson')\"\n (ngModelChange)=\"onConfigFieldChange('outputMappingJson', $event)\"\n label=\"Output mapping JSON\"\n hint=\"JSON object or expression map for child output/status returned to the parent.\"\n rows=\"7\"\n />\n </div>\n </section>\n }\n }\n @case (\"ParallelStart\") {\n @if (sectionInMain(\"parallelStart\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"flex items-center justify-between gap-3 border-b border-surface-200 bg-surface-50 px-4 py-3\">\n <div class=\"fp-ae-section-title !m-0\">Parallel start</div>\n <mt-button size=\"small\" variant=\"outlined\" icon=\"general.plus\" label=\"Add branch\" (onClick)=\"addParallelBranch()\" />\n </div>\n <div class=\"space-y-3 p-4\">\n @for (branch of parallelBranches(); track branch.key; let i = $index) {\n <div class=\"rounded-md border border-surface-200 bg-surface-0 p-3\">\n <div class=\"grid gap-3 md:grid-cols-[160px_1fr_1fr_auto]\">\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"branch.key\"\n (ngModelChange)=\"updateParallelBranch(i, 'key', $event)\"\n label=\"Branch key\"\n hint=\"Persisted route key. Do not use array index names.\"\n />\n <mt-text-field\n [ngModel]=\"branch.label\"\n (ngModelChange)=\"updateParallelBranch(i, 'label', $event)\"\n label=\"Label\"\n hint=\"Visual branch label. Changing it does not change existing routes.\"\n />\n <mt-text-field\n [ngModel]=\"branch.description\"\n (ngModelChange)=\"updateParallelBranch(i, 'description', $event)\"\n label=\"Description\"\n hint=\"Optional branch note for reviewers.\"\n />\n <div class=\"flex items-end gap-1\">\n <mt-button size=\"small\" variant=\"outlined\" icon=\"arrow.arrow-up\" tooltip=\"Move up\" (onClick)=\"moveParallelBranch(i, -1)\" />\n <mt-button size=\"small\" variant=\"outlined\" icon=\"arrow.arrow-down\" tooltip=\"Move down\" (onClick)=\"moveParallelBranch(i, 1)\" />\n <mt-button size=\"small\" variant=\"outlined\" severity=\"danger\" icon=\"general.trash-01\" tooltip=\"Remove branch\" (onClick)=\"removeParallelBranch(i)\" />\n </div>\n </div>\n <div class=\"mt-3 flex flex-wrap items-center gap-1.5 text-[11px]\">\n <span class=\"inline-flex h-7 items-center rounded-md border border-surface-200 bg-surface-50 px-2 font-mono font-semibold text-(--p-text-color)\">{{ branch.key }}</span>\n <span class=\"inline-flex h-7 items-center rounded-md border border-surface-200 bg-surface-0 px-2 font-medium text-(--p-text-muted-color)\">{{ branch.routeCount }} connected route{{ branch.routeCount === 1 ? \"\" : \"s\" }}</span>\n </div>\n </div>\n }\n @if (parallelBranches().length === 0) {\n <p class=\"fp-ae-copy\">Add at least two stable branch keys. Backend validation blocks publish until branches are valid.</p>\n }\n </div>\n </section>\n }\n }\n @case (\"ParallelJoin\") {\n @if (sectionInMain(\"parallelJoin\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Parallel join</div>\n <div class=\"grid gap-3 md:grid-cols-2\">\n <mt-select-field\n [ngModel]=\"config()['joinPolicy'] ?? 'All'\"\n (ngModelChange)=\"onConfigFieldChange('joinPolicy', $event)\"\n label=\"Join policy\"\n hint=\"Backend policy used to decide when branch wait is complete.\"\n [options]=\"joinPolicyOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n @if (config()['joinPolicy'] === \"Threshold\") {\n <mt-number-field\n [ngModel]=\"numberValue(config()['threshold'])\"\n (ngModelChange)=\"onConfigFieldChange('threshold', $event)\"\n label=\"Threshold\"\n hint=\"Minimum completed branch count required for Threshold policy.\"\n [min]=\"1\"\n />\n }\n <mt-select-field\n [ngModel]=\"config()['aggregationStrategy'] ?? 'MergeObjects'\"\n (ngModelChange)=\"onConfigFieldChange('aggregationStrategy', $event)\"\n label=\"Aggregation strategy\"\n hint=\"How backend joins branch outputs into the join node output.\"\n [options]=\"aggregationStrategyOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"config()['outputTargetPath'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:outputTargetPath')\"\n (ngModelChange)=\"onConfigFieldChange('outputTargetPath', $event)\"\n label=\"Output target path\"\n hint=\"Optional context path where joined output should be written.\"\n />\n </div>\n </section>\n }\n }\n @case (\"Switch\") {\n @if (sectionInMain(\"switch\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Switch</div>\n <div class=\"grid gap-4\">\n <div class=\"grid items-end gap-3 md:grid-cols-[minmax(0,1fr)_minmax(280px,0.7fr)]\">\n <mt-select-field\n [ngModel]=\"config()['mode'] ?? 'value'\"\n (ngModelChange)=\"onConfigFieldChange('mode', $event)\"\n label=\"Mode\"\n hint=\"Backend evaluation mode: expression, rules, or source-value matching.\"\n [options]=\"switchModeOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-toggle-field\n class=\"self-end pb-1\"\n size=\"small\"\n labelPosition=\"end\"\n [ngModel]=\"config()['firstMatch'] !== false\"\n (ngModelChange)=\"onConfigFieldChange('firstMatch', $event === true)\"\n label=\"First match\"\n hint=\"Use the first matching case unless backend explicitly supports multi-match.\"\n />\n </div>\n @if (showSwitchSourceValue() || showSwitchExpression()) {\n <div class=\"grid gap-3 md:grid-cols-2\">\n @if (showSwitchSourceValue()) {\n <mt-text-field\n [class]=\"showSwitchExpression() ? 'font-mono' : 'font-mono md:col-span-2'\"\n [ngModel]=\"config()['sourceValue'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:sourceValue')\"\n (ngModelChange)=\"onConfigFieldChange('sourceValue', $event)\"\n label=\"Source value\"\n hint=\"Value or expression used for value matching.\"\n />\n }\n @if (showSwitchExpression()) {\n <mt-text-field\n [class]=\"showSwitchSourceValue() ? 'font-mono' : 'font-mono md:col-span-2'\"\n [ngModel]=\"config()['expression'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:expression')\"\n (ngModelChange)=\"onConfigFieldChange('expression', $event)\"\n label=\"Expression\"\n hint=\"Expression evaluated when mode is expression or rules.\"\n />\n }\n </div>\n }\n <div class=\"grid items-end gap-3 md:grid-cols-[minmax(0,1fr)_minmax(280px,0.7fr)]\">\n @if (showSwitchDefaultOutputKey()) {\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"config()['defaultOutputKey'] ?? 'default'\"\n (ngModelChange)=\"onConfigFieldChange('defaultOutputKey', $event)\"\n label=\"Default output key\"\n hint=\"Stable default route key used when no case matches.\"\n />\n }\n <mt-toggle-field\n [class]=\"showSwitchDefaultOutputKey() ? 'self-end pb-1' : 'self-end pb-1 md:col-span-2'\"\n size=\"small\"\n labelPosition=\"end\"\n [ngModel]=\"config()['includeDefaultOutput'] !== false\"\n (ngModelChange)=\"onConfigFieldChange('includeDefaultOutput', $event === true)\"\n label=\"Include default output\"\n hint=\"Expose the default route output key in the canvas.\"\n />\n </div>\n </div>\n </section>\n <section class=\"fp-ae-panel\">\n <div class=\"flex items-center justify-between gap-3 border-b border-surface-200 bg-surface-50 px-4 py-3\">\n <div class=\"fp-ae-section-title !m-0\">Switch cases</div>\n <mt-button size=\"small\" variant=\"outlined\" icon=\"general.plus\" label=\"Add case\" (onClick)=\"addSwitchCase()\" />\n </div>\n <div class=\"space-y-3 p-4\">\n @for (item of switchCases(); track item.key; let i = $index) {\n <div class=\"rounded-md border border-surface-200 bg-surface-0 p-3\">\n <div class=\"grid gap-3 md:grid-cols-[160px_1fr_1fr_auto]\">\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"item.key\"\n (ngModelChange)=\"updateSwitchCase(i, 'key', $event)\"\n label=\"Case key\"\n hint=\"Persisted stable key. The route output becomes case_key.\"\n />\n <mt-text-field\n [ngModel]=\"item.label\"\n (ngModelChange)=\"updateSwitchCase(i, 'label', $event)\"\n label=\"Label\"\n hint=\"Visual case label. Routes stay attached to the case key.\"\n />\n <mt-text-field\n [ngModel]=\"item.value\"\n (focusin)=\"setExpressionTarget('config:cases:' + item.key + ':value')\"\n (ngModelChange)=\"updateSwitchCase(i, 'value', $event)\"\n label=\"Value\"\n hint=\"Expected value for value matching.\"\n />\n <div class=\"flex items-end gap-1\">\n <mt-button size=\"small\" variant=\"outlined\" icon=\"arrow.arrow-up\" tooltip=\"Move up\" (onClick)=\"moveSwitchCase(i, -1)\" />\n <mt-button size=\"small\" variant=\"outlined\" icon=\"arrow.arrow-down\" tooltip=\"Move down\" (onClick)=\"moveSwitchCase(i, 1)\" />\n <mt-button size=\"small\" variant=\"outlined\" severity=\"danger\" icon=\"general.trash-01\" tooltip=\"Remove case\" (onClick)=\"removeSwitchCase(i)\" />\n </div>\n </div>\n <div class=\"mt-3 grid gap-3 md:grid-cols-2\">\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"item.condition\"\n (focusin)=\"setExpressionTarget('config:cases:' + item.key + ':condition')\"\n (ngModelChange)=\"updateSwitchCase(i, 'condition', $event)\"\n label=\"Condition\"\n hint=\"Optional condition for rule matching.\"\n />\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"item.expression\"\n (focusin)=\"setExpressionTarget('config:cases:' + item.key + ':expression')\"\n (ngModelChange)=\"updateSwitchCase(i, 'expression', $event)\"\n label=\"Case expression\"\n hint=\"Optional expression for this case.\"\n />\n </div>\n <div class=\"mt-3 flex flex-wrap items-center gap-1.5 text-[11px]\">\n <span class=\"inline-flex h-7 items-center rounded-md border border-surface-200 bg-surface-50 px-2 font-mono font-semibold text-(--p-text-color)\">{{ item.routeKey }}</span>\n <span class=\"inline-flex h-7 items-center rounded-md border border-surface-200 bg-surface-0 px-2 font-medium text-(--p-text-muted-color)\">{{ item.routeCount }} connected route{{ item.routeCount === 1 ? \"\" : \"s\" }}</span>\n <span class=\"inline-flex h-7 items-center rounded-md border border-surface-200 bg-surface-0 px-2 font-medium text-(--p-text-muted-color)\">Visual order {{ i + 1 }}</span>\n </div>\n </div>\n }\n @if (switchCases().length === 0) {\n <p class=\"fp-ae-copy\">Add at least one stable case key. Backend validation points routes at missing case keys if a case is deleted.</p>\n }\n </div>\n </section>\n }\n }\n @case (\"Stop\") {\n @if (sectionInMain(\"stop\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">End execution</div>\n @if (supportsConfigKey('status') || supportsConfigKey('message') || supportsConfigKey('output')) {\n <div class=\"grid gap-3 md:grid-cols-2\">\n @if (supportsConfigKey('status')) {\n <mt-text-field\n [ngModel]=\"config()['status'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('status', $event)\"\n label=\"Result status\"\n hint=\"Terminal status emitted by the backend stop node.\"\n />\n }\n @if (supportsConfigKey('message')) {\n <mt-text-field\n [ngModel]=\"config()['message'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:message')\"\n (ngModelChange)=\"onConfigFieldChange('message', $event)\"\n label=\"Message\"\n hint=\"Optional message or reason saved with the terminal result.\"\n />\n }\n @if (supportsConfigKey('output')) {\n <mt-textarea-field\n class=\"w-full font-mono md:col-span-2\"\n [ngModel]=\"valueText(config()['output'] ?? '')\"\n (focusin)=\"setExpressionTarget('config:output')\"\n (ngModelChange)=\"onConfigFieldChange('output', $event)\"\n label=\"Output payload\"\n hint=\"Terminal output payload when exposed by the backend schema.\"\n rows=\"5\"\n />\n }\n </div>\n } @else {\n <p class=\"fp-ae-copy\">\n This terminal node has no backend-exposed settings. It ends execution when reached.\n </p>\n }\n </section>\n }\n }\n }\n\n @if (sectionInMain(\"backendSchema\") && step() && editorType() !== \"HumanApproval\") {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Output data</div>\n @if (outputFields().length > 0) {\n <div class=\"grid gap-2 md:grid-cols-2\">\n @for (field of outputFields(); track field.key) {\n <div class=\"fp-ae-kv\">\n <span>{{ field.label }}</span>\n <strong>{{ field.type }}</strong>\n </div>\n }\n </div>\n }\n @if (routeOutputKeys().length > 0) {\n <div class=\"mt-3 fp-ae-label mb-2\">Route outputs</div>\n <div class=\"flex flex-wrap gap-1.5\">\n @for (key of routeOutputKeys(); track key) {\n <span class=\"rounded-md bg-(--p-surface-100) px-2 py-1 font-mono text-[11px]\">{{ key }}</span>\n }\n </div>\n }\n </section>\n }\n\n @if (sectionInAdvanced(\"mapping\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Request and result mapping</div>\n <div class=\"grid gap-3 md:grid-cols-2\">\n <div>\n <div class=\"fp-ae-label mb-2\">Request data</div>\n @for (row of inputMappingRows(); track row.key) {\n <label class=\"mb-2 block space-y-1\">\n <span class=\"text-[11px] text-(--p-text-muted-color)\">{{ row.key }}</span>\n <mt-text-field\n [ngModel]=\"valueText(row.value)\"\n (focusin)=\"setExpressionTarget('config:inputMapping:' + row.key)\"\n (ngModelChange)=\"updateJsonField('inputMappingJson', row.key, $event)\"\n hint=\"Expression or literal value mapped into this node input.\"\n />\n </label>\n }\n </div>\n <div>\n <div class=\"fp-ae-label mb-2\">Output data</div>\n @for (row of outputMappingRows(); track row.key) {\n <label class=\"mb-2 block space-y-1\">\n <span class=\"text-[11px] text-(--p-text-muted-color)\">{{ row.key }}</span>\n <mt-text-field\n [ngModel]=\"valueText(row.value)\"\n (focusin)=\"setExpressionTarget('config:outputMapping:' + row.key)\"\n (ngModelChange)=\"updateJsonField('outputMappingJson', row.key, $event)\"\n hint=\"Expression or literal value mapped from this node output.\"\n />\n </label>\n }\n </div>\n </div>\n @if (\n sectionHasAdvancedEmptyState(\"mapping\") &&\n inputMappingRows().length === 0 &&\n outputMappingRows().length === 0\n ) {\n <p class=\"fp-ae-copy mt-3\">\n This node is using backend default request and output data. Add mappings only when this step needs a custom payload.\n </p>\n }\n </section>\n }\n\n @if (sectionInAdvanced(\"credentials\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Auth and credentials</div>\n <ng-container *ngTemplateOutlet=\"credentialSelector\" />\n </section>\n }\n\n @if (sectionInAdvanced(\"policy\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Retry, timeout, error policy</div>\n <div class=\"grid gap-3 md:grid-cols-3\">\n <mt-number-field\n [ngModel]=\"numberValue(policyObject('timeoutPolicyJson')['timeoutSeconds'])\"\n (ngModelChange)=\"updateJsonField('timeoutPolicyJson', 'timeoutSeconds', $event)\"\n label=\"Timeout seconds\"\n hint=\"Maximum runtime before the backend times out this node.\"\n [min]=\"0\"\n />\n <mt-number-field\n [ngModel]=\"numberValue(policyObject('retryPolicyJson')['maxAttempts'])\"\n (ngModelChange)=\"updateJsonField('retryPolicyJson', 'maxAttempts', $event)\"\n label=\"Max attempts\"\n hint=\"Maximum retry attempts after retryable failures.\"\n [min]=\"0\"\n />\n <mt-select-field\n [ngModel]=\"policyObject('errorPolicyJson')['onFailure'] ?? ''\"\n (ngModelChange)=\"updateJsonField('errorPolicyJson', 'onFailure', $event)\"\n label=\"On failure\"\n hint=\"Backend error policy to apply when this node fails.\"\n [options]=\"errorPolicyOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n </div>\n @if (sectionHasAdvancedEmptyState(\"policy\")) {\n <p class=\"fp-ae-copy mt-3\">\n Backend defaults apply until you override a timeout, retry, or error policy here.\n </p>\n }\n </section>\n }\n\n @if (sectionInAdvanced(\"triggerContext\") && trigger()) {\n <details class=\"fp-ae-panel\" open>\n <summary class=\"fp-ae-section-title cursor-pointer select-none\">\n Trigger payload and context\n </summary>\n <p class=\"fp-ae-copy mt-3\">\n Trigger payload/context is documentation for expressions only. Triggers\n do not edit node input or output mappings here.\n </p>\n <div class=\"grid gap-3 md:grid-cols-2\">\n <details class=\"rounded-lg border border-(--p-content-border-color)\">\n <summary class=\"cursor-pointer px-3 py-2 text-[12px] font-semibold\">Config schema</summary>\n <pre class=\"fp-ae-code\">{{ schemaText(configSchema()) }}</pre>\n </details>\n @if (hasTriggerPayloadSchema() && triggerPayloadSchema(); as schema) {\n <details class=\"rounded-lg border border-(--p-content-border-color)\">\n <summary class=\"cursor-pointer px-3 py-2 text-[12px] font-semibold\">Payload schema</summary>\n <pre class=\"fp-ae-code\">{{ schemaText(schema) }}</pre>\n </details>\n }\n @if (authPolicySchema()) {\n <details class=\"rounded-lg border border-(--p-content-border-color)\">\n <summary class=\"cursor-pointer px-3 py-2 text-[12px] font-semibold\">Auth policy schema</summary>\n <pre class=\"fp-ae-code\">{{ schemaText(authPolicySchema()) }}</pre>\n </details>\n }\n @if (triggerPayloadSample(); as sample) {\n <details class=\"rounded-lg border border-(--p-content-border-color)\">\n <summary class=\"cursor-pointer px-3 py-2 text-[12px] font-semibold\">Payload or request sample</summary>\n <pre class=\"fp-ae-code\">{{ schemaText(sample) }}</pre>\n </details>\n }\n </div>\n </details>\n }\n\n @if (sectionInAdvanced(\"backendSchema\") && step()) {\n <details class=\"fp-ae-panel\" open>\n <summary class=\"fp-ae-section-title cursor-pointer select-none\">\n Backend schemas and outputs\n </summary>\n <div class=\"mt-3 grid gap-3 md:grid-cols-2\">\n <details class=\"rounded-lg border border-(--p-content-border-color)\">\n <summary class=\"cursor-pointer px-3 py-2 text-[12px] font-semibold\">Config schema</summary>\n <pre class=\"fp-ae-code\">{{ schemaText(configSchema()) }}</pre>\n </details>\n <details class=\"rounded-lg border border-(--p-content-border-color)\">\n <summary class=\"cursor-pointer px-3 py-2 text-[12px] font-semibold\">Input schema</summary>\n <pre class=\"fp-ae-code\">{{ schemaText(inputSchema()) }}</pre>\n </details>\n <details class=\"rounded-lg border border-(--p-content-border-color)\">\n <summary class=\"cursor-pointer px-3 py-2 text-[12px] font-semibold\">Output schema</summary>\n <pre class=\"fp-ae-code\">{{ schemaText(outputSchema()) }}</pre>\n </details>\n <div class=\"rounded-lg border border-(--p-content-border-color) p-3\">\n <div class=\"fp-ae-label mb-2\">Route output keys</div>\n <div class=\"flex flex-wrap gap-1.5\">\n @for (key of routeOutputKeys(); track key) {\n <span class=\"rounded-md bg-(--p-surface-100) px-2 py-1 font-mono text-[11px]\">{{ key }}</span>\n }\n @if (routeOutputKeys().length === 0) {\n <span class=\"text-[12px] text-(--p-text-muted-color)\">No outgoing route keys</span>\n }\n </div>\n </div>\n </div>\n </details>\n }\n\n @if (sectionInAdvanced(\"schemaFields\") && configRows().length > 0) {\n <details class=\"fp-ae-panel\" open>\n <summary class=\"fp-ae-section-title cursor-pointer select-none\">\n Backend schema fields\n </summary>\n <div class=\"mt-3 grid gap-3 md:grid-cols-2\">\n @for (field of configRows(); track field.key) {\n <div\n class=\"space-y-1.5\"\n [class.md:col-span-2]=\"field.type === 'object' || field.type === 'array'\"\n >\n @if (field.enumValues.length) {\n <mt-select-field\n [ngModel]=\"fieldValue(field.key)\"\n (ngModelChange)=\"onConfigFieldChange(field.key, $event)\"\n [label]=\"field.label + (field.required ? ' *' : '')\"\n [hint]=\"field.description ?? ''\"\n [options]=\"enumOptions(field.enumValues)\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n } @else if (field.type === \"boolean\") {\n <mt-toggle-field\n size=\"small\"\n labelPosition=\"end\"\n [ngModel]=\"!!fieldValue(field.key)\"\n (ngModelChange)=\"onConfigFieldChange(field.key, $event === true)\"\n [label]=\"field.label + (field.required ? ' *' : '')\"\n [hint]=\"field.description ?? ''\"\n />\n } @else if (field.type === \"number\") {\n <mt-number-field\n [ngModel]=\"numberValue(fieldValue(field.key))\"\n (ngModelChange)=\"onConfigFieldChange(field.key, $event)\"\n [label]=\"field.label + (field.required ? ' *' : '')\"\n [hint]=\"field.description ?? ''\"\n />\n } @else if (field.type === \"date\") {\n <mt-date-field\n [ngModel]=\"fieldValue(field.key)\"\n (ngModelChange)=\"onConfigFieldChange(field.key, $event)\"\n [label]=\"field.label + (field.required ? ' *' : '')\"\n [hint]=\"field.description ?? ''\"\n [showTime]=\"field.format !== 'date'\"\n [showClear]=\"true\"\n [pInputs]=\"dateTimePickerInputs\"\n />\n } @else if (field.type === \"object\" || field.type === \"array\") {\n <mt-textarea-field\n class=\"w-full font-mono\"\n [ngModel]=\"valueText(fieldValue(field.key))\"\n (ngModelChange)=\"onConfigFieldChange(field.key, $event)\"\n [label]=\"field.label + (field.required ? ' *' : '')\"\n [hint]=\"field.description ?? ''\"\n rows=\"5\"\n />\n } @else {\n <mt-text-field\n [ngModel]=\"fieldText(field.key)\"\n (focusin)=\"field.expressionEnabled && setExpressionTarget('config:' + field.key)\"\n (ngModelChange)=\"onConfigFieldChange(field.key, $event)\"\n [label]=\"field.label + (field.required ? ' *' : '')\"\n [hint]=\"field.description ?? ''\"\n />\n }\n </div>\n }\n </div>\n </details>\n }\n </div>\n</div>\n\n<ng-template #credentialSelector>\n @if (credentials().length) {\n <div class=\"space-y-2\">\n @for (credential of credentials(); track credential.credentialRef) {\n <label class=\"flex items-center justify-between gap-3 rounded-lg border border-(--p-content-border-color) bg-(--p-surface-50) px-3 py-2\">\n <span class=\"min-w-0\">\n <span class=\"block truncate text-[12px] font-semibold\">{{ credential.displayName ?? credential.credentialRef }}</span>\n <span class=\"block truncate font-mono text-[11px] text-(--p-text-muted-color)\">\n {{ credential.credentialRef }} / {{ credential.status ?? (credential.resolved ? \"Resolved\" : \"Unresolved\") }}\n </span>\n </span>\n <span class=\"flex items-center gap-2\">\n <mt-button\n size=\"small\"\n variant=\"text\"\n label=\"Test\"\n (onClick)=\"testCredential(credential.credentialRef)\"\n />\n <mt-toggle-field\n size=\"small\"\n [ngModel]=\"credentialRefs().includes(credential.credentialRef)\"\n (ngModelChange)=\"toggleCredential(credential.credentialRef, $event === true)\"\n hint=\"Attach or detach this backend credential reference. Masked secrets are preserved.\"\n />\n </span>\n </label>\n }\n </div>\n } @else if (sectionHasAdvancedEmptyState(\"credentials\")) {\n <p class=\"fp-ae-copy\">\n The backend helper did not return credential choices for this node. Existing masked references remain preserved in the saved JSON.\n </p>\n }\n @if (credentialTest(); as test) {\n <div class=\"mt-3 rounded-lg bg-(--p-surface-100) px-3 py-2 text-[12px]\">\n {{ test.status ?? (test.succeeded ? \"Succeeded\" : \"Placeholder\") }}\n @if (test.message) { <span>- {{ test.message }}</span> }\n </div>\n }\n</ng-template>\n\n<ng-template\n #mapEditor\n let-objectKey=\"objectKey\"\n let-rows=\"rows\"\n let-keyLabel=\"keyLabel\"\n let-valueLabel=\"valueLabel\"\n let-addLabel=\"addLabel\"\n>\n <div class=\"space-y-2\">\n @for (row of rows; track row.key) {\n <div class=\"grid gap-2 md:grid-cols-[180px_1fr_auto]\">\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"row.key\"\n (ngModelChange)=\"updateObjectRow(objectKey, row.key, $event, row.value)\"\n [label]=\"keyLabel\"\n hint=\"Configuration key saved to the backend JSON object.\"\n />\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"valueText(row.value)\"\n (focusin)=\"setExpressionTarget('config:' + objectKey + ':' + row.key)\"\n (ngModelChange)=\"updateObjectRow(objectKey, row.key, row.key, $event)\"\n [label]=\"valueLabel\"\n hint=\"Configuration value. Use expressions when this field should resolve at runtime.\"\n />\n <mt-button\n class=\"self-end\"\n size=\"small\"\n severity=\"secondary\"\n variant=\"outlined\"\n label=\"Remove\"\n (onClick)=\"removeObjectRow(objectKey, row.key)\"\n />\n </div>\n }\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n [label]=\"addLabel || ('Add ' + keyLabel)\"\n (onClick)=\"addObjectRow(objectKey)\"\n />\n </div>\n</ng-template>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: TranslocoModule }, { kind: "component", type: Button, selector: "mt-button", inputs: ["icon", "label", "tooltip", "class", "type", "styleClass", "severity", "badge", "variant", "badgeSeverity", "size", "iconPos", "autofocus", "fluid", "raised", "rounded", "text", "plain", "outlined", "link", "disabled", "loading", "pInputs"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: DateField, selector: "mt-date-field", inputs: ["field", "hint", "label", "placeholder", "class", "readonly", "showIcon", "showClear", "showTime", "pInputs", "required"] }, { kind: "component", type: TextField, selector: "mt-text-field", inputs: ["field", "hint", "label", "placeholder", "class", "type", "readonly", "pInputs", "required", "maxLength", "icon", "iconPosition"] }, { kind: "component", type: TextareaField, selector: "mt-textarea-field", inputs: ["field", "hint", "label", "placeholder", "class", "readonly", "noErrorStyle", "pInputs", "rows", "required", "maxLength"] }, { kind: "component", type: NumberField, selector: "mt-number-field", inputs: ["field", "hint", "label", "placeholder", "class", "readonly", "pInputs", "format", "useGrouping", "maxFractionDigits", "min", "max", "required"] }, { kind: "component", type: SelectField, selector: "mt-select-field", inputs: ["field", "hint", "label", "placeholder", "hasPlaceholderPrefix", "class", "readonly", "pInputs", "options", "optionValue", "optionLabel", "filter", "filterBy", "dataKey", "showClear", "clearAfterSelect", "required", "group", "size", "optionGroupLabel", "optionGroupChildren", "loading", "optionIcon", "optionIconColor", "optionIconShape", "optionAvatarShape", "optionGroupIcon", "optionGroupIconColor", "optionGroupIconShape", "optionGroupAvatarShape", "markCurrentUser"], outputs: ["onChange"] }, { kind: "component", type: ToggleField, selector: "mt-toggle-field", inputs: ["label", "inputId", "labelPosition", "placeholder", "readonly", "pInputs", "required", "toggleShape", "size", "icon", "descriptionCard"], outputs: ["onChange"] }, { kind: "directive", type: DropDataDirective, selector: "[fpDropData]", inputs: ["fpDropAutoInsert"], outputs: ["dataDrop"] }] });
14261
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: AutomationSmartEditorComponent, isStandalone: true, selector: "fp-automation-smart-editor", inputs: { step: { classPropertyName: "step", publicName: "step", isSignal: true, isRequired: false, transformFunction: null }, trigger: { classPropertyName: "trigger", publicName: "trigger", isSignal: true, isRequired: false, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, view: { classPropertyName: "view", publicName: "view", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "block h-full min-h-0" }, ngImport: i0, template: "<div\n class=\"fp-scroll flex h-full min-h-0 flex-col overflow-y-auto\"\n fpDropData\n [fpDropAutoInsert]=\"false\"\n (dataDrop)=\"insertExpression($event)\"\n>\n <div class=\"space-y-4 px-5 py-5\">\n @if (helperError()) {\n <div\n class=\"rounded-lg border border-[rgb(var(--fp-warning))]/30 bg-[rgb(var(--fp-warning))]/10 px-3 py-2 text-[12px] leading-5 text-(--p-text-color)\"\n >\n {{ helperError() }}\n </div>\n }\n\n @if (sectionInMain(\"startConnection\") && trigger()) {\n <section\n class=\"flex flex-col gap-0 overflow-hidden rounded-md border border-surface-200 bg-surface-0\"\n >\n <h3\n class=\"m-0 border-b border-surface-200 bg-surface-50 px-4 py-3 text-lg font-semibold text-color\"\n >\n Start connection\n </h3>\n <div class=\"space-y-3 p-4\">\n @if (startConnection().key) {\n @if (startConnection().step) {\n <div class=\"flex flex-wrap items-start justify-between gap-3\">\n <div class=\"min-w-0 space-y-1\">\n <div class=\"text-[12px] font-semibold text-(--p-text-muted-color)\">\n First node connected\n </div>\n <div class=\"truncate text-[14px] font-semibold text-(--p-text-color)\">\n {{ startConnection().label }}\n </div>\n <div class=\"flex flex-wrap items-center gap-2 text-[12px] text-(--p-text-muted-color)\">\n <span>Managed on canvas</span>\n <span\n class=\"rounded-md bg-(--p-surface-100) px-2 py-1 font-mono text-[11px]\"\n >\n {{ startConnection().key }}\n </span>\n </div>\n </div>\n <div class=\"flex shrink-0 flex-wrap gap-2\">\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n severity=\"secondary\"\n label=\"Focus connected node\"\n (onClick)=\"focusStartConnection()\"\n />\n </div>\n </div>\n } @else {\n <div class=\"space-y-2\">\n <div class=\"text-[14px] font-semibold text-(--p-text-color)\">\n No first node connected\n </div>\n <p class=\"m-0 text-[12px] leading-5 text-(--p-text-muted-color)\">\n The saved start connection points to a node key that is not on\n the canvas. Connect this trigger to the first node on the\n canvas.\n </p>\n <div class=\"flex flex-wrap items-center gap-2 text-[12px] text-(--p-text-muted-color)\">\n <span>Technical key</span>\n <span\n class=\"rounded-md bg-(--p-surface-100) px-2 py-1 font-mono text-[11px]\"\n >\n {{ startConnection().key }}\n </span>\n <span>Managed on canvas</span>\n </div>\n </div>\n }\n } @else {\n <div class=\"rounded-lg border border-dashed border-(--p-content-border-color) bg-(--p-surface-50) p-3\">\n <div class=\"text-[14px] font-semibold text-(--p-text-color)\">\n No first node connected\n </div>\n <p class=\"m-0 mt-1 text-[12px] leading-5 text-(--p-text-muted-color)\">\n Connect this trigger to the first node on the canvas.\n </p>\n </div>\n }\n </div>\n </section>\n }\n\n @switch (editorType()) {\n @case (\"ManualTrigger\") {\n @if (sectionInMain(\"manualTrigger\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Manual run input</div>\n @if (hasTriggerPayloadSchema() && triggerPayloadSchema(); as schema) {\n <div class=\"rounded-md border border-surface-200 bg-surface-0 p-3\">\n <div class=\"fp-ae-label mb-2\">Payload schema</div>\n <pre class=\"fp-ae-code\">{{ schemaText(schema) }}</pre>\n </div>\n }\n @if (triggerPayloadSample(); as sample) {\n <div class=\"mt-3 rounded-md border border-surface-200 bg-surface-0 p-3\">\n <div class=\"fp-ae-label mb-2\">Sample payload</div>\n <pre class=\"fp-ae-code\">{{ schemaText(sample) }}</pre>\n </div>\n }\n @if (!hasTriggerPayloadSchema() && !triggerPayloadSample()) {\n <p class=\"fp-ae-copy\">\n This manual trigger has no backend-provided input schema. It can still be connected to the first step on the canvas.\n </p>\n }\n </section>\n }\n }\n @case (\"WebhookTrigger\") {\n @if (sectionInMain(\"webhookSetup\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Webhook setup</div>\n @if (webhookSetup(); as setup) {\n <div class=\"flex gap-2\">\n <mt-text-field\n class=\"flex-1 font-mono\"\n [ngModel]=\"setup.webhookUrl ?? ''\"\n [readonly]=\"true\"\n label=\"Webhook URL\"\n hint=\"Backend-generated endpoint clients should call to start this trigger.\"\n />\n <mt-button class=\"self-end\" size=\"small\" variant=\"outlined\" label=\"Copy\" (onClick)=\"copyWebhookUrl()\" />\n </div>\n <div class=\"grid gap-2 md:grid-cols-2\">\n <div class=\"fp-ae-kv\">\n <span>Auth mode</span>\n <strong>{{ setup.authMode ?? \"Backend default\" }}</strong>\n </div>\n <div class=\"fp-ae-kv\">\n <span>Required headers</span>\n <strong>{{ (setup.requiredHeaders ?? []).join(\", \") || \"-\" }}</strong>\n </div>\n </div>\n @if (setup.hmacSigning) {\n <p class=\"fp-ae-copy\">{{ setup.hmacSigning }}</p>\n }\n @if (setup.sampleRequest) {\n <details class=\"mt-3 rounded-lg border border-(--p-content-border-color)\">\n <summary class=\"cursor-pointer px-3 py-2 text-[12px] font-semibold\">\n Sample request\n </summary>\n <pre class=\"fp-ae-code\">{{ schemaText(setup.sampleRequest) }}</pre>\n </details>\n }\n } @else {\n <p class=\"fp-ae-copy\">\n Webhook setup is not available for this draft yet.\n </p>\n }\n </section>\n }\n @if (sectionInMain(\"authPolicy\") && hasAuthPolicy()) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Authentication policy</div>\n <div class=\"grid gap-3 md:grid-cols-3\">\n <mt-select-field\n [ngModel]=\"authPolicy()['mode'] ?? ''\"\n (ngModelChange)=\"onAuthFieldChange('mode', $event)\"\n label=\"Mode\"\n hint=\"Authentication mode required by the backend webhook policy.\"\n [options]=\"authModeOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-text-field\n [ngModel]=\"authPolicy()['signatureHeaderName'] ?? ''\"\n (ngModelChange)=\"onAuthFieldChange('signatureHeaderName', $event)\"\n label=\"Signature header\"\n hint=\"Header name that carries the webhook signature when the policy requires signed requests.\"\n />\n <mt-text-field\n [ngModel]=\"authPolicy()['timestampHeaderName'] ?? ''\"\n (ngModelChange)=\"onAuthFieldChange('timestampHeaderName', $event)\"\n label=\"Timestamp header\"\n hint=\"Header name that carries the request timestamp for replay protection.\"\n />\n </div>\n </section>\n }\n }\n @case (\"FormSubmitTrigger\") {\n @if (sectionInMain(\"formBinding\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Form binding</div>\n <div class=\"grid gap-3 md:grid-cols-2\">\n <mt-select-field\n [ngModel]=\"selectedFormId() || formBinding()?.formId || ''\"\n (ngModelChange)=\"onFormChange($event)\"\n label=\"Form\"\n hint=\"Choose a backend form that will submit data into this trigger.\"\n [options]=\"formOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-select-field\n [ngModel]=\"selectedFormVersionId() || formBinding()?.formVersionId || ''\"\n (ngModelChange)=\"onFormVersionChange($event)\"\n label=\"Form version\"\n hint=\"Persist the exact backend formVersionId. Do not type or generate IDs manually.\"\n [options]=\"formVersionOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n </div>\n <div class=\"mt-3 flex flex-wrap items-center gap-2\">\n <mt-button size=\"small\" severity=\"primary\" label=\"Save binding\" (onClick)=\"saveFormBinding()\" />\n @if (formBinding()) {\n <span class=\"rounded-lg bg-(--p-surface-100) px-2 py-1 font-mono text-[11px] text-(--p-text-muted-color)\">\n {{ formBinding()!.formVersionId }}\n </span>\n }\n </div>\n @if (formSchema(); as schema) {\n <details class=\"mt-3 rounded-lg border border-(--p-content-border-color)\">\n <summary class=\"cursor-pointer px-3 py-2 text-[12px] font-semibold\">\n Form schema preview\n </summary>\n <pre class=\"fp-ae-code\">{{ schemaText(schema.schema) }}</pre>\n </details>\n }\n </section>\n }\n }\n @case (\"ScheduleTrigger\") {\n @if (sectionInMain(\"schedule\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Schedule</div>\n <div class=\"grid gap-3 md:grid-cols-2\">\n <mt-select-field\n [ngModel]=\"scheduleMode()\"\n (ngModelChange)=\"onScheduleModeChange($event)\"\n label=\"Mode\"\n hint=\"Pick one schedule mode: cron, interval, or once. Backend validation requires exactly one mode.\"\n [options]=\"scheduleModeOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-select-field\n [ngModel]=\"config()['timezone'] ?? 'UTC'\"\n (ngModelChange)=\"onConfigFieldChange('timezone', $event)\"\n label=\"Timezone\"\n hint=\"Timezone used to calculate the next fire time.\"\n [options]=\"timeZoneOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n @if (showScheduleCron()) {\n <mt-text-field\n class=\"font-mono md:col-span-2\"\n [ngModel]=\"config()['cron'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('cron', $event)\"\n label=\"Cron\"\n hint=\"Cron expression, for example 0 9 * * *.\"\n />\n }\n @if (showScheduleInterval()) {\n <mt-number-field\n [ngModel]=\"numberValue(config()['intervalSeconds'])\"\n (ngModelChange)=\"onConfigFieldChange('intervalSeconds', $event)\"\n label=\"Interval seconds\"\n hint=\"Repeat interval in seconds.\"\n [min]=\"0\"\n />\n }\n @if (showScheduleOnce()) {\n <mt-date-field\n class=\"md:col-span-2\"\n [ngModel]=\"config()['runAt'] ?? config()['runAtUtc'] ?? config()['oneTimeAt'] ?? config()['at'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('runAt', $event)\"\n label=\"Run at UTC\"\n hint=\"UTC date/time for the one-time schedule.\"\n [showTime]=\"true\"\n [showClear]=\"true\"\n [pInputs]=\"dateTimePickerInputs\"\n />\n }\n @if (showScheduleStartDate()) {\n <mt-date-field\n [ngModel]=\"config()['startDate'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('startDate', $event)\"\n label=\"Start date UTC\"\n hint=\"Optional first allowed run time for this schedule.\"\n [showTime]=\"true\"\n [showClear]=\"true\"\n [pInputs]=\"dateTimePickerInputs\"\n />\n }\n <mt-select-field\n [ngModel]=\"config()['misfirePolicy'] ?? 'SkipMissed'\"\n (ngModelChange)=\"onConfigFieldChange('misfirePolicy', $event)\"\n label=\"Misfire policy\"\n hint=\"Backend behavior when a scheduled run is missed while the automation is unavailable.\"\n [options]=\"misfirePolicyOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n </div>\n <div class=\"mt-3 flex gap-2\">\n <mt-button size=\"small\" variant=\"outlined\" label=\"Validate\" (onClick)=\"validateSchedule()\" />\n <mt-button size=\"small\" severity=\"primary\" label=\"Preview next fire\" (onClick)=\"previewSchedule()\" />\n </div>\n @if (schedulePreview(); as preview) {\n <div class=\"mt-3 rounded-lg bg-(--p-surface-100) px-3 py-2 text-[12px]\">\n <strong class=\"text-emerald-600\">Valid</strong>\n <span class=\"ms-2\">Next fire: {{ preview.nextFireAtUtc ?? \"-\" }}</span>\n </div>\n }\n </section>\n }\n }\n @case (\"SetFields\") {\n @if (sectionInMain(\"setFields\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Set fields</div>\n <ng-container *ngTemplateOutlet=\"mapEditor; context: { objectKey: 'fields', rows: fieldsRows(), keyLabel: 'Field', valueLabel: 'Value', addLabel: 'Add field' }\" />\n </section>\n }\n }\n @case (\"If\") {\n @if (sectionInMain(\"condition\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Condition</div>\n <div class=\"grid gap-3 md:grid-cols-[1fr_180px_1fr]\">\n <mt-text-field\n [ngModel]=\"fieldText('left')\"\n (focusin)=\"setExpressionTarget('config:left')\"\n (ngModelChange)=\"onConfigFieldChange('left', $event)\"\n label=\"Left\"\n hint=\"Left-side value or expression to compare.\"\n />\n <mt-select-field\n [ngModel]=\"config()['operator'] ?? 'equals'\"\n (ngModelChange)=\"onConfigFieldChange('operator', $event)\"\n label=\"Operator\"\n hint=\"Comparison operator used by the backend condition evaluator.\"\n [options]=\"ifOperatorOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-text-field\n [ngModel]=\"fieldText('right')\"\n (focusin)=\"setExpressionTarget('config:right')\"\n (ngModelChange)=\"onConfigFieldChange('right', $event)\"\n label=\"Right\"\n hint=\"Right-side value or expression to compare against.\"\n />\n </div>\n <div class=\"mt-3 rounded-md border border-surface-200 bg-surface-0 p-3\">\n <div class=\"fp-ae-label mb-2\">Route outputs</div>\n <div class=\"flex flex-wrap gap-1.5\">\n @for (key of routeOutputKeys(); track key) {\n <span class=\"rounded-md bg-(--p-surface-100) px-2 py-1 font-mono text-[11px]\">{{ key }}</span>\n }\n </div>\n </div>\n </section>\n }\n }\n @case (\"HTTP\") {\n @if (sectionInMain(\"httpRequest\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">HTTP request</div>\n <div class=\"grid gap-3 md:grid-cols-[150px_1fr]\">\n <mt-select-field\n [ngModel]=\"config()['method'] ?? 'GET'\"\n (ngModelChange)=\"onConfigFieldChange('method', $event)\"\n label=\"Method\"\n hint=\"HTTP method used for the outbound request.\"\n [options]=\"httpMethodOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"config()['url'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:url')\"\n (ngModelChange)=\"onConfigFieldChange('url', $event)\"\n label=\"URL\"\n hint=\"Target URL. Expressions are supported for dynamic hosts, paths, and query values.\"\n />\n </div>\n <div class=\"mt-3\">\n <ng-container *ngTemplateOutlet=\"mapEditor; context: { objectKey: 'headers', rows: headerRows(), keyLabel: 'Header', valueLabel: 'Value', addLabel: 'Add header' }\" />\n </div>\n <div class=\"mt-3\">\n <ng-container *ngTemplateOutlet=\"mapEditor; context: { objectKey: 'query', rows: queryRows(), keyLabel: 'Query param', valueLabel: 'Value', addLabel: 'Add query param' }\" />\n </div>\n @if (supportsConfigKey('bodyMode')) {\n <mt-select-field\n class=\"mt-3\"\n [ngModel]=\"config()['bodyMode'] ?? 'json'\"\n (ngModelChange)=\"onConfigFieldChange('bodyMode', $event)\"\n label=\"Body mode\"\n hint=\"Backend-supported request body serialization mode.\"\n [options]=\"httpBodyModeOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n }\n <mt-textarea-field\n class=\"mt-3 w-full font-mono\"\n [ngModel]=\"valueText(config()['body'] ?? '')\"\n (focusin)=\"setExpressionTarget('config:body')\"\n (ngModelChange)=\"onConfigFieldChange('body', $event)\"\n label=\"Body\"\n hint=\"Request body sent by the HTTP node. Use JSON or expressions when the backend schema allows it.\"\n rows=\"6\"\n />\n @if (supportsConfigKey('timeoutSeconds') || supportsConfigKey('responseHandling')) {\n <div class=\"mt-3 grid gap-3 md:grid-cols-2\">\n @if (supportsConfigKey('timeoutSeconds')) {\n <mt-number-field\n [ngModel]=\"numberValue(config()['timeoutSeconds'])\"\n (ngModelChange)=\"onConfigFieldChange('timeoutSeconds', $event)\"\n label=\"Timeout seconds\"\n hint=\"Request timeout when exposed by the backend schema.\"\n [min]=\"0\"\n />\n }\n @if (supportsConfigKey('responseHandling')) {\n <mt-text-field\n [ngModel]=\"config()['responseHandling'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('responseHandling', $event)\"\n label=\"Response handling\"\n hint=\"Backend-supported response handling mode or expression.\"\n />\n }\n </div>\n }\n @if (sectionInMain(\"credentials\")) {\n <div class=\"mt-3 rounded-md border border-surface-200 bg-surface-0 p-3\">\n <div class=\"fp-ae-label mb-2\">Credential</div>\n <ng-container *ngTemplateOutlet=\"credentialSelector\" />\n </div>\n }\n </section>\n }\n }\n @case (\"Wait\") {\n @if (sectionInMain(\"wait\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Wait</div>\n <div class=\"grid gap-3 md:grid-cols-3\">\n <mt-select-field\n [ngModel]=\"config()['mode'] ?? 'duration'\"\n (ngModelChange)=\"onConfigFieldChange('mode', $event)\"\n label=\"Mode\"\n hint=\"Choose whether this wait uses a duration or a specific date/time.\"\n [options]=\"waitModeOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-number-field\n [ngModel]=\"numberValue(config()['durationSeconds'])\"\n (ngModelChange)=\"onConfigFieldChange('durationSeconds', $event)\"\n label=\"Duration seconds\"\n hint=\"How long execution should wait when mode is duration.\"\n [min]=\"0\"\n />\n <mt-date-field\n [ngModel]=\"config()['waitUntil'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('waitUntil', $event)\"\n label=\"Wait until\"\n hint=\"Date/time that resolves to when execution should resume.\"\n [showTime]=\"true\"\n [showClear]=\"true\"\n [pInputs]=\"dateTimePickerInputs\"\n />\n </div>\n @if (supportsConfigKey('resumePayloadSchema') || supportsConfigKey('resumePayload')) {\n <mt-textarea-field\n class=\"mt-3 w-full font-mono\"\n [ngModel]=\"valueText(config()['resumePayloadSchema'] ?? config()['resumePayload'] ?? '')\"\n (focusin)=\"setExpressionTarget('config:resumePayload')\"\n (ngModelChange)=\"onConfigFieldChange('resumePayload', $event)\"\n label=\"Resume payload\"\n hint=\"Expected payload when the backend supports manual or external resume data.\"\n rows=\"5\"\n />\n }\n </section>\n }\n }\n @case (\"HumanApproval\") {\n @if (sectionInMain(\"approvalTask\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Approval task</div>\n @if (assignmentOptions()?.providerStatus && assignmentOptions()?.providerStatus !== \"Available\") {\n <p class=\"fp-ae-copy\">\n Assignment provider status: {{ assignmentOptions()?.providerStatus }}.\n The backend provider is the source of truth for available assignees.\n </p>\n }\n <div class=\"grid gap-3 xl:grid-cols-3\">\n <mt-text-field\n [ngModel]=\"config()['title'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:title')\"\n (ngModelChange)=\"onConfigFieldChange('title', $event)\"\n label=\"Approval title\"\n hint=\"Approval title shown to the assigned human approver.\"\n />\n <mt-select-field\n [ngModel]=\"selectedAssignmentKey()\"\n (ngModelChange)=\"onAssignmentOptionChange($event)\"\n label=\"Assignment\"\n hint=\"Backend-provided assignee, role, or group that can decide this approval.\"\n [options]=\"assignmentSelectOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n @if (humanApprovalSupportsConfig('priority')) {\n <mt-text-field\n [ngModel]=\"config()['priority'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('priority', $event)\"\n label=\"Priority\"\n hint=\"Approval priority when supported by the backend schema.\"\n />\n }\n </div>\n <mt-textarea-field\n class=\"mt-3 w-full\"\n [ngModel]=\"config()['message'] ?? config()['instructions'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:message')\"\n (ngModelChange)=\"onConfigFieldChange('message', $event)\"\n label=\"Approval message\"\n hint=\"Decision instructions shown to the approver.\"\n rows=\"4\"\n />\n @if (humanApprovalSupportsConfig('dueDate') || humanApprovalSupportsConfig('dueInSeconds') || humanApprovalSupportsConfig('timeoutSeconds') || humanApprovalSupportsConfig('expiresAt') || humanApprovalSupportsConfig('commentsRequired') || humanApprovalSupportsConfig('allowReturn')) {\n <div class=\"mt-3 grid gap-3 xl:grid-cols-4\">\n @if (humanApprovalSupportsConfig('dueDate')) {\n <mt-date-field\n [ngModel]=\"config()['dueDate'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('dueDate', $event)\"\n label=\"Due date\"\n hint=\"Backend-supported approval due date.\"\n [showTime]=\"true\"\n [showClear]=\"true\"\n [pInputs]=\"dateTimePickerInputs\"\n />\n }\n @if (humanApprovalSupportsConfig('dueInSeconds')) {\n <mt-number-field\n [ngModel]=\"numberValue(config()['dueInSeconds'])\"\n (ngModelChange)=\"onConfigFieldChange('dueInSeconds', $event)\"\n label=\"Due in seconds\"\n hint=\"Relative approval due duration when supported by the backend schema.\"\n [min]=\"0\"\n />\n }\n @if (humanApprovalSupportsConfig('timeoutSeconds')) {\n <mt-number-field\n [ngModel]=\"numberValue(config()['timeoutSeconds'])\"\n (ngModelChange)=\"onConfigFieldChange('timeoutSeconds', $event)\"\n label=\"Timeout seconds\"\n hint=\"Approval timeout emitted according to backend approval runtime support.\"\n [min]=\"0\"\n />\n }\n @if (humanApprovalSupportsConfig('expiresAt')) {\n <mt-date-field\n [ngModel]=\"config()['expiresAt'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('expiresAt', $event)\"\n label=\"Expires at\"\n hint=\"Backend-supported approval expiry timestamp.\"\n [showTime]=\"true\"\n [showClear]=\"true\"\n [pInputs]=\"dateTimePickerInputs\"\n />\n }\n @if (humanApprovalSupportsConfig('commentsRequired')) {\n <div class=\"flex min-h-[4.25rem] items-end rounded-md border border-surface-200 bg-surface-0 px-3 py-2\">\n <mt-toggle-field\n size=\"small\"\n label=\"Comments required\"\n labelPosition=\"end\"\n [ngModel]=\"config()['commentsRequired'] === true\"\n (ngModelChange)=\"onConfigFieldChange('commentsRequired', $event === true)\"\n hint=\"Require approver comments when the backend supports this flag.\"\n />\n </div>\n }\n @if (humanApprovalSupportsConfig('allowReturn')) {\n <div class=\"flex min-h-[4.25rem] items-end rounded-md border border-surface-200 bg-surface-0 px-3 py-2\">\n <mt-toggle-field\n size=\"small\"\n label=\"Allow return for changes\"\n labelPosition=\"end\"\n [ngModel]=\"config()['allowReturn'] !== false\"\n (ngModelChange)=\"onConfigFieldChange('allowReturn', $event === true)\"\n hint=\"Allow ReturnForChanges when the backend schema exposes this option.\"\n />\n </div>\n }\n </div>\n }\n <div class=\"mt-3 space-y-3\">\n <div class=\"fp-ae-section-title\">Decision options</div>\n <div class=\"overflow-hidden rounded-md border border-surface-200 bg-surface-0\">\n @if (selectedApprovalDecisionRows().length > 0) {\n <div class=\"hidden border-b border-surface-200 bg-surface-50 px-3 py-2 text-[12px] font-semibold text-(--p-text-muted-color) xl:grid xl:grid-cols-[1.1fr_1fr_1.2fr_80px_44px] xl:gap-3\">\n <span>Decision label</span>\n <span>Canonical value</span>\n <span>Route output key</span>\n <span>Routes</span>\n <span>Action</span>\n </div>\n @for (decision of selectedApprovalDecisionRows(); track decision.value) {\n <div class=\"grid gap-2 border-b border-surface-100 px-3 py-3 last:border-b-0 xl:grid-cols-[1.1fr_1fr_1.2fr_80px_44px] xl:items-center xl:gap-3\">\n <div class=\"min-w-0\">\n <div class=\"text-[11px] font-semibold text-(--p-text-muted-color) xl:hidden\">Decision label</div>\n <div class=\"truncate text-[13px] font-semibold text-(--p-text-color)\">{{ decision.label }}</div>\n </div>\n <div class=\"min-w-0\">\n <div class=\"text-[11px] font-semibold text-(--p-text-muted-color) xl:hidden\">Canonical value</div>\n <div class=\"truncate font-mono text-[12px] text-(--p-text-color)\">{{ decision.value }}</div>\n </div>\n <div class=\"min-w-0\">\n <div class=\"text-[11px] font-semibold text-(--p-text-muted-color) xl:hidden\">Route output key</div>\n <div class=\"truncate font-mono text-[12px] text-(--p-text-muted-color)\">{{ decision.routeOutputKey }}</div>\n </div>\n <div>\n <div class=\"text-[11px] font-semibold text-(--p-text-muted-color) xl:hidden\">Routes</div>\n <div class=\"text-[13px] text-(--p-text-color)\">{{ decision.routeCount }}</div>\n </div>\n <mt-button\n class=\"justify-self-start xl:justify-self-end\"\n size=\"small\"\n variant=\"outlined\"\n severity=\"danger\"\n icon=\"general.trash-01\"\n tooltip=\"Remove decision option\"\n (onClick)=\"removeApprovalDecision(decision.value)\"\n />\n </div>\n }\n } @else {\n <div class=\"px-3 py-4 text-[12px] text-(--p-text-muted-color)\">\n Add at least one backend-supported approval decision.\n </div>\n }\n </div>\n @if (approvalDecisionIssues().length > 0) {\n <div class=\"rounded-lg border border-[rgb(var(--fp-error))]/30 bg-[rgb(var(--fp-error))]/10 px-3 py-2 text-[12px] leading-5 text-[rgb(var(--fp-error))]\">\n @for (issue of approvalDecisionIssues(); track issue) {\n <div>{{ issue }}</div>\n }\n </div>\n }\n <div class=\"grid gap-2 md:grid-cols-[minmax(0,1fr)_auto]\">\n <mt-select-field\n [ngModel]=\"approvalDecisionToAdd()\"\n (ngModelChange)=\"approvalDecisionToAdd.set($event)\"\n label=\"Decision to add\"\n hint=\"Only backend-supported decisions with matching route outputs are available.\"\n [options]=\"addableApprovalDecisionOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-button\n class=\"self-end\"\n size=\"small\"\n variant=\"outlined\"\n icon=\"general.plus\"\n label=\"Add decision option\"\n [disabled]=\"!approvalDecisionToAdd()\"\n (onClick)=\"addApprovalDecision()\"\n />\n </div>\n </div>\n @if (humanApprovalSupportsConfig('payload') || humanApprovalSupportsConfig('context') || humanApprovalSupportsConfig('metadata')) {\n <div class=\"mt-3 grid gap-3\">\n @if (humanApprovalSupportsConfig('payload')) {\n <mt-textarea-field\n class=\"w-full font-mono\"\n [ngModel]=\"valueText(config()['payload'] ?? '')\"\n (focusin)=\"setExpressionTarget('config:payload')\"\n (ngModelChange)=\"onConfigFieldChange('payload', $event)\"\n label=\"Payload\"\n hint=\"Approval task payload or context fields supported by backend schema.\"\n rows=\"5\"\n />\n }\n @if (humanApprovalSupportsConfig('context')) {\n <mt-textarea-field\n class=\"w-full font-mono\"\n [ngModel]=\"valueText(config()['context'] ?? '')\"\n (focusin)=\"setExpressionTarget('config:context')\"\n (ngModelChange)=\"onConfigFieldChange('context', $event)\"\n label=\"Context\"\n hint=\"Additional approval context supported by backend schema.\"\n rows=\"5\"\n />\n }\n @if (humanApprovalSupportsConfig('metadata')) {\n <mt-textarea-field\n class=\"w-full font-mono\"\n [ngModel]=\"valueText(config()['metadata'] ?? '')\"\n (focusin)=\"setExpressionTarget('config:metadata')\"\n (ngModelChange)=\"onConfigFieldChange('metadata', $event)\"\n label=\"Metadata\"\n hint=\"Additional approval metadata supported by the backend schema.\"\n rows=\"4\"\n />\n }\n </div>\n }\n <div class=\"mt-3 flex gap-2\">\n <mt-button size=\"small\" variant=\"outlined\" label=\"Validate assignment\" (onClick)=\"validateAssignment()\" />\n </div>\n @if (assignmentValidation(); as result) {\n <div class=\"mt-3 rounded-lg bg-(--p-surface-100) px-3 py-2 text-[12px]\">\n {{ result.isValid === false ? \"Assignment invalid\" : \"Assignment accepted by helper\" }}\n @for (issue of resultIssues(result); track issue) {\n <div class=\"mt-1 text-[11px] text-(--p-text-muted-color)\">{{ issue }}</div>\n }\n </div>\n }\n @if (sectionInMain(\"approvalOutput\")) {\n <div class=\"mt-3 rounded-md border border-surface-200 bg-surface-0 p-3\">\n <div class=\"fp-ae-label mb-2\">Output data</div>\n <div class=\"flex flex-wrap gap-1.5\">\n @for (field of approvalOutputFieldLabels; track field) {\n <span class=\"rounded-md bg-(--p-surface-100) px-2 py-1 font-mono text-[11px]\">{{ field }}</span>\n }\n </div>\n <div class=\"mt-3 fp-ae-label mb-2\">Approval routes</div>\n <div class=\"flex flex-wrap gap-1.5\">\n @for (key of routeOutputKeys(); track key) {\n <span class=\"rounded-md bg-(--p-surface-100) px-2 py-1 font-mono text-[11px]\">{{ key }}</span>\n }\n </div>\n </div>\n }\n </section>\n }\n }\n @case (\"FlowPlusCommit\") {\n @if (sectionInMain(\"flowplusCommit\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">FlowPlus commit</div>\n <p class=\"fp-ae-copy\">\n This node is the explicit module-data write boundary. Approval does\n not commit data unless this node is reached.\n </p>\n <div class=\"grid gap-3 md:grid-cols-2\">\n <mt-select-field\n [ngModel]=\"config()['targetModule'] ?? ''\"\n (ngModelChange)=\"onModuleChange($event)\"\n label=\"Module\"\n hint=\"Module whose records will be written by this explicit FlowPlus commit.\"\n [options]=\"moduleOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-select-field\n [ngModel]=\"config()['operation'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('operation', $event)\"\n label=\"Operation\"\n hint=\"Write operation supported by the selected backend module schema.\"\n [options]=\"operationOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n </div>\n <mt-text-field\n class=\"mt-3 font-mono\"\n [ngModel]=\"config()['idempotencyKey'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:idempotencyKey')\"\n (ngModelChange)=\"onConfigFieldChange('idempotencyKey', $event)\"\n label=\"Idempotency key\"\n hint=\"Optional stable key used by the backend to prevent duplicate writes.\"\n />\n @if (selectedModuleFields().length) {\n <div class=\"mt-3 space-y-2\">\n <div class=\"fp-ae-section-title\">Module schema</div>\n <div class=\"grid gap-2 md:grid-cols-2\">\n @for (field of selectedModuleFields(); track field.key) {\n <div class=\"fp-ae-kv\">\n <span>{{ field.displayName ?? field.key }}</span>\n <strong>{{ field.viewType ?? \"Value\" }} @if (field.required) { * }</strong>\n </div>\n }\n </div>\n </div>\n }\n <div class=\"mt-3\">\n <ng-container *ngTemplateOutlet=\"mapEditor; context: { objectKey: 'mapping', rows: mappingRows(), keyLabel: 'Module field', valueLabel: 'Expression / value', addLabel: 'Add module field' }\" />\n </div>\n <div class=\"mt-3 flex gap-2\">\n <mt-button size=\"small\" severity=\"primary\" label=\"Validate mapping\" (onClick)=\"validateCommitMapping()\" />\n </div>\n @if (commitValidation(); as result) {\n <div class=\"mt-3 rounded-lg bg-(--p-surface-100) px-3 py-2 text-[12px]\">\n {{ result.isValid === false ? \"Mapping invalid\" : \"Mapping accepted by helper\" }}\n @for (issue of resultIssues(result); track issue) {\n <div class=\"mt-1 text-[11px] text-(--p-text-muted-color)\">{{ issue }}</div>\n }\n </div>\n }\n @if (sectionInMain(\"credentials\")) {\n <div class=\"mt-3 rounded-md border border-surface-200 bg-surface-0 p-3\">\n <div class=\"fp-ae-label mb-2\">Credential</div>\n <ng-container *ngTemplateOutlet=\"credentialSelector\" />\n </div>\n }\n </section>\n }\n }\n @case (\"WebhookResponse\") {\n @if (sectionInMain(\"webhookResponse\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Webhook response</div>\n <div class=\"grid gap-3 md:grid-cols-2\">\n <mt-number-field\n [ngModel]=\"numberValue(config()['statusCode']) ?? 200\"\n (ngModelChange)=\"onConfigFieldChange('statusCode', $event)\"\n label=\"Status code\"\n hint=\"HTTP status code sent back by the webhook response node.\"\n [min]=\"100\"\n [max]=\"599\"\n />\n <mt-select-field\n [ngModel]=\"config()['responseMode'] ?? 'json'\"\n (ngModelChange)=\"onConfigFieldChange('responseMode', $event)\"\n label=\"Response mode\"\n hint=\"How the webhook response body should be serialized.\"\n [options]=\"responseModeOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n </div>\n <div class=\"mt-3\">\n <ng-container *ngTemplateOutlet=\"mapEditor; context: { objectKey: 'headers', rows: headerRows(), keyLabel: 'Header', valueLabel: 'Value', addLabel: 'Add header' }\" />\n </div>\n <mt-textarea-field\n class=\"mt-3 w-full font-mono\"\n [ngModel]=\"valueText(config()['body'] ?? '')\"\n (focusin)=\"setExpressionTarget('config:body')\"\n (ngModelChange)=\"onConfigFieldChange('body', $event)\"\n label=\"Body\"\n hint=\"Body returned to the webhook caller. Expressions can use current execution data.\"\n rows=\"6\"\n />\n </section>\n }\n }\n @case (\"CallAutomation\") {\n @if (sectionInMain(\"callAutomation\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Subworkflow</div>\n <div class=\"grid gap-3 md:grid-cols-2\">\n <mt-select-field\n [ngModel]=\"config()['targetAutomationId'] ?? ''\"\n (ngModelChange)=\"onSubworkflowTargetChange($event)\"\n label=\"Target automation\"\n hint=\"Backend-provided automation candidate. Tenant and recursion validation stay on the backend.\"\n [options]=\"subworkflowAutomationOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-select-field\n [ngModel]=\"config()['revisionMode'] ?? 'Active'\"\n (ngModelChange)=\"onConfigFieldChange('revisionMode', $event)\"\n label=\"Revision mode\"\n hint=\"Choose which published or active child revision to call.\"\n [options]=\"revisionModeOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n @if (config()['revisionMode'] === \"SpecificRevision\") {\n <mt-text-field\n [ngModel]=\"config()['specificRevisionId'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('specificRevisionId', $event)\"\n label=\"Specific revision id\"\n hint=\"Required only when revision mode is Specific revision.\"\n />\n }\n <mt-toggle-field\n size=\"small\"\n labelPosition=\"end\"\n [ngModel]=\"config()['waitForCompletion'] !== false\"\n (ngModelChange)=\"onConfigFieldChange('waitForCompletion', $event === true)\"\n label=\"Wait for completion\"\n hint=\"When disabled, the parent continues after dispatch and backend records the child linkage.\"\n />\n </div>\n <div class=\"mt-3 grid gap-3 md:grid-cols-2\">\n <mt-textarea-field\n class=\"w-full font-mono md:col-span-2\"\n [ngModel]=\"valueText(config()['inputMappingJson'] ?? '{}')\"\n (focusin)=\"setExpressionTarget('config:inputMappingJson')\"\n (ngModelChange)=\"onConfigFieldChange('inputMappingJson', $event)\"\n label=\"Input mapping JSON\"\n hint=\"JSON object or expression map sent into the child automation input.\"\n rows=\"7\"\n />\n <mt-textarea-field\n class=\"w-full font-mono md:col-span-2\"\n [ngModel]=\"valueText(config()['outputMappingJson'] ?? '{}')\"\n (focusin)=\"setExpressionTarget('config:outputMappingJson')\"\n (ngModelChange)=\"onConfigFieldChange('outputMappingJson', $event)\"\n label=\"Output mapping JSON\"\n hint=\"JSON object or expression map for child output/status returned to the parent.\"\n rows=\"7\"\n />\n </div>\n </section>\n }\n }\n @case (\"Subworkflow\") {\n @if (sectionInMain(\"callAutomation\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Subworkflow</div>\n <div class=\"grid gap-3 md:grid-cols-2\">\n <mt-select-field\n [ngModel]=\"config()['targetAutomationId'] ?? ''\"\n (ngModelChange)=\"onSubworkflowTargetChange($event)\"\n label=\"Target automation\"\n hint=\"Backend-provided automation candidate. Tenant and recursion validation stay on the backend.\"\n [options]=\"subworkflowAutomationOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-select-field\n [ngModel]=\"config()['revisionMode'] ?? 'Active'\"\n (ngModelChange)=\"onConfigFieldChange('revisionMode', $event)\"\n label=\"Revision mode\"\n hint=\"Choose which published or active child revision to call.\"\n [options]=\"revisionModeOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n @if (config()['revisionMode'] === \"SpecificRevision\") {\n <mt-text-field\n [ngModel]=\"config()['specificRevisionId'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('specificRevisionId', $event)\"\n label=\"Specific revision id\"\n hint=\"Required only when revision mode is Specific revision.\"\n />\n }\n <mt-toggle-field\n size=\"small\"\n labelPosition=\"end\"\n [ngModel]=\"config()['waitForCompletion'] !== false\"\n (ngModelChange)=\"onConfigFieldChange('waitForCompletion', $event === true)\"\n label=\"Wait for completion\"\n hint=\"When disabled, the parent continues after dispatch and backend records the child linkage.\"\n />\n </div>\n <div class=\"mt-3 grid gap-3 md:grid-cols-2\">\n <mt-textarea-field\n class=\"w-full font-mono md:col-span-2\"\n [ngModel]=\"valueText(config()['inputMappingJson'] ?? '{}')\"\n (focusin)=\"setExpressionTarget('config:inputMappingJson')\"\n (ngModelChange)=\"onConfigFieldChange('inputMappingJson', $event)\"\n label=\"Input mapping JSON\"\n hint=\"JSON object or expression map sent into the child automation input.\"\n rows=\"7\"\n />\n <mt-textarea-field\n class=\"w-full font-mono md:col-span-2\"\n [ngModel]=\"valueText(config()['outputMappingJson'] ?? '{}')\"\n (focusin)=\"setExpressionTarget('config:outputMappingJson')\"\n (ngModelChange)=\"onConfigFieldChange('outputMappingJson', $event)\"\n label=\"Output mapping JSON\"\n hint=\"JSON object or expression map for child output/status returned to the parent.\"\n rows=\"7\"\n />\n </div>\n </section>\n }\n }\n @case (\"ParallelStart\") {\n @if (sectionInMain(\"parallelStart\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"flex items-center justify-between gap-3 border-b border-surface-200 bg-surface-50 px-4 py-3\">\n <div class=\"fp-ae-section-title !m-0\">Parallel start</div>\n <mt-button size=\"small\" variant=\"outlined\" icon=\"general.plus\" label=\"Add branch\" (onClick)=\"addParallelBranch()\" />\n </div>\n <div class=\"space-y-3 p-4\">\n @for (branch of parallelBranches(); track branch.key; let i = $index) {\n <div class=\"rounded-md border border-surface-200 bg-surface-0 p-3\">\n <div class=\"grid gap-3 md:grid-cols-[160px_1fr_1fr_auto]\">\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"branch.key\"\n (ngModelChange)=\"updateParallelBranch(i, 'key', $event)\"\n label=\"Branch key\"\n hint=\"Persisted route key. Do not use array index names.\"\n />\n <mt-text-field\n [ngModel]=\"branch.label\"\n (ngModelChange)=\"updateParallelBranch(i, 'label', $event)\"\n label=\"Label\"\n hint=\"Visual branch label. Changing it does not change existing routes.\"\n />\n <mt-text-field\n [ngModel]=\"branch.description\"\n (ngModelChange)=\"updateParallelBranch(i, 'description', $event)\"\n label=\"Description\"\n hint=\"Optional branch note for reviewers.\"\n />\n <div class=\"flex items-end gap-1\">\n <mt-button size=\"small\" variant=\"outlined\" icon=\"arrow.arrow-up\" tooltip=\"Move up\" (onClick)=\"moveParallelBranch(i, -1)\" />\n <mt-button size=\"small\" variant=\"outlined\" icon=\"arrow.arrow-down\" tooltip=\"Move down\" (onClick)=\"moveParallelBranch(i, 1)\" />\n <mt-button size=\"small\" variant=\"outlined\" severity=\"danger\" icon=\"general.trash-01\" tooltip=\"Remove branch\" (onClick)=\"removeParallelBranch(i)\" />\n </div>\n </div>\n <div class=\"mt-3 flex flex-wrap items-center gap-1.5 text-[11px]\">\n <span class=\"inline-flex h-7 items-center rounded-md border border-surface-200 bg-surface-50 px-2 font-mono font-semibold text-(--p-text-color)\">{{ branch.key }}</span>\n <span class=\"inline-flex h-7 items-center rounded-md border border-surface-200 bg-surface-0 px-2 font-medium text-(--p-text-muted-color)\">{{ branch.routeCount }} connected route{{ branch.routeCount === 1 ? \"\" : \"s\" }}</span>\n </div>\n </div>\n }\n @if (parallelBranches().length === 0) {\n <p class=\"fp-ae-copy\">Add at least two stable branch keys. Backend validation blocks publish until branches are valid.</p>\n }\n </div>\n </section>\n }\n }\n @case (\"ParallelJoin\") {\n @if (sectionInMain(\"parallelJoin\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Parallel join</div>\n <div class=\"grid gap-3 md:grid-cols-2\">\n <mt-select-field\n [ngModel]=\"config()['joinPolicy'] ?? 'All'\"\n (ngModelChange)=\"onConfigFieldChange('joinPolicy', $event)\"\n label=\"Join policy\"\n hint=\"Backend policy used to decide when branch wait is complete.\"\n [options]=\"joinPolicyOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n @if (config()['joinPolicy'] === \"Threshold\") {\n <mt-number-field\n [ngModel]=\"numberValue(config()['threshold'])\"\n (ngModelChange)=\"onConfigFieldChange('threshold', $event)\"\n label=\"Threshold\"\n hint=\"Minimum completed branch count required for Threshold policy.\"\n [min]=\"1\"\n />\n }\n <mt-select-field\n [ngModel]=\"config()['aggregationStrategy'] ?? 'MergeObjects'\"\n (ngModelChange)=\"onConfigFieldChange('aggregationStrategy', $event)\"\n label=\"Aggregation strategy\"\n hint=\"How backend joins branch outputs into the join node output.\"\n [options]=\"aggregationStrategyOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"config()['outputTargetPath'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:outputTargetPath')\"\n (ngModelChange)=\"onConfigFieldChange('outputTargetPath', $event)\"\n label=\"Output target path\"\n hint=\"Optional context path where joined output should be written.\"\n />\n </div>\n </section>\n }\n }\n @case (\"Switch\") {\n @if (sectionInMain(\"switch\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Switch</div>\n <div class=\"grid gap-4\">\n <div class=\"grid items-end gap-3 md:grid-cols-[minmax(0,1fr)_minmax(280px,0.7fr)]\">\n <mt-select-field\n [ngModel]=\"config()['mode'] ?? 'value'\"\n (ngModelChange)=\"onConfigFieldChange('mode', $event)\"\n label=\"Mode\"\n hint=\"Backend evaluation mode: expression, rules, or source-value matching.\"\n [options]=\"switchModeOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-toggle-field\n class=\"self-end pb-1\"\n size=\"small\"\n labelPosition=\"end\"\n [ngModel]=\"config()['firstMatch'] !== false\"\n (ngModelChange)=\"onConfigFieldChange('firstMatch', $event === true)\"\n label=\"First match\"\n hint=\"Use the first matching case unless backend explicitly supports multi-match.\"\n />\n </div>\n @if (showSwitchSourceValue() || showSwitchExpression()) {\n <div class=\"grid gap-3 md:grid-cols-2\">\n @if (showSwitchSourceValue()) {\n <mt-text-field\n [class]=\"showSwitchExpression() ? 'font-mono' : 'font-mono md:col-span-2'\"\n [ngModel]=\"config()['sourceValue'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:sourceValue')\"\n (ngModelChange)=\"onConfigFieldChange('sourceValue', $event)\"\n label=\"Source value\"\n hint=\"Value or expression used for value matching.\"\n />\n }\n @if (showSwitchExpression()) {\n <mt-text-field\n [class]=\"showSwitchSourceValue() ? 'font-mono' : 'font-mono md:col-span-2'\"\n [ngModel]=\"config()['expression'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:expression')\"\n (ngModelChange)=\"onConfigFieldChange('expression', $event)\"\n label=\"Expression\"\n hint=\"Expression evaluated when mode is expression or rules.\"\n />\n }\n </div>\n }\n <div class=\"grid items-end gap-3 md:grid-cols-[minmax(0,1fr)_minmax(280px,0.7fr)]\">\n @if (showSwitchDefaultOutputKey()) {\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"config()['defaultOutputKey'] ?? 'default'\"\n (ngModelChange)=\"onConfigFieldChange('defaultOutputKey', $event)\"\n label=\"Default output key\"\n hint=\"Stable default route key used when no case matches.\"\n />\n }\n <mt-toggle-field\n [class]=\"showSwitchDefaultOutputKey() ? 'self-end pb-1' : 'self-end pb-1 md:col-span-2'\"\n size=\"small\"\n labelPosition=\"end\"\n [ngModel]=\"config()['includeDefaultOutput'] !== false\"\n (ngModelChange)=\"onConfigFieldChange('includeDefaultOutput', $event === true)\"\n label=\"Include default output\"\n hint=\"Expose the default route output key in the canvas.\"\n />\n </div>\n </div>\n </section>\n <section class=\"fp-ae-panel\">\n <div class=\"flex items-center justify-between gap-3 border-b border-surface-200 bg-surface-50 px-4 py-3\">\n <div class=\"fp-ae-section-title !m-0\">Switch cases</div>\n <mt-button size=\"small\" variant=\"outlined\" icon=\"general.plus\" label=\"Add case\" (onClick)=\"addSwitchCase()\" />\n </div>\n <div class=\"space-y-3 p-4\">\n @for (item of switchCases(); track item.key; let i = $index) {\n <div class=\"rounded-md border border-surface-200 bg-surface-0 p-3\">\n <div class=\"grid gap-3 md:grid-cols-[160px_1fr_1fr_auto]\">\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"item.key\"\n (ngModelChange)=\"updateSwitchCase(i, 'key', $event)\"\n label=\"Case key\"\n hint=\"Persisted stable key. The route output becomes case_key.\"\n />\n <mt-text-field\n [ngModel]=\"item.label\"\n (ngModelChange)=\"updateSwitchCase(i, 'label', $event)\"\n label=\"Label\"\n hint=\"Visual case label. Routes stay attached to the case key.\"\n />\n <mt-text-field\n [ngModel]=\"item.value\"\n (focusin)=\"setExpressionTarget('config:cases:' + item.key + ':value')\"\n (ngModelChange)=\"updateSwitchCase(i, 'value', $event)\"\n label=\"Value\"\n hint=\"Expected value for value matching.\"\n />\n <div class=\"flex items-end gap-1\">\n <mt-button size=\"small\" variant=\"outlined\" icon=\"arrow.arrow-up\" tooltip=\"Move up\" (onClick)=\"moveSwitchCase(i, -1)\" />\n <mt-button size=\"small\" variant=\"outlined\" icon=\"arrow.arrow-down\" tooltip=\"Move down\" (onClick)=\"moveSwitchCase(i, 1)\" />\n <mt-button size=\"small\" variant=\"outlined\" severity=\"danger\" icon=\"general.trash-01\" tooltip=\"Remove case\" (onClick)=\"removeSwitchCase(i)\" />\n </div>\n </div>\n <div class=\"mt-3 grid gap-3 md:grid-cols-2\">\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"item.condition\"\n (focusin)=\"setExpressionTarget('config:cases:' + item.key + ':condition')\"\n (ngModelChange)=\"updateSwitchCase(i, 'condition', $event)\"\n label=\"Condition\"\n hint=\"Optional condition for rule matching.\"\n />\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"item.expression\"\n (focusin)=\"setExpressionTarget('config:cases:' + item.key + ':expression')\"\n (ngModelChange)=\"updateSwitchCase(i, 'expression', $event)\"\n label=\"Case expression\"\n hint=\"Optional expression for this case.\"\n />\n </div>\n <div class=\"mt-3 flex flex-wrap items-center gap-1.5 text-[11px]\">\n <span class=\"inline-flex h-7 items-center rounded-md border border-surface-200 bg-surface-50 px-2 font-mono font-semibold text-(--p-text-color)\">{{ item.routeKey }}</span>\n <span class=\"inline-flex h-7 items-center rounded-md border border-surface-200 bg-surface-0 px-2 font-medium text-(--p-text-muted-color)\">{{ item.routeCount }} connected route{{ item.routeCount === 1 ? \"\" : \"s\" }}</span>\n <span class=\"inline-flex h-7 items-center rounded-md border border-surface-200 bg-surface-0 px-2 font-medium text-(--p-text-muted-color)\">Visual order {{ i + 1 }}</span>\n </div>\n </div>\n }\n @if (switchCases().length === 0) {\n <p class=\"fp-ae-copy\">Add at least one stable case key. Backend validation points routes at missing case keys if a case is deleted.</p>\n }\n </div>\n </section>\n }\n }\n @case (\"Stop\") {\n @if (sectionInMain(\"stop\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">End execution</div>\n @if (supportsConfigKey('status') || supportsConfigKey('message') || supportsConfigKey('output')) {\n <div class=\"grid gap-3 md:grid-cols-2\">\n @if (supportsConfigKey('status')) {\n <mt-text-field\n [ngModel]=\"config()['status'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('status', $event)\"\n label=\"Result status\"\n hint=\"Terminal status emitted by the backend stop node.\"\n />\n }\n @if (supportsConfigKey('message')) {\n <mt-text-field\n [ngModel]=\"config()['message'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:message')\"\n (ngModelChange)=\"onConfigFieldChange('message', $event)\"\n label=\"Message\"\n hint=\"Optional message or reason saved with the terminal result.\"\n />\n }\n @if (supportsConfigKey('output')) {\n <mt-textarea-field\n class=\"w-full font-mono md:col-span-2\"\n [ngModel]=\"valueText(config()['output'] ?? '')\"\n (focusin)=\"setExpressionTarget('config:output')\"\n (ngModelChange)=\"onConfigFieldChange('output', $event)\"\n label=\"Output payload\"\n hint=\"Terminal output payload when exposed by the backend schema.\"\n rows=\"5\"\n />\n }\n </div>\n } @else {\n <p class=\"fp-ae-copy\">\n This terminal node has no backend-exposed settings. It ends execution when reached.\n </p>\n }\n </section>\n }\n }\n }\n\n @if (sectionInMain(\"backendSchema\") && step() && editorType() !== \"HumanApproval\") {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Output data</div>\n @if (outputFields().length > 0) {\n <div class=\"grid gap-2 md:grid-cols-2\">\n @for (field of outputFields(); track field.key) {\n <div class=\"fp-ae-kv\">\n <span>{{ field.label }}</span>\n <strong>{{ field.type }}</strong>\n </div>\n }\n </div>\n }\n @if (routeOutputKeys().length > 0) {\n <div class=\"mt-3 fp-ae-label mb-2\">Route outputs</div>\n <div class=\"flex flex-wrap gap-1.5\">\n @for (key of routeOutputKeys(); track key) {\n <span class=\"rounded-md bg-(--p-surface-100) px-2 py-1 font-mono text-[11px]\">{{ key }}</span>\n }\n </div>\n }\n </section>\n }\n\n @if (sectionInAdvanced(\"mapping\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Request and result mapping</div>\n <div class=\"grid gap-3 md:grid-cols-2\">\n <div>\n <div class=\"fp-ae-label mb-2\">Request data</div>\n @for (row of inputMappingRows(); track row.key) {\n <label class=\"mb-2 block space-y-1\">\n <span class=\"text-[11px] text-(--p-text-muted-color)\">{{ row.key }}</span>\n <mt-text-field\n [ngModel]=\"valueText(row.value)\"\n (focusin)=\"setExpressionTarget('config:inputMapping:' + row.key)\"\n (ngModelChange)=\"updateJsonField('inputMappingJson', row.key, $event)\"\n hint=\"Expression or literal value mapped into this node input.\"\n />\n </label>\n }\n </div>\n <div>\n <div class=\"fp-ae-label mb-2\">Output data</div>\n @for (row of outputMappingRows(); track row.key) {\n <label class=\"mb-2 block space-y-1\">\n <span class=\"text-[11px] text-(--p-text-muted-color)\">{{ row.key }}</span>\n <mt-text-field\n [ngModel]=\"valueText(row.value)\"\n (focusin)=\"setExpressionTarget('config:outputMapping:' + row.key)\"\n (ngModelChange)=\"updateJsonField('outputMappingJson', row.key, $event)\"\n hint=\"Expression or literal value mapped from this node output.\"\n />\n </label>\n }\n </div>\n </div>\n @if (\n sectionHasAdvancedEmptyState(\"mapping\") &&\n inputMappingRows().length === 0 &&\n outputMappingRows().length === 0\n ) {\n <p class=\"fp-ae-copy mt-3\">\n This node is using backend default request and output data. Add mappings only when this step needs a custom payload.\n </p>\n }\n </section>\n }\n\n @if (sectionInAdvanced(\"credentials\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Auth and credentials</div>\n <ng-container *ngTemplateOutlet=\"credentialSelector\" />\n </section>\n }\n\n @if (sectionInAdvanced(\"policy\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Retry, timeout, error policy</div>\n <div class=\"grid gap-3 md:grid-cols-3\">\n <mt-number-field\n [ngModel]=\"numberValue(policyObject('timeoutPolicyJson')['timeoutSeconds'])\"\n (ngModelChange)=\"updateJsonField('timeoutPolicyJson', 'timeoutSeconds', $event)\"\n label=\"Timeout seconds\"\n hint=\"Maximum runtime before the backend times out this node.\"\n [min]=\"0\"\n />\n <mt-number-field\n [ngModel]=\"numberValue(policyObject('retryPolicyJson')['maxAttempts'])\"\n (ngModelChange)=\"updateJsonField('retryPolicyJson', 'maxAttempts', $event)\"\n label=\"Max attempts\"\n hint=\"Maximum retry attempts after retryable failures.\"\n [min]=\"0\"\n />\n <mt-select-field\n [ngModel]=\"policyObject('errorPolicyJson')['onFailure'] ?? ''\"\n (ngModelChange)=\"updateJsonField('errorPolicyJson', 'onFailure', $event)\"\n label=\"On failure\"\n hint=\"Backend error policy to apply when this node fails.\"\n [options]=\"errorPolicyOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n </div>\n @if (sectionHasAdvancedEmptyState(\"policy\")) {\n <p class=\"fp-ae-copy mt-3\">\n Backend defaults apply until you override a timeout, retry, or error policy here.\n </p>\n }\n </section>\n }\n\n @if (sectionInAdvanced(\"triggerContext\") && trigger()) {\n <details class=\"fp-ae-panel\" open>\n <summary class=\"fp-ae-section-title cursor-pointer select-none\">\n Trigger payload and context\n </summary>\n <p class=\"fp-ae-copy mt-3\">\n Trigger payload/context is documentation for expressions only. Triggers\n do not edit node input or output mappings here.\n </p>\n <div class=\"grid gap-3 md:grid-cols-2\">\n <details class=\"rounded-lg border border-(--p-content-border-color)\">\n <summary class=\"cursor-pointer px-3 py-2 text-[12px] font-semibold\">Config schema</summary>\n <pre class=\"fp-ae-code\">{{ schemaText(configSchema()) }}</pre>\n </details>\n @if (hasTriggerPayloadSchema() && triggerPayloadSchema(); as schema) {\n <details class=\"rounded-lg border border-(--p-content-border-color)\">\n <summary class=\"cursor-pointer px-3 py-2 text-[12px] font-semibold\">Payload schema</summary>\n <pre class=\"fp-ae-code\">{{ schemaText(schema) }}</pre>\n </details>\n }\n @if (authPolicySchema()) {\n <details class=\"rounded-lg border border-(--p-content-border-color)\">\n <summary class=\"cursor-pointer px-3 py-2 text-[12px] font-semibold\">Auth policy schema</summary>\n <pre class=\"fp-ae-code\">{{ schemaText(authPolicySchema()) }}</pre>\n </details>\n }\n @if (triggerPayloadSample(); as sample) {\n <details class=\"rounded-lg border border-(--p-content-border-color)\">\n <summary class=\"cursor-pointer px-3 py-2 text-[12px] font-semibold\">Payload or request sample</summary>\n <pre class=\"fp-ae-code\">{{ schemaText(sample) }}</pre>\n </details>\n }\n </div>\n </details>\n }\n\n @if (sectionInAdvanced(\"backendSchema\") && step()) {\n <details class=\"fp-ae-panel\" open>\n <summary class=\"fp-ae-section-title cursor-pointer select-none\">\n Backend schemas and outputs\n </summary>\n <div class=\"mt-3 grid gap-3 md:grid-cols-2\">\n <details class=\"rounded-lg border border-(--p-content-border-color)\">\n <summary class=\"cursor-pointer px-3 py-2 text-[12px] font-semibold\">Config schema</summary>\n <pre class=\"fp-ae-code\">{{ schemaText(configSchema()) }}</pre>\n </details>\n <details class=\"rounded-lg border border-(--p-content-border-color)\">\n <summary class=\"cursor-pointer px-3 py-2 text-[12px] font-semibold\">Input schema</summary>\n <pre class=\"fp-ae-code\">{{ schemaText(inputSchema()) }}</pre>\n </details>\n <details class=\"rounded-lg border border-(--p-content-border-color)\">\n <summary class=\"cursor-pointer px-3 py-2 text-[12px] font-semibold\">Output schema</summary>\n <pre class=\"fp-ae-code\">{{ schemaText(outputSchema()) }}</pre>\n </details>\n <div class=\"rounded-lg border border-(--p-content-border-color) p-3\">\n <div class=\"fp-ae-label mb-2\">Route output keys</div>\n <div class=\"flex flex-wrap gap-1.5\">\n @for (key of routeOutputKeys(); track key) {\n <span class=\"rounded-md bg-(--p-surface-100) px-2 py-1 font-mono text-[11px]\">{{ key }}</span>\n }\n @if (routeOutputKeys().length === 0) {\n <span class=\"text-[12px] text-(--p-text-muted-color)\">No outgoing route keys</span>\n }\n </div>\n </div>\n </div>\n </details>\n }\n\n @if (sectionInAdvanced(\"schemaFields\") && configRows().length > 0) {\n <details class=\"fp-ae-panel\" open>\n <summary class=\"fp-ae-section-title cursor-pointer select-none\">\n Backend schema fields\n </summary>\n <div class=\"mt-3 grid gap-3 md:grid-cols-2\">\n @for (field of configRows(); track field.key) {\n <div\n class=\"space-y-1.5\"\n [class.md:col-span-2]=\"field.type === 'object' || field.type === 'array'\"\n >\n @if (field.enumValues.length) {\n <mt-select-field\n [ngModel]=\"fieldValue(field.key)\"\n (ngModelChange)=\"onConfigFieldChange(field.key, $event)\"\n [label]=\"field.label + (field.required ? ' *' : '')\"\n [hint]=\"field.description ?? ''\"\n [options]=\"enumOptions(field.enumValues)\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n } @else if (field.type === \"boolean\") {\n <mt-toggle-field\n size=\"small\"\n labelPosition=\"end\"\n [ngModel]=\"!!fieldValue(field.key)\"\n (ngModelChange)=\"onConfigFieldChange(field.key, $event === true)\"\n [label]=\"field.label + (field.required ? ' *' : '')\"\n [hint]=\"field.description ?? ''\"\n />\n } @else if (field.type === \"number\") {\n <mt-number-field\n [ngModel]=\"numberValue(fieldValue(field.key))\"\n (ngModelChange)=\"onConfigFieldChange(field.key, $event)\"\n [label]=\"field.label + (field.required ? ' *' : '')\"\n [hint]=\"field.description ?? ''\"\n />\n } @else if (field.type === \"date\") {\n <mt-date-field\n [ngModel]=\"fieldValue(field.key)\"\n (ngModelChange)=\"onConfigFieldChange(field.key, $event)\"\n [label]=\"field.label + (field.required ? ' *' : '')\"\n [hint]=\"field.description ?? ''\"\n [showTime]=\"field.format !== 'date'\"\n [showClear]=\"true\"\n [pInputs]=\"dateTimePickerInputs\"\n />\n } @else if (field.type === \"object\" || field.type === \"array\") {\n <mt-textarea-field\n class=\"w-full font-mono\"\n [ngModel]=\"valueText(fieldValue(field.key))\"\n (ngModelChange)=\"onConfigFieldChange(field.key, $event)\"\n [label]=\"field.label + (field.required ? ' *' : '')\"\n [hint]=\"field.description ?? ''\"\n rows=\"5\"\n />\n } @else {\n <mt-text-field\n [ngModel]=\"fieldText(field.key)\"\n (focusin)=\"field.expressionEnabled && setExpressionTarget('config:' + field.key)\"\n (ngModelChange)=\"onConfigFieldChange(field.key, $event)\"\n [label]=\"field.label + (field.required ? ' *' : '')\"\n [hint]=\"field.description ?? ''\"\n />\n }\n </div>\n }\n </div>\n </details>\n }\n </div>\n</div>\n\n<ng-template #credentialSelector>\n @if (credentials().length) {\n <div class=\"space-y-2\">\n @for (credential of credentials(); track credential.credentialRef) {\n <label class=\"flex items-center justify-between gap-3 rounded-lg border border-(--p-content-border-color) bg-(--p-surface-50) px-3 py-2\">\n <span class=\"min-w-0\">\n <span class=\"block truncate text-[12px] font-semibold\">{{ credential.displayName ?? credential.credentialRef }}</span>\n <span class=\"block truncate font-mono text-[11px] text-(--p-text-muted-color)\">\n {{ credential.credentialRef }} / {{ credential.status ?? (credential.resolved ? \"Resolved\" : \"Unresolved\") }}\n </span>\n </span>\n <span class=\"flex items-center gap-2\">\n <mt-button\n size=\"small\"\n variant=\"text\"\n label=\"Test\"\n (onClick)=\"testCredential(credential.credentialRef)\"\n />\n <mt-toggle-field\n size=\"small\"\n [ngModel]=\"credentialRefs().includes(credential.credentialRef)\"\n (ngModelChange)=\"toggleCredential(credential.credentialRef, $event === true)\"\n hint=\"Attach or detach this backend credential reference. Masked secrets are preserved.\"\n />\n </span>\n </label>\n }\n </div>\n } @else if (sectionHasAdvancedEmptyState(\"credentials\")) {\n <p class=\"fp-ae-copy\">\n The backend helper did not return credential choices for this node. Existing masked references remain preserved in the saved JSON.\n </p>\n }\n @if (credentialTest(); as test) {\n <div class=\"mt-3 rounded-lg bg-(--p-surface-100) px-3 py-2 text-[12px]\">\n {{ test.status ?? (test.succeeded ? \"Succeeded\" : \"Placeholder\") }}\n @if (test.message) { <span>- {{ test.message }}</span> }\n </div>\n }\n</ng-template>\n\n<ng-template\n #mapEditor\n let-objectKey=\"objectKey\"\n let-rows=\"rows\"\n let-keyLabel=\"keyLabel\"\n let-valueLabel=\"valueLabel\"\n let-addLabel=\"addLabel\"\n>\n <div class=\"space-y-2\">\n @for (row of rows; track row.key) {\n <div class=\"grid gap-2 md:grid-cols-[180px_1fr_auto]\">\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"row.key\"\n (ngModelChange)=\"updateObjectRow(objectKey, row.key, $event, row.value)\"\n [label]=\"keyLabel\"\n hint=\"Configuration key saved to the backend JSON object.\"\n />\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"valueText(row.value)\"\n (focusin)=\"setExpressionTarget('config:' + objectKey + ':' + row.key)\"\n (ngModelChange)=\"updateObjectRow(objectKey, row.key, row.key, $event)\"\n [label]=\"valueLabel\"\n hint=\"Configuration value. Use expressions when this field should resolve at runtime.\"\n />\n <mt-button\n class=\"self-end\"\n size=\"small\"\n severity=\"secondary\"\n variant=\"outlined\"\n label=\"Remove\"\n (onClick)=\"removeObjectRow(objectKey, row.key)\"\n />\n </div>\n }\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n [label]=\"addLabel || ('Add ' + keyLabel)\"\n (onClick)=\"addObjectRow(objectKey)\"\n />\n </div>\n</ng-template>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: TranslocoModule }, { kind: "component", type: Button, selector: "mt-button", inputs: ["icon", "label", "tooltip", "class", "type", "styleClass", "severity", "badge", "variant", "badgeSeverity", "size", "iconPos", "autofocus", "fluid", "raised", "rounded", "text", "plain", "outlined", "link", "disabled", "loading", "pInputs"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: DateField, selector: "mt-date-field", inputs: ["field", "hint", "label", "placeholder", "class", "readonly", "showIcon", "showClear", "showTime", "pInputs", "required"] }, { kind: "component", type: TextField, selector: "mt-text-field", inputs: ["field", "hint", "label", "placeholder", "class", "type", "readonly", "pInputs", "required", "maxLength", "icon", "iconPosition"] }, { kind: "component", type: TextareaField, selector: "mt-textarea-field", inputs: ["field", "hint", "label", "placeholder", "class", "readonly", "noErrorStyle", "pInputs", "rows", "required", "maxLength"] }, { kind: "component", type: NumberField, selector: "mt-number-field", inputs: ["field", "hint", "label", "placeholder", "class", "readonly", "pInputs", "format", "useGrouping", "maxFractionDigits", "min", "max", "required"] }, { kind: "component", type: SelectField, selector: "mt-select-field", inputs: ["field", "hint", "label", "placeholder", "hasPlaceholderPrefix", "class", "readonly", "pInputs", "options", "optionValue", "optionLabel", "filter", "filterBy", "dataKey", "showClear", "clearAfterSelect", "required", "group", "size", "optionGroupLabel", "optionGroupChildren", "loading", "optionIcon", "optionIconColor", "optionIconShape", "optionAvatarShape", "optionGroupIcon", "optionGroupIconColor", "optionGroupIconShape", "optionGroupAvatarShape", "markCurrentUser"], outputs: ["onChange"] }, { kind: "component", type: ToggleField, selector: "mt-toggle-field", inputs: ["label", "inputId", "labelPosition", "placeholder", "readonly", "pInputs", "required", "toggleShape", "size", "icon", "descriptionCard"], outputs: ["onChange"] }, { kind: "directive", type: DropDataDirective, selector: "[fpDropData]", inputs: ["fpDropAutoInsert"], outputs: ["dataDrop"] }] });
13811
14262
  }
13812
14263
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: AutomationSmartEditorComponent, decorators: [{
13813
14264
  type: Component,
@@ -13823,7 +14274,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
13823
14274
  SelectField,
13824
14275
  ToggleField,
13825
14276
  ...DATA_PILL_DRAG,
13826
- ], host: { class: 'block h-full min-h-0' }, template: "<div\n class=\"fp-scroll flex h-full min-h-0 flex-col overflow-y-auto\"\n fpDropData\n [fpDropAutoInsert]=\"false\"\n (dataDrop)=\"insertExpression($event)\"\n>\n <div class=\"space-y-4 px-5 py-5\">\n @if (helperError()) {\n <div\n class=\"rounded-lg border border-[rgb(var(--fp-warning))]/30 bg-[rgb(var(--fp-warning))]/10 px-3 py-2 text-[12px] leading-5 text-(--p-text-color)\"\n >\n {{ helperError() }}\n </div>\n }\n\n @if (sectionInMain(\"startConnection\") && trigger()) {\n <section\n class=\"flex flex-col gap-0 overflow-hidden rounded-md border border-surface-200 bg-surface-0\"\n >\n <h3\n class=\"m-0 border-b border-surface-200 bg-surface-50 px-4 py-3 text-lg font-semibold text-color\"\n >\n Start connection\n </h3>\n <div class=\"space-y-3 p-4\">\n @if (startConnection().key) {\n @if (startConnection().step) {\n <div class=\"flex flex-wrap items-start justify-between gap-3\">\n <div class=\"min-w-0 space-y-1\">\n <div class=\"text-[12px] font-semibold text-(--p-text-muted-color)\">\n First node connected\n </div>\n <div class=\"truncate text-[14px] font-semibold text-(--p-text-color)\">\n {{ startConnection().label }}\n </div>\n <div class=\"flex flex-wrap items-center gap-2 text-[12px] text-(--p-text-muted-color)\">\n <span>Managed on canvas</span>\n <span\n class=\"rounded-md bg-(--p-surface-100) px-2 py-1 font-mono text-[11px]\"\n >\n {{ startConnection().key }}\n </span>\n </div>\n </div>\n <div class=\"flex shrink-0 flex-wrap gap-2\">\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n severity=\"secondary\"\n label=\"Focus connected node\"\n (onClick)=\"focusStartConnection()\"\n />\n </div>\n </div>\n } @else {\n <div class=\"space-y-2\">\n <div class=\"text-[14px] font-semibold text-(--p-text-color)\">\n No first node connected\n </div>\n <p class=\"m-0 text-[12px] leading-5 text-(--p-text-muted-color)\">\n The saved start connection points to a node key that is not on\n the canvas. Connect this trigger to the first node on the\n canvas.\n </p>\n <div class=\"flex flex-wrap items-center gap-2 text-[12px] text-(--p-text-muted-color)\">\n <span>Technical key</span>\n <span\n class=\"rounded-md bg-(--p-surface-100) px-2 py-1 font-mono text-[11px]\"\n >\n {{ startConnection().key }}\n </span>\n <span>Managed on canvas</span>\n </div>\n </div>\n }\n } @else {\n <div class=\"rounded-lg border border-dashed border-(--p-content-border-color) bg-(--p-surface-50) p-3\">\n <div class=\"text-[14px] font-semibold text-(--p-text-color)\">\n No first node connected\n </div>\n <p class=\"m-0 mt-1 text-[12px] leading-5 text-(--p-text-muted-color)\">\n Connect this trigger to the first node on the canvas.\n </p>\n </div>\n }\n </div>\n </section>\n }\n\n @switch (editorType()) {\n @case (\"ManualTrigger\") {\n @if (sectionInMain(\"manualTrigger\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Manual run input</div>\n @if (hasTriggerPayloadSchema() && triggerPayloadSchema(); as schema) {\n <div class=\"rounded-md border border-surface-200 bg-surface-0 p-3\">\n <div class=\"fp-ae-label mb-2\">Payload schema</div>\n <pre class=\"fp-ae-code\">{{ schemaText(schema) }}</pre>\n </div>\n }\n @if (triggerPayloadSample(); as sample) {\n <div class=\"mt-3 rounded-md border border-surface-200 bg-surface-0 p-3\">\n <div class=\"fp-ae-label mb-2\">Sample payload</div>\n <pre class=\"fp-ae-code\">{{ schemaText(sample) }}</pre>\n </div>\n }\n @if (!hasTriggerPayloadSchema() && !triggerPayloadSample()) {\n <p class=\"fp-ae-copy\">\n This manual trigger has no backend-provided input schema. It can still be connected to the first step on the canvas.\n </p>\n }\n </section>\n }\n }\n @case (\"WebhookTrigger\") {\n @if (sectionInMain(\"webhookSetup\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Webhook setup</div>\n @if (webhookSetup(); as setup) {\n <div class=\"flex gap-2\">\n <mt-text-field\n class=\"flex-1 font-mono\"\n [ngModel]=\"setup.webhookUrl ?? ''\"\n [readonly]=\"true\"\n label=\"Webhook URL\"\n hint=\"Backend-generated endpoint clients should call to start this trigger.\"\n />\n <mt-button class=\"self-end\" size=\"small\" variant=\"outlined\" label=\"Copy\" (onClick)=\"copyWebhookUrl()\" />\n </div>\n <div class=\"grid gap-2 md:grid-cols-2\">\n <div class=\"fp-ae-kv\">\n <span>Auth mode</span>\n <strong>{{ setup.authMode ?? \"Backend default\" }}</strong>\n </div>\n <div class=\"fp-ae-kv\">\n <span>Required headers</span>\n <strong>{{ (setup.requiredHeaders ?? []).join(\", \") || \"-\" }}</strong>\n </div>\n </div>\n @if (setup.hmacSigning) {\n <p class=\"fp-ae-copy\">{{ setup.hmacSigning }}</p>\n }\n @if (setup.sampleRequest) {\n <details class=\"mt-3 rounded-lg border border-(--p-content-border-color)\">\n <summary class=\"cursor-pointer px-3 py-2 text-[12px] font-semibold\">\n Sample request\n </summary>\n <pre class=\"fp-ae-code\">{{ schemaText(setup.sampleRequest) }}</pre>\n </details>\n }\n } @else {\n <p class=\"fp-ae-copy\">\n Webhook setup is not available for this draft yet.\n </p>\n }\n </section>\n }\n @if (sectionInMain(\"authPolicy\") && hasAuthPolicy()) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Authentication policy</div>\n <div class=\"grid gap-3 md:grid-cols-3\">\n <mt-select-field\n [ngModel]=\"authPolicy()['mode'] ?? ''\"\n (ngModelChange)=\"onAuthFieldChange('mode', $event)\"\n label=\"Mode\"\n hint=\"Authentication mode required by the backend webhook policy.\"\n [options]=\"authModeOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-text-field\n [ngModel]=\"authPolicy()['signatureHeaderName'] ?? ''\"\n (ngModelChange)=\"onAuthFieldChange('signatureHeaderName', $event)\"\n label=\"Signature header\"\n hint=\"Header name that carries the webhook signature when the policy requires signed requests.\"\n />\n <mt-text-field\n [ngModel]=\"authPolicy()['timestampHeaderName'] ?? ''\"\n (ngModelChange)=\"onAuthFieldChange('timestampHeaderName', $event)\"\n label=\"Timestamp header\"\n hint=\"Header name that carries the request timestamp for replay protection.\"\n />\n </div>\n </section>\n }\n }\n @case (\"FormSubmitTrigger\") {\n @if (sectionInMain(\"formBinding\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Form binding</div>\n <div class=\"grid gap-3 md:grid-cols-2\">\n <mt-select-field\n [ngModel]=\"selectedFormId() || formBinding()?.formId || ''\"\n (ngModelChange)=\"onFormChange($event)\"\n label=\"Form\"\n hint=\"Choose a backend form that will submit data into this trigger.\"\n [options]=\"formOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-select-field\n [ngModel]=\"selectedFormVersionId() || formBinding()?.formVersionId || ''\"\n (ngModelChange)=\"onFormVersionChange($event)\"\n label=\"Form version\"\n hint=\"Persist the exact backend formVersionId. Do not type or generate IDs manually.\"\n [options]=\"formVersionOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n </div>\n <div class=\"mt-3 flex flex-wrap items-center gap-2\">\n <mt-button size=\"small\" severity=\"primary\" label=\"Save binding\" (onClick)=\"saveFormBinding()\" />\n @if (formBinding()) {\n <span class=\"rounded-lg bg-(--p-surface-100) px-2 py-1 font-mono text-[11px] text-(--p-text-muted-color)\">\n {{ formBinding()!.formVersionId }}\n </span>\n }\n </div>\n @if (formSchema(); as schema) {\n <details class=\"mt-3 rounded-lg border border-(--p-content-border-color)\">\n <summary class=\"cursor-pointer px-3 py-2 text-[12px] font-semibold\">\n Form schema preview\n </summary>\n <pre class=\"fp-ae-code\">{{ schemaText(schema.schema) }}</pre>\n </details>\n }\n </section>\n }\n }\n @case (\"ScheduleTrigger\") {\n @if (sectionInMain(\"schedule\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Schedule</div>\n <div class=\"grid gap-3 md:grid-cols-2\">\n <mt-select-field\n [ngModel]=\"config()['mode'] ?? 'cron'\"\n (ngModelChange)=\"onConfigFieldChange('mode', $event)\"\n label=\"Mode\"\n hint=\"Pick one schedule mode: cron, interval, or once. Backend validation requires exactly one mode.\"\n [options]=\"scheduleModeOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-select-field\n [ngModel]=\"config()['timezone'] ?? 'UTC'\"\n (ngModelChange)=\"onConfigFieldChange('timezone', $event)\"\n label=\"Timezone\"\n hint=\"Timezone used to calculate the next fire time.\"\n [options]=\"timeZoneOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"config()['cron'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('cron', $event)\"\n label=\"Cron\"\n hint=\"Cron expression used only when mode is cron, for example 0 9 * * *.\"\n />\n <mt-number-field\n [ngModel]=\"numberValue(config()['intervalSeconds'])\"\n (ngModelChange)=\"onConfigFieldChange('intervalSeconds', $event)\"\n label=\"Interval seconds\"\n hint=\"Repeat interval in seconds used only when mode is interval.\"\n [min]=\"0\"\n />\n <mt-date-field\n [ngModel]=\"config()['startDate'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('startDate', $event)\"\n label=\"Start date UTC\"\n hint=\"UTC date/time used for once schedules or as the first allowed run time.\"\n [showTime]=\"true\"\n [showClear]=\"true\"\n [pInputs]=\"dateTimePickerInputs\"\n />\n <mt-select-field\n [ngModel]=\"config()['misfirePolicy'] ?? 'SkipMissed'\"\n (ngModelChange)=\"onConfigFieldChange('misfirePolicy', $event)\"\n label=\"Misfire policy\"\n hint=\"Backend behavior when a scheduled run is missed while the automation is unavailable.\"\n [options]=\"misfirePolicyOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n </div>\n <div class=\"mt-3 flex gap-2\">\n <mt-button size=\"small\" variant=\"outlined\" label=\"Validate\" (onClick)=\"validateSchedule()\" />\n <mt-button size=\"small\" severity=\"primary\" label=\"Preview next fire\" (onClick)=\"previewSchedule()\" />\n </div>\n @if (schedulePreview(); as preview) {\n <div class=\"mt-3 rounded-lg bg-(--p-surface-100) px-3 py-2 text-[12px]\">\n <strong class=\"text-emerald-600\">Valid</strong>\n <span class=\"ms-2\">Next fire: {{ preview.nextFireAtUtc ?? \"-\" }}</span>\n </div>\n }\n </section>\n }\n }\n @case (\"SetFields\") {\n @if (sectionInMain(\"setFields\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Set fields</div>\n <ng-container *ngTemplateOutlet=\"mapEditor; context: { objectKey: 'fields', rows: fieldsRows(), keyLabel: 'Field', valueLabel: 'Value', addLabel: 'Add field' }\" />\n </section>\n }\n }\n @case (\"If\") {\n @if (sectionInMain(\"condition\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Condition</div>\n <div class=\"grid gap-3 md:grid-cols-[1fr_180px_1fr]\">\n <mt-text-field\n [ngModel]=\"fieldText('left')\"\n (focusin)=\"setExpressionTarget('config:left')\"\n (ngModelChange)=\"onConfigFieldChange('left', $event)\"\n label=\"Left\"\n hint=\"Left-side value or expression to compare.\"\n />\n <mt-select-field\n [ngModel]=\"config()['operator'] ?? 'equals'\"\n (ngModelChange)=\"onConfigFieldChange('operator', $event)\"\n label=\"Operator\"\n hint=\"Comparison operator used by the backend condition evaluator.\"\n [options]=\"ifOperatorOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-text-field\n [ngModel]=\"fieldText('right')\"\n (focusin)=\"setExpressionTarget('config:right')\"\n (ngModelChange)=\"onConfigFieldChange('right', $event)\"\n label=\"Right\"\n hint=\"Right-side value or expression to compare against.\"\n />\n </div>\n <div class=\"mt-3 rounded-md border border-surface-200 bg-surface-0 p-3\">\n <div class=\"fp-ae-label mb-2\">Route outputs</div>\n <div class=\"flex flex-wrap gap-1.5\">\n @for (key of routeOutputKeys(); track key) {\n <span class=\"rounded-md bg-(--p-surface-100) px-2 py-1 font-mono text-[11px]\">{{ key }}</span>\n }\n </div>\n </div>\n </section>\n }\n }\n @case (\"HTTP\") {\n @if (sectionInMain(\"httpRequest\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">HTTP request</div>\n <div class=\"grid gap-3 md:grid-cols-[150px_1fr]\">\n <mt-select-field\n [ngModel]=\"config()['method'] ?? 'GET'\"\n (ngModelChange)=\"onConfigFieldChange('method', $event)\"\n label=\"Method\"\n hint=\"HTTP method used for the outbound request.\"\n [options]=\"httpMethodOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"config()['url'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:url')\"\n (ngModelChange)=\"onConfigFieldChange('url', $event)\"\n label=\"URL\"\n hint=\"Target URL. Expressions are supported for dynamic hosts, paths, and query values.\"\n />\n </div>\n <div class=\"mt-3\">\n <ng-container *ngTemplateOutlet=\"mapEditor; context: { objectKey: 'headers', rows: headerRows(), keyLabel: 'Header', valueLabel: 'Value', addLabel: 'Add header' }\" />\n </div>\n <div class=\"mt-3\">\n <ng-container *ngTemplateOutlet=\"mapEditor; context: { objectKey: 'query', rows: queryRows(), keyLabel: 'Query param', valueLabel: 'Value', addLabel: 'Add query param' }\" />\n </div>\n @if (supportsConfigKey('bodyMode')) {\n <mt-select-field\n class=\"mt-3\"\n [ngModel]=\"config()['bodyMode'] ?? 'json'\"\n (ngModelChange)=\"onConfigFieldChange('bodyMode', $event)\"\n label=\"Body mode\"\n hint=\"Backend-supported request body serialization mode.\"\n [options]=\"httpBodyModeOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n }\n <mt-textarea-field\n class=\"mt-3 w-full font-mono\"\n [ngModel]=\"valueText(config()['body'] ?? '')\"\n (focusin)=\"setExpressionTarget('config:body')\"\n (ngModelChange)=\"onConfigFieldChange('body', $event)\"\n label=\"Body\"\n hint=\"Request body sent by the HTTP node. Use JSON or expressions when the backend schema allows it.\"\n rows=\"6\"\n />\n @if (supportsConfigKey('timeoutSeconds') || supportsConfigKey('responseHandling')) {\n <div class=\"mt-3 grid gap-3 md:grid-cols-2\">\n @if (supportsConfigKey('timeoutSeconds')) {\n <mt-number-field\n [ngModel]=\"numberValue(config()['timeoutSeconds'])\"\n (ngModelChange)=\"onConfigFieldChange('timeoutSeconds', $event)\"\n label=\"Timeout seconds\"\n hint=\"Request timeout when exposed by the backend schema.\"\n [min]=\"0\"\n />\n }\n @if (supportsConfigKey('responseHandling')) {\n <mt-text-field\n [ngModel]=\"config()['responseHandling'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('responseHandling', $event)\"\n label=\"Response handling\"\n hint=\"Backend-supported response handling mode or expression.\"\n />\n }\n </div>\n }\n @if (sectionInMain(\"credentials\")) {\n <div class=\"mt-3 rounded-md border border-surface-200 bg-surface-0 p-3\">\n <div class=\"fp-ae-label mb-2\">Credential</div>\n <ng-container *ngTemplateOutlet=\"credentialSelector\" />\n </div>\n }\n </section>\n }\n }\n @case (\"Wait\") {\n @if (sectionInMain(\"wait\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Wait</div>\n <div class=\"grid gap-3 md:grid-cols-3\">\n <mt-select-field\n [ngModel]=\"config()['mode'] ?? 'duration'\"\n (ngModelChange)=\"onConfigFieldChange('mode', $event)\"\n label=\"Mode\"\n hint=\"Choose whether this wait uses a duration or a specific date/time.\"\n [options]=\"waitModeOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-number-field\n [ngModel]=\"numberValue(config()['durationSeconds'])\"\n (ngModelChange)=\"onConfigFieldChange('durationSeconds', $event)\"\n label=\"Duration seconds\"\n hint=\"How long execution should wait when mode is duration.\"\n [min]=\"0\"\n />\n <mt-date-field\n [ngModel]=\"config()['waitUntil'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('waitUntil', $event)\"\n label=\"Wait until\"\n hint=\"Date/time that resolves to when execution should resume.\"\n [showTime]=\"true\"\n [showClear]=\"true\"\n [pInputs]=\"dateTimePickerInputs\"\n />\n </div>\n @if (supportsConfigKey('resumePayloadSchema') || supportsConfigKey('resumePayload')) {\n <mt-textarea-field\n class=\"mt-3 w-full font-mono\"\n [ngModel]=\"valueText(config()['resumePayloadSchema'] ?? config()['resumePayload'] ?? '')\"\n (focusin)=\"setExpressionTarget('config:resumePayload')\"\n (ngModelChange)=\"onConfigFieldChange('resumePayload', $event)\"\n label=\"Resume payload\"\n hint=\"Expected payload when the backend supports manual or external resume data.\"\n rows=\"5\"\n />\n }\n </section>\n }\n }\n @case (\"HumanApproval\") {\n @if (sectionInMain(\"approvalTask\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Approval task</div>\n @if (assignmentOptions()?.providerStatus && assignmentOptions()?.providerStatus !== \"Available\") {\n <p class=\"fp-ae-copy\">\n Assignment provider status: {{ assignmentOptions()?.providerStatus }}.\n The backend provider is the source of truth for available assignees.\n </p>\n }\n <div class=\"grid gap-3 xl:grid-cols-3\">\n <mt-text-field\n [ngModel]=\"config()['title'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:title')\"\n (ngModelChange)=\"onConfigFieldChange('title', $event)\"\n label=\"Approval title\"\n hint=\"Approval title shown to the assigned human approver.\"\n />\n <mt-select-field\n [ngModel]=\"selectedAssignmentKey()\"\n (ngModelChange)=\"onAssignmentOptionChange($event)\"\n label=\"Assignment\"\n hint=\"Backend-provided assignee, role, or group that can decide this approval.\"\n [options]=\"assignmentSelectOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n @if (humanApprovalSupportsConfig('priority')) {\n <mt-text-field\n [ngModel]=\"config()['priority'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('priority', $event)\"\n label=\"Priority\"\n hint=\"Approval priority when supported by the backend schema.\"\n />\n }\n </div>\n <mt-textarea-field\n class=\"mt-3 w-full\"\n [ngModel]=\"config()['message'] ?? config()['instructions'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:message')\"\n (ngModelChange)=\"onConfigFieldChange('message', $event)\"\n label=\"Approval message\"\n hint=\"Decision instructions shown to the approver.\"\n rows=\"4\"\n />\n @if (humanApprovalSupportsConfig('dueDate') || humanApprovalSupportsConfig('dueInSeconds') || humanApprovalSupportsConfig('timeoutSeconds') || humanApprovalSupportsConfig('expiresAt') || humanApprovalSupportsConfig('commentsRequired') || humanApprovalSupportsConfig('allowReturn')) {\n <div class=\"mt-3 grid gap-3 xl:grid-cols-4\">\n @if (humanApprovalSupportsConfig('dueDate')) {\n <mt-date-field\n [ngModel]=\"config()['dueDate'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('dueDate', $event)\"\n label=\"Due date\"\n hint=\"Backend-supported approval due date.\"\n [showTime]=\"true\"\n [showClear]=\"true\"\n [pInputs]=\"dateTimePickerInputs\"\n />\n }\n @if (humanApprovalSupportsConfig('dueInSeconds')) {\n <mt-number-field\n [ngModel]=\"numberValue(config()['dueInSeconds'])\"\n (ngModelChange)=\"onConfigFieldChange('dueInSeconds', $event)\"\n label=\"Due in seconds\"\n hint=\"Relative approval due duration when supported by the backend schema.\"\n [min]=\"0\"\n />\n }\n @if (humanApprovalSupportsConfig('timeoutSeconds')) {\n <mt-number-field\n [ngModel]=\"numberValue(config()['timeoutSeconds'])\"\n (ngModelChange)=\"onConfigFieldChange('timeoutSeconds', $event)\"\n label=\"Timeout seconds\"\n hint=\"Approval timeout emitted according to backend approval runtime support.\"\n [min]=\"0\"\n />\n }\n @if (humanApprovalSupportsConfig('expiresAt')) {\n <mt-date-field\n [ngModel]=\"config()['expiresAt'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('expiresAt', $event)\"\n label=\"Expires at\"\n hint=\"Backend-supported approval expiry timestamp.\"\n [showTime]=\"true\"\n [showClear]=\"true\"\n [pInputs]=\"dateTimePickerInputs\"\n />\n }\n @if (humanApprovalSupportsConfig('commentsRequired')) {\n <div class=\"flex min-h-[4.25rem] items-end rounded-md border border-surface-200 bg-surface-0 px-3 py-2\">\n <mt-toggle-field\n size=\"small\"\n label=\"Comments required\"\n labelPosition=\"end\"\n [ngModel]=\"config()['commentsRequired'] === true\"\n (ngModelChange)=\"onConfigFieldChange('commentsRequired', $event === true)\"\n hint=\"Require approver comments when the backend supports this flag.\"\n />\n </div>\n }\n @if (humanApprovalSupportsConfig('allowReturn')) {\n <div class=\"flex min-h-[4.25rem] items-end rounded-md border border-surface-200 bg-surface-0 px-3 py-2\">\n <mt-toggle-field\n size=\"small\"\n label=\"Allow return for changes\"\n labelPosition=\"end\"\n [ngModel]=\"config()['allowReturn'] !== false\"\n (ngModelChange)=\"onConfigFieldChange('allowReturn', $event === true)\"\n hint=\"Allow ReturnForChanges when the backend schema exposes this option.\"\n />\n </div>\n }\n </div>\n }\n <div class=\"mt-3 space-y-3\">\n <div class=\"fp-ae-section-title\">Decision options</div>\n <div class=\"overflow-hidden rounded-md border border-surface-200 bg-surface-0\">\n @if (selectedApprovalDecisionRows().length > 0) {\n <div class=\"hidden border-b border-surface-200 bg-surface-50 px-3 py-2 text-[12px] font-semibold text-(--p-text-muted-color) xl:grid xl:grid-cols-[1.1fr_1fr_1.2fr_80px_44px] xl:gap-3\">\n <span>Decision label</span>\n <span>Canonical value</span>\n <span>Route output key</span>\n <span>Routes</span>\n <span>Action</span>\n </div>\n @for (decision of selectedApprovalDecisionRows(); track decision.value) {\n <div class=\"grid gap-2 border-b border-surface-100 px-3 py-3 last:border-b-0 xl:grid-cols-[1.1fr_1fr_1.2fr_80px_44px] xl:items-center xl:gap-3\">\n <div class=\"min-w-0\">\n <div class=\"text-[11px] font-semibold text-(--p-text-muted-color) xl:hidden\">Decision label</div>\n <div class=\"truncate text-[13px] font-semibold text-(--p-text-color)\">{{ decision.label }}</div>\n </div>\n <div class=\"min-w-0\">\n <div class=\"text-[11px] font-semibold text-(--p-text-muted-color) xl:hidden\">Canonical value</div>\n <div class=\"truncate font-mono text-[12px] text-(--p-text-color)\">{{ decision.value }}</div>\n </div>\n <div class=\"min-w-0\">\n <div class=\"text-[11px] font-semibold text-(--p-text-muted-color) xl:hidden\">Route output key</div>\n <div class=\"truncate font-mono text-[12px] text-(--p-text-muted-color)\">{{ decision.routeOutputKey }}</div>\n </div>\n <div>\n <div class=\"text-[11px] font-semibold text-(--p-text-muted-color) xl:hidden\">Routes</div>\n <div class=\"text-[13px] text-(--p-text-color)\">{{ decision.routeCount }}</div>\n </div>\n <mt-button\n class=\"justify-self-start xl:justify-self-end\"\n size=\"small\"\n variant=\"outlined\"\n severity=\"danger\"\n icon=\"general.trash-01\"\n tooltip=\"Remove decision option\"\n (onClick)=\"removeApprovalDecision(decision.value)\"\n />\n </div>\n }\n } @else {\n <div class=\"px-3 py-4 text-[12px] text-(--p-text-muted-color)\">\n Add at least one backend-supported approval decision.\n </div>\n }\n </div>\n @if (approvalDecisionIssues().length > 0) {\n <div class=\"rounded-lg border border-[rgb(var(--fp-error))]/30 bg-[rgb(var(--fp-error))]/10 px-3 py-2 text-[12px] leading-5 text-[rgb(var(--fp-error))]\">\n @for (issue of approvalDecisionIssues(); track issue) {\n <div>{{ issue }}</div>\n }\n </div>\n }\n <div class=\"grid gap-2 md:grid-cols-[minmax(0,1fr)_auto]\">\n <mt-select-field\n [ngModel]=\"approvalDecisionToAdd()\"\n (ngModelChange)=\"approvalDecisionToAdd.set($event)\"\n label=\"Decision to add\"\n hint=\"Only backend-supported decisions with matching route outputs are available.\"\n [options]=\"addableApprovalDecisionOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-button\n class=\"self-end\"\n size=\"small\"\n variant=\"outlined\"\n icon=\"general.plus\"\n label=\"Add decision option\"\n [disabled]=\"!approvalDecisionToAdd()\"\n (onClick)=\"addApprovalDecision()\"\n />\n </div>\n </div>\n @if (humanApprovalSupportsConfig('payload') || humanApprovalSupportsConfig('context') || humanApprovalSupportsConfig('metadata')) {\n <div class=\"mt-3 grid gap-3\">\n @if (humanApprovalSupportsConfig('payload')) {\n <mt-textarea-field\n class=\"w-full font-mono\"\n [ngModel]=\"valueText(config()['payload'] ?? '')\"\n (focusin)=\"setExpressionTarget('config:payload')\"\n (ngModelChange)=\"onConfigFieldChange('payload', $event)\"\n label=\"Payload\"\n hint=\"Approval task payload or context fields supported by backend schema.\"\n rows=\"5\"\n />\n }\n @if (humanApprovalSupportsConfig('context')) {\n <mt-textarea-field\n class=\"w-full font-mono\"\n [ngModel]=\"valueText(config()['context'] ?? '')\"\n (focusin)=\"setExpressionTarget('config:context')\"\n (ngModelChange)=\"onConfigFieldChange('context', $event)\"\n label=\"Context\"\n hint=\"Additional approval context supported by backend schema.\"\n rows=\"5\"\n />\n }\n @if (humanApprovalSupportsConfig('metadata')) {\n <mt-textarea-field\n class=\"w-full font-mono\"\n [ngModel]=\"valueText(config()['metadata'] ?? '')\"\n (focusin)=\"setExpressionTarget('config:metadata')\"\n (ngModelChange)=\"onConfigFieldChange('metadata', $event)\"\n label=\"Metadata\"\n hint=\"Additional approval metadata supported by the backend schema.\"\n rows=\"4\"\n />\n }\n </div>\n }\n <div class=\"mt-3 flex gap-2\">\n <mt-button size=\"small\" variant=\"outlined\" label=\"Validate assignment\" (onClick)=\"validateAssignment()\" />\n </div>\n @if (assignmentValidation(); as result) {\n <div class=\"mt-3 rounded-lg bg-(--p-surface-100) px-3 py-2 text-[12px]\">\n {{ result.isValid === false ? \"Assignment invalid\" : \"Assignment accepted by helper\" }}\n @for (issue of resultIssues(result); track issue) {\n <div class=\"mt-1 text-[11px] text-(--p-text-muted-color)\">{{ issue }}</div>\n }\n </div>\n }\n @if (sectionInMain(\"approvalOutput\")) {\n <div class=\"mt-3 rounded-md border border-surface-200 bg-surface-0 p-3\">\n <div class=\"fp-ae-label mb-2\">Output data</div>\n <div class=\"flex flex-wrap gap-1.5\">\n @for (field of approvalOutputFieldLabels; track field) {\n <span class=\"rounded-md bg-(--p-surface-100) px-2 py-1 font-mono text-[11px]\">{{ field }}</span>\n }\n </div>\n <div class=\"mt-3 fp-ae-label mb-2\">Approval routes</div>\n <div class=\"flex flex-wrap gap-1.5\">\n @for (key of routeOutputKeys(); track key) {\n <span class=\"rounded-md bg-(--p-surface-100) px-2 py-1 font-mono text-[11px]\">{{ key }}</span>\n }\n </div>\n </div>\n }\n </section>\n }\n }\n @case (\"FlowPlusCommit\") {\n @if (sectionInMain(\"flowplusCommit\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">FlowPlus commit</div>\n <p class=\"fp-ae-copy\">\n This node is the explicit module-data write boundary. Approval does\n not commit data unless this node is reached.\n </p>\n <div class=\"grid gap-3 md:grid-cols-2\">\n <mt-select-field\n [ngModel]=\"config()['targetModule'] ?? ''\"\n (ngModelChange)=\"onModuleChange($event)\"\n label=\"Module\"\n hint=\"Module whose records will be written by this explicit FlowPlus commit.\"\n [options]=\"moduleOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-select-field\n [ngModel]=\"config()['operation'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('operation', $event)\"\n label=\"Operation\"\n hint=\"Write operation supported by the selected backend module schema.\"\n [options]=\"operationOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n </div>\n <mt-text-field\n class=\"mt-3 font-mono\"\n [ngModel]=\"config()['idempotencyKey'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:idempotencyKey')\"\n (ngModelChange)=\"onConfigFieldChange('idempotencyKey', $event)\"\n label=\"Idempotency key\"\n hint=\"Optional stable key used by the backend to prevent duplicate writes.\"\n />\n @if (selectedModuleFields().length) {\n <div class=\"mt-3 space-y-2\">\n <div class=\"fp-ae-section-title\">Module schema</div>\n <div class=\"grid gap-2 md:grid-cols-2\">\n @for (field of selectedModuleFields(); track field.key) {\n <div class=\"fp-ae-kv\">\n <span>{{ field.displayName ?? field.key }}</span>\n <strong>{{ field.viewType ?? \"Value\" }} @if (field.required) { * }</strong>\n </div>\n }\n </div>\n </div>\n }\n <div class=\"mt-3\">\n <ng-container *ngTemplateOutlet=\"mapEditor; context: { objectKey: 'mapping', rows: mappingRows(), keyLabel: 'Module field', valueLabel: 'Expression / value', addLabel: 'Add module field' }\" />\n </div>\n <div class=\"mt-3 flex gap-2\">\n <mt-button size=\"small\" severity=\"primary\" label=\"Validate mapping\" (onClick)=\"validateCommitMapping()\" />\n </div>\n @if (commitValidation(); as result) {\n <div class=\"mt-3 rounded-lg bg-(--p-surface-100) px-3 py-2 text-[12px]\">\n {{ result.isValid === false ? \"Mapping invalid\" : \"Mapping accepted by helper\" }}\n @for (issue of resultIssues(result); track issue) {\n <div class=\"mt-1 text-[11px] text-(--p-text-muted-color)\">{{ issue }}</div>\n }\n </div>\n }\n @if (sectionInMain(\"credentials\")) {\n <div class=\"mt-3 rounded-md border border-surface-200 bg-surface-0 p-3\">\n <div class=\"fp-ae-label mb-2\">Credential</div>\n <ng-container *ngTemplateOutlet=\"credentialSelector\" />\n </div>\n }\n </section>\n }\n }\n @case (\"WebhookResponse\") {\n @if (sectionInMain(\"webhookResponse\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Webhook response</div>\n <div class=\"grid gap-3 md:grid-cols-2\">\n <mt-number-field\n [ngModel]=\"numberValue(config()['statusCode']) ?? 200\"\n (ngModelChange)=\"onConfigFieldChange('statusCode', $event)\"\n label=\"Status code\"\n hint=\"HTTP status code sent back by the webhook response node.\"\n [min]=\"100\"\n [max]=\"599\"\n />\n <mt-select-field\n [ngModel]=\"config()['responseMode'] ?? 'json'\"\n (ngModelChange)=\"onConfigFieldChange('responseMode', $event)\"\n label=\"Response mode\"\n hint=\"How the webhook response body should be serialized.\"\n [options]=\"responseModeOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n </div>\n <div class=\"mt-3\">\n <ng-container *ngTemplateOutlet=\"mapEditor; context: { objectKey: 'headers', rows: headerRows(), keyLabel: 'Header', valueLabel: 'Value', addLabel: 'Add header' }\" />\n </div>\n <mt-textarea-field\n class=\"mt-3 w-full font-mono\"\n [ngModel]=\"valueText(config()['body'] ?? '')\"\n (focusin)=\"setExpressionTarget('config:body')\"\n (ngModelChange)=\"onConfigFieldChange('body', $event)\"\n label=\"Body\"\n hint=\"Body returned to the webhook caller. Expressions can use current execution data.\"\n rows=\"6\"\n />\n </section>\n }\n }\n @case (\"CallAutomation\") {\n @if (sectionInMain(\"callAutomation\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Subworkflow</div>\n <div class=\"grid gap-3 md:grid-cols-2\">\n <mt-select-field\n [ngModel]=\"config()['targetAutomationId'] ?? ''\"\n (ngModelChange)=\"onSubworkflowTargetChange($event)\"\n label=\"Target automation\"\n hint=\"Backend-provided automation candidate. Tenant and recursion validation stay on the backend.\"\n [options]=\"subworkflowAutomationOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-select-field\n [ngModel]=\"config()['revisionMode'] ?? 'Active'\"\n (ngModelChange)=\"onConfigFieldChange('revisionMode', $event)\"\n label=\"Revision mode\"\n hint=\"Choose which published or active child revision to call.\"\n [options]=\"revisionModeOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n @if (config()['revisionMode'] === \"SpecificRevision\") {\n <mt-text-field\n [ngModel]=\"config()['specificRevisionId'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('specificRevisionId', $event)\"\n label=\"Specific revision id\"\n hint=\"Required only when revision mode is Specific revision.\"\n />\n }\n <mt-toggle-field\n size=\"small\"\n labelPosition=\"end\"\n [ngModel]=\"config()['waitForCompletion'] !== false\"\n (ngModelChange)=\"onConfigFieldChange('waitForCompletion', $event === true)\"\n label=\"Wait for completion\"\n hint=\"When disabled, the parent continues after dispatch and backend records the child linkage.\"\n />\n </div>\n <div class=\"mt-3 grid gap-3 md:grid-cols-2\">\n <mt-textarea-field\n class=\"w-full font-mono md:col-span-2\"\n [ngModel]=\"valueText(config()['inputMappingJson'] ?? '{}')\"\n (focusin)=\"setExpressionTarget('config:inputMappingJson')\"\n (ngModelChange)=\"onConfigFieldChange('inputMappingJson', $event)\"\n label=\"Input mapping JSON\"\n hint=\"JSON object or expression map sent into the child automation input.\"\n rows=\"7\"\n />\n <mt-textarea-field\n class=\"w-full font-mono md:col-span-2\"\n [ngModel]=\"valueText(config()['outputMappingJson'] ?? '{}')\"\n (focusin)=\"setExpressionTarget('config:outputMappingJson')\"\n (ngModelChange)=\"onConfigFieldChange('outputMappingJson', $event)\"\n label=\"Output mapping JSON\"\n hint=\"JSON object or expression map for child output/status returned to the parent.\"\n rows=\"7\"\n />\n </div>\n </section>\n }\n }\n @case (\"Subworkflow\") {\n @if (sectionInMain(\"callAutomation\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Subworkflow</div>\n <div class=\"grid gap-3 md:grid-cols-2\">\n <mt-select-field\n [ngModel]=\"config()['targetAutomationId'] ?? ''\"\n (ngModelChange)=\"onSubworkflowTargetChange($event)\"\n label=\"Target automation\"\n hint=\"Backend-provided automation candidate. Tenant and recursion validation stay on the backend.\"\n [options]=\"subworkflowAutomationOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-select-field\n [ngModel]=\"config()['revisionMode'] ?? 'Active'\"\n (ngModelChange)=\"onConfigFieldChange('revisionMode', $event)\"\n label=\"Revision mode\"\n hint=\"Choose which published or active child revision to call.\"\n [options]=\"revisionModeOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n @if (config()['revisionMode'] === \"SpecificRevision\") {\n <mt-text-field\n [ngModel]=\"config()['specificRevisionId'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('specificRevisionId', $event)\"\n label=\"Specific revision id\"\n hint=\"Required only when revision mode is Specific revision.\"\n />\n }\n <mt-toggle-field\n size=\"small\"\n labelPosition=\"end\"\n [ngModel]=\"config()['waitForCompletion'] !== false\"\n (ngModelChange)=\"onConfigFieldChange('waitForCompletion', $event === true)\"\n label=\"Wait for completion\"\n hint=\"When disabled, the parent continues after dispatch and backend records the child linkage.\"\n />\n </div>\n <div class=\"mt-3 grid gap-3 md:grid-cols-2\">\n <mt-textarea-field\n class=\"w-full font-mono md:col-span-2\"\n [ngModel]=\"valueText(config()['inputMappingJson'] ?? '{}')\"\n (focusin)=\"setExpressionTarget('config:inputMappingJson')\"\n (ngModelChange)=\"onConfigFieldChange('inputMappingJson', $event)\"\n label=\"Input mapping JSON\"\n hint=\"JSON object or expression map sent into the child automation input.\"\n rows=\"7\"\n />\n <mt-textarea-field\n class=\"w-full font-mono md:col-span-2\"\n [ngModel]=\"valueText(config()['outputMappingJson'] ?? '{}')\"\n (focusin)=\"setExpressionTarget('config:outputMappingJson')\"\n (ngModelChange)=\"onConfigFieldChange('outputMappingJson', $event)\"\n label=\"Output mapping JSON\"\n hint=\"JSON object or expression map for child output/status returned to the parent.\"\n rows=\"7\"\n />\n </div>\n </section>\n }\n }\n @case (\"ParallelStart\") {\n @if (sectionInMain(\"parallelStart\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"flex items-center justify-between gap-3 border-b border-surface-200 bg-surface-50 px-4 py-3\">\n <div class=\"fp-ae-section-title !m-0\">Parallel start</div>\n <mt-button size=\"small\" variant=\"outlined\" icon=\"general.plus\" label=\"Add branch\" (onClick)=\"addParallelBranch()\" />\n </div>\n <div class=\"space-y-3 p-4\">\n @for (branch of parallelBranches(); track branch.key; let i = $index) {\n <div class=\"rounded-md border border-surface-200 bg-surface-0 p-3\">\n <div class=\"grid gap-3 md:grid-cols-[160px_1fr_1fr_auto]\">\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"branch.key\"\n (ngModelChange)=\"updateParallelBranch(i, 'key', $event)\"\n label=\"Branch key\"\n hint=\"Persisted route key. Do not use array index names.\"\n />\n <mt-text-field\n [ngModel]=\"branch.label\"\n (ngModelChange)=\"updateParallelBranch(i, 'label', $event)\"\n label=\"Label\"\n hint=\"Visual branch label. Changing it does not change existing routes.\"\n />\n <mt-text-field\n [ngModel]=\"branch.description\"\n (ngModelChange)=\"updateParallelBranch(i, 'description', $event)\"\n label=\"Description\"\n hint=\"Optional branch note for reviewers.\"\n />\n <div class=\"flex items-end gap-1\">\n <mt-button size=\"small\" variant=\"outlined\" icon=\"arrow.arrow-up\" tooltip=\"Move up\" (onClick)=\"moveParallelBranch(i, -1)\" />\n <mt-button size=\"small\" variant=\"outlined\" icon=\"arrow.arrow-down\" tooltip=\"Move down\" (onClick)=\"moveParallelBranch(i, 1)\" />\n <mt-button size=\"small\" variant=\"outlined\" severity=\"danger\" icon=\"general.trash-01\" tooltip=\"Remove branch\" (onClick)=\"removeParallelBranch(i)\" />\n </div>\n </div>\n <div class=\"mt-3 flex flex-wrap items-center gap-1.5 text-[11px]\">\n <span class=\"inline-flex h-7 items-center rounded-md border border-surface-200 bg-surface-50 px-2 font-mono font-semibold text-(--p-text-color)\">{{ branch.key }}</span>\n <span class=\"inline-flex h-7 items-center rounded-md border border-surface-200 bg-surface-0 px-2 font-medium text-(--p-text-muted-color)\">{{ branch.routeCount }} connected route{{ branch.routeCount === 1 ? \"\" : \"s\" }}</span>\n </div>\n </div>\n }\n @if (parallelBranches().length === 0) {\n <p class=\"fp-ae-copy\">Add at least two stable branch keys. Backend validation blocks publish until branches are valid.</p>\n }\n </div>\n </section>\n }\n }\n @case (\"ParallelJoin\") {\n @if (sectionInMain(\"parallelJoin\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Parallel join</div>\n <div class=\"grid gap-3 md:grid-cols-2\">\n <mt-select-field\n [ngModel]=\"config()['joinPolicy'] ?? 'All'\"\n (ngModelChange)=\"onConfigFieldChange('joinPolicy', $event)\"\n label=\"Join policy\"\n hint=\"Backend policy used to decide when branch wait is complete.\"\n [options]=\"joinPolicyOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n @if (config()['joinPolicy'] === \"Threshold\") {\n <mt-number-field\n [ngModel]=\"numberValue(config()['threshold'])\"\n (ngModelChange)=\"onConfigFieldChange('threshold', $event)\"\n label=\"Threshold\"\n hint=\"Minimum completed branch count required for Threshold policy.\"\n [min]=\"1\"\n />\n }\n <mt-select-field\n [ngModel]=\"config()['aggregationStrategy'] ?? 'MergeObjects'\"\n (ngModelChange)=\"onConfigFieldChange('aggregationStrategy', $event)\"\n label=\"Aggregation strategy\"\n hint=\"How backend joins branch outputs into the join node output.\"\n [options]=\"aggregationStrategyOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"config()['outputTargetPath'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:outputTargetPath')\"\n (ngModelChange)=\"onConfigFieldChange('outputTargetPath', $event)\"\n label=\"Output target path\"\n hint=\"Optional context path where joined output should be written.\"\n />\n </div>\n </section>\n }\n }\n @case (\"Switch\") {\n @if (sectionInMain(\"switch\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Switch</div>\n <div class=\"grid gap-4\">\n <div class=\"grid items-end gap-3 md:grid-cols-[minmax(0,1fr)_minmax(280px,0.7fr)]\">\n <mt-select-field\n [ngModel]=\"config()['mode'] ?? 'value'\"\n (ngModelChange)=\"onConfigFieldChange('mode', $event)\"\n label=\"Mode\"\n hint=\"Backend evaluation mode: expression, rules, or source-value matching.\"\n [options]=\"switchModeOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-toggle-field\n class=\"self-end pb-1\"\n size=\"small\"\n labelPosition=\"end\"\n [ngModel]=\"config()['firstMatch'] !== false\"\n (ngModelChange)=\"onConfigFieldChange('firstMatch', $event === true)\"\n label=\"First match\"\n hint=\"Use the first matching case unless backend explicitly supports multi-match.\"\n />\n </div>\n @if (showSwitchSourceValue() || showSwitchExpression()) {\n <div class=\"grid gap-3 md:grid-cols-2\">\n @if (showSwitchSourceValue()) {\n <mt-text-field\n [class]=\"showSwitchExpression() ? 'font-mono' : 'font-mono md:col-span-2'\"\n [ngModel]=\"config()['sourceValue'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:sourceValue')\"\n (ngModelChange)=\"onConfigFieldChange('sourceValue', $event)\"\n label=\"Source value\"\n hint=\"Value or expression used for value matching.\"\n />\n }\n @if (showSwitchExpression()) {\n <mt-text-field\n [class]=\"showSwitchSourceValue() ? 'font-mono' : 'font-mono md:col-span-2'\"\n [ngModel]=\"config()['expression'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:expression')\"\n (ngModelChange)=\"onConfigFieldChange('expression', $event)\"\n label=\"Expression\"\n hint=\"Expression evaluated when mode is expression or rules.\"\n />\n }\n </div>\n }\n <div class=\"grid items-end gap-3 md:grid-cols-[minmax(0,1fr)_minmax(280px,0.7fr)]\">\n @if (showSwitchDefaultOutputKey()) {\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"config()['defaultOutputKey'] ?? 'default'\"\n (ngModelChange)=\"onConfigFieldChange('defaultOutputKey', $event)\"\n label=\"Default output key\"\n hint=\"Stable default route key used when no case matches.\"\n />\n }\n <mt-toggle-field\n [class]=\"showSwitchDefaultOutputKey() ? 'self-end pb-1' : 'self-end pb-1 md:col-span-2'\"\n size=\"small\"\n labelPosition=\"end\"\n [ngModel]=\"config()['includeDefaultOutput'] !== false\"\n (ngModelChange)=\"onConfigFieldChange('includeDefaultOutput', $event === true)\"\n label=\"Include default output\"\n hint=\"Expose the default route output key in the canvas.\"\n />\n </div>\n </div>\n </section>\n <section class=\"fp-ae-panel\">\n <div class=\"flex items-center justify-between gap-3 border-b border-surface-200 bg-surface-50 px-4 py-3\">\n <div class=\"fp-ae-section-title !m-0\">Switch cases</div>\n <mt-button size=\"small\" variant=\"outlined\" icon=\"general.plus\" label=\"Add case\" (onClick)=\"addSwitchCase()\" />\n </div>\n <div class=\"space-y-3 p-4\">\n @for (item of switchCases(); track item.key; let i = $index) {\n <div class=\"rounded-md border border-surface-200 bg-surface-0 p-3\">\n <div class=\"grid gap-3 md:grid-cols-[160px_1fr_1fr_auto]\">\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"item.key\"\n (ngModelChange)=\"updateSwitchCase(i, 'key', $event)\"\n label=\"Case key\"\n hint=\"Persisted stable key. The route output becomes case_key.\"\n />\n <mt-text-field\n [ngModel]=\"item.label\"\n (ngModelChange)=\"updateSwitchCase(i, 'label', $event)\"\n label=\"Label\"\n hint=\"Visual case label. Routes stay attached to the case key.\"\n />\n <mt-text-field\n [ngModel]=\"item.value\"\n (focusin)=\"setExpressionTarget('config:cases:' + item.key + ':value')\"\n (ngModelChange)=\"updateSwitchCase(i, 'value', $event)\"\n label=\"Value\"\n hint=\"Expected value for value matching.\"\n />\n <div class=\"flex items-end gap-1\">\n <mt-button size=\"small\" variant=\"outlined\" icon=\"arrow.arrow-up\" tooltip=\"Move up\" (onClick)=\"moveSwitchCase(i, -1)\" />\n <mt-button size=\"small\" variant=\"outlined\" icon=\"arrow.arrow-down\" tooltip=\"Move down\" (onClick)=\"moveSwitchCase(i, 1)\" />\n <mt-button size=\"small\" variant=\"outlined\" severity=\"danger\" icon=\"general.trash-01\" tooltip=\"Remove case\" (onClick)=\"removeSwitchCase(i)\" />\n </div>\n </div>\n <div class=\"mt-3 grid gap-3 md:grid-cols-2\">\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"item.condition\"\n (focusin)=\"setExpressionTarget('config:cases:' + item.key + ':condition')\"\n (ngModelChange)=\"updateSwitchCase(i, 'condition', $event)\"\n label=\"Condition\"\n hint=\"Optional condition for rule matching.\"\n />\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"item.expression\"\n (focusin)=\"setExpressionTarget('config:cases:' + item.key + ':expression')\"\n (ngModelChange)=\"updateSwitchCase(i, 'expression', $event)\"\n label=\"Case expression\"\n hint=\"Optional expression for this case.\"\n />\n </div>\n <div class=\"mt-3 flex flex-wrap items-center gap-1.5 text-[11px]\">\n <span class=\"inline-flex h-7 items-center rounded-md border border-surface-200 bg-surface-50 px-2 font-mono font-semibold text-(--p-text-color)\">{{ item.routeKey }}</span>\n <span class=\"inline-flex h-7 items-center rounded-md border border-surface-200 bg-surface-0 px-2 font-medium text-(--p-text-muted-color)\">{{ item.routeCount }} connected route{{ item.routeCount === 1 ? \"\" : \"s\" }}</span>\n <span class=\"inline-flex h-7 items-center rounded-md border border-surface-200 bg-surface-0 px-2 font-medium text-(--p-text-muted-color)\">Visual order {{ i + 1 }}</span>\n </div>\n </div>\n }\n @if (switchCases().length === 0) {\n <p class=\"fp-ae-copy\">Add at least one stable case key. Backend validation points routes at missing case keys if a case is deleted.</p>\n }\n </div>\n </section>\n }\n }\n @case (\"Stop\") {\n @if (sectionInMain(\"stop\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">End execution</div>\n @if (supportsConfigKey('status') || supportsConfigKey('message') || supportsConfigKey('output')) {\n <div class=\"grid gap-3 md:grid-cols-2\">\n @if (supportsConfigKey('status')) {\n <mt-text-field\n [ngModel]=\"config()['status'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('status', $event)\"\n label=\"Result status\"\n hint=\"Terminal status emitted by the backend stop node.\"\n />\n }\n @if (supportsConfigKey('message')) {\n <mt-text-field\n [ngModel]=\"config()['message'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:message')\"\n (ngModelChange)=\"onConfigFieldChange('message', $event)\"\n label=\"Message\"\n hint=\"Optional message or reason saved with the terminal result.\"\n />\n }\n @if (supportsConfigKey('output')) {\n <mt-textarea-field\n class=\"w-full font-mono md:col-span-2\"\n [ngModel]=\"valueText(config()['output'] ?? '')\"\n (focusin)=\"setExpressionTarget('config:output')\"\n (ngModelChange)=\"onConfigFieldChange('output', $event)\"\n label=\"Output payload\"\n hint=\"Terminal output payload when exposed by the backend schema.\"\n rows=\"5\"\n />\n }\n </div>\n } @else {\n <p class=\"fp-ae-copy\">\n This terminal node has no backend-exposed settings. It ends execution when reached.\n </p>\n }\n </section>\n }\n }\n }\n\n @if (sectionInMain(\"backendSchema\") && step() && editorType() !== \"HumanApproval\") {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Output data</div>\n @if (outputFields().length > 0) {\n <div class=\"grid gap-2 md:grid-cols-2\">\n @for (field of outputFields(); track field.key) {\n <div class=\"fp-ae-kv\">\n <span>{{ field.label }}</span>\n <strong>{{ field.type }}</strong>\n </div>\n }\n </div>\n }\n @if (routeOutputKeys().length > 0) {\n <div class=\"mt-3 fp-ae-label mb-2\">Route outputs</div>\n <div class=\"flex flex-wrap gap-1.5\">\n @for (key of routeOutputKeys(); track key) {\n <span class=\"rounded-md bg-(--p-surface-100) px-2 py-1 font-mono text-[11px]\">{{ key }}</span>\n }\n </div>\n }\n </section>\n }\n\n @if (sectionInAdvanced(\"mapping\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Request and result mapping</div>\n <div class=\"grid gap-3 md:grid-cols-2\">\n <div>\n <div class=\"fp-ae-label mb-2\">Request data</div>\n @for (row of inputMappingRows(); track row.key) {\n <label class=\"mb-2 block space-y-1\">\n <span class=\"text-[11px] text-(--p-text-muted-color)\">{{ row.key }}</span>\n <mt-text-field\n [ngModel]=\"valueText(row.value)\"\n (focusin)=\"setExpressionTarget('config:inputMapping:' + row.key)\"\n (ngModelChange)=\"updateJsonField('inputMappingJson', row.key, $event)\"\n hint=\"Expression or literal value mapped into this node input.\"\n />\n </label>\n }\n </div>\n <div>\n <div class=\"fp-ae-label mb-2\">Output data</div>\n @for (row of outputMappingRows(); track row.key) {\n <label class=\"mb-2 block space-y-1\">\n <span class=\"text-[11px] text-(--p-text-muted-color)\">{{ row.key }}</span>\n <mt-text-field\n [ngModel]=\"valueText(row.value)\"\n (focusin)=\"setExpressionTarget('config:outputMapping:' + row.key)\"\n (ngModelChange)=\"updateJsonField('outputMappingJson', row.key, $event)\"\n hint=\"Expression or literal value mapped from this node output.\"\n />\n </label>\n }\n </div>\n </div>\n @if (\n sectionHasAdvancedEmptyState(\"mapping\") &&\n inputMappingRows().length === 0 &&\n outputMappingRows().length === 0\n ) {\n <p class=\"fp-ae-copy mt-3\">\n This node is using backend default request and output data. Add mappings only when this step needs a custom payload.\n </p>\n }\n </section>\n }\n\n @if (sectionInAdvanced(\"credentials\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Auth and credentials</div>\n <ng-container *ngTemplateOutlet=\"credentialSelector\" />\n </section>\n }\n\n @if (sectionInAdvanced(\"policy\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Retry, timeout, error policy</div>\n <div class=\"grid gap-3 md:grid-cols-3\">\n <mt-number-field\n [ngModel]=\"numberValue(policyObject('timeoutPolicyJson')['timeoutSeconds'])\"\n (ngModelChange)=\"updateJsonField('timeoutPolicyJson', 'timeoutSeconds', $event)\"\n label=\"Timeout seconds\"\n hint=\"Maximum runtime before the backend times out this node.\"\n [min]=\"0\"\n />\n <mt-number-field\n [ngModel]=\"numberValue(policyObject('retryPolicyJson')['maxAttempts'])\"\n (ngModelChange)=\"updateJsonField('retryPolicyJson', 'maxAttempts', $event)\"\n label=\"Max attempts\"\n hint=\"Maximum retry attempts after retryable failures.\"\n [min]=\"0\"\n />\n <mt-select-field\n [ngModel]=\"policyObject('errorPolicyJson')['onFailure'] ?? ''\"\n (ngModelChange)=\"updateJsonField('errorPolicyJson', 'onFailure', $event)\"\n label=\"On failure\"\n hint=\"Backend error policy to apply when this node fails.\"\n [options]=\"errorPolicyOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n </div>\n @if (sectionHasAdvancedEmptyState(\"policy\")) {\n <p class=\"fp-ae-copy mt-3\">\n Backend defaults apply until you override a timeout, retry, or error policy here.\n </p>\n }\n </section>\n }\n\n @if (sectionInAdvanced(\"triggerContext\") && trigger()) {\n <details class=\"fp-ae-panel\" open>\n <summary class=\"fp-ae-section-title cursor-pointer select-none\">\n Trigger payload and context\n </summary>\n <p class=\"fp-ae-copy mt-3\">\n Trigger payload/context is documentation for expressions only. Triggers\n do not edit node input or output mappings here.\n </p>\n <div class=\"grid gap-3 md:grid-cols-2\">\n <details class=\"rounded-lg border border-(--p-content-border-color)\">\n <summary class=\"cursor-pointer px-3 py-2 text-[12px] font-semibold\">Config schema</summary>\n <pre class=\"fp-ae-code\">{{ schemaText(configSchema()) }}</pre>\n </details>\n @if (hasTriggerPayloadSchema() && triggerPayloadSchema(); as schema) {\n <details class=\"rounded-lg border border-(--p-content-border-color)\">\n <summary class=\"cursor-pointer px-3 py-2 text-[12px] font-semibold\">Payload schema</summary>\n <pre class=\"fp-ae-code\">{{ schemaText(schema) }}</pre>\n </details>\n }\n @if (authPolicySchema()) {\n <details class=\"rounded-lg border border-(--p-content-border-color)\">\n <summary class=\"cursor-pointer px-3 py-2 text-[12px] font-semibold\">Auth policy schema</summary>\n <pre class=\"fp-ae-code\">{{ schemaText(authPolicySchema()) }}</pre>\n </details>\n }\n @if (triggerPayloadSample(); as sample) {\n <details class=\"rounded-lg border border-(--p-content-border-color)\">\n <summary class=\"cursor-pointer px-3 py-2 text-[12px] font-semibold\">Payload or request sample</summary>\n <pre class=\"fp-ae-code\">{{ schemaText(sample) }}</pre>\n </details>\n }\n </div>\n </details>\n }\n\n @if (sectionInAdvanced(\"backendSchema\") && step()) {\n <details class=\"fp-ae-panel\" open>\n <summary class=\"fp-ae-section-title cursor-pointer select-none\">\n Backend schemas and outputs\n </summary>\n <div class=\"mt-3 grid gap-3 md:grid-cols-2\">\n <details class=\"rounded-lg border border-(--p-content-border-color)\">\n <summary class=\"cursor-pointer px-3 py-2 text-[12px] font-semibold\">Config schema</summary>\n <pre class=\"fp-ae-code\">{{ schemaText(configSchema()) }}</pre>\n </details>\n <details class=\"rounded-lg border border-(--p-content-border-color)\">\n <summary class=\"cursor-pointer px-3 py-2 text-[12px] font-semibold\">Input schema</summary>\n <pre class=\"fp-ae-code\">{{ schemaText(inputSchema()) }}</pre>\n </details>\n <details class=\"rounded-lg border border-(--p-content-border-color)\">\n <summary class=\"cursor-pointer px-3 py-2 text-[12px] font-semibold\">Output schema</summary>\n <pre class=\"fp-ae-code\">{{ schemaText(outputSchema()) }}</pre>\n </details>\n <div class=\"rounded-lg border border-(--p-content-border-color) p-3\">\n <div class=\"fp-ae-label mb-2\">Route output keys</div>\n <div class=\"flex flex-wrap gap-1.5\">\n @for (key of routeOutputKeys(); track key) {\n <span class=\"rounded-md bg-(--p-surface-100) px-2 py-1 font-mono text-[11px]\">{{ key }}</span>\n }\n @if (routeOutputKeys().length === 0) {\n <span class=\"text-[12px] text-(--p-text-muted-color)\">No outgoing route keys</span>\n }\n </div>\n </div>\n </div>\n </details>\n }\n\n @if (sectionInAdvanced(\"schemaFields\") && configRows().length > 0) {\n <details class=\"fp-ae-panel\" open>\n <summary class=\"fp-ae-section-title cursor-pointer select-none\">\n Backend schema fields\n </summary>\n <div class=\"mt-3 grid gap-3 md:grid-cols-2\">\n @for (field of configRows(); track field.key) {\n <div\n class=\"space-y-1.5\"\n [class.md:col-span-2]=\"field.type === 'object' || field.type === 'array'\"\n >\n @if (field.enumValues.length) {\n <mt-select-field\n [ngModel]=\"fieldValue(field.key)\"\n (ngModelChange)=\"onConfigFieldChange(field.key, $event)\"\n [label]=\"field.label + (field.required ? ' *' : '')\"\n [hint]=\"field.description ?? ''\"\n [options]=\"enumOptions(field.enumValues)\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n } @else if (field.type === \"boolean\") {\n <mt-toggle-field\n size=\"small\"\n labelPosition=\"end\"\n [ngModel]=\"!!fieldValue(field.key)\"\n (ngModelChange)=\"onConfigFieldChange(field.key, $event === true)\"\n [label]=\"field.label + (field.required ? ' *' : '')\"\n [hint]=\"field.description ?? ''\"\n />\n } @else if (field.type === \"number\") {\n <mt-number-field\n [ngModel]=\"numberValue(fieldValue(field.key))\"\n (ngModelChange)=\"onConfigFieldChange(field.key, $event)\"\n [label]=\"field.label + (field.required ? ' *' : '')\"\n [hint]=\"field.description ?? ''\"\n />\n } @else if (field.type === \"date\") {\n <mt-date-field\n [ngModel]=\"fieldValue(field.key)\"\n (ngModelChange)=\"onConfigFieldChange(field.key, $event)\"\n [label]=\"field.label + (field.required ? ' *' : '')\"\n [hint]=\"field.description ?? ''\"\n [showTime]=\"field.format !== 'date'\"\n [showClear]=\"true\"\n [pInputs]=\"dateTimePickerInputs\"\n />\n } @else if (field.type === \"object\" || field.type === \"array\") {\n <mt-textarea-field\n class=\"w-full font-mono\"\n [ngModel]=\"valueText(fieldValue(field.key))\"\n (ngModelChange)=\"onConfigFieldChange(field.key, $event)\"\n [label]=\"field.label + (field.required ? ' *' : '')\"\n [hint]=\"field.description ?? ''\"\n rows=\"5\"\n />\n } @else {\n <mt-text-field\n [ngModel]=\"fieldText(field.key)\"\n (focusin)=\"field.expressionEnabled && setExpressionTarget('config:' + field.key)\"\n (ngModelChange)=\"onConfigFieldChange(field.key, $event)\"\n [label]=\"field.label + (field.required ? ' *' : '')\"\n [hint]=\"field.description ?? ''\"\n />\n }\n </div>\n }\n </div>\n </details>\n }\n </div>\n</div>\n\n<ng-template #credentialSelector>\n @if (credentials().length) {\n <div class=\"space-y-2\">\n @for (credential of credentials(); track credential.credentialRef) {\n <label class=\"flex items-center justify-between gap-3 rounded-lg border border-(--p-content-border-color) bg-(--p-surface-50) px-3 py-2\">\n <span class=\"min-w-0\">\n <span class=\"block truncate text-[12px] font-semibold\">{{ credential.displayName ?? credential.credentialRef }}</span>\n <span class=\"block truncate font-mono text-[11px] text-(--p-text-muted-color)\">\n {{ credential.credentialRef }} / {{ credential.status ?? (credential.resolved ? \"Resolved\" : \"Unresolved\") }}\n </span>\n </span>\n <span class=\"flex items-center gap-2\">\n <mt-button\n size=\"small\"\n variant=\"text\"\n label=\"Test\"\n (onClick)=\"testCredential(credential.credentialRef)\"\n />\n <mt-toggle-field\n size=\"small\"\n [ngModel]=\"credentialRefs().includes(credential.credentialRef)\"\n (ngModelChange)=\"toggleCredential(credential.credentialRef, $event === true)\"\n hint=\"Attach or detach this backend credential reference. Masked secrets are preserved.\"\n />\n </span>\n </label>\n }\n </div>\n } @else if (sectionHasAdvancedEmptyState(\"credentials\")) {\n <p class=\"fp-ae-copy\">\n The backend helper did not return credential choices for this node. Existing masked references remain preserved in the saved JSON.\n </p>\n }\n @if (credentialTest(); as test) {\n <div class=\"mt-3 rounded-lg bg-(--p-surface-100) px-3 py-2 text-[12px]\">\n {{ test.status ?? (test.succeeded ? \"Succeeded\" : \"Placeholder\") }}\n @if (test.message) { <span>- {{ test.message }}</span> }\n </div>\n }\n</ng-template>\n\n<ng-template\n #mapEditor\n let-objectKey=\"objectKey\"\n let-rows=\"rows\"\n let-keyLabel=\"keyLabel\"\n let-valueLabel=\"valueLabel\"\n let-addLabel=\"addLabel\"\n>\n <div class=\"space-y-2\">\n @for (row of rows; track row.key) {\n <div class=\"grid gap-2 md:grid-cols-[180px_1fr_auto]\">\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"row.key\"\n (ngModelChange)=\"updateObjectRow(objectKey, row.key, $event, row.value)\"\n [label]=\"keyLabel\"\n hint=\"Configuration key saved to the backend JSON object.\"\n />\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"valueText(row.value)\"\n (focusin)=\"setExpressionTarget('config:' + objectKey + ':' + row.key)\"\n (ngModelChange)=\"updateObjectRow(objectKey, row.key, row.key, $event)\"\n [label]=\"valueLabel\"\n hint=\"Configuration value. Use expressions when this field should resolve at runtime.\"\n />\n <mt-button\n class=\"self-end\"\n size=\"small\"\n severity=\"secondary\"\n variant=\"outlined\"\n label=\"Remove\"\n (onClick)=\"removeObjectRow(objectKey, row.key)\"\n />\n </div>\n }\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n [label]=\"addLabel || ('Add ' + keyLabel)\"\n (onClick)=\"addObjectRow(objectKey)\"\n />\n </div>\n</ng-template>\n" }]
14277
+ ], host: { class: 'block h-full min-h-0' }, template: "<div\n class=\"fp-scroll flex h-full min-h-0 flex-col overflow-y-auto\"\n fpDropData\n [fpDropAutoInsert]=\"false\"\n (dataDrop)=\"insertExpression($event)\"\n>\n <div class=\"space-y-4 px-5 py-5\">\n @if (helperError()) {\n <div\n class=\"rounded-lg border border-[rgb(var(--fp-warning))]/30 bg-[rgb(var(--fp-warning))]/10 px-3 py-2 text-[12px] leading-5 text-(--p-text-color)\"\n >\n {{ helperError() }}\n </div>\n }\n\n @if (sectionInMain(\"startConnection\") && trigger()) {\n <section\n class=\"flex flex-col gap-0 overflow-hidden rounded-md border border-surface-200 bg-surface-0\"\n >\n <h3\n class=\"m-0 border-b border-surface-200 bg-surface-50 px-4 py-3 text-lg font-semibold text-color\"\n >\n Start connection\n </h3>\n <div class=\"space-y-3 p-4\">\n @if (startConnection().key) {\n @if (startConnection().step) {\n <div class=\"flex flex-wrap items-start justify-between gap-3\">\n <div class=\"min-w-0 space-y-1\">\n <div class=\"text-[12px] font-semibold text-(--p-text-muted-color)\">\n First node connected\n </div>\n <div class=\"truncate text-[14px] font-semibold text-(--p-text-color)\">\n {{ startConnection().label }}\n </div>\n <div class=\"flex flex-wrap items-center gap-2 text-[12px] text-(--p-text-muted-color)\">\n <span>Managed on canvas</span>\n <span\n class=\"rounded-md bg-(--p-surface-100) px-2 py-1 font-mono text-[11px]\"\n >\n {{ startConnection().key }}\n </span>\n </div>\n </div>\n <div class=\"flex shrink-0 flex-wrap gap-2\">\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n severity=\"secondary\"\n label=\"Focus connected node\"\n (onClick)=\"focusStartConnection()\"\n />\n </div>\n </div>\n } @else {\n <div class=\"space-y-2\">\n <div class=\"text-[14px] font-semibold text-(--p-text-color)\">\n No first node connected\n </div>\n <p class=\"m-0 text-[12px] leading-5 text-(--p-text-muted-color)\">\n The saved start connection points to a node key that is not on\n the canvas. Connect this trigger to the first node on the\n canvas.\n </p>\n <div class=\"flex flex-wrap items-center gap-2 text-[12px] text-(--p-text-muted-color)\">\n <span>Technical key</span>\n <span\n class=\"rounded-md bg-(--p-surface-100) px-2 py-1 font-mono text-[11px]\"\n >\n {{ startConnection().key }}\n </span>\n <span>Managed on canvas</span>\n </div>\n </div>\n }\n } @else {\n <div class=\"rounded-lg border border-dashed border-(--p-content-border-color) bg-(--p-surface-50) p-3\">\n <div class=\"text-[14px] font-semibold text-(--p-text-color)\">\n No first node connected\n </div>\n <p class=\"m-0 mt-1 text-[12px] leading-5 text-(--p-text-muted-color)\">\n Connect this trigger to the first node on the canvas.\n </p>\n </div>\n }\n </div>\n </section>\n }\n\n @switch (editorType()) {\n @case (\"ManualTrigger\") {\n @if (sectionInMain(\"manualTrigger\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Manual run input</div>\n @if (hasTriggerPayloadSchema() && triggerPayloadSchema(); as schema) {\n <div class=\"rounded-md border border-surface-200 bg-surface-0 p-3\">\n <div class=\"fp-ae-label mb-2\">Payload schema</div>\n <pre class=\"fp-ae-code\">{{ schemaText(schema) }}</pre>\n </div>\n }\n @if (triggerPayloadSample(); as sample) {\n <div class=\"mt-3 rounded-md border border-surface-200 bg-surface-0 p-3\">\n <div class=\"fp-ae-label mb-2\">Sample payload</div>\n <pre class=\"fp-ae-code\">{{ schemaText(sample) }}</pre>\n </div>\n }\n @if (!hasTriggerPayloadSchema() && !triggerPayloadSample()) {\n <p class=\"fp-ae-copy\">\n This manual trigger has no backend-provided input schema. It can still be connected to the first step on the canvas.\n </p>\n }\n </section>\n }\n }\n @case (\"WebhookTrigger\") {\n @if (sectionInMain(\"webhookSetup\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Webhook setup</div>\n @if (webhookSetup(); as setup) {\n <div class=\"flex gap-2\">\n <mt-text-field\n class=\"flex-1 font-mono\"\n [ngModel]=\"setup.webhookUrl ?? ''\"\n [readonly]=\"true\"\n label=\"Webhook URL\"\n hint=\"Backend-generated endpoint clients should call to start this trigger.\"\n />\n <mt-button class=\"self-end\" size=\"small\" variant=\"outlined\" label=\"Copy\" (onClick)=\"copyWebhookUrl()\" />\n </div>\n <div class=\"grid gap-2 md:grid-cols-2\">\n <div class=\"fp-ae-kv\">\n <span>Auth mode</span>\n <strong>{{ setup.authMode ?? \"Backend default\" }}</strong>\n </div>\n <div class=\"fp-ae-kv\">\n <span>Required headers</span>\n <strong>{{ (setup.requiredHeaders ?? []).join(\", \") || \"-\" }}</strong>\n </div>\n </div>\n @if (setup.hmacSigning) {\n <p class=\"fp-ae-copy\">{{ setup.hmacSigning }}</p>\n }\n @if (setup.sampleRequest) {\n <details class=\"mt-3 rounded-lg border border-(--p-content-border-color)\">\n <summary class=\"cursor-pointer px-3 py-2 text-[12px] font-semibold\">\n Sample request\n </summary>\n <pre class=\"fp-ae-code\">{{ schemaText(setup.sampleRequest) }}</pre>\n </details>\n }\n } @else {\n <p class=\"fp-ae-copy\">\n Webhook setup is not available for this draft yet.\n </p>\n }\n </section>\n }\n @if (sectionInMain(\"authPolicy\") && hasAuthPolicy()) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Authentication policy</div>\n <div class=\"grid gap-3 md:grid-cols-3\">\n <mt-select-field\n [ngModel]=\"authPolicy()['mode'] ?? ''\"\n (ngModelChange)=\"onAuthFieldChange('mode', $event)\"\n label=\"Mode\"\n hint=\"Authentication mode required by the backend webhook policy.\"\n [options]=\"authModeOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-text-field\n [ngModel]=\"authPolicy()['signatureHeaderName'] ?? ''\"\n (ngModelChange)=\"onAuthFieldChange('signatureHeaderName', $event)\"\n label=\"Signature header\"\n hint=\"Header name that carries the webhook signature when the policy requires signed requests.\"\n />\n <mt-text-field\n [ngModel]=\"authPolicy()['timestampHeaderName'] ?? ''\"\n (ngModelChange)=\"onAuthFieldChange('timestampHeaderName', $event)\"\n label=\"Timestamp header\"\n hint=\"Header name that carries the request timestamp for replay protection.\"\n />\n </div>\n </section>\n }\n }\n @case (\"FormSubmitTrigger\") {\n @if (sectionInMain(\"formBinding\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Form binding</div>\n <div class=\"grid gap-3 md:grid-cols-2\">\n <mt-select-field\n [ngModel]=\"selectedFormId() || formBinding()?.formId || ''\"\n (ngModelChange)=\"onFormChange($event)\"\n label=\"Form\"\n hint=\"Choose a backend form that will submit data into this trigger.\"\n [options]=\"formOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-select-field\n [ngModel]=\"selectedFormVersionId() || formBinding()?.formVersionId || ''\"\n (ngModelChange)=\"onFormVersionChange($event)\"\n label=\"Form version\"\n hint=\"Persist the exact backend formVersionId. Do not type or generate IDs manually.\"\n [options]=\"formVersionOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n </div>\n <div class=\"mt-3 flex flex-wrap items-center gap-2\">\n <mt-button size=\"small\" severity=\"primary\" label=\"Save binding\" (onClick)=\"saveFormBinding()\" />\n @if (formBinding()) {\n <span class=\"rounded-lg bg-(--p-surface-100) px-2 py-1 font-mono text-[11px] text-(--p-text-muted-color)\">\n {{ formBinding()!.formVersionId }}\n </span>\n }\n </div>\n @if (formSchema(); as schema) {\n <details class=\"mt-3 rounded-lg border border-(--p-content-border-color)\">\n <summary class=\"cursor-pointer px-3 py-2 text-[12px] font-semibold\">\n Form schema preview\n </summary>\n <pre class=\"fp-ae-code\">{{ schemaText(schema.schema) }}</pre>\n </details>\n }\n </section>\n }\n }\n @case (\"ScheduleTrigger\") {\n @if (sectionInMain(\"schedule\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Schedule</div>\n <div class=\"grid gap-3 md:grid-cols-2\">\n <mt-select-field\n [ngModel]=\"scheduleMode()\"\n (ngModelChange)=\"onScheduleModeChange($event)\"\n label=\"Mode\"\n hint=\"Pick one schedule mode: cron, interval, or once. Backend validation requires exactly one mode.\"\n [options]=\"scheduleModeOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-select-field\n [ngModel]=\"config()['timezone'] ?? 'UTC'\"\n (ngModelChange)=\"onConfigFieldChange('timezone', $event)\"\n label=\"Timezone\"\n hint=\"Timezone used to calculate the next fire time.\"\n [options]=\"timeZoneOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n @if (showScheduleCron()) {\n <mt-text-field\n class=\"font-mono md:col-span-2\"\n [ngModel]=\"config()['cron'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('cron', $event)\"\n label=\"Cron\"\n hint=\"Cron expression, for example 0 9 * * *.\"\n />\n }\n @if (showScheduleInterval()) {\n <mt-number-field\n [ngModel]=\"numberValue(config()['intervalSeconds'])\"\n (ngModelChange)=\"onConfigFieldChange('intervalSeconds', $event)\"\n label=\"Interval seconds\"\n hint=\"Repeat interval in seconds.\"\n [min]=\"0\"\n />\n }\n @if (showScheduleOnce()) {\n <mt-date-field\n class=\"md:col-span-2\"\n [ngModel]=\"config()['runAt'] ?? config()['runAtUtc'] ?? config()['oneTimeAt'] ?? config()['at'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('runAt', $event)\"\n label=\"Run at UTC\"\n hint=\"UTC date/time for the one-time schedule.\"\n [showTime]=\"true\"\n [showClear]=\"true\"\n [pInputs]=\"dateTimePickerInputs\"\n />\n }\n @if (showScheduleStartDate()) {\n <mt-date-field\n [ngModel]=\"config()['startDate'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('startDate', $event)\"\n label=\"Start date UTC\"\n hint=\"Optional first allowed run time for this schedule.\"\n [showTime]=\"true\"\n [showClear]=\"true\"\n [pInputs]=\"dateTimePickerInputs\"\n />\n }\n <mt-select-field\n [ngModel]=\"config()['misfirePolicy'] ?? 'SkipMissed'\"\n (ngModelChange)=\"onConfigFieldChange('misfirePolicy', $event)\"\n label=\"Misfire policy\"\n hint=\"Backend behavior when a scheduled run is missed while the automation is unavailable.\"\n [options]=\"misfirePolicyOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n </div>\n <div class=\"mt-3 flex gap-2\">\n <mt-button size=\"small\" variant=\"outlined\" label=\"Validate\" (onClick)=\"validateSchedule()\" />\n <mt-button size=\"small\" severity=\"primary\" label=\"Preview next fire\" (onClick)=\"previewSchedule()\" />\n </div>\n @if (schedulePreview(); as preview) {\n <div class=\"mt-3 rounded-lg bg-(--p-surface-100) px-3 py-2 text-[12px]\">\n <strong class=\"text-emerald-600\">Valid</strong>\n <span class=\"ms-2\">Next fire: {{ preview.nextFireAtUtc ?? \"-\" }}</span>\n </div>\n }\n </section>\n }\n }\n @case (\"SetFields\") {\n @if (sectionInMain(\"setFields\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Set fields</div>\n <ng-container *ngTemplateOutlet=\"mapEditor; context: { objectKey: 'fields', rows: fieldsRows(), keyLabel: 'Field', valueLabel: 'Value', addLabel: 'Add field' }\" />\n </section>\n }\n }\n @case (\"If\") {\n @if (sectionInMain(\"condition\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Condition</div>\n <div class=\"grid gap-3 md:grid-cols-[1fr_180px_1fr]\">\n <mt-text-field\n [ngModel]=\"fieldText('left')\"\n (focusin)=\"setExpressionTarget('config:left')\"\n (ngModelChange)=\"onConfigFieldChange('left', $event)\"\n label=\"Left\"\n hint=\"Left-side value or expression to compare.\"\n />\n <mt-select-field\n [ngModel]=\"config()['operator'] ?? 'equals'\"\n (ngModelChange)=\"onConfigFieldChange('operator', $event)\"\n label=\"Operator\"\n hint=\"Comparison operator used by the backend condition evaluator.\"\n [options]=\"ifOperatorOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-text-field\n [ngModel]=\"fieldText('right')\"\n (focusin)=\"setExpressionTarget('config:right')\"\n (ngModelChange)=\"onConfigFieldChange('right', $event)\"\n label=\"Right\"\n hint=\"Right-side value or expression to compare against.\"\n />\n </div>\n <div class=\"mt-3 rounded-md border border-surface-200 bg-surface-0 p-3\">\n <div class=\"fp-ae-label mb-2\">Route outputs</div>\n <div class=\"flex flex-wrap gap-1.5\">\n @for (key of routeOutputKeys(); track key) {\n <span class=\"rounded-md bg-(--p-surface-100) px-2 py-1 font-mono text-[11px]\">{{ key }}</span>\n }\n </div>\n </div>\n </section>\n }\n }\n @case (\"HTTP\") {\n @if (sectionInMain(\"httpRequest\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">HTTP request</div>\n <div class=\"grid gap-3 md:grid-cols-[150px_1fr]\">\n <mt-select-field\n [ngModel]=\"config()['method'] ?? 'GET'\"\n (ngModelChange)=\"onConfigFieldChange('method', $event)\"\n label=\"Method\"\n hint=\"HTTP method used for the outbound request.\"\n [options]=\"httpMethodOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"config()['url'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:url')\"\n (ngModelChange)=\"onConfigFieldChange('url', $event)\"\n label=\"URL\"\n hint=\"Target URL. Expressions are supported for dynamic hosts, paths, and query values.\"\n />\n </div>\n <div class=\"mt-3\">\n <ng-container *ngTemplateOutlet=\"mapEditor; context: { objectKey: 'headers', rows: headerRows(), keyLabel: 'Header', valueLabel: 'Value', addLabel: 'Add header' }\" />\n </div>\n <div class=\"mt-3\">\n <ng-container *ngTemplateOutlet=\"mapEditor; context: { objectKey: 'query', rows: queryRows(), keyLabel: 'Query param', valueLabel: 'Value', addLabel: 'Add query param' }\" />\n </div>\n @if (supportsConfigKey('bodyMode')) {\n <mt-select-field\n class=\"mt-3\"\n [ngModel]=\"config()['bodyMode'] ?? 'json'\"\n (ngModelChange)=\"onConfigFieldChange('bodyMode', $event)\"\n label=\"Body mode\"\n hint=\"Backend-supported request body serialization mode.\"\n [options]=\"httpBodyModeOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n }\n <mt-textarea-field\n class=\"mt-3 w-full font-mono\"\n [ngModel]=\"valueText(config()['body'] ?? '')\"\n (focusin)=\"setExpressionTarget('config:body')\"\n (ngModelChange)=\"onConfigFieldChange('body', $event)\"\n label=\"Body\"\n hint=\"Request body sent by the HTTP node. Use JSON or expressions when the backend schema allows it.\"\n rows=\"6\"\n />\n @if (supportsConfigKey('timeoutSeconds') || supportsConfigKey('responseHandling')) {\n <div class=\"mt-3 grid gap-3 md:grid-cols-2\">\n @if (supportsConfigKey('timeoutSeconds')) {\n <mt-number-field\n [ngModel]=\"numberValue(config()['timeoutSeconds'])\"\n (ngModelChange)=\"onConfigFieldChange('timeoutSeconds', $event)\"\n label=\"Timeout seconds\"\n hint=\"Request timeout when exposed by the backend schema.\"\n [min]=\"0\"\n />\n }\n @if (supportsConfigKey('responseHandling')) {\n <mt-text-field\n [ngModel]=\"config()['responseHandling'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('responseHandling', $event)\"\n label=\"Response handling\"\n hint=\"Backend-supported response handling mode or expression.\"\n />\n }\n </div>\n }\n @if (sectionInMain(\"credentials\")) {\n <div class=\"mt-3 rounded-md border border-surface-200 bg-surface-0 p-3\">\n <div class=\"fp-ae-label mb-2\">Credential</div>\n <ng-container *ngTemplateOutlet=\"credentialSelector\" />\n </div>\n }\n </section>\n }\n }\n @case (\"Wait\") {\n @if (sectionInMain(\"wait\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Wait</div>\n <div class=\"grid gap-3 md:grid-cols-3\">\n <mt-select-field\n [ngModel]=\"config()['mode'] ?? 'duration'\"\n (ngModelChange)=\"onConfigFieldChange('mode', $event)\"\n label=\"Mode\"\n hint=\"Choose whether this wait uses a duration or a specific date/time.\"\n [options]=\"waitModeOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-number-field\n [ngModel]=\"numberValue(config()['durationSeconds'])\"\n (ngModelChange)=\"onConfigFieldChange('durationSeconds', $event)\"\n label=\"Duration seconds\"\n hint=\"How long execution should wait when mode is duration.\"\n [min]=\"0\"\n />\n <mt-date-field\n [ngModel]=\"config()['waitUntil'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('waitUntil', $event)\"\n label=\"Wait until\"\n hint=\"Date/time that resolves to when execution should resume.\"\n [showTime]=\"true\"\n [showClear]=\"true\"\n [pInputs]=\"dateTimePickerInputs\"\n />\n </div>\n @if (supportsConfigKey('resumePayloadSchema') || supportsConfigKey('resumePayload')) {\n <mt-textarea-field\n class=\"mt-3 w-full font-mono\"\n [ngModel]=\"valueText(config()['resumePayloadSchema'] ?? config()['resumePayload'] ?? '')\"\n (focusin)=\"setExpressionTarget('config:resumePayload')\"\n (ngModelChange)=\"onConfigFieldChange('resumePayload', $event)\"\n label=\"Resume payload\"\n hint=\"Expected payload when the backend supports manual or external resume data.\"\n rows=\"5\"\n />\n }\n </section>\n }\n }\n @case (\"HumanApproval\") {\n @if (sectionInMain(\"approvalTask\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Approval task</div>\n @if (assignmentOptions()?.providerStatus && assignmentOptions()?.providerStatus !== \"Available\") {\n <p class=\"fp-ae-copy\">\n Assignment provider status: {{ assignmentOptions()?.providerStatus }}.\n The backend provider is the source of truth for available assignees.\n </p>\n }\n <div class=\"grid gap-3 xl:grid-cols-3\">\n <mt-text-field\n [ngModel]=\"config()['title'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:title')\"\n (ngModelChange)=\"onConfigFieldChange('title', $event)\"\n label=\"Approval title\"\n hint=\"Approval title shown to the assigned human approver.\"\n />\n <mt-select-field\n [ngModel]=\"selectedAssignmentKey()\"\n (ngModelChange)=\"onAssignmentOptionChange($event)\"\n label=\"Assignment\"\n hint=\"Backend-provided assignee, role, or group that can decide this approval.\"\n [options]=\"assignmentSelectOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n @if (humanApprovalSupportsConfig('priority')) {\n <mt-text-field\n [ngModel]=\"config()['priority'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('priority', $event)\"\n label=\"Priority\"\n hint=\"Approval priority when supported by the backend schema.\"\n />\n }\n </div>\n <mt-textarea-field\n class=\"mt-3 w-full\"\n [ngModel]=\"config()['message'] ?? config()['instructions'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:message')\"\n (ngModelChange)=\"onConfigFieldChange('message', $event)\"\n label=\"Approval message\"\n hint=\"Decision instructions shown to the approver.\"\n rows=\"4\"\n />\n @if (humanApprovalSupportsConfig('dueDate') || humanApprovalSupportsConfig('dueInSeconds') || humanApprovalSupportsConfig('timeoutSeconds') || humanApprovalSupportsConfig('expiresAt') || humanApprovalSupportsConfig('commentsRequired') || humanApprovalSupportsConfig('allowReturn')) {\n <div class=\"mt-3 grid gap-3 xl:grid-cols-4\">\n @if (humanApprovalSupportsConfig('dueDate')) {\n <mt-date-field\n [ngModel]=\"config()['dueDate'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('dueDate', $event)\"\n label=\"Due date\"\n hint=\"Backend-supported approval due date.\"\n [showTime]=\"true\"\n [showClear]=\"true\"\n [pInputs]=\"dateTimePickerInputs\"\n />\n }\n @if (humanApprovalSupportsConfig('dueInSeconds')) {\n <mt-number-field\n [ngModel]=\"numberValue(config()['dueInSeconds'])\"\n (ngModelChange)=\"onConfigFieldChange('dueInSeconds', $event)\"\n label=\"Due in seconds\"\n hint=\"Relative approval due duration when supported by the backend schema.\"\n [min]=\"0\"\n />\n }\n @if (humanApprovalSupportsConfig('timeoutSeconds')) {\n <mt-number-field\n [ngModel]=\"numberValue(config()['timeoutSeconds'])\"\n (ngModelChange)=\"onConfigFieldChange('timeoutSeconds', $event)\"\n label=\"Timeout seconds\"\n hint=\"Approval timeout emitted according to backend approval runtime support.\"\n [min]=\"0\"\n />\n }\n @if (humanApprovalSupportsConfig('expiresAt')) {\n <mt-date-field\n [ngModel]=\"config()['expiresAt'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('expiresAt', $event)\"\n label=\"Expires at\"\n hint=\"Backend-supported approval expiry timestamp.\"\n [showTime]=\"true\"\n [showClear]=\"true\"\n [pInputs]=\"dateTimePickerInputs\"\n />\n }\n @if (humanApprovalSupportsConfig('commentsRequired')) {\n <div class=\"flex min-h-[4.25rem] items-end rounded-md border border-surface-200 bg-surface-0 px-3 py-2\">\n <mt-toggle-field\n size=\"small\"\n label=\"Comments required\"\n labelPosition=\"end\"\n [ngModel]=\"config()['commentsRequired'] === true\"\n (ngModelChange)=\"onConfigFieldChange('commentsRequired', $event === true)\"\n hint=\"Require approver comments when the backend supports this flag.\"\n />\n </div>\n }\n @if (humanApprovalSupportsConfig('allowReturn')) {\n <div class=\"flex min-h-[4.25rem] items-end rounded-md border border-surface-200 bg-surface-0 px-3 py-2\">\n <mt-toggle-field\n size=\"small\"\n label=\"Allow return for changes\"\n labelPosition=\"end\"\n [ngModel]=\"config()['allowReturn'] !== false\"\n (ngModelChange)=\"onConfigFieldChange('allowReturn', $event === true)\"\n hint=\"Allow ReturnForChanges when the backend schema exposes this option.\"\n />\n </div>\n }\n </div>\n }\n <div class=\"mt-3 space-y-3\">\n <div class=\"fp-ae-section-title\">Decision options</div>\n <div class=\"overflow-hidden rounded-md border border-surface-200 bg-surface-0\">\n @if (selectedApprovalDecisionRows().length > 0) {\n <div class=\"hidden border-b border-surface-200 bg-surface-50 px-3 py-2 text-[12px] font-semibold text-(--p-text-muted-color) xl:grid xl:grid-cols-[1.1fr_1fr_1.2fr_80px_44px] xl:gap-3\">\n <span>Decision label</span>\n <span>Canonical value</span>\n <span>Route output key</span>\n <span>Routes</span>\n <span>Action</span>\n </div>\n @for (decision of selectedApprovalDecisionRows(); track decision.value) {\n <div class=\"grid gap-2 border-b border-surface-100 px-3 py-3 last:border-b-0 xl:grid-cols-[1.1fr_1fr_1.2fr_80px_44px] xl:items-center xl:gap-3\">\n <div class=\"min-w-0\">\n <div class=\"text-[11px] font-semibold text-(--p-text-muted-color) xl:hidden\">Decision label</div>\n <div class=\"truncate text-[13px] font-semibold text-(--p-text-color)\">{{ decision.label }}</div>\n </div>\n <div class=\"min-w-0\">\n <div class=\"text-[11px] font-semibold text-(--p-text-muted-color) xl:hidden\">Canonical value</div>\n <div class=\"truncate font-mono text-[12px] text-(--p-text-color)\">{{ decision.value }}</div>\n </div>\n <div class=\"min-w-0\">\n <div class=\"text-[11px] font-semibold text-(--p-text-muted-color) xl:hidden\">Route output key</div>\n <div class=\"truncate font-mono text-[12px] text-(--p-text-muted-color)\">{{ decision.routeOutputKey }}</div>\n </div>\n <div>\n <div class=\"text-[11px] font-semibold text-(--p-text-muted-color) xl:hidden\">Routes</div>\n <div class=\"text-[13px] text-(--p-text-color)\">{{ decision.routeCount }}</div>\n </div>\n <mt-button\n class=\"justify-self-start xl:justify-self-end\"\n size=\"small\"\n variant=\"outlined\"\n severity=\"danger\"\n icon=\"general.trash-01\"\n tooltip=\"Remove decision option\"\n (onClick)=\"removeApprovalDecision(decision.value)\"\n />\n </div>\n }\n } @else {\n <div class=\"px-3 py-4 text-[12px] text-(--p-text-muted-color)\">\n Add at least one backend-supported approval decision.\n </div>\n }\n </div>\n @if (approvalDecisionIssues().length > 0) {\n <div class=\"rounded-lg border border-[rgb(var(--fp-error))]/30 bg-[rgb(var(--fp-error))]/10 px-3 py-2 text-[12px] leading-5 text-[rgb(var(--fp-error))]\">\n @for (issue of approvalDecisionIssues(); track issue) {\n <div>{{ issue }}</div>\n }\n </div>\n }\n <div class=\"grid gap-2 md:grid-cols-[minmax(0,1fr)_auto]\">\n <mt-select-field\n [ngModel]=\"approvalDecisionToAdd()\"\n (ngModelChange)=\"approvalDecisionToAdd.set($event)\"\n label=\"Decision to add\"\n hint=\"Only backend-supported decisions with matching route outputs are available.\"\n [options]=\"addableApprovalDecisionOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-button\n class=\"self-end\"\n size=\"small\"\n variant=\"outlined\"\n icon=\"general.plus\"\n label=\"Add decision option\"\n [disabled]=\"!approvalDecisionToAdd()\"\n (onClick)=\"addApprovalDecision()\"\n />\n </div>\n </div>\n @if (humanApprovalSupportsConfig('payload') || humanApprovalSupportsConfig('context') || humanApprovalSupportsConfig('metadata')) {\n <div class=\"mt-3 grid gap-3\">\n @if (humanApprovalSupportsConfig('payload')) {\n <mt-textarea-field\n class=\"w-full font-mono\"\n [ngModel]=\"valueText(config()['payload'] ?? '')\"\n (focusin)=\"setExpressionTarget('config:payload')\"\n (ngModelChange)=\"onConfigFieldChange('payload', $event)\"\n label=\"Payload\"\n hint=\"Approval task payload or context fields supported by backend schema.\"\n rows=\"5\"\n />\n }\n @if (humanApprovalSupportsConfig('context')) {\n <mt-textarea-field\n class=\"w-full font-mono\"\n [ngModel]=\"valueText(config()['context'] ?? '')\"\n (focusin)=\"setExpressionTarget('config:context')\"\n (ngModelChange)=\"onConfigFieldChange('context', $event)\"\n label=\"Context\"\n hint=\"Additional approval context supported by backend schema.\"\n rows=\"5\"\n />\n }\n @if (humanApprovalSupportsConfig('metadata')) {\n <mt-textarea-field\n class=\"w-full font-mono\"\n [ngModel]=\"valueText(config()['metadata'] ?? '')\"\n (focusin)=\"setExpressionTarget('config:metadata')\"\n (ngModelChange)=\"onConfigFieldChange('metadata', $event)\"\n label=\"Metadata\"\n hint=\"Additional approval metadata supported by the backend schema.\"\n rows=\"4\"\n />\n }\n </div>\n }\n <div class=\"mt-3 flex gap-2\">\n <mt-button size=\"small\" variant=\"outlined\" label=\"Validate assignment\" (onClick)=\"validateAssignment()\" />\n </div>\n @if (assignmentValidation(); as result) {\n <div class=\"mt-3 rounded-lg bg-(--p-surface-100) px-3 py-2 text-[12px]\">\n {{ result.isValid === false ? \"Assignment invalid\" : \"Assignment accepted by helper\" }}\n @for (issue of resultIssues(result); track issue) {\n <div class=\"mt-1 text-[11px] text-(--p-text-muted-color)\">{{ issue }}</div>\n }\n </div>\n }\n @if (sectionInMain(\"approvalOutput\")) {\n <div class=\"mt-3 rounded-md border border-surface-200 bg-surface-0 p-3\">\n <div class=\"fp-ae-label mb-2\">Output data</div>\n <div class=\"flex flex-wrap gap-1.5\">\n @for (field of approvalOutputFieldLabels; track field) {\n <span class=\"rounded-md bg-(--p-surface-100) px-2 py-1 font-mono text-[11px]\">{{ field }}</span>\n }\n </div>\n <div class=\"mt-3 fp-ae-label mb-2\">Approval routes</div>\n <div class=\"flex flex-wrap gap-1.5\">\n @for (key of routeOutputKeys(); track key) {\n <span class=\"rounded-md bg-(--p-surface-100) px-2 py-1 font-mono text-[11px]\">{{ key }}</span>\n }\n </div>\n </div>\n }\n </section>\n }\n }\n @case (\"FlowPlusCommit\") {\n @if (sectionInMain(\"flowplusCommit\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">FlowPlus commit</div>\n <p class=\"fp-ae-copy\">\n This node is the explicit module-data write boundary. Approval does\n not commit data unless this node is reached.\n </p>\n <div class=\"grid gap-3 md:grid-cols-2\">\n <mt-select-field\n [ngModel]=\"config()['targetModule'] ?? ''\"\n (ngModelChange)=\"onModuleChange($event)\"\n label=\"Module\"\n hint=\"Module whose records will be written by this explicit FlowPlus commit.\"\n [options]=\"moduleOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-select-field\n [ngModel]=\"config()['operation'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('operation', $event)\"\n label=\"Operation\"\n hint=\"Write operation supported by the selected backend module schema.\"\n [options]=\"operationOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n </div>\n <mt-text-field\n class=\"mt-3 font-mono\"\n [ngModel]=\"config()['idempotencyKey'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:idempotencyKey')\"\n (ngModelChange)=\"onConfigFieldChange('idempotencyKey', $event)\"\n label=\"Idempotency key\"\n hint=\"Optional stable key used by the backend to prevent duplicate writes.\"\n />\n @if (selectedModuleFields().length) {\n <div class=\"mt-3 space-y-2\">\n <div class=\"fp-ae-section-title\">Module schema</div>\n <div class=\"grid gap-2 md:grid-cols-2\">\n @for (field of selectedModuleFields(); track field.key) {\n <div class=\"fp-ae-kv\">\n <span>{{ field.displayName ?? field.key }}</span>\n <strong>{{ field.viewType ?? \"Value\" }} @if (field.required) { * }</strong>\n </div>\n }\n </div>\n </div>\n }\n <div class=\"mt-3\">\n <ng-container *ngTemplateOutlet=\"mapEditor; context: { objectKey: 'mapping', rows: mappingRows(), keyLabel: 'Module field', valueLabel: 'Expression / value', addLabel: 'Add module field' }\" />\n </div>\n <div class=\"mt-3 flex gap-2\">\n <mt-button size=\"small\" severity=\"primary\" label=\"Validate mapping\" (onClick)=\"validateCommitMapping()\" />\n </div>\n @if (commitValidation(); as result) {\n <div class=\"mt-3 rounded-lg bg-(--p-surface-100) px-3 py-2 text-[12px]\">\n {{ result.isValid === false ? \"Mapping invalid\" : \"Mapping accepted by helper\" }}\n @for (issue of resultIssues(result); track issue) {\n <div class=\"mt-1 text-[11px] text-(--p-text-muted-color)\">{{ issue }}</div>\n }\n </div>\n }\n @if (sectionInMain(\"credentials\")) {\n <div class=\"mt-3 rounded-md border border-surface-200 bg-surface-0 p-3\">\n <div class=\"fp-ae-label mb-2\">Credential</div>\n <ng-container *ngTemplateOutlet=\"credentialSelector\" />\n </div>\n }\n </section>\n }\n }\n @case (\"WebhookResponse\") {\n @if (sectionInMain(\"webhookResponse\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Webhook response</div>\n <div class=\"grid gap-3 md:grid-cols-2\">\n <mt-number-field\n [ngModel]=\"numberValue(config()['statusCode']) ?? 200\"\n (ngModelChange)=\"onConfigFieldChange('statusCode', $event)\"\n label=\"Status code\"\n hint=\"HTTP status code sent back by the webhook response node.\"\n [min]=\"100\"\n [max]=\"599\"\n />\n <mt-select-field\n [ngModel]=\"config()['responseMode'] ?? 'json'\"\n (ngModelChange)=\"onConfigFieldChange('responseMode', $event)\"\n label=\"Response mode\"\n hint=\"How the webhook response body should be serialized.\"\n [options]=\"responseModeOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n </div>\n <div class=\"mt-3\">\n <ng-container *ngTemplateOutlet=\"mapEditor; context: { objectKey: 'headers', rows: headerRows(), keyLabel: 'Header', valueLabel: 'Value', addLabel: 'Add header' }\" />\n </div>\n <mt-textarea-field\n class=\"mt-3 w-full font-mono\"\n [ngModel]=\"valueText(config()['body'] ?? '')\"\n (focusin)=\"setExpressionTarget('config:body')\"\n (ngModelChange)=\"onConfigFieldChange('body', $event)\"\n label=\"Body\"\n hint=\"Body returned to the webhook caller. Expressions can use current execution data.\"\n rows=\"6\"\n />\n </section>\n }\n }\n @case (\"CallAutomation\") {\n @if (sectionInMain(\"callAutomation\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Subworkflow</div>\n <div class=\"grid gap-3 md:grid-cols-2\">\n <mt-select-field\n [ngModel]=\"config()['targetAutomationId'] ?? ''\"\n (ngModelChange)=\"onSubworkflowTargetChange($event)\"\n label=\"Target automation\"\n hint=\"Backend-provided automation candidate. Tenant and recursion validation stay on the backend.\"\n [options]=\"subworkflowAutomationOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-select-field\n [ngModel]=\"config()['revisionMode'] ?? 'Active'\"\n (ngModelChange)=\"onConfigFieldChange('revisionMode', $event)\"\n label=\"Revision mode\"\n hint=\"Choose which published or active child revision to call.\"\n [options]=\"revisionModeOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n @if (config()['revisionMode'] === \"SpecificRevision\") {\n <mt-text-field\n [ngModel]=\"config()['specificRevisionId'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('specificRevisionId', $event)\"\n label=\"Specific revision id\"\n hint=\"Required only when revision mode is Specific revision.\"\n />\n }\n <mt-toggle-field\n size=\"small\"\n labelPosition=\"end\"\n [ngModel]=\"config()['waitForCompletion'] !== false\"\n (ngModelChange)=\"onConfigFieldChange('waitForCompletion', $event === true)\"\n label=\"Wait for completion\"\n hint=\"When disabled, the parent continues after dispatch and backend records the child linkage.\"\n />\n </div>\n <div class=\"mt-3 grid gap-3 md:grid-cols-2\">\n <mt-textarea-field\n class=\"w-full font-mono md:col-span-2\"\n [ngModel]=\"valueText(config()['inputMappingJson'] ?? '{}')\"\n (focusin)=\"setExpressionTarget('config:inputMappingJson')\"\n (ngModelChange)=\"onConfigFieldChange('inputMappingJson', $event)\"\n label=\"Input mapping JSON\"\n hint=\"JSON object or expression map sent into the child automation input.\"\n rows=\"7\"\n />\n <mt-textarea-field\n class=\"w-full font-mono md:col-span-2\"\n [ngModel]=\"valueText(config()['outputMappingJson'] ?? '{}')\"\n (focusin)=\"setExpressionTarget('config:outputMappingJson')\"\n (ngModelChange)=\"onConfigFieldChange('outputMappingJson', $event)\"\n label=\"Output mapping JSON\"\n hint=\"JSON object or expression map for child output/status returned to the parent.\"\n rows=\"7\"\n />\n </div>\n </section>\n }\n }\n @case (\"Subworkflow\") {\n @if (sectionInMain(\"callAutomation\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Subworkflow</div>\n <div class=\"grid gap-3 md:grid-cols-2\">\n <mt-select-field\n [ngModel]=\"config()['targetAutomationId'] ?? ''\"\n (ngModelChange)=\"onSubworkflowTargetChange($event)\"\n label=\"Target automation\"\n hint=\"Backend-provided automation candidate. Tenant and recursion validation stay on the backend.\"\n [options]=\"subworkflowAutomationOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-select-field\n [ngModel]=\"config()['revisionMode'] ?? 'Active'\"\n (ngModelChange)=\"onConfigFieldChange('revisionMode', $event)\"\n label=\"Revision mode\"\n hint=\"Choose which published or active child revision to call.\"\n [options]=\"revisionModeOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n @if (config()['revisionMode'] === \"SpecificRevision\") {\n <mt-text-field\n [ngModel]=\"config()['specificRevisionId'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('specificRevisionId', $event)\"\n label=\"Specific revision id\"\n hint=\"Required only when revision mode is Specific revision.\"\n />\n }\n <mt-toggle-field\n size=\"small\"\n labelPosition=\"end\"\n [ngModel]=\"config()['waitForCompletion'] !== false\"\n (ngModelChange)=\"onConfigFieldChange('waitForCompletion', $event === true)\"\n label=\"Wait for completion\"\n hint=\"When disabled, the parent continues after dispatch and backend records the child linkage.\"\n />\n </div>\n <div class=\"mt-3 grid gap-3 md:grid-cols-2\">\n <mt-textarea-field\n class=\"w-full font-mono md:col-span-2\"\n [ngModel]=\"valueText(config()['inputMappingJson'] ?? '{}')\"\n (focusin)=\"setExpressionTarget('config:inputMappingJson')\"\n (ngModelChange)=\"onConfigFieldChange('inputMappingJson', $event)\"\n label=\"Input mapping JSON\"\n hint=\"JSON object or expression map sent into the child automation input.\"\n rows=\"7\"\n />\n <mt-textarea-field\n class=\"w-full font-mono md:col-span-2\"\n [ngModel]=\"valueText(config()['outputMappingJson'] ?? '{}')\"\n (focusin)=\"setExpressionTarget('config:outputMappingJson')\"\n (ngModelChange)=\"onConfigFieldChange('outputMappingJson', $event)\"\n label=\"Output mapping JSON\"\n hint=\"JSON object or expression map for child output/status returned to the parent.\"\n rows=\"7\"\n />\n </div>\n </section>\n }\n }\n @case (\"ParallelStart\") {\n @if (sectionInMain(\"parallelStart\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"flex items-center justify-between gap-3 border-b border-surface-200 bg-surface-50 px-4 py-3\">\n <div class=\"fp-ae-section-title !m-0\">Parallel start</div>\n <mt-button size=\"small\" variant=\"outlined\" icon=\"general.plus\" label=\"Add branch\" (onClick)=\"addParallelBranch()\" />\n </div>\n <div class=\"space-y-3 p-4\">\n @for (branch of parallelBranches(); track branch.key; let i = $index) {\n <div class=\"rounded-md border border-surface-200 bg-surface-0 p-3\">\n <div class=\"grid gap-3 md:grid-cols-[160px_1fr_1fr_auto]\">\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"branch.key\"\n (ngModelChange)=\"updateParallelBranch(i, 'key', $event)\"\n label=\"Branch key\"\n hint=\"Persisted route key. Do not use array index names.\"\n />\n <mt-text-field\n [ngModel]=\"branch.label\"\n (ngModelChange)=\"updateParallelBranch(i, 'label', $event)\"\n label=\"Label\"\n hint=\"Visual branch label. Changing it does not change existing routes.\"\n />\n <mt-text-field\n [ngModel]=\"branch.description\"\n (ngModelChange)=\"updateParallelBranch(i, 'description', $event)\"\n label=\"Description\"\n hint=\"Optional branch note for reviewers.\"\n />\n <div class=\"flex items-end gap-1\">\n <mt-button size=\"small\" variant=\"outlined\" icon=\"arrow.arrow-up\" tooltip=\"Move up\" (onClick)=\"moveParallelBranch(i, -1)\" />\n <mt-button size=\"small\" variant=\"outlined\" icon=\"arrow.arrow-down\" tooltip=\"Move down\" (onClick)=\"moveParallelBranch(i, 1)\" />\n <mt-button size=\"small\" variant=\"outlined\" severity=\"danger\" icon=\"general.trash-01\" tooltip=\"Remove branch\" (onClick)=\"removeParallelBranch(i)\" />\n </div>\n </div>\n <div class=\"mt-3 flex flex-wrap items-center gap-1.5 text-[11px]\">\n <span class=\"inline-flex h-7 items-center rounded-md border border-surface-200 bg-surface-50 px-2 font-mono font-semibold text-(--p-text-color)\">{{ branch.key }}</span>\n <span class=\"inline-flex h-7 items-center rounded-md border border-surface-200 bg-surface-0 px-2 font-medium text-(--p-text-muted-color)\">{{ branch.routeCount }} connected route{{ branch.routeCount === 1 ? \"\" : \"s\" }}</span>\n </div>\n </div>\n }\n @if (parallelBranches().length === 0) {\n <p class=\"fp-ae-copy\">Add at least two stable branch keys. Backend validation blocks publish until branches are valid.</p>\n }\n </div>\n </section>\n }\n }\n @case (\"ParallelJoin\") {\n @if (sectionInMain(\"parallelJoin\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Parallel join</div>\n <div class=\"grid gap-3 md:grid-cols-2\">\n <mt-select-field\n [ngModel]=\"config()['joinPolicy'] ?? 'All'\"\n (ngModelChange)=\"onConfigFieldChange('joinPolicy', $event)\"\n label=\"Join policy\"\n hint=\"Backend policy used to decide when branch wait is complete.\"\n [options]=\"joinPolicyOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n @if (config()['joinPolicy'] === \"Threshold\") {\n <mt-number-field\n [ngModel]=\"numberValue(config()['threshold'])\"\n (ngModelChange)=\"onConfigFieldChange('threshold', $event)\"\n label=\"Threshold\"\n hint=\"Minimum completed branch count required for Threshold policy.\"\n [min]=\"1\"\n />\n }\n <mt-select-field\n [ngModel]=\"config()['aggregationStrategy'] ?? 'MergeObjects'\"\n (ngModelChange)=\"onConfigFieldChange('aggregationStrategy', $event)\"\n label=\"Aggregation strategy\"\n hint=\"How backend joins branch outputs into the join node output.\"\n [options]=\"aggregationStrategyOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"config()['outputTargetPath'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:outputTargetPath')\"\n (ngModelChange)=\"onConfigFieldChange('outputTargetPath', $event)\"\n label=\"Output target path\"\n hint=\"Optional context path where joined output should be written.\"\n />\n </div>\n </section>\n }\n }\n @case (\"Switch\") {\n @if (sectionInMain(\"switch\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Switch</div>\n <div class=\"grid gap-4\">\n <div class=\"grid items-end gap-3 md:grid-cols-[minmax(0,1fr)_minmax(280px,0.7fr)]\">\n <mt-select-field\n [ngModel]=\"config()['mode'] ?? 'value'\"\n (ngModelChange)=\"onConfigFieldChange('mode', $event)\"\n label=\"Mode\"\n hint=\"Backend evaluation mode: expression, rules, or source-value matching.\"\n [options]=\"switchModeOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-toggle-field\n class=\"self-end pb-1\"\n size=\"small\"\n labelPosition=\"end\"\n [ngModel]=\"config()['firstMatch'] !== false\"\n (ngModelChange)=\"onConfigFieldChange('firstMatch', $event === true)\"\n label=\"First match\"\n hint=\"Use the first matching case unless backend explicitly supports multi-match.\"\n />\n </div>\n @if (showSwitchSourceValue() || showSwitchExpression()) {\n <div class=\"grid gap-3 md:grid-cols-2\">\n @if (showSwitchSourceValue()) {\n <mt-text-field\n [class]=\"showSwitchExpression() ? 'font-mono' : 'font-mono md:col-span-2'\"\n [ngModel]=\"config()['sourceValue'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:sourceValue')\"\n (ngModelChange)=\"onConfigFieldChange('sourceValue', $event)\"\n label=\"Source value\"\n hint=\"Value or expression used for value matching.\"\n />\n }\n @if (showSwitchExpression()) {\n <mt-text-field\n [class]=\"showSwitchSourceValue() ? 'font-mono' : 'font-mono md:col-span-2'\"\n [ngModel]=\"config()['expression'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:expression')\"\n (ngModelChange)=\"onConfigFieldChange('expression', $event)\"\n label=\"Expression\"\n hint=\"Expression evaluated when mode is expression or rules.\"\n />\n }\n </div>\n }\n <div class=\"grid items-end gap-3 md:grid-cols-[minmax(0,1fr)_minmax(280px,0.7fr)]\">\n @if (showSwitchDefaultOutputKey()) {\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"config()['defaultOutputKey'] ?? 'default'\"\n (ngModelChange)=\"onConfigFieldChange('defaultOutputKey', $event)\"\n label=\"Default output key\"\n hint=\"Stable default route key used when no case matches.\"\n />\n }\n <mt-toggle-field\n [class]=\"showSwitchDefaultOutputKey() ? 'self-end pb-1' : 'self-end pb-1 md:col-span-2'\"\n size=\"small\"\n labelPosition=\"end\"\n [ngModel]=\"config()['includeDefaultOutput'] !== false\"\n (ngModelChange)=\"onConfigFieldChange('includeDefaultOutput', $event === true)\"\n label=\"Include default output\"\n hint=\"Expose the default route output key in the canvas.\"\n />\n </div>\n </div>\n </section>\n <section class=\"fp-ae-panel\">\n <div class=\"flex items-center justify-between gap-3 border-b border-surface-200 bg-surface-50 px-4 py-3\">\n <div class=\"fp-ae-section-title !m-0\">Switch cases</div>\n <mt-button size=\"small\" variant=\"outlined\" icon=\"general.plus\" label=\"Add case\" (onClick)=\"addSwitchCase()\" />\n </div>\n <div class=\"space-y-3 p-4\">\n @for (item of switchCases(); track item.key; let i = $index) {\n <div class=\"rounded-md border border-surface-200 bg-surface-0 p-3\">\n <div class=\"grid gap-3 md:grid-cols-[160px_1fr_1fr_auto]\">\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"item.key\"\n (ngModelChange)=\"updateSwitchCase(i, 'key', $event)\"\n label=\"Case key\"\n hint=\"Persisted stable key. The route output becomes case_key.\"\n />\n <mt-text-field\n [ngModel]=\"item.label\"\n (ngModelChange)=\"updateSwitchCase(i, 'label', $event)\"\n label=\"Label\"\n hint=\"Visual case label. Routes stay attached to the case key.\"\n />\n <mt-text-field\n [ngModel]=\"item.value\"\n (focusin)=\"setExpressionTarget('config:cases:' + item.key + ':value')\"\n (ngModelChange)=\"updateSwitchCase(i, 'value', $event)\"\n label=\"Value\"\n hint=\"Expected value for value matching.\"\n />\n <div class=\"flex items-end gap-1\">\n <mt-button size=\"small\" variant=\"outlined\" icon=\"arrow.arrow-up\" tooltip=\"Move up\" (onClick)=\"moveSwitchCase(i, -1)\" />\n <mt-button size=\"small\" variant=\"outlined\" icon=\"arrow.arrow-down\" tooltip=\"Move down\" (onClick)=\"moveSwitchCase(i, 1)\" />\n <mt-button size=\"small\" variant=\"outlined\" severity=\"danger\" icon=\"general.trash-01\" tooltip=\"Remove case\" (onClick)=\"removeSwitchCase(i)\" />\n </div>\n </div>\n <div class=\"mt-3 grid gap-3 md:grid-cols-2\">\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"item.condition\"\n (focusin)=\"setExpressionTarget('config:cases:' + item.key + ':condition')\"\n (ngModelChange)=\"updateSwitchCase(i, 'condition', $event)\"\n label=\"Condition\"\n hint=\"Optional condition for rule matching.\"\n />\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"item.expression\"\n (focusin)=\"setExpressionTarget('config:cases:' + item.key + ':expression')\"\n (ngModelChange)=\"updateSwitchCase(i, 'expression', $event)\"\n label=\"Case expression\"\n hint=\"Optional expression for this case.\"\n />\n </div>\n <div class=\"mt-3 flex flex-wrap items-center gap-1.5 text-[11px]\">\n <span class=\"inline-flex h-7 items-center rounded-md border border-surface-200 bg-surface-50 px-2 font-mono font-semibold text-(--p-text-color)\">{{ item.routeKey }}</span>\n <span class=\"inline-flex h-7 items-center rounded-md border border-surface-200 bg-surface-0 px-2 font-medium text-(--p-text-muted-color)\">{{ item.routeCount }} connected route{{ item.routeCount === 1 ? \"\" : \"s\" }}</span>\n <span class=\"inline-flex h-7 items-center rounded-md border border-surface-200 bg-surface-0 px-2 font-medium text-(--p-text-muted-color)\">Visual order {{ i + 1 }}</span>\n </div>\n </div>\n }\n @if (switchCases().length === 0) {\n <p class=\"fp-ae-copy\">Add at least one stable case key. Backend validation points routes at missing case keys if a case is deleted.</p>\n }\n </div>\n </section>\n }\n }\n @case (\"Stop\") {\n @if (sectionInMain(\"stop\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">End execution</div>\n @if (supportsConfigKey('status') || supportsConfigKey('message') || supportsConfigKey('output')) {\n <div class=\"grid gap-3 md:grid-cols-2\">\n @if (supportsConfigKey('status')) {\n <mt-text-field\n [ngModel]=\"config()['status'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('status', $event)\"\n label=\"Result status\"\n hint=\"Terminal status emitted by the backend stop node.\"\n />\n }\n @if (supportsConfigKey('message')) {\n <mt-text-field\n [ngModel]=\"config()['message'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:message')\"\n (ngModelChange)=\"onConfigFieldChange('message', $event)\"\n label=\"Message\"\n hint=\"Optional message or reason saved with the terminal result.\"\n />\n }\n @if (supportsConfigKey('output')) {\n <mt-textarea-field\n class=\"w-full font-mono md:col-span-2\"\n [ngModel]=\"valueText(config()['output'] ?? '')\"\n (focusin)=\"setExpressionTarget('config:output')\"\n (ngModelChange)=\"onConfigFieldChange('output', $event)\"\n label=\"Output payload\"\n hint=\"Terminal output payload when exposed by the backend schema.\"\n rows=\"5\"\n />\n }\n </div>\n } @else {\n <p class=\"fp-ae-copy\">\n This terminal node has no backend-exposed settings. It ends execution when reached.\n </p>\n }\n </section>\n }\n }\n }\n\n @if (sectionInMain(\"backendSchema\") && step() && editorType() !== \"HumanApproval\") {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Output data</div>\n @if (outputFields().length > 0) {\n <div class=\"grid gap-2 md:grid-cols-2\">\n @for (field of outputFields(); track field.key) {\n <div class=\"fp-ae-kv\">\n <span>{{ field.label }}</span>\n <strong>{{ field.type }}</strong>\n </div>\n }\n </div>\n }\n @if (routeOutputKeys().length > 0) {\n <div class=\"mt-3 fp-ae-label mb-2\">Route outputs</div>\n <div class=\"flex flex-wrap gap-1.5\">\n @for (key of routeOutputKeys(); track key) {\n <span class=\"rounded-md bg-(--p-surface-100) px-2 py-1 font-mono text-[11px]\">{{ key }}</span>\n }\n </div>\n }\n </section>\n }\n\n @if (sectionInAdvanced(\"mapping\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Request and result mapping</div>\n <div class=\"grid gap-3 md:grid-cols-2\">\n <div>\n <div class=\"fp-ae-label mb-2\">Request data</div>\n @for (row of inputMappingRows(); track row.key) {\n <label class=\"mb-2 block space-y-1\">\n <span class=\"text-[11px] text-(--p-text-muted-color)\">{{ row.key }}</span>\n <mt-text-field\n [ngModel]=\"valueText(row.value)\"\n (focusin)=\"setExpressionTarget('config:inputMapping:' + row.key)\"\n (ngModelChange)=\"updateJsonField('inputMappingJson', row.key, $event)\"\n hint=\"Expression or literal value mapped into this node input.\"\n />\n </label>\n }\n </div>\n <div>\n <div class=\"fp-ae-label mb-2\">Output data</div>\n @for (row of outputMappingRows(); track row.key) {\n <label class=\"mb-2 block space-y-1\">\n <span class=\"text-[11px] text-(--p-text-muted-color)\">{{ row.key }}</span>\n <mt-text-field\n [ngModel]=\"valueText(row.value)\"\n (focusin)=\"setExpressionTarget('config:outputMapping:' + row.key)\"\n (ngModelChange)=\"updateJsonField('outputMappingJson', row.key, $event)\"\n hint=\"Expression or literal value mapped from this node output.\"\n />\n </label>\n }\n </div>\n </div>\n @if (\n sectionHasAdvancedEmptyState(\"mapping\") &&\n inputMappingRows().length === 0 &&\n outputMappingRows().length === 0\n ) {\n <p class=\"fp-ae-copy mt-3\">\n This node is using backend default request and output data. Add mappings only when this step needs a custom payload.\n </p>\n }\n </section>\n }\n\n @if (sectionInAdvanced(\"credentials\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Auth and credentials</div>\n <ng-container *ngTemplateOutlet=\"credentialSelector\" />\n </section>\n }\n\n @if (sectionInAdvanced(\"policy\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Retry, timeout, error policy</div>\n <div class=\"grid gap-3 md:grid-cols-3\">\n <mt-number-field\n [ngModel]=\"numberValue(policyObject('timeoutPolicyJson')['timeoutSeconds'])\"\n (ngModelChange)=\"updateJsonField('timeoutPolicyJson', 'timeoutSeconds', $event)\"\n label=\"Timeout seconds\"\n hint=\"Maximum runtime before the backend times out this node.\"\n [min]=\"0\"\n />\n <mt-number-field\n [ngModel]=\"numberValue(policyObject('retryPolicyJson')['maxAttempts'])\"\n (ngModelChange)=\"updateJsonField('retryPolicyJson', 'maxAttempts', $event)\"\n label=\"Max attempts\"\n hint=\"Maximum retry attempts after retryable failures.\"\n [min]=\"0\"\n />\n <mt-select-field\n [ngModel]=\"policyObject('errorPolicyJson')['onFailure'] ?? ''\"\n (ngModelChange)=\"updateJsonField('errorPolicyJson', 'onFailure', $event)\"\n label=\"On failure\"\n hint=\"Backend error policy to apply when this node fails.\"\n [options]=\"errorPolicyOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n </div>\n @if (sectionHasAdvancedEmptyState(\"policy\")) {\n <p class=\"fp-ae-copy mt-3\">\n Backend defaults apply until you override a timeout, retry, or error policy here.\n </p>\n }\n </section>\n }\n\n @if (sectionInAdvanced(\"triggerContext\") && trigger()) {\n <details class=\"fp-ae-panel\" open>\n <summary class=\"fp-ae-section-title cursor-pointer select-none\">\n Trigger payload and context\n </summary>\n <p class=\"fp-ae-copy mt-3\">\n Trigger payload/context is documentation for expressions only. Triggers\n do not edit node input or output mappings here.\n </p>\n <div class=\"grid gap-3 md:grid-cols-2\">\n <details class=\"rounded-lg border border-(--p-content-border-color)\">\n <summary class=\"cursor-pointer px-3 py-2 text-[12px] font-semibold\">Config schema</summary>\n <pre class=\"fp-ae-code\">{{ schemaText(configSchema()) }}</pre>\n </details>\n @if (hasTriggerPayloadSchema() && triggerPayloadSchema(); as schema) {\n <details class=\"rounded-lg border border-(--p-content-border-color)\">\n <summary class=\"cursor-pointer px-3 py-2 text-[12px] font-semibold\">Payload schema</summary>\n <pre class=\"fp-ae-code\">{{ schemaText(schema) }}</pre>\n </details>\n }\n @if (authPolicySchema()) {\n <details class=\"rounded-lg border border-(--p-content-border-color)\">\n <summary class=\"cursor-pointer px-3 py-2 text-[12px] font-semibold\">Auth policy schema</summary>\n <pre class=\"fp-ae-code\">{{ schemaText(authPolicySchema()) }}</pre>\n </details>\n }\n @if (triggerPayloadSample(); as sample) {\n <details class=\"rounded-lg border border-(--p-content-border-color)\">\n <summary class=\"cursor-pointer px-3 py-2 text-[12px] font-semibold\">Payload or request sample</summary>\n <pre class=\"fp-ae-code\">{{ schemaText(sample) }}</pre>\n </details>\n }\n </div>\n </details>\n }\n\n @if (sectionInAdvanced(\"backendSchema\") && step()) {\n <details class=\"fp-ae-panel\" open>\n <summary class=\"fp-ae-section-title cursor-pointer select-none\">\n Backend schemas and outputs\n </summary>\n <div class=\"mt-3 grid gap-3 md:grid-cols-2\">\n <details class=\"rounded-lg border border-(--p-content-border-color)\">\n <summary class=\"cursor-pointer px-3 py-2 text-[12px] font-semibold\">Config schema</summary>\n <pre class=\"fp-ae-code\">{{ schemaText(configSchema()) }}</pre>\n </details>\n <details class=\"rounded-lg border border-(--p-content-border-color)\">\n <summary class=\"cursor-pointer px-3 py-2 text-[12px] font-semibold\">Input schema</summary>\n <pre class=\"fp-ae-code\">{{ schemaText(inputSchema()) }}</pre>\n </details>\n <details class=\"rounded-lg border border-(--p-content-border-color)\">\n <summary class=\"cursor-pointer px-3 py-2 text-[12px] font-semibold\">Output schema</summary>\n <pre class=\"fp-ae-code\">{{ schemaText(outputSchema()) }}</pre>\n </details>\n <div class=\"rounded-lg border border-(--p-content-border-color) p-3\">\n <div class=\"fp-ae-label mb-2\">Route output keys</div>\n <div class=\"flex flex-wrap gap-1.5\">\n @for (key of routeOutputKeys(); track key) {\n <span class=\"rounded-md bg-(--p-surface-100) px-2 py-1 font-mono text-[11px]\">{{ key }}</span>\n }\n @if (routeOutputKeys().length === 0) {\n <span class=\"text-[12px] text-(--p-text-muted-color)\">No outgoing route keys</span>\n }\n </div>\n </div>\n </div>\n </details>\n }\n\n @if (sectionInAdvanced(\"schemaFields\") && configRows().length > 0) {\n <details class=\"fp-ae-panel\" open>\n <summary class=\"fp-ae-section-title cursor-pointer select-none\">\n Backend schema fields\n </summary>\n <div class=\"mt-3 grid gap-3 md:grid-cols-2\">\n @for (field of configRows(); track field.key) {\n <div\n class=\"space-y-1.5\"\n [class.md:col-span-2]=\"field.type === 'object' || field.type === 'array'\"\n >\n @if (field.enumValues.length) {\n <mt-select-field\n [ngModel]=\"fieldValue(field.key)\"\n (ngModelChange)=\"onConfigFieldChange(field.key, $event)\"\n [label]=\"field.label + (field.required ? ' *' : '')\"\n [hint]=\"field.description ?? ''\"\n [options]=\"enumOptions(field.enumValues)\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n } @else if (field.type === \"boolean\") {\n <mt-toggle-field\n size=\"small\"\n labelPosition=\"end\"\n [ngModel]=\"!!fieldValue(field.key)\"\n (ngModelChange)=\"onConfigFieldChange(field.key, $event === true)\"\n [label]=\"field.label + (field.required ? ' *' : '')\"\n [hint]=\"field.description ?? ''\"\n />\n } @else if (field.type === \"number\") {\n <mt-number-field\n [ngModel]=\"numberValue(fieldValue(field.key))\"\n (ngModelChange)=\"onConfigFieldChange(field.key, $event)\"\n [label]=\"field.label + (field.required ? ' *' : '')\"\n [hint]=\"field.description ?? ''\"\n />\n } @else if (field.type === \"date\") {\n <mt-date-field\n [ngModel]=\"fieldValue(field.key)\"\n (ngModelChange)=\"onConfigFieldChange(field.key, $event)\"\n [label]=\"field.label + (field.required ? ' *' : '')\"\n [hint]=\"field.description ?? ''\"\n [showTime]=\"field.format !== 'date'\"\n [showClear]=\"true\"\n [pInputs]=\"dateTimePickerInputs\"\n />\n } @else if (field.type === \"object\" || field.type === \"array\") {\n <mt-textarea-field\n class=\"w-full font-mono\"\n [ngModel]=\"valueText(fieldValue(field.key))\"\n (ngModelChange)=\"onConfigFieldChange(field.key, $event)\"\n [label]=\"field.label + (field.required ? ' *' : '')\"\n [hint]=\"field.description ?? ''\"\n rows=\"5\"\n />\n } @else {\n <mt-text-field\n [ngModel]=\"fieldText(field.key)\"\n (focusin)=\"field.expressionEnabled && setExpressionTarget('config:' + field.key)\"\n (ngModelChange)=\"onConfigFieldChange(field.key, $event)\"\n [label]=\"field.label + (field.required ? ' *' : '')\"\n [hint]=\"field.description ?? ''\"\n />\n }\n </div>\n }\n </div>\n </details>\n }\n </div>\n</div>\n\n<ng-template #credentialSelector>\n @if (credentials().length) {\n <div class=\"space-y-2\">\n @for (credential of credentials(); track credential.credentialRef) {\n <label class=\"flex items-center justify-between gap-3 rounded-lg border border-(--p-content-border-color) bg-(--p-surface-50) px-3 py-2\">\n <span class=\"min-w-0\">\n <span class=\"block truncate text-[12px] font-semibold\">{{ credential.displayName ?? credential.credentialRef }}</span>\n <span class=\"block truncate font-mono text-[11px] text-(--p-text-muted-color)\">\n {{ credential.credentialRef }} / {{ credential.status ?? (credential.resolved ? \"Resolved\" : \"Unresolved\") }}\n </span>\n </span>\n <span class=\"flex items-center gap-2\">\n <mt-button\n size=\"small\"\n variant=\"text\"\n label=\"Test\"\n (onClick)=\"testCredential(credential.credentialRef)\"\n />\n <mt-toggle-field\n size=\"small\"\n [ngModel]=\"credentialRefs().includes(credential.credentialRef)\"\n (ngModelChange)=\"toggleCredential(credential.credentialRef, $event === true)\"\n hint=\"Attach or detach this backend credential reference. Masked secrets are preserved.\"\n />\n </span>\n </label>\n }\n </div>\n } @else if (sectionHasAdvancedEmptyState(\"credentials\")) {\n <p class=\"fp-ae-copy\">\n The backend helper did not return credential choices for this node. Existing masked references remain preserved in the saved JSON.\n </p>\n }\n @if (credentialTest(); as test) {\n <div class=\"mt-3 rounded-lg bg-(--p-surface-100) px-3 py-2 text-[12px]\">\n {{ test.status ?? (test.succeeded ? \"Succeeded\" : \"Placeholder\") }}\n @if (test.message) { <span>- {{ test.message }}</span> }\n </div>\n }\n</ng-template>\n\n<ng-template\n #mapEditor\n let-objectKey=\"objectKey\"\n let-rows=\"rows\"\n let-keyLabel=\"keyLabel\"\n let-valueLabel=\"valueLabel\"\n let-addLabel=\"addLabel\"\n>\n <div class=\"space-y-2\">\n @for (row of rows; track row.key) {\n <div class=\"grid gap-2 md:grid-cols-[180px_1fr_auto]\">\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"row.key\"\n (ngModelChange)=\"updateObjectRow(objectKey, row.key, $event, row.value)\"\n [label]=\"keyLabel\"\n hint=\"Configuration key saved to the backend JSON object.\"\n />\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"valueText(row.value)\"\n (focusin)=\"setExpressionTarget('config:' + objectKey + ':' + row.key)\"\n (ngModelChange)=\"updateObjectRow(objectKey, row.key, row.key, $event)\"\n [label]=\"valueLabel\"\n hint=\"Configuration value. Use expressions when this field should resolve at runtime.\"\n />\n <mt-button\n class=\"self-end\"\n size=\"small\"\n severity=\"secondary\"\n variant=\"outlined\"\n label=\"Remove\"\n (onClick)=\"removeObjectRow(objectKey, row.key)\"\n />\n </div>\n }\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n [label]=\"addLabel || ('Add ' + keyLabel)\"\n (onClick)=\"addObjectRow(objectKey)\"\n />\n </div>\n</ng-template>\n" }]
13827
14278
  }], ctorParameters: () => [], propDecorators: { step: [{ type: i0.Input, args: [{ isSignal: true, alias: "step", required: false }] }], trigger: [{ type: i0.Input, args: [{ isSignal: true, alias: "trigger", required: false }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], view: [{ type: i0.Input, args: [{ isSignal: true, alias: "view", required: false }] }] } });
13828
14279
  function schemaFieldsFrom$1(schema, locale) {
13829
14280
  const raw = asRecord$4(schema);
@@ -13979,6 +14430,7 @@ function normalizeSchemaType(type, format, key = '') {
13979
14430
  }
13980
14431
  }
13981
14432
  function coerceFieldValue(value) {
14433
+ value = selectScalarValue(value);
13982
14434
  if (value instanceof Date)
13983
14435
  return value.toISOString();
13984
14436
  if (typeof value !== 'string')
@@ -14003,6 +14455,29 @@ function coerceFieldValue(value) {
14003
14455
  return false;
14004
14456
  return value;
14005
14457
  }
14458
+ function selectScalarValue(value) {
14459
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
14460
+ return value;
14461
+ }
14462
+ const record = value;
14463
+ return Object.prototype.hasOwnProperty.call(record, 'value')
14464
+ ? record['value']
14465
+ : value;
14466
+ }
14467
+ function normalizeScheduleMode(value) {
14468
+ const normalized = String(value ?? '').trim().toLowerCase();
14469
+ return normalized === 'interval' || normalized === 'once' ? normalized : 'cron';
14470
+ }
14471
+ function scheduleModeLabel(mode) {
14472
+ switch (normalizeScheduleMode(mode)) {
14473
+ case 'interval':
14474
+ return 'Interval';
14475
+ case 'once':
14476
+ return 'Once';
14477
+ default:
14478
+ return 'Cron';
14479
+ }
14480
+ }
14006
14481
  function isDateLikeConfigKey(key) {
14007
14482
  const normalized = key.trim().toLowerCase();
14008
14483
  return (normalized.includes('date') ||
@@ -18290,6 +18765,135 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
18290
18765
  type: Output
18291
18766
  }] } });
18292
18767
 
18768
+ class CanvasNoteComponent {
18769
+ host = inject((ElementRef));
18770
+ note = input.required(...(ngDevMode ? [{ debugName: "note" }] : /* istanbul ignore next */ []));
18771
+ selected = input(false, ...(ngDevMode ? [{ debugName: "selected" }] : /* istanbul ignore next */ []));
18772
+ editing = input(false, ...(ngDevMode ? [{ debugName: "editing" }] : /* istanbul ignore next */ []));
18773
+ colors = input(CANVAS_NOTE_COLORS, ...(ngDevMode ? [{ debugName: "colors" }] : /* istanbul ignore next */ []));
18774
+ contentChange = output();
18775
+ colorChange = output();
18776
+ duplicate = output();
18777
+ remove = output();
18778
+ editStart = output();
18779
+ editEnd = output();
18780
+ resizeHandle = EFResizeHandleType;
18781
+ draft = signal('', ...(ngDevMode ? [{ debugName: "draft" }] : /* istanbul ignore next */ []));
18782
+ textareaInputs = {
18783
+ spellcheck: true,
18784
+ };
18785
+ color = computed(() => resolveCanvasNoteColor(this.note().color), ...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
18786
+ noteLabel = computed(() => `Canvas note ${this.note().text.split(/\r?\n/)[0] ?? ''}`.trim(), ...(ngDevMode ? [{ debugName: "noteLabel" }] : /* istanbul ignore next */ []));
18787
+ markdownLines = computed(() => parseMarkdown(this.note().text), ...(ngDevMode ? [{ debugName: "markdownLines" }] : /* istanbul ignore next */ []));
18788
+ constructor() {
18789
+ effect(() => {
18790
+ if (this.editing())
18791
+ return;
18792
+ this.draft.set(this.note().text);
18793
+ });
18794
+ effect(() => {
18795
+ if (!this.editing())
18796
+ return;
18797
+ requestAnimationFrame(() => {
18798
+ this.host.nativeElement.querySelector('textarea')?.focus();
18799
+ });
18800
+ });
18801
+ }
18802
+ startEdit(event) {
18803
+ event?.stopPropagation();
18804
+ this.draft.set(this.note().text);
18805
+ this.editStart.emit({ noteId: this.note().id });
18806
+ }
18807
+ updateDraft(value) {
18808
+ this.draft.set(value ?? '');
18809
+ }
18810
+ finishEdit(event) {
18811
+ event?.stopPropagation();
18812
+ const text = this.draft().trimEnd();
18813
+ if (text !== this.note().text) {
18814
+ this.contentChange.emit({ noteId: this.note().id, text });
18815
+ }
18816
+ this.editEnd.emit({ noteId: this.note().id });
18817
+ }
18818
+ finishEditFromKeyboard(event) {
18819
+ event.preventDefault();
18820
+ this.finishEdit(event);
18821
+ }
18822
+ iconForColor(color) {
18823
+ return color.key === this.note().color ? 'general.check' : undefined;
18824
+ }
18825
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: CanvasNoteComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
18826
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: CanvasNoteComponent, isStandalone: true, selector: "fp-canvas-note", inputs: { note: { classPropertyName: "note", publicName: "note", isSignal: true, isRequired: true, transformFunction: null }, selected: { classPropertyName: "selected", publicName: "selected", isSignal: true, isRequired: false, transformFunction: null }, editing: { classPropertyName: "editing", publicName: "editing", isSignal: true, isRequired: false, transformFunction: null }, colors: { classPropertyName: "colors", publicName: "colors", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { contentChange: "contentChange", colorChange: "colorChange", duplicate: "duplicate", remove: "remove", editStart: "editStart", editEnd: "editEnd" }, host: { attributes: { "tabindex": "0" }, properties: { "class.is-selected": "selected()", "style.width.px": "note().width", "style.height.px": "note().height", "style.--fp-note-bg": "color().background", "style.--fp-note-border": "color().border", "style.--fp-note-text": "color().text", "attr.aria-label": "noteLabel()" }, classAttribute: "fp-canvas-note-host group/note block touch-none select-none outline-none" }, ngImport: i0, template: "@if (selected()) {\n <div\n class=\"absolute -top-10 start-2 z-10 flex items-center gap-1.5 rounded-lg border border-(--p-content-border-color) bg-(--p-content-background)/95 px-1.5 py-1 shadow-lg backdrop-blur\"\n role=\"toolbar\"\n [attr.aria-label]=\"'flowplus.canvas.noteActions' | transloco\"\n fDragBlocker\n (pointerdown)=\"$event.stopPropagation()\"\n (click)=\"$event.stopPropagation()\"\n >\n <mt-button\n variant=\"text\"\n severity=\"secondary\"\n size=\"small\"\n icon=\"general.edit-05\"\n styleClass=\"fp-note-tool-button\"\n [tooltip]=\"'flowplus.canvas.editNote' | transloco\"\n (onClick)=\"startEdit($event)\"\n />\n <mt-button\n variant=\"text\"\n severity=\"secondary\"\n size=\"small\"\n icon=\"general.copy-05\"\n styleClass=\"fp-note-tool-button\"\n [tooltip]=\"'flowplus.canvas.duplicateNote' | transloco\"\n (onClick)=\"duplicate.emit({ noteId: note().id })\"\n />\n <mt-button\n variant=\"text\"\n severity=\"danger\"\n size=\"small\"\n icon=\"general.trash-01\"\n styleClass=\"fp-note-tool-button\"\n [tooltip]=\"'flowplus.canvas.deleteNote' | transloco\"\n (onClick)=\"remove.emit({ noteId: note().id })\"\n />\n <span class=\"mx-1 h-4 w-px bg-(--p-content-border-color)\"></span>\n @for (option of colors(); track option.key) {\n <mt-button\n variant=\"text\"\n severity=\"secondary\"\n size=\"small\"\n [icon]=\"iconForColor(option)\"\n styleClass=\"fp-note-swatch\"\n [style.--fp-note-swatch]=\"option.background\"\n [attr.aria-label]=\"option.label\"\n [tooltip]=\"option.label\"\n (onClick)=\"colorChange.emit({ noteId: note().id, color: option.key })\"\n />\n }\n </div>\n}\n\n<article\n class=\"fp-canvas-note-card relative flex h-full w-full overflow-hidden rounded-[9px] border-2 backdrop-blur-[1px] transition-[border-color,box-shadow,transform] duration-150\"\n (dblclick)=\"startEdit($event)\"\n>\n @if (editing()) {\n <div\n class=\"flex h-full w-full flex-col p-3\"\n fDragBlocker\n (pointerdown)=\"$event.stopPropagation()\"\n (click)=\"$event.stopPropagation()\"\n (focusout)=\"finishEdit($event)\"\n >\n <mt-textarea-field\n class=\"fp-note-editor min-h-0 flex-1\"\n [field]=\"false\"\n [rows]=\"'8'\"\n [maxLength]=\"5000\"\n [pInputs]=\"textareaInputs\"\n [placeholder]=\"'flowplus.canvas.notePlaceholder' | transloco\"\n [ngModel]=\"draft()\"\n (ngModelChange)=\"updateDraft($event)\"\n (keydown.control.enter)=\"finishEditFromKeyboard($event)\"\n (keydown.meta.enter)=\"finishEditFromKeyboard($event)\"\n />\n </div>\n } @else {\n <div class=\"fp-scroll min-h-0 flex-1 overflow-auto px-5 py-4\">\n @for (line of markdownLines(); track $index) {\n @if (line.kind === \"empty\") {\n <div class=\"h-3\"></div>\n } @else {\n <p\n class=\"m-0 max-w-full break-words\"\n [class.text-[28px]]=\"line.kind === 'h1'\"\n [class.text-[24px]]=\"line.kind === 'h2'\"\n [class.text-[15px]]=\"line.kind === 'body'\"\n [class.font-bold]=\"line.kind !== 'body'\"\n [class.leading-tight]=\"line.kind !== 'body'\"\n [class.leading-6]=\"line.kind === 'body'\"\n [class.mb-2]=\"line.kind !== 'body'\"\n [class.mb-1]=\"line.kind === 'body'\"\n >\n @for (segment of line.segments; track $index) {\n <span [class.font-bold]=\"segment.bold\">{{ segment.text }}</span>\n }\n </p>\n }\n }\n </div>\n }\n\n <span\n fResizeHandle\n [fResizeHandleType]=\"resizeHandle.RIGHT_BOTTOM\"\n class=\"fp-note-resize-handle absolute end-1.5 bottom-1.5 h-4 w-4 cursor-nwse-resize rounded-sm border-e-2 border-b-2 opacity-0 transition-opacity\"\n [attr.aria-label]=\"'flowplus.canvas.resizeNote' | transloco\"\n ></span>\n</article>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: FFlowModule }, { kind: "directive", type: i1$2.FResizeHandleDirective, selector: "[fResizeHandle]", inputs: ["fResizeHandleType"] }, { kind: "directive", type: i1$2.FDragBlockerDirective, selector: "[fDragBlocker]" }, { kind: "ngmodule", type: TranslocoModule }, { kind: "component", type: Button, selector: "mt-button", inputs: ["icon", "label", "tooltip", "class", "type", "styleClass", "severity", "badge", "variant", "badgeSeverity", "size", "iconPos", "autofocus", "fluid", "raised", "rounded", "text", "plain", "outlined", "link", "disabled", "loading", "pInputs"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: TextareaField, selector: "mt-textarea-field", inputs: ["field", "hint", "label", "placeholder", "class", "readonly", "noErrorStyle", "pInputs", "rows", "required", "maxLength"] }, { kind: "pipe", type: i2.TranslocoPipe, name: "transloco" }] });
18827
+ }
18828
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: CanvasNoteComponent, decorators: [{
18829
+ type: Component,
18830
+ args: [{ selector: 'fp-canvas-note', standalone: true, imports: [
18831
+ CommonModule,
18832
+ FormsModule,
18833
+ FFlowModule,
18834
+ TranslocoModule,
18835
+ Button,
18836
+ TextareaField,
18837
+ Tooltip,
18838
+ Icon,
18839
+ ], host: {
18840
+ class: 'fp-canvas-note-host group/note block touch-none select-none outline-none',
18841
+ '[class.is-selected]': 'selected()',
18842
+ '[style.width.px]': 'note().width',
18843
+ '[style.height.px]': 'note().height',
18844
+ '[style.--fp-note-bg]': 'color().background',
18845
+ '[style.--fp-note-border]': 'color().border',
18846
+ '[style.--fp-note-text]': 'color().text',
18847
+ '[attr.aria-label]': 'noteLabel()',
18848
+ tabindex: '0',
18849
+ }, template: "@if (selected()) {\n <div\n class=\"absolute -top-10 start-2 z-10 flex items-center gap-1.5 rounded-lg border border-(--p-content-border-color) bg-(--p-content-background)/95 px-1.5 py-1 shadow-lg backdrop-blur\"\n role=\"toolbar\"\n [attr.aria-label]=\"'flowplus.canvas.noteActions' | transloco\"\n fDragBlocker\n (pointerdown)=\"$event.stopPropagation()\"\n (click)=\"$event.stopPropagation()\"\n >\n <mt-button\n variant=\"text\"\n severity=\"secondary\"\n size=\"small\"\n icon=\"general.edit-05\"\n styleClass=\"fp-note-tool-button\"\n [tooltip]=\"'flowplus.canvas.editNote' | transloco\"\n (onClick)=\"startEdit($event)\"\n />\n <mt-button\n variant=\"text\"\n severity=\"secondary\"\n size=\"small\"\n icon=\"general.copy-05\"\n styleClass=\"fp-note-tool-button\"\n [tooltip]=\"'flowplus.canvas.duplicateNote' | transloco\"\n (onClick)=\"duplicate.emit({ noteId: note().id })\"\n />\n <mt-button\n variant=\"text\"\n severity=\"danger\"\n size=\"small\"\n icon=\"general.trash-01\"\n styleClass=\"fp-note-tool-button\"\n [tooltip]=\"'flowplus.canvas.deleteNote' | transloco\"\n (onClick)=\"remove.emit({ noteId: note().id })\"\n />\n <span class=\"mx-1 h-4 w-px bg-(--p-content-border-color)\"></span>\n @for (option of colors(); track option.key) {\n <mt-button\n variant=\"text\"\n severity=\"secondary\"\n size=\"small\"\n [icon]=\"iconForColor(option)\"\n styleClass=\"fp-note-swatch\"\n [style.--fp-note-swatch]=\"option.background\"\n [attr.aria-label]=\"option.label\"\n [tooltip]=\"option.label\"\n (onClick)=\"colorChange.emit({ noteId: note().id, color: option.key })\"\n />\n }\n </div>\n}\n\n<article\n class=\"fp-canvas-note-card relative flex h-full w-full overflow-hidden rounded-[9px] border-2 backdrop-blur-[1px] transition-[border-color,box-shadow,transform] duration-150\"\n (dblclick)=\"startEdit($event)\"\n>\n @if (editing()) {\n <div\n class=\"flex h-full w-full flex-col p-3\"\n fDragBlocker\n (pointerdown)=\"$event.stopPropagation()\"\n (click)=\"$event.stopPropagation()\"\n (focusout)=\"finishEdit($event)\"\n >\n <mt-textarea-field\n class=\"fp-note-editor min-h-0 flex-1\"\n [field]=\"false\"\n [rows]=\"'8'\"\n [maxLength]=\"5000\"\n [pInputs]=\"textareaInputs\"\n [placeholder]=\"'flowplus.canvas.notePlaceholder' | transloco\"\n [ngModel]=\"draft()\"\n (ngModelChange)=\"updateDraft($event)\"\n (keydown.control.enter)=\"finishEditFromKeyboard($event)\"\n (keydown.meta.enter)=\"finishEditFromKeyboard($event)\"\n />\n </div>\n } @else {\n <div class=\"fp-scroll min-h-0 flex-1 overflow-auto px-5 py-4\">\n @for (line of markdownLines(); track $index) {\n @if (line.kind === \"empty\") {\n <div class=\"h-3\"></div>\n } @else {\n <p\n class=\"m-0 max-w-full break-words\"\n [class.text-[28px]]=\"line.kind === 'h1'\"\n [class.text-[24px]]=\"line.kind === 'h2'\"\n [class.text-[15px]]=\"line.kind === 'body'\"\n [class.font-bold]=\"line.kind !== 'body'\"\n [class.leading-tight]=\"line.kind !== 'body'\"\n [class.leading-6]=\"line.kind === 'body'\"\n [class.mb-2]=\"line.kind !== 'body'\"\n [class.mb-1]=\"line.kind === 'body'\"\n >\n @for (segment of line.segments; track $index) {\n <span [class.font-bold]=\"segment.bold\">{{ segment.text }}</span>\n }\n </p>\n }\n }\n </div>\n }\n\n <span\n fResizeHandle\n [fResizeHandleType]=\"resizeHandle.RIGHT_BOTTOM\"\n class=\"fp-note-resize-handle absolute end-1.5 bottom-1.5 h-4 w-4 cursor-nwse-resize rounded-sm border-e-2 border-b-2 opacity-0 transition-opacity\"\n [attr.aria-label]=\"'flowplus.canvas.resizeNote' | transloco\"\n ></span>\n</article>\n" }]
18850
+ }], ctorParameters: () => [], propDecorators: { note: [{ type: i0.Input, args: [{ isSignal: true, alias: "note", required: true }] }], selected: [{ type: i0.Input, args: [{ isSignal: true, alias: "selected", required: false }] }], editing: [{ type: i0.Input, args: [{ isSignal: true, alias: "editing", required: false }] }], colors: [{ type: i0.Input, args: [{ isSignal: true, alias: "colors", required: false }] }], contentChange: [{ type: i0.Output, args: ["contentChange"] }], colorChange: [{ type: i0.Output, args: ["colorChange"] }], duplicate: [{ type: i0.Output, args: ["duplicate"] }], remove: [{ type: i0.Output, args: ["remove"] }], editStart: [{ type: i0.Output, args: ["editStart"] }], editEnd: [{ type: i0.Output, args: ["editEnd"] }] } });
18851
+ function parseMarkdown(text) {
18852
+ const rawLines = text.split(/\r?\n/);
18853
+ const lines = rawLines.length ? rawLines : [''];
18854
+ return lines.map((line) => {
18855
+ const trimmed = line.trim();
18856
+ if (!trimmed)
18857
+ return { kind: 'empty', segments: [] };
18858
+ if (trimmed.startsWith('## ')) {
18859
+ return {
18860
+ kind: 'h2',
18861
+ segments: parseInline(trimmed.slice(3).trim()),
18862
+ };
18863
+ }
18864
+ if (trimmed.startsWith('# ')) {
18865
+ return {
18866
+ kind: 'h1',
18867
+ segments: parseInline(trimmed.slice(2).trim()),
18868
+ };
18869
+ }
18870
+ return { kind: 'body', segments: parseInline(line) };
18871
+ });
18872
+ }
18873
+ function parseInline(text) {
18874
+ const parts = [];
18875
+ let rest = text;
18876
+ while (rest.length) {
18877
+ const start = rest.indexOf('**');
18878
+ if (start < 0) {
18879
+ parts.push({ text: rest, bold: false });
18880
+ break;
18881
+ }
18882
+ if (start > 0) {
18883
+ parts.push({ text: rest.slice(0, start), bold: false });
18884
+ }
18885
+ const afterStart = rest.slice(start + 2);
18886
+ const end = afterStart.indexOf('**');
18887
+ if (end < 0) {
18888
+ parts.push({ text: rest.slice(start), bold: false });
18889
+ break;
18890
+ }
18891
+ parts.push({ text: afterStart.slice(0, end), bold: true });
18892
+ rest = afterStart.slice(end + 2);
18893
+ }
18894
+ return parts;
18895
+ }
18896
+
18293
18897
  function canvasIssueSeverity(issues) {
18294
18898
  const list = issues ?? [];
18295
18899
  if (list.some((issue) => issue.severity === 'Error'))
@@ -19240,12 +19844,23 @@ class FlowCanvasComponent {
19240
19844
  canvasBackgroundClick = new EventEmitter();
19241
19845
  /** Add affordance → page opens the left palette (no floating popover). */
19242
19846
  requestAddStep = new EventEmitter();
19847
+ noteUpdate = new EventEmitter();
19848
+ noteDuplicate = new EventEmitter();
19849
+ noteDelete = new EventEmitter();
19243
19850
  /* -------- VM signals -------- */
19851
+ canvasLayers = [
19852
+ EFCanvasLayer.GROUPS,
19853
+ EFCanvasLayer.CONNECTIONS,
19854
+ EFCanvasLayer.NODES,
19855
+ ];
19244
19856
  nodes = computed(() => this.store.nodeVms(), ...(ngDevMode ? [{ debugName: "nodes" }] : /* istanbul ignore next */ []));
19857
+ canvasNotes = computed(() => this.store.canvasNotes(), ...(ngDevMode ? [{ debugName: "canvasNotes" }] : /* istanbul ignore next */ []));
19245
19858
  edges = computed(() => this.store.edgeVms(), ...(ngDevMode ? [{ debugName: "edges" }] : /* istanbul ignore next */ []));
19246
19859
  triggerNodes = computed(() => this.store.triggerNodeVms(), ...(ngDevMode ? [{ debugName: "triggerNodes" }] : /* istanbul ignore next */ []));
19247
19860
  branchLanes = computed(() => this.store.branchLaneVms(), ...(ngDevMode ? [{ debugName: "branchLanes" }] : /* istanbul ignore next */ []));
19248
19861
  viewport = computed(() => this.store.viewport(), ...(ngDevMode ? [{ debugName: "viewport" }] : /* istanbul ignore next */ []));
19862
+ selectedCanvasNoteIds = computed(() => this.store.selectedCanvasNoteIds(), ...(ngDevMode ? [{ debugName: "selectedCanvasNoteIds" }] : /* istanbul ignore next */ []));
19863
+ editingNoteId = signal(null, ...(ngDevMode ? [{ debugName: "editingNoteId" }] : /* istanbul ignore next */ []));
19249
19864
  /**
19250
19865
  * Minimum world size the minimap renders. A very small value (Foblex's
19251
19866
  * default-ish 80) makes one or two nodes fill the whole minimap, which
@@ -19410,7 +20025,7 @@ class FlowCanvasComponent {
19410
20025
  }
19411
20026
  /** Real Foblex nodes on the canvas: steps + virtual trigger nodes. */
19412
20027
  totalNodeCount() {
19413
- return this.nodes().length + this.triggerNodes().length;
20028
+ return (this.nodes().length + this.triggerNodes().length + this.canvasNotes().length);
19414
20029
  }
19415
20030
  /**
19416
20031
  * Keep the toolbar zoom label and the persisted viewport in sync with
@@ -19452,6 +20067,11 @@ class FlowCanvasComponent {
19452
20067
  focusTrigger(triggerId) {
19453
20068
  this.requestFocus(`trigger:${triggerId}`, false);
19454
20069
  }
20070
+ /** Public: center + select a sticky canvas note. */
20071
+ focusNote(noteId) {
20072
+ this.requestFocus(canvasNoteGroupId(noteId), false);
20073
+ this.store.selectCanvasNote(noteId);
20074
+ }
19455
20075
  /**
19456
20076
  * Public: center + select a connection. Foblex centers nodes/groups, not
19457
20077
  * edges, so we center on the connection's source node and highlight the
@@ -19492,6 +20112,10 @@ class FlowCanvasComponent {
19492
20112
  });
19493
20113
  }
19494
20114
  canvasNodeExists(canvasNodeId) {
20115
+ const noteId = parseCanvasNoteGroupId(canvasNodeId);
20116
+ if (noteId) {
20117
+ return this.canvasNotes().some((note) => note.id === noteId);
20118
+ }
19495
20119
  if (canvasNodeId.startsWith('trigger:')) {
19496
20120
  const tid = Number(canvasNodeId.slice('trigger:'.length));
19497
20121
  return this.triggerNodes().some((t) => t.triggerId === tid);
@@ -19935,7 +20559,17 @@ class FlowCanvasComponent {
19935
20559
  onMoveNodes(event) {
19936
20560
  const nodePositions = [];
19937
20561
  const triggerPositions = [];
20562
+ const notePositions = [];
19938
20563
  for (const move of event.nodes) {
20564
+ const noteId = parseCanvasNoteGroupId(move.id);
20565
+ if (noteId) {
20566
+ notePositions.push({
20567
+ noteId,
20568
+ x: move.position.x,
20569
+ y: move.position.y,
20570
+ });
20571
+ continue;
20572
+ }
19939
20573
  const parsed = parseNodeId(move.id);
19940
20574
  if (parsed.kind === 'step' && parsed.stepId != null) {
19941
20575
  nodePositions.push({
@@ -19952,18 +20586,49 @@ class FlowCanvasComponent {
19952
20586
  });
19953
20587
  }
19954
20588
  }
19955
- this.commitCanvasPositions(nodePositions, triggerPositions);
20589
+ this.commitCanvasPositions(nodePositions, triggerPositions, notePositions);
19956
20590
  }
19957
20591
  onStepPositionChange(stepId, position) {
19958
- this.commitCanvasPositions([{ stepId, x: position.x, y: position.y }], []);
20592
+ this.commitCanvasPositions([{ stepId, x: position.x, y: position.y }], [], []);
19959
20593
  }
19960
20594
  onTriggerPositionChange(triggerId, position) {
19961
- this.commitCanvasPositions([], [{ triggerId, x: position.x, y: position.y }]);
20595
+ this.commitCanvasPositions([], [{ triggerId, x: position.x, y: position.y }], []);
20596
+ }
20597
+ onNotePositionChange(noteId, position) {
20598
+ this.commitCanvasPositions([], [], [{ noteId, x: position.x, y: position.y }]);
20599
+ }
20600
+ onNoteSizeChange(noteId, size) {
20601
+ this.noteUpdate.emit({
20602
+ noteId,
20603
+ patch: { width: size.width, height: size.height },
20604
+ });
20605
+ }
20606
+ onNoteContentChange(event) {
20607
+ this.noteUpdate.emit({
20608
+ noteId: event.noteId,
20609
+ patch: { text: event.text },
20610
+ });
20611
+ }
20612
+ onNoteColorChange(event) {
20613
+ this.noteUpdate.emit({
20614
+ noteId: event.noteId,
20615
+ patch: { color: event.color },
20616
+ });
20617
+ }
20618
+ onNoteDuplicate(event) {
20619
+ this.noteDuplicate.emit(event);
20620
+ }
20621
+ onNoteDelete(event) {
20622
+ this.noteDelete.emit(event);
19962
20623
  }
19963
- commitCanvasPositions(nodes, triggers) {
19964
- if (!nodes.length && !triggers.length)
20624
+ onNoteEditStart(noteId) {
20625
+ this.store.selectCanvasNote(noteId);
20626
+ this.editingNoteId.set(noteId);
20627
+ }
20628
+ commitCanvasPositions(nodes, triggers, notes = []) {
20629
+ if (!nodes.length && !triggers.length && !notes.length)
19965
20630
  return false;
19966
- const changed = this.store.setLayoutPositions({ nodes, triggers }, { scheduleSave: !this.positionDragActive });
20631
+ const changed = this.store.setLayoutPositions({ nodes, triggers, notes }, { scheduleSave: !this.positionDragActive });
19967
20632
  if (this.positionDragActive && changed) {
19968
20633
  this.positionChangedDuringDrag = true;
19969
20634
  }
@@ -19971,7 +20636,7 @@ class FlowCanvasComponent {
19971
20636
  }
19972
20637
  syncCanvasPositionsFromDom() {
19973
20638
  const positions = this.readCanvasPositionSnapshot();
19974
- return this.commitCanvasPositions(positions.nodes, positions.triggers);
20639
+ return this.commitCanvasPositions(positions.nodes, positions.triggers, positions.notes);
19975
20640
  }
19976
20641
  syncStableCanvasPositions() {
19977
20642
  if (this.isPositionSyncSuppressed())
@@ -19982,7 +20647,7 @@ class FlowCanvasComponent {
19982
20647
  this.lastObservedPositionSignature = signature;
19983
20648
  return;
19984
20649
  }
19985
- const changed = this.commitCanvasPositions(positions.nodes, positions.triggers);
20650
+ const changed = this.commitCanvasPositions(positions.nodes, positions.triggers, positions.notes);
19986
20651
  if (this.positionDragActive) {
19987
20652
  const shouldSave = changed || this.positionChangedDuringDrag;
19988
20653
  this.positionDragActive = false;
@@ -20015,7 +20680,16 @@ class FlowCanvasComponent {
20015
20680
  : null;
20016
20681
  })
20017
20682
  .filter((item) => item != null);
20018
- return { nodes, triggers };
20683
+ const notes = Array.from(this.hostEl.querySelectorAll('fp-canvas-note[data-fp-note]'))
20684
+ .map((el) => {
20685
+ const noteId = el.dataset['fpNote'];
20686
+ const position = readTranslatePosition(el);
20687
+ return noteId && position
20688
+ ? { noteId, x: position.x, y: position.y }
20689
+ : null;
20690
+ })
20691
+ .filter((item) => item != null);
20692
+ return { nodes, triggers, notes };
20019
20693
  }
20020
20694
  schedulePointerPositionSync(delayMs) {
20021
20695
  if (this.isPositionSyncSuppressed())
@@ -20135,8 +20809,18 @@ class FlowCanvasComponent {
20135
20809
  if (c != null)
20136
20810
  connectionIds.push(c);
20137
20811
  }
20138
- this.store.setSelectionFromCanvas(stepIds, connectionIds);
20139
- if (stepIds.length === 0 && connectionIds.length === 0) {
20812
+ const canvasNoteIds = [];
20813
+ for (const id of event.fGroupIds) {
20814
+ const noteId = parseCanvasNoteGroupId(id);
20815
+ if (noteId)
20816
+ canvasNoteIds.push(noteId);
20817
+ }
20818
+ this.store.setSelectionFromCanvas(stepIds, connectionIds, canvasNoteIds);
20819
+ if (!canvasNoteIds.length)
20820
+ this.editingNoteId.set(null);
20821
+ if (stepIds.length === 0 &&
20822
+ connectionIds.length === 0 &&
20823
+ canvasNoteIds.length === 0) {
20140
20824
  this.canvasBackgroundClick.emit();
20141
20825
  }
20142
20826
  }
@@ -20194,6 +20878,9 @@ class FlowCanvasComponent {
20194
20878
  isConnectionSelected(connectionId) {
20195
20879
  return this.selectionConnectionIds().includes(connectionId);
20196
20880
  }
20881
+ canvasNoteGroupId(noteId) {
20882
+ return canvasNoteGroupId(noteId);
20883
+ }
20197
20884
  /* -------- node hover-bar forwards -------- */
20198
20885
  onNodeQuickAdd(e) {
20199
20886
  this.nodeQuickAdd.emit(e);
@@ -20247,7 +20934,7 @@ class FlowCanvasComponent {
20247
20934
  this.triggerStartDisconnect.emit(e);
20248
20935
  }
20249
20936
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: FlowCanvasComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
20250
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: FlowCanvasComponent, isStandalone: true, selector: "fp-flow-canvas", outputs: { paletteDrop: "paletteDrop", connectionCreate: "connectionCreate", connectionReassign: "connectionReassign", triggerStartConnect: "triggerStartConnect", triggerStartReassign: "triggerStartReassign", triggerStartDisconnect: "triggerStartDisconnect", connectionQuickAdd: "connectionQuickAdd", quickAddPick: "quickAddPick", autoLayoutRequested: "autoLayoutRequested", loaded: "loaded", nodeQuickAdd: "nodeQuickAdd", nodePortPlusClick: "nodePortPlusClick", nodeDuplicate: "nodeDuplicate", nodeRemove: "nodeRemove", nodeOpenDetails: "nodeOpenDetails", edgeInsertStep: "edgeInsertStep", edgeRemove: "edgeRemove", edgeEditFormula: "edgeEditFormula", edgeOpenDetails: "edgeOpenDetails", assignNodeToConnection: "assignNodeToConnection", openChildWorkflow: "openChildWorkflow", starterAddTrigger: "starterAddTrigger", triggerOpenDetails: "triggerOpenDetails", triggerExecute: "triggerExecute", triggerToggleEnabled: "triggerToggleEnabled", triggerDelete: "triggerDelete", canvasBackgroundClick: "canvasBackgroundClick", requestAddStep: "requestAddStep" }, providers: [
20937
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: FlowCanvasComponent, isStandalone: true, selector: "fp-flow-canvas", outputs: { paletteDrop: "paletteDrop", connectionCreate: "connectionCreate", connectionReassign: "connectionReassign", triggerStartConnect: "triggerStartConnect", triggerStartReassign: "triggerStartReassign", triggerStartDisconnect: "triggerStartDisconnect", connectionQuickAdd: "connectionQuickAdd", quickAddPick: "quickAddPick", autoLayoutRequested: "autoLayoutRequested", loaded: "loaded", nodeQuickAdd: "nodeQuickAdd", nodePortPlusClick: "nodePortPlusClick", nodeDuplicate: "nodeDuplicate", nodeRemove: "nodeRemove", nodeOpenDetails: "nodeOpenDetails", edgeInsertStep: "edgeInsertStep", edgeRemove: "edgeRemove", edgeEditFormula: "edgeEditFormula", edgeOpenDetails: "edgeOpenDetails", assignNodeToConnection: "assignNodeToConnection", openChildWorkflow: "openChildWorkflow", starterAddTrigger: "starterAddTrigger", triggerOpenDetails: "triggerOpenDetails", triggerExecute: "triggerExecute", triggerToggleEnabled: "triggerToggleEnabled", triggerDelete: "triggerDelete", canvasBackgroundClick: "canvasBackgroundClick", requestAddStep: "requestAddStep", noteUpdate: "noteUpdate", noteDuplicate: "noteDuplicate", noteDelete: "noteDelete" }, providers: [
20251
20938
  provideFFlow({ id: 'flowplus-builder' }, withReflowOnResize({
20252
20939
  collision: EFReflowCollision.STOP,
20253
20940
  deltaSource: EFReflowDeltaSource.EDGE_BASED,
@@ -20255,7 +20942,7 @@ class FlowCanvasComponent {
20255
20942
  maxCascadeDepth: 8,
20256
20943
  maxAbsoluteShiftPerPlan: 10000,
20257
20944
  })),
20258
- ], viewQueries: [{ propertyName: "flow", first: true, predicate: ["flow"], descendants: true, isSignal: true }, { propertyName: "canvas", first: true, predicate: ["canvas"], descendants: true, isSignal: true }], ngImport: i0, template: "<f-flow\n #flow\n fDraggable\n [vCellSize]=\"gridCellSize\"\n [hCellSize]=\"gridCellSize\"\n [fCellSizeWhileDragging]=\"false\"\n (fFullRendered)=\"onFullRendered()\"\n (fNodesRendered)=\"onNodesRendered()\"\n (fCreateNode)=\"onCreateNode($event)\"\n (fCreateConnection)=\"onCreateConnection($event)\"\n (fReassignConnection)=\"onReassignConnection($event)\"\n (fDragStarted)=\"onDragStarted($event)\"\n (fDragEnded)=\"onDragEnded()\"\n (fMoveNodes)=\"onMoveNodes($event)\"\n (fSelectionChange)=\"onSelectionChange($event)\"\n>\n <!-- Background dot grid \u2014 its spacing is locked to the drag cell size so\n nodes snap exactly onto the visible dots (Foblex grid-system). -->\n <f-background>\n <f-circle-pattern [radius]=\"gridCellSize\" />\n </f-background>\n\n <!-- Alignment guides + marquee selection are `f-flow` children (NOT\n `f-canvas` children \u2014 `f-canvas` only projects connections/nodes/groups,\n so helpers placed inside it are silently dropped). This mirrors the\n Foblex call-center reference layout. -->\n <f-line-alignment [fAlignThreshold]=\"20\" />\n <f-selection-area />\n\n <!-- Auto-pan: when dragging a node / connection near the viewport edge, the\n canvas pans to follow, so you can wire across off-screen nodes without\n letting go. (Foblex `f-auto-pan` plugin.) -->\n <f-auto-pan [fEdgeThreshold]=\"36\" [fSpeed]=\"8\" />\n\n <f-canvas\n #canvas\n fZoom\n [debounceTime]=\"200\"\n (fCanvasChange)=\"onCanvasChange($event)\"\n >\n <!-- Floating behavior anchors the preview at the connector boundary along\n the center\u2192pointer axis, so a connection dragged from the \"+\" outlet\n leaves it cleanly (NOT from its bottom edge \u2014 `fixed` defaults an AUTO\n connectable side to bottom) and snaps to the target's nearest edge. -->\n <f-connection-for-create\n [fBehavior]=\"'floating'\"\n fType=\"adaptive-curve\"\n [fOffset]=\"0\"\n >\n <f-connection-marker-arrow />\n </f-connection-for-create>\n\n <!-- Auto-snap: while dragging a new/reassigned connection, snap to the\n nearest connector within the threshold (forgiving connect UX). -->\n <f-snap-connection\n [fBehavior]=\"'floating'\"\n fType=\"adaptive-curve\"\n [fSnapThreshold]=\"40\"\n [fOffset]=\"0\"\n >\n <f-connection-marker-arrow />\n </f-snap-connection>\n\n <!-- ngProjectAs is REQUIRED: `f-canvas` distributes projected content\n into its layer containers via selective `<ng-content select=\"...\">`\n with NO catch-all slot. A wrapper component (`fp-flow-node`, etc.)\n does not match `[fNode]` / `f-connection` on its host, so without\n `ngProjectAs` Foblex silently drops it and the canvas renders empty.\n The actual Foblex directive lives on the wrapper's inner element and\n self-registers via the mediator, so routing the wrapper into the\n right layer is all that's needed. -->\n @for (lane of branchLanes(); track lane.id) {\n <div\n ngProjectAs=\"[fGroup]\"\n class=\"pointer-events-none absolute z-0 rounded-[14px] border border-dashed border-[color-mix(in_srgb,rgb(var(--fp-parallel))_55%,transparent)] bg-[color-mix(in_srgb,rgb(var(--fp-parallel))_8%,transparent)]\"\n [attr.data-color-token]=\"lane.colorToken\"\n [style.left.px]=\"lane.x\"\n [style.top.px]=\"lane.y\"\n [style.width.px]=\"lane.width\"\n [style.height.px]=\"lane.height\"\n >\n <span\n class=\"pointer-events-auto absolute -top-2.5 start-3 rounded-full border border-[color-mix(in_srgb,rgb(var(--fp-parallel))_40%,transparent)] bg-(--p-content-background) px-2 py-px text-[10.5px] font-semibold text-[rgb(var(--fp-parallel))]\"\n >{{ lane.label }}</span\n >\n </div>\n }\n\n <!-- Connections are inlined here (NOT wrapped in a component) so each\n <f-connection> is a DIRECT child of Foblex's connections container.\n A wrapper would make `hostElement.parentElement` the wrapper instead\n of the container, which makes Foblex's select layer-raise throw\n \"Unknown container\" the moment an edge is selected. Same call-center\n pattern used for nodes/triggers. -->\n @for (edge of edges(); track edge.id) {\n <f-connection\n class=\"group\"\n [attr.data-fp-edge]=\"edge.connectionId\"\n [fConnectionId]=\"edge.id\"\n [fOutputId]=\"edge.sourcePortId\"\n [fInputId]=\"edge.targetPortId\"\n [fBehavior]=\"'fixed'\"\n [fType]=\"'adaptive-curve'\"\n [fOffset]=\"32\"\n fInputSide=\"calculate\"\n [fReassignableStart]=\"edge.edgeKind !== 'triggerStart'\"\n [fReassignDisabled]=\"edge.edgeKind === 'triggerStart'\"\n [class.fp-edge]=\"true\"\n [class.is-trigger-start]=\"edge.edgeKind === 'triggerStart'\"\n [class.is-drop-target]=\"isDropTargetEdge(edge.connectionId)\"\n [class.is-selected]=\"isConnectionSelected(edge.connectionId)\"\n [class.is-error]=\"edge.isInvalid\"\n [class.is-formula]=\"edge.isFormula\"\n [class.is-runtime-active]=\"edge.runtimeState === 'active'\"\n [class.is-runtime-completed]=\"edge.runtimeState === 'completed'\"\n [class.is-runtime-failed]=\"edge.runtimeState === 'failed'\"\n [class.is-runtime-waiting]=\"edge.runtimeState === 'waiting'\"\n (dblclick)=\"\n edge.edgeKind === 'triggerStart' && edge.triggerId != null\n ? onTriggerOpenDetails({ triggerId: edge.triggerId })\n : onEdgeOpenDetails({ connectionId: edge.connectionId })\n \"\n >\n <!-- Arrow marker at the target \u2014 the connection carries the arrowhead;\n the input lives on the node host (no separate input element). -->\n <f-connection-marker-arrow [type]=\"markerEnd\" />\n\n <!-- Editable waypoints are intentionally NOT rendered: there is no\n backend persistence wired for manual bends, so exposing draggable\n waypoints would silently lose the user's edits on reload. -->\n\n <!-- Label / formula badge \u2014 pinned to the CENTER of the line\n (connection-content position 0.5). Fades out on hover/selected so\n the centered action box can take its place. -->\n @if (edge.label) {\n <div\n fConnectionContent\n [position]=\"0.5\"\n class=\"pointer-events-none opacity-100 transition-opacity duration-150 group-hover:opacity-0 group-[.is-selected]:opacity-0\"\n >\n <span\n class=\"inline-flex items-center gap-1.5 whitespace-nowrap rounded-full border border-[rgb(var(--fp-connector))] bg-(--p-content-background) py-1 pe-2.5 ps-2 text-[11px] font-semibold text-(--p-text-color) shadow-[0_1px_3px_rgba(15,23,42,0.1)] transition-colors group-[.is-selected]:border-(--p-primary-color) group-[.is-selected]:text-(--p-primary-color)\"\n >\n <span\n class=\"size-1.5 flex-none rounded-full bg-[rgb(var(--fp-connector))] group-[.is-selected]:bg-(--p-primary-color)\"\n ></span>\n {{ edge.label }}\n </span>\n </div>\n } @else if (edge.isFormula) {\n <div\n fConnectionContent\n [position]=\"0.5\"\n class=\"pointer-events-none opacity-100 transition-opacity duration-150 group-hover:opacity-0 group-[.is-selected]:opacity-0\"\n >\n <span\n class=\"grid h-5 w-5 place-items-center rounded-full border border-(--p-content-border-color) bg-(--p-content-background) font-mono text-[12px] italic text-(--p-primary-color) shadow-sm\"\n title=\"Conditional route\"\n >\u0192</span\n >\n </div>\n }\n\n <!-- Centered action box \u2014 edit (opens the connection MODAL) + delete.\n Same content anchor (position 0.5), revealed on hover/selected and\n overlapping the label so the actions stay centered ON the line. -->\n @if (edgeIssueCount(edge) > 0) {\n <div\n fConnectionContent\n [position]=\"0.38\"\n class=\"pointer-events-auto\"\n >\n <span\n class=\"fp-node-badge grid h-[18px] w-[18px] cursor-help place-items-center rounded-sm bg-(--p-content-background) shadow-sm outline-none transition-transform focus-visible:ring-2 focus-visible:ring-(--p-primary-color)\"\n [style.color]=\"edgeIssueColor(edge)\"\n tabindex=\"0\"\n role=\"img\"\n [attr.aria-label]=\"edgeIssueTooltip(edge)\"\n [mtTooltip]=\"edgeIssueTooltip(edge)\"\n [tooltipStyleClass]=\"edgeIssueTooltipClass(edge)\"\n tooltipPosition=\"top\"\n appendTo=\"body\"\n [escape]=\"true\"\n [showDelay]=\"180\"\n [hideDelay]=\"80\"\n (pointerdown)=\"$event.stopPropagation()\"\n (click)=\"\n $event.stopPropagation();\n edge.edgeKind === 'triggerStart' && edge.triggerId != null\n ? onTriggerOpenDetails({ triggerId: edge.triggerId })\n : onEdgeOpenDetails({ connectionId: edge.connectionId })\n \"\n (dblclick)=\"$event.stopPropagation()\"\n >\n <svg width=\"15\" height=\"15\" viewBox=\"0 0 20 20\" aria-hidden=\"true\">\n <path\n d=\"M10 3.4 18 16.8 2 16.8 Z\"\n fill=\"currentColor\"\n stroke=\"currentColor\"\n stroke-width=\"2.6\"\n stroke-linejoin=\"round\"\n />\n <rect x=\"9.05\" y=\"8\" width=\"1.9\" height=\"4.6\" rx=\"0.95\" fill=\"#fff\" />\n <circle cx=\"10\" cy=\"14.6\" r=\"1.05\" fill=\"#fff\" />\n </svg>\n </span>\n </div>\n }\n\n @if (edge.runtimeState && edge.runtimeTooltip) {\n <div\n fConnectionContent\n [position]=\"0.62\"\n class=\"pointer-events-auto\"\n >\n <span\n class=\"inline-flex h-[18px] w-[18px] cursor-help items-center justify-center rounded-full border bg-(--p-content-background) shadow-sm outline-none focus-visible:ring-2 focus-visible:ring-(--p-primary-color)\"\n [class.animate-pulse]=\"edge.runtimeState === 'active'\"\n [style.borderColor]=\"edgeRuntimeColor(edge)\"\n [style.color]=\"edgeRuntimeColor(edge)\"\n tabindex=\"0\"\n role=\"status\"\n [attr.aria-label]=\"edge.runtimeTooltip\"\n [mtTooltip]=\"edge.runtimeTooltip\"\n tooltipPosition=\"top\"\n appendTo=\"body\"\n [escape]=\"true\"\n [showDelay]=\"180\"\n [hideDelay]=\"80\"\n (pointerdown)=\"$event.stopPropagation()\"\n (click)=\"$event.stopPropagation()\"\n (dblclick)=\"$event.stopPropagation()\"\n >\n <mt-icon [icon]=\"edgeRuntimeIcon(edge)\" class=\"[&_svg]:size-3\" />\n </span>\n </div>\n }\n\n <div\n fConnectionContent\n [position]=\"0.5\"\n class=\"pointer-events-none opacity-0 transition-opacity duration-150 group-hover:pointer-events-auto group-hover:opacity-100 group-[.is-selected]:pointer-events-auto group-[.is-selected]:opacity-100\"\n >\n <div\n class=\"flex items-center gap-0.5 rounded-lg border border-(--p-content-border-color) bg-(--p-content-background) p-1 shadow-md\"\n role=\"toolbar\"\n aria-label=\"Connection actions\"\n (pointerdown)=\"$event.stopPropagation()\"\n (click)=\"$event.stopPropagation()\"\n >\n @if (edge.edgeKind === \"triggerStart\" && edge.triggerId != null) {\n <mt-button\n variant=\"text\"\n severity=\"secondary\"\n size=\"small\"\n icon=\"general.settings-01\"\n [tooltip]=\"'flowplus.trigger.node.configure' | transloco\"\n (onClick)=\"onTriggerOpenDetails({ triggerId: edge.triggerId })\"\n />\n <mt-button\n variant=\"text\"\n severity=\"danger\"\n size=\"small\"\n icon=\"general.link-broken-01\"\n [tooltip]=\"'flowplus.trigger.node.unlink' | transloco\"\n (onClick)=\"\n onTriggerStartDisconnect({ triggerId: edge.triggerId })\n \"\n />\n } @else {\n <mt-button\n variant=\"text\"\n severity=\"secondary\"\n size=\"small\"\n icon=\"general.edit-05\"\n [tooltip]=\"'flowplus.inspector.connection.title' | transloco\"\n (onClick)=\"\n onEdgeOpenDetails({ connectionId: edge.connectionId })\n \"\n />\n <mt-button\n variant=\"text\"\n severity=\"danger\"\n size=\"small\"\n icon=\"general.trash-01\"\n [tooltip]=\"'flowplus.edge.delete' | transloco\"\n (onClick)=\"onEdgeRemove({ connectionId: edge.connectionId })\"\n />\n }\n </div>\n </div>\n </f-connection>\n }\n\n <!-- The Foblex `fNode` directive lives on the component element itself\n (not an inner div). This keeps the node element a DIRECT child of\n Foblex's nodes container, so `hostElement.parentElement` IS that\n container \u2014 required by the select/move layer-raise (otherwise it\n throws \"Unknown container\"). This is the Foblex call-center pattern;\n the `fNode` attribute also routes it into the `[fNode]` projection\n slot, so `ngProjectAs` is not needed. -->\n @for (node of nodes(); track node.id) {\n <fp-flow-node\n fNode\n fDragHandle\n fNodeInput\n [attr.data-fp-node]=\"node.stepId\"\n [fInputId]=\"node.inputs[0]?.id\"\n [fInputMultiple]=\"node.inputs[0]?.allowMultiple ?? true\"\n [fInputDisabled]=\"!node.inputs.length\"\n [fNodeId]=\"node.id\"\n [fNodePosition]=\"{ x: node.x, y: node.y }\"\n (fNodePositionChange)=\"onStepPositionChange(node.stepId, $event)\"\n [node]=\"node\"\n [connectedOutputKeys]=\"connectedOutputKeys(node.stepId)\"\n (quickAdd)=\"onNodeQuickAdd($event)\"\n (portPlusClick)=\"onNodePortPlus($event)\"\n (duplicate)=\"onNodeDuplicate($event)\"\n (remove)=\"onNodeRemove($event)\"\n (openDetails)=\"onNodeOpenDetails($event)\"\n (openChild)=\"onOpenChild($event)\"\n />\n }\n\n @for (t of triggerNodes(); track t.triggerId) {\n <fp-trigger-node\n fNode\n fDragHandle\n [attr.data-fp-trigger]=\"t.triggerId\"\n [fNodeId]=\"'trigger:' + t.triggerId\"\n [fNodePosition]=\"{ x: t.x, y: t.y }\"\n (fNodePositionChange)=\"onTriggerPositionChange(t.triggerId, $event)\"\n [trigger]=\"t\"\n [noFirstStep]=\"!t.startStepId\"\n (addFirstStep)=\"onTriggerAddFirstStep($event)\"\n (configure)=\"onTriggerOpenDetails($event)\"\n (execute)=\"onTriggerExecute($event)\"\n (toggleEnabled)=\"onTriggerToggleEnabled($event)\"\n (remove)=\"onTriggerDelete($event)\"\n />\n }\n </f-canvas>\n\n <!-- Minimap \u2014 `f-flow` child (NOT inside `f-canvas`). Toggled from the\n canvas controls; `fMinSize` keeps a couple of nodes zoomed-out enough\n to give real spatial context. -->\n @if (minimapVisible()) {\n <f-minimap [fMinSize]=\"minimapMinSize\" />\n }\n\n @if (showStarter() && !store.loading()) {\n <fp-starter-card\n (addTrigger)=\"onStarterAddTrigger($event)\"\n (directPick)=\"onStarterDirectPick($event)\"\n />\n }\n\n <fp-canvas-controls\n [zoom]=\"zoom()\"\n [minimapVisible]=\"minimapVisible()\"\n (zoomIn)=\"zoomIn()\"\n (zoomOut)=\"zoomOut()\"\n (fitView)=\"fitView()\"\n (resetView)=\"resetView()\"\n (toggleMinimap)=\"toggleMinimap()\"\n (autoLayout)=\"autoLayout()\"\n />\n</f-flow>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FFlowModule }, { kind: "component", type: i1$2.FFlowComponent, selector: "f-flow", inputs: ["fFlowId", "fCache"], outputs: ["fNodesRendered", "fFullRendered", "fLoaded"] }, { kind: "component", type: i1$2.FCanvasComponent, selector: "f-canvas", inputs: ["position", "scale", "debounceTime", "fLayers"], outputs: ["fCanvasChange"] }, { kind: "component", type: i1$2.FBackgroundComponent, selector: "f-background" }, { kind: "component", type: i1$2.FCirclePatternComponent, selector: "f-circle-pattern", inputs: ["id", "color", "radius"] }, { kind: "component", type: i1$2.FAutoPan, selector: "f-auto-pan", inputs: ["fEdgeThreshold", "fSpeed", "fAcceleration"] }, { kind: "directive", type: i1$2.FZoomDirective, selector: "f-canvas[fZoom]", inputs: ["fZoom", "fWheelTrigger", "fDblClickTrigger", "fZoomMinimum", "fZoomMaximum", "fZoomStep", "fZoomDblClickStep"] }, { kind: "component", type: i1$2.FSelectionArea, selector: "f-selection-area", inputs: ["fTrigger"] }, { kind: "directive", type: i1$2.FConnectionContent, selector: "[fConnectionContent]", inputs: ["position", "offset", "align"] }, { kind: "component", type: i1$2.FConnectionMarkerArrow, selector: "f-connection-marker-arrow", inputs: ["type"] }, { kind: "component", type: i1$2.FConnectionComponent, selector: "f-connection", inputs: ["fConnectionId", "fOutputId", "fInputId", "fRadius", "fOffset", "fBehavior", "fType", "fSelectionDisabled", "fReassignableStart", "fReassignDisabled", "fInputSide", "fOutputSide"], exportAs: ["fComponent"] }, { kind: "component", type: i1$2.FConnectionForCreateComponent, selector: "f-connection-for-create", inputs: ["fRadius", "fOffset", "fBehavior", "fType", "fInputSide", "fOutputSide"] }, { kind: "component", type: i1$2.FSnapConnectionComponent, selector: "f-snap-connection", inputs: ["fSnapThreshold", "fRadius", "fOffset", "fBehavior", "fType", "fInputSide", "fOutputSide"] }, { kind: "directive", type: i1$2.FNodeInputDirective, selector: "[fNodeInput]", inputs: ["fInputId", "fInputCategory", "fInputMultiple", "fInputDisabled", "fInputConnectableSide"], exportAs: ["fNodeInput"] }, { kind: "component", type: i1$2.FLineAlignmentComponent, selector: "f-line-alignment", inputs: ["fAlignThreshold"], exportAs: ["fComponent"] }, { kind: "component", type: i1$2.FMinimapComponent, selector: "f-minimap", inputs: ["fMinSize", "fNodeRenderLimit"], exportAs: ["fComponent"] }, { kind: "directive", type: i1$2.FNodeDirective, selector: "[fNode]", inputs: ["fNodeId", "fNodeParentId", "fNodePosition", "fNodeSize", "fNodeRotate", "fConnectOnNode", "fMinimapClass", "fNodeDraggingDisabled", "fNodeSelectionDisabled", "fIncludePadding", "fAutoExpandOnChildHit", "fAutoSizeToFitChildren"], outputs: ["fNodePositionChange", "fNodeSizeChange", "fNodeRotateChange"], exportAs: ["fComponent"] }, { kind: "directive", type: i1$2.FDragHandleDirective, selector: "[fDragHandle]" }, { kind: "directive", type: i1$2.FDraggableDirective, selector: "f-flow[fDraggable]", inputs: ["fDraggableDisabled", "fMultiSelectTrigger", "fReassignConnectionTrigger", "fCreateConnectionTrigger", "fConnectionWaypointsTrigger", "fMoveControlPointTrigger", "fNodeResizeTrigger", "fNodeRotateTrigger", "fNodeMoveTrigger", "fCanvasMoveTrigger", "fExternalItemTrigger", "fEmitOnNodeIntersect", "vCellSize", "hCellSize", "fCellSizeWhileDragging"], outputs: ["fSelectionChange", "fNodeIntersectedWithConnections", "fNodeConnectionsIntersection", "fCreateNode", "fMoveNodes", "fReassignConnection", "fCreateConnection", "fConnectionWaypointsChanged", "fDropToGroup", "fDragStarted", "fDragEnded"], exportAs: ["fDraggable"] }, { kind: "ngmodule", type: TranslocoModule }, { kind: "component", type: Button, selector: "mt-button", inputs: ["icon", "label", "tooltip", "class", "type", "styleClass", "severity", "badge", "variant", "badgeSeverity", "size", "iconPos", "autofocus", "fluid", "raised", "rounded", "text", "plain", "outlined", "link", "disabled", "loading", "pInputs"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "directive", type: Tooltip, selector: "[mtTooltip]" }, { kind: "component", type: FlowNodeComponent, selector: "fp-flow-node", inputs: ["node", "connectedOutputKeys"], outputs: ["positionChange", "nodeClick", "quickAdd", "portPlusClick", "duplicate", "remove", "testStep", "openChild", "openDetails"] }, { kind: "component", type: CanvasControlsComponent, selector: "fp-canvas-controls", inputs: ["zoom", "minimapVisible"], outputs: ["zoomIn", "zoomOut", "fitView", "resetView", "toggleMinimap", "autoLayout"] }, { kind: "component", type: StarterCardComponent, selector: "fp-starter-card", outputs: ["addTrigger", "directPick"] }, { kind: "component", type: TriggerNodeComponent, selector: "fp-trigger-node", inputs: ["trigger", "noFirstStep"], outputs: ["addFirstStep", "configure", "execute", "toggleEnabled", "remove", "positionChange"] }, { kind: "pipe", type: i2.TranslocoPipe, name: "transloco" }] });
20945
+ ], viewQueries: [{ propertyName: "flow", first: true, predicate: ["flow"], descendants: true, isSignal: true }, { propertyName: "canvas", first: true, predicate: ["canvas"], descendants: true, isSignal: true }], ngImport: i0, template: "<f-flow\n #flow\n fDraggable\n [vCellSize]=\"gridCellSize\"\n [hCellSize]=\"gridCellSize\"\n [fCellSizeWhileDragging]=\"false\"\n (fFullRendered)=\"onFullRendered()\"\n (fNodesRendered)=\"onNodesRendered()\"\n (fCreateNode)=\"onCreateNode($event)\"\n (fCreateConnection)=\"onCreateConnection($event)\"\n (fReassignConnection)=\"onReassignConnection($event)\"\n (fDragStarted)=\"onDragStarted($event)\"\n (fDragEnded)=\"onDragEnded()\"\n (fMoveNodes)=\"onMoveNodes($event)\"\n (fSelectionChange)=\"onSelectionChange($event)\"\n>\n <!-- Background dot grid \u2014 its spacing is locked to the drag cell size so\n nodes snap exactly onto the visible dots (Foblex grid-system). -->\n <f-background>\n <f-circle-pattern [radius]=\"gridCellSize\" />\n </f-background>\n\n <!-- Alignment guides + marquee selection are `f-flow` children (NOT\n `f-canvas` children \u2014 `f-canvas` only projects connections/nodes/groups,\n so helpers placed inside it are silently dropped). This mirrors the\n Foblex call-center reference layout. -->\n <f-line-alignment [fAlignThreshold]=\"20\" />\n <f-selection-area />\n\n <!-- Auto-pan: when dragging a node / connection near the viewport edge, the\n canvas pans to follow, so you can wire across off-screen nodes without\n letting go. (Foblex `f-auto-pan` plugin.) -->\n <f-auto-pan [fEdgeThreshold]=\"36\" [fSpeed]=\"8\" />\n\n <f-canvas\n #canvas\n fZoom\n [debounceTime]=\"200\"\n [fLayers]=\"canvasLayers\"\n (fCanvasChange)=\"onCanvasChange($event)\"\n >\n <!-- Floating behavior anchors the preview at the connector boundary along\n the center\u2192pointer axis, so a connection dragged from the \"+\" outlet\n leaves it cleanly (NOT from its bottom edge \u2014 `fixed` defaults an AUTO\n connectable side to bottom) and snaps to the target's nearest edge. -->\n <f-connection-for-create\n [fBehavior]=\"'floating'\"\n fType=\"adaptive-curve\"\n [fOffset]=\"0\"\n >\n <f-connection-marker-arrow />\n </f-connection-for-create>\n\n <!-- Auto-snap: while dragging a new/reassigned connection, snap to the\n nearest connector within the threshold (forgiving connect UX). -->\n <f-snap-connection\n [fBehavior]=\"'floating'\"\n fType=\"adaptive-curve\"\n [fSnapThreshold]=\"40\"\n [fOffset]=\"0\"\n >\n <f-connection-marker-arrow />\n </f-snap-connection>\n\n <!-- ngProjectAs is REQUIRED: `f-canvas` distributes projected content\n into its layer containers via selective `<ng-content select=\"...\">`\n with NO catch-all slot. A wrapper component (`fp-flow-node`, etc.)\n does not match `[fNode]` / `f-connection` on its host, so without\n `ngProjectAs` Foblex silently drops it and the canvas renders empty.\n The actual Foblex directive lives on the wrapper's inner element and\n self-registers via the mediator, so routing the wrapper into the\n right layer is all that's needed. -->\n @for (note of canvasNotes(); track note.id) {\n <fp-canvas-note\n fGroup\n fDragHandle\n [attr.data-fp-note]=\"note.id\"\n [fGroupId]=\"canvasNoteGroupId(note.id)\"\n [fGroupPosition]=\"{ x: note.x, y: note.y }\"\n [fGroupSize]=\"{ width: note.width, height: note.height }\"\n [fGroupDraggingDisabled]=\"editingNoteId() === note.id\"\n [note]=\"note\"\n [selected]=\"selectedCanvasNoteIds().includes(note.id)\"\n [editing]=\"editingNoteId() === note.id\"\n (fGroupPositionChange)=\"onNotePositionChange(note.id, $event)\"\n (fGroupSizeChange)=\"onNoteSizeChange(note.id, $event)\"\n (contentChange)=\"onNoteContentChange($event)\"\n (colorChange)=\"onNoteColorChange($event)\"\n (duplicate)=\"onNoteDuplicate($event)\"\n (remove)=\"onNoteDelete($event)\"\n (editStart)=\"onNoteEditStart($event.noteId)\"\n (editEnd)=\"editingNoteId.set(null)\"\n />\n }\n\n @for (lane of branchLanes(); track lane.id) {\n <div\n ngProjectAs=\"[fGroup]\"\n class=\"pointer-events-none absolute z-0 rounded-[14px] border border-dashed border-[color-mix(in_srgb,rgb(var(--fp-parallel))_55%,transparent)] bg-[color-mix(in_srgb,rgb(var(--fp-parallel))_8%,transparent)]\"\n [attr.data-color-token]=\"lane.colorToken\"\n [style.left.px]=\"lane.x\"\n [style.top.px]=\"lane.y\"\n [style.width.px]=\"lane.width\"\n [style.height.px]=\"lane.height\"\n >\n <span\n class=\"pointer-events-auto absolute -top-2.5 start-3 rounded-full border border-[color-mix(in_srgb,rgb(var(--fp-parallel))_40%,transparent)] bg-(--p-content-background) px-2 py-px text-[10.5px] font-semibold text-[rgb(var(--fp-parallel))]\"\n >{{ lane.label }}</span\n >\n </div>\n }\n\n <!-- Connections are inlined here (NOT wrapped in a component) so each\n <f-connection> is a DIRECT child of Foblex's connections container.\n A wrapper would make `hostElement.parentElement` the wrapper instead\n of the container, which makes Foblex's select layer-raise throw\n \"Unknown container\" the moment an edge is selected. Same call-center\n pattern used for nodes/triggers. -->\n @for (edge of edges(); track edge.id) {\n <f-connection\n class=\"group\"\n [attr.data-fp-edge]=\"edge.connectionId\"\n [fConnectionId]=\"edge.id\"\n [fOutputId]=\"edge.sourcePortId\"\n [fInputId]=\"edge.targetPortId\"\n [fBehavior]=\"'fixed'\"\n [fType]=\"'adaptive-curve'\"\n [fOffset]=\"32\"\n fInputSide=\"calculate\"\n [fReassignableStart]=\"edge.edgeKind !== 'triggerStart'\"\n [fReassignDisabled]=\"edge.edgeKind === 'triggerStart'\"\n [class.fp-edge]=\"true\"\n [class.is-trigger-start]=\"edge.edgeKind === 'triggerStart'\"\n [class.is-drop-target]=\"isDropTargetEdge(edge.connectionId)\"\n [class.is-selected]=\"isConnectionSelected(edge.connectionId)\"\n [class.is-error]=\"edge.isInvalid\"\n [class.is-formula]=\"edge.isFormula\"\n [class.is-runtime-active]=\"edge.runtimeState === 'active'\"\n [class.is-runtime-completed]=\"edge.runtimeState === 'completed'\"\n [class.is-runtime-failed]=\"edge.runtimeState === 'failed'\"\n [class.is-runtime-waiting]=\"edge.runtimeState === 'waiting'\"\n (dblclick)=\"\n edge.edgeKind === 'triggerStart' && edge.triggerId != null\n ? onTriggerOpenDetails({ triggerId: edge.triggerId })\n : onEdgeOpenDetails({ connectionId: edge.connectionId })\n \"\n >\n <!-- Arrow marker at the target \u2014 the connection carries the arrowhead;\n the input lives on the node host (no separate input element). -->\n <f-connection-marker-arrow [type]=\"markerEnd\" />\n\n <!-- Editable waypoints are intentionally NOT rendered: there is no\n backend persistence wired for manual bends, so exposing draggable\n waypoints would silently lose the user's edits on reload. -->\n\n <!-- Label / formula badge \u2014 pinned to the CENTER of the line\n (connection-content position 0.5). Fades out on hover/selected so\n the centered action box can take its place. -->\n @if (edge.label) {\n <div\n fConnectionContent\n [position]=\"0.5\"\n class=\"pointer-events-none opacity-100 transition-opacity duration-150 group-hover:opacity-0 group-[.is-selected]:opacity-0\"\n >\n <span\n class=\"inline-flex items-center gap-1.5 whitespace-nowrap rounded-full border border-[rgb(var(--fp-connector))] bg-(--p-content-background) py-1 pe-2.5 ps-2 text-[11px] font-semibold text-(--p-text-color) shadow-[0_1px_3px_rgba(15,23,42,0.1)] transition-colors group-[.is-selected]:border-(--p-primary-color) group-[.is-selected]:text-(--p-primary-color)\"\n >\n <span\n class=\"size-1.5 flex-none rounded-full bg-[rgb(var(--fp-connector))] group-[.is-selected]:bg-(--p-primary-color)\"\n ></span>\n {{ edge.label }}\n </span>\n </div>\n } @else if (edge.isFormula) {\n <div\n fConnectionContent\n [position]=\"0.5\"\n class=\"pointer-events-none opacity-100 transition-opacity duration-150 group-hover:opacity-0 group-[.is-selected]:opacity-0\"\n >\n <span\n class=\"grid h-5 w-5 place-items-center rounded-full border border-(--p-content-border-color) bg-(--p-content-background) font-mono text-[12px] italic text-(--p-primary-color) shadow-sm\"\n title=\"Conditional route\"\n >\u0192</span\n >\n </div>\n }\n\n <!-- Centered action box \u2014 edit (opens the connection MODAL) + delete.\n Same content anchor (position 0.5), revealed on hover/selected and\n overlapping the label so the actions stay centered ON the line. -->\n @if (edgeIssueCount(edge) > 0) {\n <div\n fConnectionContent\n [position]=\"0.38\"\n class=\"pointer-events-auto\"\n >\n <span\n class=\"fp-node-badge grid h-[18px] w-[18px] cursor-help place-items-center rounded-sm bg-(--p-content-background) shadow-sm outline-none transition-transform focus-visible:ring-2 focus-visible:ring-(--p-primary-color)\"\n [style.color]=\"edgeIssueColor(edge)\"\n tabindex=\"0\"\n role=\"img\"\n [attr.aria-label]=\"edgeIssueTooltip(edge)\"\n [mtTooltip]=\"edgeIssueTooltip(edge)\"\n [tooltipStyleClass]=\"edgeIssueTooltipClass(edge)\"\n tooltipPosition=\"top\"\n appendTo=\"body\"\n [escape]=\"true\"\n [showDelay]=\"180\"\n [hideDelay]=\"80\"\n (pointerdown)=\"$event.stopPropagation()\"\n (click)=\"\n $event.stopPropagation();\n edge.edgeKind === 'triggerStart' && edge.triggerId != null\n ? onTriggerOpenDetails({ triggerId: edge.triggerId })\n : onEdgeOpenDetails({ connectionId: edge.connectionId })\n \"\n (dblclick)=\"$event.stopPropagation()\"\n >\n <svg width=\"15\" height=\"15\" viewBox=\"0 0 20 20\" aria-hidden=\"true\">\n <path\n d=\"M10 3.4 18 16.8 2 16.8 Z\"\n fill=\"currentColor\"\n stroke=\"currentColor\"\n stroke-width=\"2.6\"\n stroke-linejoin=\"round\"\n />\n <rect x=\"9.05\" y=\"8\" width=\"1.9\" height=\"4.6\" rx=\"0.95\" fill=\"#fff\" />\n <circle cx=\"10\" cy=\"14.6\" r=\"1.05\" fill=\"#fff\" />\n </svg>\n </span>\n </div>\n }\n\n @if (edge.runtimeState && edge.runtimeTooltip) {\n <div\n fConnectionContent\n [position]=\"0.62\"\n class=\"pointer-events-auto\"\n >\n <span\n class=\"inline-flex h-[18px] w-[18px] cursor-help items-center justify-center rounded-full border bg-(--p-content-background) shadow-sm outline-none focus-visible:ring-2 focus-visible:ring-(--p-primary-color)\"\n [class.animate-pulse]=\"edge.runtimeState === 'active'\"\n [style.borderColor]=\"edgeRuntimeColor(edge)\"\n [style.color]=\"edgeRuntimeColor(edge)\"\n tabindex=\"0\"\n role=\"status\"\n [attr.aria-label]=\"edge.runtimeTooltip\"\n [mtTooltip]=\"edge.runtimeTooltip\"\n tooltipPosition=\"top\"\n appendTo=\"body\"\n [escape]=\"true\"\n [showDelay]=\"180\"\n [hideDelay]=\"80\"\n (pointerdown)=\"$event.stopPropagation()\"\n (click)=\"$event.stopPropagation()\"\n (dblclick)=\"$event.stopPropagation()\"\n >\n <mt-icon [icon]=\"edgeRuntimeIcon(edge)\" class=\"[&_svg]:size-3\" />\n </span>\n </div>\n }\n\n <div\n fConnectionContent\n [position]=\"0.5\"\n class=\"pointer-events-none opacity-0 transition-opacity duration-150 group-hover:pointer-events-auto group-hover:opacity-100 group-[.is-selected]:pointer-events-auto group-[.is-selected]:opacity-100\"\n >\n <div\n class=\"flex items-center gap-0.5 rounded-lg border border-(--p-content-border-color) bg-(--p-content-background) p-1 shadow-md\"\n role=\"toolbar\"\n aria-label=\"Connection actions\"\n (pointerdown)=\"$event.stopPropagation()\"\n (click)=\"$event.stopPropagation()\"\n >\n @if (edge.edgeKind === \"triggerStart\" && edge.triggerId != null) {\n <mt-button\n variant=\"text\"\n severity=\"secondary\"\n size=\"small\"\n icon=\"general.settings-01\"\n [tooltip]=\"'flowplus.trigger.node.configure' | transloco\"\n (onClick)=\"onTriggerOpenDetails({ triggerId: edge.triggerId })\"\n />\n <mt-button\n variant=\"text\"\n severity=\"danger\"\n size=\"small\"\n icon=\"general.link-broken-01\"\n [tooltip]=\"'flowplus.trigger.node.unlink' | transloco\"\n (onClick)=\"\n onTriggerStartDisconnect({ triggerId: edge.triggerId })\n \"\n />\n } @else {\n <mt-button\n variant=\"text\"\n severity=\"secondary\"\n size=\"small\"\n icon=\"general.edit-05\"\n [tooltip]=\"'flowplus.inspector.connection.title' | transloco\"\n (onClick)=\"\n onEdgeOpenDetails({ connectionId: edge.connectionId })\n \"\n />\n <mt-button\n variant=\"text\"\n severity=\"danger\"\n size=\"small\"\n icon=\"general.trash-01\"\n [tooltip]=\"'flowplus.edge.delete' | transloco\"\n (onClick)=\"onEdgeRemove({ connectionId: edge.connectionId })\"\n />\n }\n </div>\n </div>\n </f-connection>\n }\n\n <!-- The Foblex `fNode` directive lives on the component element itself\n (not an inner div). This keeps the node element a DIRECT child of\n Foblex's nodes container, so `hostElement.parentElement` IS that\n container \u2014 required by the select/move layer-raise (otherwise it\n throws \"Unknown container\"). This is the Foblex call-center pattern;\n the `fNode` attribute also routes it into the `[fNode]` projection\n slot, so `ngProjectAs` is not needed. -->\n @for (node of nodes(); track node.id) {\n <fp-flow-node\n fNode\n fDragHandle\n fNodeInput\n [attr.data-fp-node]=\"node.stepId\"\n [fInputId]=\"node.inputs[0]?.id\"\n [fInputMultiple]=\"node.inputs[0]?.allowMultiple ?? true\"\n [fInputDisabled]=\"!node.inputs.length\"\n [fNodeId]=\"node.id\"\n [fNodePosition]=\"{ x: node.x, y: node.y }\"\n (fNodePositionChange)=\"onStepPositionChange(node.stepId, $event)\"\n [node]=\"node\"\n [connectedOutputKeys]=\"connectedOutputKeys(node.stepId)\"\n (quickAdd)=\"onNodeQuickAdd($event)\"\n (portPlusClick)=\"onNodePortPlus($event)\"\n (duplicate)=\"onNodeDuplicate($event)\"\n (remove)=\"onNodeRemove($event)\"\n (openDetails)=\"onNodeOpenDetails($event)\"\n (openChild)=\"onOpenChild($event)\"\n />\n }\n\n @for (t of triggerNodes(); track t.triggerId) {\n <fp-trigger-node\n fNode\n fDragHandle\n [attr.data-fp-trigger]=\"t.triggerId\"\n [fNodeId]=\"'trigger:' + t.triggerId\"\n [fNodePosition]=\"{ x: t.x, y: t.y }\"\n (fNodePositionChange)=\"onTriggerPositionChange(t.triggerId, $event)\"\n [trigger]=\"t\"\n [noFirstStep]=\"!t.startStepId\"\n (addFirstStep)=\"onTriggerAddFirstStep($event)\"\n (configure)=\"onTriggerOpenDetails($event)\"\n (execute)=\"onTriggerExecute($event)\"\n (toggleEnabled)=\"onTriggerToggleEnabled($event)\"\n (remove)=\"onTriggerDelete($event)\"\n />\n }\n </f-canvas>\n\n <!-- Minimap \u2014 `f-flow` child (NOT inside `f-canvas`). Toggled from the\n canvas controls; `fMinSize` keeps a couple of nodes zoomed-out enough\n to give real spatial context. -->\n @if (minimapVisible()) {\n <f-minimap [fMinSize]=\"minimapMinSize\" />\n }\n\n @if (showStarter() && !store.loading()) {\n <fp-starter-card\n (addTrigger)=\"onStarterAddTrigger($event)\"\n (directPick)=\"onStarterDirectPick($event)\"\n />\n }\n\n <fp-canvas-controls\n [zoom]=\"zoom()\"\n [minimapVisible]=\"minimapVisible()\"\n (zoomIn)=\"zoomIn()\"\n (zoomOut)=\"zoomOut()\"\n (fitView)=\"fitView()\"\n (resetView)=\"resetView()\"\n (toggleMinimap)=\"toggleMinimap()\"\n (autoLayout)=\"autoLayout()\"\n />\n</f-flow>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FFlowModule }, { kind: "component", type: i1$2.FFlowComponent, selector: "f-flow", inputs: ["fFlowId", "fCache"], outputs: ["fNodesRendered", "fFullRendered", "fLoaded"] }, { kind: "component", type: i1$2.FCanvasComponent, selector: "f-canvas", inputs: ["position", "scale", "debounceTime", "fLayers"], outputs: ["fCanvasChange"] }, { kind: "component", type: i1$2.FBackgroundComponent, selector: "f-background" }, { kind: "component", type: i1$2.FCirclePatternComponent, selector: "f-circle-pattern", inputs: ["id", "color", "radius"] }, { kind: "component", type: i1$2.FAutoPan, selector: "f-auto-pan", inputs: ["fEdgeThreshold", "fSpeed", "fAcceleration"] }, { kind: "directive", type: i1$2.FZoomDirective, selector: "f-canvas[fZoom]", inputs: ["fZoom", "fWheelTrigger", "fDblClickTrigger", "fZoomMinimum", "fZoomMaximum", "fZoomStep", "fZoomDblClickStep"] }, { kind: "component", type: i1$2.FSelectionArea, selector: "f-selection-area", inputs: ["fTrigger"] }, { kind: "directive", type: i1$2.FConnectionContent, selector: "[fConnectionContent]", inputs: ["position", "offset", "align"] }, { kind: "component", type: i1$2.FConnectionMarkerArrow, selector: "f-connection-marker-arrow", inputs: ["type"] }, { kind: "component", type: i1$2.FConnectionComponent, selector: "f-connection", inputs: ["fConnectionId", "fOutputId", "fInputId", "fRadius", "fOffset", "fBehavior", "fType", "fSelectionDisabled", "fReassignableStart", "fReassignDisabled", "fInputSide", "fOutputSide"], exportAs: ["fComponent"] }, { kind: "component", type: i1$2.FConnectionForCreateComponent, selector: "f-connection-for-create", inputs: ["fRadius", "fOffset", "fBehavior", "fType", "fInputSide", "fOutputSide"] }, { kind: "component", type: i1$2.FSnapConnectionComponent, selector: "f-snap-connection", inputs: ["fSnapThreshold", "fRadius", "fOffset", "fBehavior", "fType", "fInputSide", "fOutputSide"] }, { kind: "directive", type: i1$2.FNodeInputDirective, selector: "[fNodeInput]", inputs: ["fInputId", "fInputCategory", "fInputMultiple", "fInputDisabled", "fInputConnectableSide"], exportAs: ["fNodeInput"] }, { kind: "component", type: i1$2.FLineAlignmentComponent, selector: "f-line-alignment", inputs: ["fAlignThreshold"], exportAs: ["fComponent"] }, { kind: "component", type: i1$2.FMinimapComponent, selector: "f-minimap", inputs: ["fMinSize", "fNodeRenderLimit"], exportAs: ["fComponent"] }, { kind: "directive", type: i1$2.FGroupDirective, selector: "[fGroup]", inputs: ["fGroupId", "fGroupParentId", "fGroupPosition", "fGroupSize", "fGroupRotate", "fConnectOnNode", "fMinimapClass", "fGroupDraggingDisabled", "fGroupSelectionDisabled", "fIncludePadding", "fAutoExpandOnChildHit", "fAutoSizeToFitChildren"], outputs: ["fGroupPositionChange", "fGroupSizeChange", "fGroupRotateChange"], exportAs: ["fComponent"] }, { kind: "directive", type: i1$2.FNodeDirective, selector: "[fNode]", inputs: ["fNodeId", "fNodeParentId", "fNodePosition", "fNodeSize", "fNodeRotate", "fConnectOnNode", "fMinimapClass", "fNodeDraggingDisabled", "fNodeSelectionDisabled", "fIncludePadding", "fAutoExpandOnChildHit", "fAutoSizeToFitChildren"], outputs: ["fNodePositionChange", "fNodeSizeChange", "fNodeRotateChange"], exportAs: ["fComponent"] }, { kind: "directive", type: i1$2.FDragHandleDirective, selector: "[fDragHandle]" }, { kind: "directive", type: i1$2.FDraggableDirective, selector: "f-flow[fDraggable]", inputs: ["fDraggableDisabled", "fMultiSelectTrigger", "fReassignConnectionTrigger", "fCreateConnectionTrigger", "fConnectionWaypointsTrigger", "fMoveControlPointTrigger", "fNodeResizeTrigger", "fNodeRotateTrigger", "fNodeMoveTrigger", "fCanvasMoveTrigger", "fExternalItemTrigger", "fEmitOnNodeIntersect", "vCellSize", "hCellSize", "fCellSizeWhileDragging"], outputs: ["fSelectionChange", "fNodeIntersectedWithConnections", "fNodeConnectionsIntersection", "fCreateNode", "fMoveNodes", "fReassignConnection", "fCreateConnection", "fConnectionWaypointsChanged", "fDropToGroup", "fDragStarted", "fDragEnded"], exportAs: ["fDraggable"] }, { kind: "ngmodule", type: TranslocoModule }, { kind: "component", type: Button, selector: "mt-button", inputs: ["icon", "label", "tooltip", "class", "type", "styleClass", "severity", "badge", "variant", "badgeSeverity", "size", "iconPos", "autofocus", "fluid", "raised", "rounded", "text", "plain", "outlined", "link", "disabled", "loading", "pInputs"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "directive", type: Tooltip, selector: "[mtTooltip]" }, { kind: "component", type: FlowNodeComponent, selector: "fp-flow-node", inputs: ["node", "connectedOutputKeys"], outputs: ["positionChange", "nodeClick", "quickAdd", "portPlusClick", "duplicate", "remove", "testStep", "openChild", "openDetails"] }, { kind: "component", type: CanvasControlsComponent, selector: "fp-canvas-controls", inputs: ["zoom", "minimapVisible"], outputs: ["zoomIn", "zoomOut", "fitView", "resetView", "toggleMinimap", "autoLayout"] }, { kind: "component", type: CanvasNoteComponent, selector: "fp-canvas-note", inputs: ["note", "selected", "editing", "colors"], outputs: ["contentChange", "colorChange", "duplicate", "remove", "editStart", "editEnd"] }, { kind: "component", type: StarterCardComponent, selector: "fp-starter-card", outputs: ["addTrigger", "directPick"] }, { kind: "component", type: TriggerNodeComponent, selector: "fp-trigger-node", inputs: ["trigger", "noFirstStep"], outputs: ["addFirstStep", "configure", "execute", "toggleEnabled", "remove", "positionChange"] }, { kind: "pipe", type: i2.TranslocoPipe, name: "transloco" }] });
20259
20946
  }
20260
20947
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: FlowCanvasComponent, decorators: [{
20261
20948
  type: Component,
@@ -20267,6 +20954,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
20267
20954
  Tooltip,
20268
20955
  FlowNodeComponent,
20269
20956
  CanvasControlsComponent,
20957
+ CanvasNoteComponent,
20270
20958
  StarterCardComponent,
20271
20959
  TriggerNodeComponent,
20272
20960
  ], providers: [
@@ -20277,7 +20965,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
20277
20965
  maxCascadeDepth: 8,
20278
20966
  maxAbsoluteShiftPerPlan: 10000,
20279
20967
  })),
20280
- ], template: "<f-flow\n #flow\n fDraggable\n [vCellSize]=\"gridCellSize\"\n [hCellSize]=\"gridCellSize\"\n [fCellSizeWhileDragging]=\"false\"\n (fFullRendered)=\"onFullRendered()\"\n (fNodesRendered)=\"onNodesRendered()\"\n (fCreateNode)=\"onCreateNode($event)\"\n (fCreateConnection)=\"onCreateConnection($event)\"\n (fReassignConnection)=\"onReassignConnection($event)\"\n (fDragStarted)=\"onDragStarted($event)\"\n (fDragEnded)=\"onDragEnded()\"\n (fMoveNodes)=\"onMoveNodes($event)\"\n (fSelectionChange)=\"onSelectionChange($event)\"\n>\n <!-- Background dot grid \u2014 its spacing is locked to the drag cell size so\n nodes snap exactly onto the visible dots (Foblex grid-system). -->\n <f-background>\n <f-circle-pattern [radius]=\"gridCellSize\" />\n </f-background>\n\n <!-- Alignment guides + marquee selection are `f-flow` children (NOT\n `f-canvas` children \u2014 `f-canvas` only projects connections/nodes/groups,\n so helpers placed inside it are silently dropped). This mirrors the\n Foblex call-center reference layout. -->\n <f-line-alignment [fAlignThreshold]=\"20\" />\n <f-selection-area />\n\n <!-- Auto-pan: when dragging a node / connection near the viewport edge, the\n canvas pans to follow, so you can wire across off-screen nodes without\n letting go. (Foblex `f-auto-pan` plugin.) -->\n <f-auto-pan [fEdgeThreshold]=\"36\" [fSpeed]=\"8\" />\n\n <f-canvas\n #canvas\n fZoom\n [debounceTime]=\"200\"\n (fCanvasChange)=\"onCanvasChange($event)\"\n >\n <!-- Floating behavior anchors the preview at the connector boundary along\n the center\u2192pointer axis, so a connection dragged from the \"+\" outlet\n leaves it cleanly (NOT from its bottom edge \u2014 `fixed` defaults an AUTO\n connectable side to bottom) and snaps to the target's nearest edge. -->\n <f-connection-for-create\n [fBehavior]=\"'floating'\"\n fType=\"adaptive-curve\"\n [fOffset]=\"0\"\n >\n <f-connection-marker-arrow />\n </f-connection-for-create>\n\n <!-- Auto-snap: while dragging a new/reassigned connection, snap to the\n nearest connector within the threshold (forgiving connect UX). -->\n <f-snap-connection\n [fBehavior]=\"'floating'\"\n fType=\"adaptive-curve\"\n [fSnapThreshold]=\"40\"\n [fOffset]=\"0\"\n >\n <f-connection-marker-arrow />\n </f-snap-connection>\n\n <!-- ngProjectAs is REQUIRED: `f-canvas` distributes projected content\n into its layer containers via selective `<ng-content select=\"...\">`\n with NO catch-all slot. A wrapper component (`fp-flow-node`, etc.)\n does not match `[fNode]` / `f-connection` on its host, so without\n `ngProjectAs` Foblex silently drops it and the canvas renders empty.\n The actual Foblex directive lives on the wrapper's inner element and\n self-registers via the mediator, so routing the wrapper into the\n right layer is all that's needed. -->\n @for (lane of branchLanes(); track lane.id) {\n <div\n ngProjectAs=\"[fGroup]\"\n class=\"pointer-events-none absolute z-0 rounded-[14px] border border-dashed border-[color-mix(in_srgb,rgb(var(--fp-parallel))_55%,transparent)] bg-[color-mix(in_srgb,rgb(var(--fp-parallel))_8%,transparent)]\"\n [attr.data-color-token]=\"lane.colorToken\"\n [style.left.px]=\"lane.x\"\n [style.top.px]=\"lane.y\"\n [style.width.px]=\"lane.width\"\n [style.height.px]=\"lane.height\"\n >\n <span\n class=\"pointer-events-auto absolute -top-2.5 start-3 rounded-full border border-[color-mix(in_srgb,rgb(var(--fp-parallel))_40%,transparent)] bg-(--p-content-background) px-2 py-px text-[10.5px] font-semibold text-[rgb(var(--fp-parallel))]\"\n >{{ lane.label }}</span\n >\n </div>\n }\n\n <!-- Connections are inlined here (NOT wrapped in a component) so each\n <f-connection> is a DIRECT child of Foblex's connections container.\n A wrapper would make `hostElement.parentElement` the wrapper instead\n of the container, which makes Foblex's select layer-raise throw\n \"Unknown container\" the moment an edge is selected. Same call-center\n pattern used for nodes/triggers. -->\n @for (edge of edges(); track edge.id) {\n <f-connection\n class=\"group\"\n [attr.data-fp-edge]=\"edge.connectionId\"\n [fConnectionId]=\"edge.id\"\n [fOutputId]=\"edge.sourcePortId\"\n [fInputId]=\"edge.targetPortId\"\n [fBehavior]=\"'fixed'\"\n [fType]=\"'adaptive-curve'\"\n [fOffset]=\"32\"\n fInputSide=\"calculate\"\n [fReassignableStart]=\"edge.edgeKind !== 'triggerStart'\"\n [fReassignDisabled]=\"edge.edgeKind === 'triggerStart'\"\n [class.fp-edge]=\"true\"\n [class.is-trigger-start]=\"edge.edgeKind === 'triggerStart'\"\n [class.is-drop-target]=\"isDropTargetEdge(edge.connectionId)\"\n [class.is-selected]=\"isConnectionSelected(edge.connectionId)\"\n [class.is-error]=\"edge.isInvalid\"\n [class.is-formula]=\"edge.isFormula\"\n [class.is-runtime-active]=\"edge.runtimeState === 'active'\"\n [class.is-runtime-completed]=\"edge.runtimeState === 'completed'\"\n [class.is-runtime-failed]=\"edge.runtimeState === 'failed'\"\n [class.is-runtime-waiting]=\"edge.runtimeState === 'waiting'\"\n (dblclick)=\"\n edge.edgeKind === 'triggerStart' && edge.triggerId != null\n ? onTriggerOpenDetails({ triggerId: edge.triggerId })\n : onEdgeOpenDetails({ connectionId: edge.connectionId })\n \"\n >\n <!-- Arrow marker at the target \u2014 the connection carries the arrowhead;\n the input lives on the node host (no separate input element). -->\n <f-connection-marker-arrow [type]=\"markerEnd\" />\n\n <!-- Editable waypoints are intentionally NOT rendered: there is no\n backend persistence wired for manual bends, so exposing draggable\n waypoints would silently lose the user's edits on reload. -->\n\n <!-- Label / formula badge \u2014 pinned to the CENTER of the line\n (connection-content position 0.5). Fades out on hover/selected so\n the centered action box can take its place. -->\n @if (edge.label) {\n <div\n fConnectionContent\n [position]=\"0.5\"\n class=\"pointer-events-none opacity-100 transition-opacity duration-150 group-hover:opacity-0 group-[.is-selected]:opacity-0\"\n >\n <span\n class=\"inline-flex items-center gap-1.5 whitespace-nowrap rounded-full border border-[rgb(var(--fp-connector))] bg-(--p-content-background) py-1 pe-2.5 ps-2 text-[11px] font-semibold text-(--p-text-color) shadow-[0_1px_3px_rgba(15,23,42,0.1)] transition-colors group-[.is-selected]:border-(--p-primary-color) group-[.is-selected]:text-(--p-primary-color)\"\n >\n <span\n class=\"size-1.5 flex-none rounded-full bg-[rgb(var(--fp-connector))] group-[.is-selected]:bg-(--p-primary-color)\"\n ></span>\n {{ edge.label }}\n </span>\n </div>\n } @else if (edge.isFormula) {\n <div\n fConnectionContent\n [position]=\"0.5\"\n class=\"pointer-events-none opacity-100 transition-opacity duration-150 group-hover:opacity-0 group-[.is-selected]:opacity-0\"\n >\n <span\n class=\"grid h-5 w-5 place-items-center rounded-full border border-(--p-content-border-color) bg-(--p-content-background) font-mono text-[12px] italic text-(--p-primary-color) shadow-sm\"\n title=\"Conditional route\"\n >\u0192</span\n >\n </div>\n }\n\n <!-- Centered action box \u2014 edit (opens the connection MODAL) + delete.\n Same content anchor (position 0.5), revealed on hover/selected and\n overlapping the label so the actions stay centered ON the line. -->\n @if (edgeIssueCount(edge) > 0) {\n <div\n fConnectionContent\n [position]=\"0.38\"\n class=\"pointer-events-auto\"\n >\n <span\n class=\"fp-node-badge grid h-[18px] w-[18px] cursor-help place-items-center rounded-sm bg-(--p-content-background) shadow-sm outline-none transition-transform focus-visible:ring-2 focus-visible:ring-(--p-primary-color)\"\n [style.color]=\"edgeIssueColor(edge)\"\n tabindex=\"0\"\n role=\"img\"\n [attr.aria-label]=\"edgeIssueTooltip(edge)\"\n [mtTooltip]=\"edgeIssueTooltip(edge)\"\n [tooltipStyleClass]=\"edgeIssueTooltipClass(edge)\"\n tooltipPosition=\"top\"\n appendTo=\"body\"\n [escape]=\"true\"\n [showDelay]=\"180\"\n [hideDelay]=\"80\"\n (pointerdown)=\"$event.stopPropagation()\"\n (click)=\"\n $event.stopPropagation();\n edge.edgeKind === 'triggerStart' && edge.triggerId != null\n ? onTriggerOpenDetails({ triggerId: edge.triggerId })\n : onEdgeOpenDetails({ connectionId: edge.connectionId })\n \"\n (dblclick)=\"$event.stopPropagation()\"\n >\n <svg width=\"15\" height=\"15\" viewBox=\"0 0 20 20\" aria-hidden=\"true\">\n <path\n d=\"M10 3.4 18 16.8 2 16.8 Z\"\n fill=\"currentColor\"\n stroke=\"currentColor\"\n stroke-width=\"2.6\"\n stroke-linejoin=\"round\"\n />\n <rect x=\"9.05\" y=\"8\" width=\"1.9\" height=\"4.6\" rx=\"0.95\" fill=\"#fff\" />\n <circle cx=\"10\" cy=\"14.6\" r=\"1.05\" fill=\"#fff\" />\n </svg>\n </span>\n </div>\n }\n\n @if (edge.runtimeState && edge.runtimeTooltip) {\n <div\n fConnectionContent\n [position]=\"0.62\"\n class=\"pointer-events-auto\"\n >\n <span\n class=\"inline-flex h-[18px] w-[18px] cursor-help items-center justify-center rounded-full border bg-(--p-content-background) shadow-sm outline-none focus-visible:ring-2 focus-visible:ring-(--p-primary-color)\"\n [class.animate-pulse]=\"edge.runtimeState === 'active'\"\n [style.borderColor]=\"edgeRuntimeColor(edge)\"\n [style.color]=\"edgeRuntimeColor(edge)\"\n tabindex=\"0\"\n role=\"status\"\n [attr.aria-label]=\"edge.runtimeTooltip\"\n [mtTooltip]=\"edge.runtimeTooltip\"\n tooltipPosition=\"top\"\n appendTo=\"body\"\n [escape]=\"true\"\n [showDelay]=\"180\"\n [hideDelay]=\"80\"\n (pointerdown)=\"$event.stopPropagation()\"\n (click)=\"$event.stopPropagation()\"\n (dblclick)=\"$event.stopPropagation()\"\n >\n <mt-icon [icon]=\"edgeRuntimeIcon(edge)\" class=\"[&_svg]:size-3\" />\n </span>\n </div>\n }\n\n <div\n fConnectionContent\n [position]=\"0.5\"\n class=\"pointer-events-none opacity-0 transition-opacity duration-150 group-hover:pointer-events-auto group-hover:opacity-100 group-[.is-selected]:pointer-events-auto group-[.is-selected]:opacity-100\"\n >\n <div\n class=\"flex items-center gap-0.5 rounded-lg border border-(--p-content-border-color) bg-(--p-content-background) p-1 shadow-md\"\n role=\"toolbar\"\n aria-label=\"Connection actions\"\n (pointerdown)=\"$event.stopPropagation()\"\n (click)=\"$event.stopPropagation()\"\n >\n @if (edge.edgeKind === \"triggerStart\" && edge.triggerId != null) {\n <mt-button\n variant=\"text\"\n severity=\"secondary\"\n size=\"small\"\n icon=\"general.settings-01\"\n [tooltip]=\"'flowplus.trigger.node.configure' | transloco\"\n (onClick)=\"onTriggerOpenDetails({ triggerId: edge.triggerId })\"\n />\n <mt-button\n variant=\"text\"\n severity=\"danger\"\n size=\"small\"\n icon=\"general.link-broken-01\"\n [tooltip]=\"'flowplus.trigger.node.unlink' | transloco\"\n (onClick)=\"\n onTriggerStartDisconnect({ triggerId: edge.triggerId })\n \"\n />\n } @else {\n <mt-button\n variant=\"text\"\n severity=\"secondary\"\n size=\"small\"\n icon=\"general.edit-05\"\n [tooltip]=\"'flowplus.inspector.connection.title' | transloco\"\n (onClick)=\"\n onEdgeOpenDetails({ connectionId: edge.connectionId })\n \"\n />\n <mt-button\n variant=\"text\"\n severity=\"danger\"\n size=\"small\"\n icon=\"general.trash-01\"\n [tooltip]=\"'flowplus.edge.delete' | transloco\"\n (onClick)=\"onEdgeRemove({ connectionId: edge.connectionId })\"\n />\n }\n </div>\n </div>\n </f-connection>\n }\n\n <!-- The Foblex `fNode` directive lives on the component element itself\n (not an inner div). This keeps the node element a DIRECT child of\n Foblex's nodes container, so `hostElement.parentElement` IS that\n container \u2014 required by the select/move layer-raise (otherwise it\n throws \"Unknown container\"). This is the Foblex call-center pattern;\n the `fNode` attribute also routes it into the `[fNode]` projection\n slot, so `ngProjectAs` is not needed. -->\n @for (node of nodes(); track node.id) {\n <fp-flow-node\n fNode\n fDragHandle\n fNodeInput\n [attr.data-fp-node]=\"node.stepId\"\n [fInputId]=\"node.inputs[0]?.id\"\n [fInputMultiple]=\"node.inputs[0]?.allowMultiple ?? true\"\n [fInputDisabled]=\"!node.inputs.length\"\n [fNodeId]=\"node.id\"\n [fNodePosition]=\"{ x: node.x, y: node.y }\"\n (fNodePositionChange)=\"onStepPositionChange(node.stepId, $event)\"\n [node]=\"node\"\n [connectedOutputKeys]=\"connectedOutputKeys(node.stepId)\"\n (quickAdd)=\"onNodeQuickAdd($event)\"\n (portPlusClick)=\"onNodePortPlus($event)\"\n (duplicate)=\"onNodeDuplicate($event)\"\n (remove)=\"onNodeRemove($event)\"\n (openDetails)=\"onNodeOpenDetails($event)\"\n (openChild)=\"onOpenChild($event)\"\n />\n }\n\n @for (t of triggerNodes(); track t.triggerId) {\n <fp-trigger-node\n fNode\n fDragHandle\n [attr.data-fp-trigger]=\"t.triggerId\"\n [fNodeId]=\"'trigger:' + t.triggerId\"\n [fNodePosition]=\"{ x: t.x, y: t.y }\"\n (fNodePositionChange)=\"onTriggerPositionChange(t.triggerId, $event)\"\n [trigger]=\"t\"\n [noFirstStep]=\"!t.startStepId\"\n (addFirstStep)=\"onTriggerAddFirstStep($event)\"\n (configure)=\"onTriggerOpenDetails($event)\"\n (execute)=\"onTriggerExecute($event)\"\n (toggleEnabled)=\"onTriggerToggleEnabled($event)\"\n (remove)=\"onTriggerDelete($event)\"\n />\n }\n </f-canvas>\n\n <!-- Minimap \u2014 `f-flow` child (NOT inside `f-canvas`). Toggled from the\n canvas controls; `fMinSize` keeps a couple of nodes zoomed-out enough\n to give real spatial context. -->\n @if (minimapVisible()) {\n <f-minimap [fMinSize]=\"minimapMinSize\" />\n }\n\n @if (showStarter() && !store.loading()) {\n <fp-starter-card\n (addTrigger)=\"onStarterAddTrigger($event)\"\n (directPick)=\"onStarterDirectPick($event)\"\n />\n }\n\n <fp-canvas-controls\n [zoom]=\"zoom()\"\n [minimapVisible]=\"minimapVisible()\"\n (zoomIn)=\"zoomIn()\"\n (zoomOut)=\"zoomOut()\"\n (fitView)=\"fitView()\"\n (resetView)=\"resetView()\"\n (toggleMinimap)=\"toggleMinimap()\"\n (autoLayout)=\"autoLayout()\"\n />\n</f-flow>\n" }]
20968
+ ], template: "<f-flow\n #flow\n fDraggable\n [vCellSize]=\"gridCellSize\"\n [hCellSize]=\"gridCellSize\"\n [fCellSizeWhileDragging]=\"false\"\n (fFullRendered)=\"onFullRendered()\"\n (fNodesRendered)=\"onNodesRendered()\"\n (fCreateNode)=\"onCreateNode($event)\"\n (fCreateConnection)=\"onCreateConnection($event)\"\n (fReassignConnection)=\"onReassignConnection($event)\"\n (fDragStarted)=\"onDragStarted($event)\"\n (fDragEnded)=\"onDragEnded()\"\n (fMoveNodes)=\"onMoveNodes($event)\"\n (fSelectionChange)=\"onSelectionChange($event)\"\n>\n <!-- Background dot grid \u2014 its spacing is locked to the drag cell size so\n nodes snap exactly onto the visible dots (Foblex grid-system). -->\n <f-background>\n <f-circle-pattern [radius]=\"gridCellSize\" />\n </f-background>\n\n <!-- Alignment guides + marquee selection are `f-flow` children (NOT\n `f-canvas` children \u2014 `f-canvas` only projects connections/nodes/groups,\n so helpers placed inside it are silently dropped). This mirrors the\n Foblex call-center reference layout. -->\n <f-line-alignment [fAlignThreshold]=\"20\" />\n <f-selection-area />\n\n <!-- Auto-pan: when dragging a node / connection near the viewport edge, the\n canvas pans to follow, so you can wire across off-screen nodes without\n letting go. (Foblex `f-auto-pan` plugin.) -->\n <f-auto-pan [fEdgeThreshold]=\"36\" [fSpeed]=\"8\" />\n\n <f-canvas\n #canvas\n fZoom\n [debounceTime]=\"200\"\n [fLayers]=\"canvasLayers\"\n (fCanvasChange)=\"onCanvasChange($event)\"\n >\n <!-- Floating behavior anchors the preview at the connector boundary along\n the center\u2192pointer axis, so a connection dragged from the \"+\" outlet\n leaves it cleanly (NOT from its bottom edge \u2014 `fixed` defaults an AUTO\n connectable side to bottom) and snaps to the target's nearest edge. -->\n <f-connection-for-create\n [fBehavior]=\"'floating'\"\n fType=\"adaptive-curve\"\n [fOffset]=\"0\"\n >\n <f-connection-marker-arrow />\n </f-connection-for-create>\n\n <!-- Auto-snap: while dragging a new/reassigned connection, snap to the\n nearest connector within the threshold (forgiving connect UX). -->\n <f-snap-connection\n [fBehavior]=\"'floating'\"\n fType=\"adaptive-curve\"\n [fSnapThreshold]=\"40\"\n [fOffset]=\"0\"\n >\n <f-connection-marker-arrow />\n </f-snap-connection>\n\n <!-- ngProjectAs is REQUIRED: `f-canvas` distributes projected content\n into its layer containers via selective `<ng-content select=\"...\">`\n with NO catch-all slot. A wrapper component (`fp-flow-node`, etc.)\n does not match `[fNode]` / `f-connection` on its host, so without\n `ngProjectAs` Foblex silently drops it and the canvas renders empty.\n The actual Foblex directive lives on the wrapper's inner element and\n self-registers via the mediator, so routing the wrapper into the\n right layer is all that's needed. -->\n @for (note of canvasNotes(); track note.id) {\n <fp-canvas-note\n fGroup\n fDragHandle\n [attr.data-fp-note]=\"note.id\"\n [fGroupId]=\"canvasNoteGroupId(note.id)\"\n [fGroupPosition]=\"{ x: note.x, y: note.y }\"\n [fGroupSize]=\"{ width: note.width, height: note.height }\"\n [fGroupDraggingDisabled]=\"editingNoteId() === note.id\"\n [note]=\"note\"\n [selected]=\"selectedCanvasNoteIds().includes(note.id)\"\n [editing]=\"editingNoteId() === note.id\"\n (fGroupPositionChange)=\"onNotePositionChange(note.id, $event)\"\n (fGroupSizeChange)=\"onNoteSizeChange(note.id, $event)\"\n (contentChange)=\"onNoteContentChange($event)\"\n (colorChange)=\"onNoteColorChange($event)\"\n (duplicate)=\"onNoteDuplicate($event)\"\n (remove)=\"onNoteDelete($event)\"\n (editStart)=\"onNoteEditStart($event.noteId)\"\n (editEnd)=\"editingNoteId.set(null)\"\n />\n }\n\n @for (lane of branchLanes(); track lane.id) {\n <div\n ngProjectAs=\"[fGroup]\"\n class=\"pointer-events-none absolute z-0 rounded-[14px] border border-dashed border-[color-mix(in_srgb,rgb(var(--fp-parallel))_55%,transparent)] bg-[color-mix(in_srgb,rgb(var(--fp-parallel))_8%,transparent)]\"\n [attr.data-color-token]=\"lane.colorToken\"\n [style.left.px]=\"lane.x\"\n [style.top.px]=\"lane.y\"\n [style.width.px]=\"lane.width\"\n [style.height.px]=\"lane.height\"\n >\n <span\n class=\"pointer-events-auto absolute -top-2.5 start-3 rounded-full border border-[color-mix(in_srgb,rgb(var(--fp-parallel))_40%,transparent)] bg-(--p-content-background) px-2 py-px text-[10.5px] font-semibold text-[rgb(var(--fp-parallel))]\"\n >{{ lane.label }}</span\n >\n </div>\n }\n\n <!-- Connections are inlined here (NOT wrapped in a component) so each\n <f-connection> is a DIRECT child of Foblex's connections container.\n A wrapper would make `hostElement.parentElement` the wrapper instead\n of the container, which makes Foblex's select layer-raise throw\n \"Unknown container\" the moment an edge is selected. Same call-center\n pattern used for nodes/triggers. -->\n @for (edge of edges(); track edge.id) {\n <f-connection\n class=\"group\"\n [attr.data-fp-edge]=\"edge.connectionId\"\n [fConnectionId]=\"edge.id\"\n [fOutputId]=\"edge.sourcePortId\"\n [fInputId]=\"edge.targetPortId\"\n [fBehavior]=\"'fixed'\"\n [fType]=\"'adaptive-curve'\"\n [fOffset]=\"32\"\n fInputSide=\"calculate\"\n [fReassignableStart]=\"edge.edgeKind !== 'triggerStart'\"\n [fReassignDisabled]=\"edge.edgeKind === 'triggerStart'\"\n [class.fp-edge]=\"true\"\n [class.is-trigger-start]=\"edge.edgeKind === 'triggerStart'\"\n [class.is-drop-target]=\"isDropTargetEdge(edge.connectionId)\"\n [class.is-selected]=\"isConnectionSelected(edge.connectionId)\"\n [class.is-error]=\"edge.isInvalid\"\n [class.is-formula]=\"edge.isFormula\"\n [class.is-runtime-active]=\"edge.runtimeState === 'active'\"\n [class.is-runtime-completed]=\"edge.runtimeState === 'completed'\"\n [class.is-runtime-failed]=\"edge.runtimeState === 'failed'\"\n [class.is-runtime-waiting]=\"edge.runtimeState === 'waiting'\"\n (dblclick)=\"\n edge.edgeKind === 'triggerStart' && edge.triggerId != null\n ? onTriggerOpenDetails({ triggerId: edge.triggerId })\n : onEdgeOpenDetails({ connectionId: edge.connectionId })\n \"\n >\n <!-- Arrow marker at the target \u2014 the connection carries the arrowhead;\n the input lives on the node host (no separate input element). -->\n <f-connection-marker-arrow [type]=\"markerEnd\" />\n\n <!-- Editable waypoints are intentionally NOT rendered: there is no\n backend persistence wired for manual bends, so exposing draggable\n waypoints would silently lose the user's edits on reload. -->\n\n <!-- Label / formula badge \u2014 pinned to the CENTER of the line\n (connection-content position 0.5). Fades out on hover/selected so\n the centered action box can take its place. -->\n @if (edge.label) {\n <div\n fConnectionContent\n [position]=\"0.5\"\n class=\"pointer-events-none opacity-100 transition-opacity duration-150 group-hover:opacity-0 group-[.is-selected]:opacity-0\"\n >\n <span\n class=\"inline-flex items-center gap-1.5 whitespace-nowrap rounded-full border border-[rgb(var(--fp-connector))] bg-(--p-content-background) py-1 pe-2.5 ps-2 text-[11px] font-semibold text-(--p-text-color) shadow-[0_1px_3px_rgba(15,23,42,0.1)] transition-colors group-[.is-selected]:border-(--p-primary-color) group-[.is-selected]:text-(--p-primary-color)\"\n >\n <span\n class=\"size-1.5 flex-none rounded-full bg-[rgb(var(--fp-connector))] group-[.is-selected]:bg-(--p-primary-color)\"\n ></span>\n {{ edge.label }}\n </span>\n </div>\n } @else if (edge.isFormula) {\n <div\n fConnectionContent\n [position]=\"0.5\"\n class=\"pointer-events-none opacity-100 transition-opacity duration-150 group-hover:opacity-0 group-[.is-selected]:opacity-0\"\n >\n <span\n class=\"grid h-5 w-5 place-items-center rounded-full border border-(--p-content-border-color) bg-(--p-content-background) font-mono text-[12px] italic text-(--p-primary-color) shadow-sm\"\n title=\"Conditional route\"\n >\u0192</span\n >\n </div>\n }\n\n <!-- Centered action box \u2014 edit (opens the connection MODAL) + delete.\n Same content anchor (position 0.5), revealed on hover/selected and\n overlapping the label so the actions stay centered ON the line. -->\n @if (edgeIssueCount(edge) > 0) {\n <div\n fConnectionContent\n [position]=\"0.38\"\n class=\"pointer-events-auto\"\n >\n <span\n class=\"fp-node-badge grid h-[18px] w-[18px] cursor-help place-items-center rounded-sm bg-(--p-content-background) shadow-sm outline-none transition-transform focus-visible:ring-2 focus-visible:ring-(--p-primary-color)\"\n [style.color]=\"edgeIssueColor(edge)\"\n tabindex=\"0\"\n role=\"img\"\n [attr.aria-label]=\"edgeIssueTooltip(edge)\"\n [mtTooltip]=\"edgeIssueTooltip(edge)\"\n [tooltipStyleClass]=\"edgeIssueTooltipClass(edge)\"\n tooltipPosition=\"top\"\n appendTo=\"body\"\n [escape]=\"true\"\n [showDelay]=\"180\"\n [hideDelay]=\"80\"\n (pointerdown)=\"$event.stopPropagation()\"\n (click)=\"\n $event.stopPropagation();\n edge.edgeKind === 'triggerStart' && edge.triggerId != null\n ? onTriggerOpenDetails({ triggerId: edge.triggerId })\n : onEdgeOpenDetails({ connectionId: edge.connectionId })\n \"\n (dblclick)=\"$event.stopPropagation()\"\n >\n <svg width=\"15\" height=\"15\" viewBox=\"0 0 20 20\" aria-hidden=\"true\">\n <path\n d=\"M10 3.4 18 16.8 2 16.8 Z\"\n fill=\"currentColor\"\n stroke=\"currentColor\"\n stroke-width=\"2.6\"\n stroke-linejoin=\"round\"\n />\n <rect x=\"9.05\" y=\"8\" width=\"1.9\" height=\"4.6\" rx=\"0.95\" fill=\"#fff\" />\n <circle cx=\"10\" cy=\"14.6\" r=\"1.05\" fill=\"#fff\" />\n </svg>\n </span>\n </div>\n }\n\n @if (edge.runtimeState && edge.runtimeTooltip) {\n <div\n fConnectionContent\n [position]=\"0.62\"\n class=\"pointer-events-auto\"\n >\n <span\n class=\"inline-flex h-[18px] w-[18px] cursor-help items-center justify-center rounded-full border bg-(--p-content-background) shadow-sm outline-none focus-visible:ring-2 focus-visible:ring-(--p-primary-color)\"\n [class.animate-pulse]=\"edge.runtimeState === 'active'\"\n [style.borderColor]=\"edgeRuntimeColor(edge)\"\n [style.color]=\"edgeRuntimeColor(edge)\"\n tabindex=\"0\"\n role=\"status\"\n [attr.aria-label]=\"edge.runtimeTooltip\"\n [mtTooltip]=\"edge.runtimeTooltip\"\n tooltipPosition=\"top\"\n appendTo=\"body\"\n [escape]=\"true\"\n [showDelay]=\"180\"\n [hideDelay]=\"80\"\n (pointerdown)=\"$event.stopPropagation()\"\n (click)=\"$event.stopPropagation()\"\n (dblclick)=\"$event.stopPropagation()\"\n >\n <mt-icon [icon]=\"edgeRuntimeIcon(edge)\" class=\"[&_svg]:size-3\" />\n </span>\n </div>\n }\n\n <div\n fConnectionContent\n [position]=\"0.5\"\n class=\"pointer-events-none opacity-0 transition-opacity duration-150 group-hover:pointer-events-auto group-hover:opacity-100 group-[.is-selected]:pointer-events-auto group-[.is-selected]:opacity-100\"\n >\n <div\n class=\"flex items-center gap-0.5 rounded-lg border border-(--p-content-border-color) bg-(--p-content-background) p-1 shadow-md\"\n role=\"toolbar\"\n aria-label=\"Connection actions\"\n (pointerdown)=\"$event.stopPropagation()\"\n (click)=\"$event.stopPropagation()\"\n >\n @if (edge.edgeKind === \"triggerStart\" && edge.triggerId != null) {\n <mt-button\n variant=\"text\"\n severity=\"secondary\"\n size=\"small\"\n icon=\"general.settings-01\"\n [tooltip]=\"'flowplus.trigger.node.configure' | transloco\"\n (onClick)=\"onTriggerOpenDetails({ triggerId: edge.triggerId })\"\n />\n <mt-button\n variant=\"text\"\n severity=\"danger\"\n size=\"small\"\n icon=\"general.link-broken-01\"\n [tooltip]=\"'flowplus.trigger.node.unlink' | transloco\"\n (onClick)=\"\n onTriggerStartDisconnect({ triggerId: edge.triggerId })\n \"\n />\n } @else {\n <mt-button\n variant=\"text\"\n severity=\"secondary\"\n size=\"small\"\n icon=\"general.edit-05\"\n [tooltip]=\"'flowplus.inspector.connection.title' | transloco\"\n (onClick)=\"\n onEdgeOpenDetails({ connectionId: edge.connectionId })\n \"\n />\n <mt-button\n variant=\"text\"\n severity=\"danger\"\n size=\"small\"\n icon=\"general.trash-01\"\n [tooltip]=\"'flowplus.edge.delete' | transloco\"\n (onClick)=\"onEdgeRemove({ connectionId: edge.connectionId })\"\n />\n }\n </div>\n </div>\n </f-connection>\n }\n\n <!-- The Foblex `fNode` directive lives on the component element itself\n (not an inner div). This keeps the node element a DIRECT child of\n Foblex's nodes container, so `hostElement.parentElement` IS that\n container \u2014 required by the select/move layer-raise (otherwise it\n throws \"Unknown container\"). This is the Foblex call-center pattern;\n the `fNode` attribute also routes it into the `[fNode]` projection\n slot, so `ngProjectAs` is not needed. -->\n @for (node of nodes(); track node.id) {\n <fp-flow-node\n fNode\n fDragHandle\n fNodeInput\n [attr.data-fp-node]=\"node.stepId\"\n [fInputId]=\"node.inputs[0]?.id\"\n [fInputMultiple]=\"node.inputs[0]?.allowMultiple ?? true\"\n [fInputDisabled]=\"!node.inputs.length\"\n [fNodeId]=\"node.id\"\n [fNodePosition]=\"{ x: node.x, y: node.y }\"\n (fNodePositionChange)=\"onStepPositionChange(node.stepId, $event)\"\n [node]=\"node\"\n [connectedOutputKeys]=\"connectedOutputKeys(node.stepId)\"\n (quickAdd)=\"onNodeQuickAdd($event)\"\n (portPlusClick)=\"onNodePortPlus($event)\"\n (duplicate)=\"onNodeDuplicate($event)\"\n (remove)=\"onNodeRemove($event)\"\n (openDetails)=\"onNodeOpenDetails($event)\"\n (openChild)=\"onOpenChild($event)\"\n />\n }\n\n @for (t of triggerNodes(); track t.triggerId) {\n <fp-trigger-node\n fNode\n fDragHandle\n [attr.data-fp-trigger]=\"t.triggerId\"\n [fNodeId]=\"'trigger:' + t.triggerId\"\n [fNodePosition]=\"{ x: t.x, y: t.y }\"\n (fNodePositionChange)=\"onTriggerPositionChange(t.triggerId, $event)\"\n [trigger]=\"t\"\n [noFirstStep]=\"!t.startStepId\"\n (addFirstStep)=\"onTriggerAddFirstStep($event)\"\n (configure)=\"onTriggerOpenDetails($event)\"\n (execute)=\"onTriggerExecute($event)\"\n (toggleEnabled)=\"onTriggerToggleEnabled($event)\"\n (remove)=\"onTriggerDelete($event)\"\n />\n }\n </f-canvas>\n\n <!-- Minimap \u2014 `f-flow` child (NOT inside `f-canvas`). Toggled from the\n canvas controls; `fMinSize` keeps a couple of nodes zoomed-out enough\n to give real spatial context. -->\n @if (minimapVisible()) {\n <f-minimap [fMinSize]=\"minimapMinSize\" />\n }\n\n @if (showStarter() && !store.loading()) {\n <fp-starter-card\n (addTrigger)=\"onStarterAddTrigger($event)\"\n (directPick)=\"onStarterDirectPick($event)\"\n />\n }\n\n <fp-canvas-controls\n [zoom]=\"zoom()\"\n [minimapVisible]=\"minimapVisible()\"\n (zoomIn)=\"zoomIn()\"\n (zoomOut)=\"zoomOut()\"\n (fitView)=\"fitView()\"\n (resetView)=\"resetView()\"\n (toggleMinimap)=\"toggleMinimap()\"\n (autoLayout)=\"autoLayout()\"\n />\n</f-flow>\n" }]
20281
20969
  }], ctorParameters: () => [], propDecorators: { flow: [{ type: i0.ViewChild, args: ['flow', { isSignal: true }] }], canvas: [{ type: i0.ViewChild, args: ['canvas', { isSignal: true }] }], paletteDrop: [{
20282
20970
  type: Output
20283
20971
  }], connectionCreate: [{
@@ -20334,6 +21022,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
20334
21022
  type: Output
20335
21023
  }], requestAddStep: [{
20336
21024
  type: Output
21025
+ }], noteUpdate: [{
21026
+ type: Output
21027
+ }], noteDuplicate: [{
21028
+ type: Output
21029
+ }], noteDelete: [{
21030
+ type: Output
20337
21031
  }] } });
20338
21032
  function triggerStarterKindMatchesCatalogItem(kind, item) {
20339
21033
  const metadata = item.metadata && typeof item.metadata === 'object'
@@ -20359,7 +21053,8 @@ function readTranslatePosition(el) {
20359
21053
  function isCanvasPositionElement(target) {
20360
21054
  return (target instanceof HTMLElement &&
20361
21055
  (target.matches('fp-flow-node[data-fp-node]') ||
20362
- target.matches('fp-trigger-node[data-fp-trigger]')));
21056
+ target.matches('fp-trigger-node[data-fp-trigger]') ||
21057
+ target.matches('fp-canvas-note[data-fp-note]')));
20363
21058
  }
20364
21059
  function normalizeOutputPortKey(portKey, fallback) {
20365
21060
  const key = portKey || fallback;
@@ -20526,6 +21221,8 @@ class PaletteComponent {
20526
21221
  add = new EventEmitter();
20527
21222
  /** Add a trigger from the "Add another trigger" section. */
20528
21223
  addTrigger = new EventEmitter();
21224
+ /** Add a sticky canvas note directly from the rail. */
21225
+ addNote = new EventEmitter();
20529
21226
  searchModel = '';
20530
21227
  /** Two-level navigation state. */
20531
21228
  view = signal('root', ...(ngDevMode ? [{ debugName: "view" }] : /* istanbul ignore next */ []));
@@ -20624,6 +21321,9 @@ class PaletteComponent {
20624
21321
  openTriggers() {
20625
21322
  this.view.set('triggers');
20626
21323
  }
21324
+ addCanvasNote() {
21325
+ this.addNote.emit();
21326
+ }
20627
21327
  back() {
20628
21328
  this.view.set('root');
20629
21329
  this.activeCategory.set(null);
@@ -20729,7 +21429,7 @@ class PaletteComponent {
20729
21429
  return v && v !== key ? v : fallback;
20730
21430
  }
20731
21431
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: PaletteComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
20732
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: PaletteComponent, isStandalone: true, selector: "fp-palette", outputs: { closeRequested: "closeRequested", openRequested: "openRequested", add: "add", addTrigger: "addTrigger" }, host: { listeners: { "document:pointerdown": "onDocumentPointerDown($event)" }, classAttribute: "block absolute top-0 bottom-0 start-0 w-11 z-[5] overflow-visible bg-(--p-surface-200) dark:bg-(--p-surface-800) border-e border-(--p-content-border-color)" }, ngImport: i0, template: "<div\r\n class=\"flex h-full w-11 flex-col items-center gap-2 bg-transparent py-3\"\r\n role=\"toolbar\"\r\n [attr.aria-label]=\"'flowplus.palette.rail' | transloco\"\r\n>\r\n <mt-button\n variant=\"text\"\n severity=\"secondary\"\n size=\"small\"\n [icon]=\"isExpanded() ? 'general.x-close' : 'general.plus'\"\n [styleClass]=\"\n isExpanded()\n ? 'fp-palette-rail-button is-active'\n : 'fp-palette-rail-button'\n \"\n [attr.aria-label]=\"'flowplus.palette.toggle' | transloco\"\n [mtTooltip]=\"'flowplus.palette.toggle' | transloco\"\n tooltipPosition=\"right\"\n appendTo=\"body\"\n [showDelay]=\"220\"\n [hideDelay]=\"80\"\n (onClick)=\"isExpanded() ? closeDrawer() : openDrawer()\"\n />\n\n <mt-button\n variant=\"text\"\n severity=\"secondary\"\n size=\"small\"\n icon=\"general.search-md\"\n [styleClass]=\"\n isExpanded()\n ? 'fp-palette-rail-button is-active'\n : 'fp-palette-rail-button'\n \"\n [attr.aria-label]=\"'flowplus.palette.search' | transloco\"\n [mtTooltip]=\"'flowplus.palette.search' | transloco\"\n tooltipPosition=\"right\"\n appendTo=\"body\"\n [showDelay]=\"220\"\n [hideDelay]=\"80\"\n (onClick)=\"openDrawer()\"\n />\n</div>\r\n\r\n@if (isExpanded()) {\r\n <aside\r\n class=\"absolute start-11 top-0 bottom-10 z-[6] flex w-80 min-h-0 flex-col overflow-hidden border-e border-(--p-content-border-color) bg-(--p-surface-50) shadow-lg animate-[fp-palette-slide-in_280ms_cubic-bezier(0,0,0.2,1)_both] rtl:animate-[fp-palette-slide-in-rtl_280ms_cubic-bezier(0,0,0.2,1)_both]\"\r\n role=\"complementary\"\r\n [attr.aria-label]=\"'flowplus.palette.title' | transloco\"\r\n >\r\n <header class=\"flex flex-col gap-2.5 px-3 pt-3 pb-2.5\">\r\n <div class=\"flex items-center gap-2\">\r\n @if (!searching() && view() !== \"root\") {\r\n <mt-button\n variant=\"text\"\n severity=\"secondary\"\n size=\"small\"\n icon=\"arrow.chevron-left\"\n styleClass=\"fp-palette-back-button\"\n (onClick)=\"back()\"\n [attr.aria-label]=\"'flowplus.palette.back' | transloco\"\n [mtTooltip]=\"'flowplus.palette.back' | transloco\"\n tooltipPosition=\"right\"\n appendTo=\"body\"\n [showDelay]=\"220\"\n [hideDelay]=\"80\"\n />\n }\r\n <h3\r\n class=\"min-w-0 flex-1 truncate text-[13.5px] font-bold text-(--p-text-color)\"\r\n >\r\n {{ headerTitle() }}\r\n </h3>\r\n </div>\r\n\r\n <mt-text-field\r\n [(ngModel)]=\"searchModel\"\r\n (ngModelChange)=\"onSearchChange($event)\"\r\n [placeholder]=\"'flowplus.palette.search' | transloco\"\r\n icon=\"general.search-md\"\r\n />\r\n </header>\r\n\r\n <div\r\n class=\"fp-scroll fp-pal-list flex flex-1 flex-col gap-0.5 overflow-y-auto px-2.5 pt-1 pb-6\"\r\n role=\"list\"\r\n >\r\n @if (searching() && view() !== \"triggers\") {\n @for (item of searchResults(); track paletteItemKey(item, $index)) {\r\n <div\r\n fExternalItem\n [fData]=\"item\"\n [fExternalItemId]=\"paletteItemKey(item, $index)\"\n [fPreviewMatchSize]=\"false\"\n class=\"group/item !flex w-full cursor-grab touch-none select-none items-center gap-3 rounded-lg px-2.5 py-2.5 transition-colors hover:bg-(--p-surface-100) active:cursor-grabbing focus-visible:bg-(--p-surface-100) focus-visible:outline-none\"\n role=\"button\"\r\n tabindex=\"0\"\r\n [attr.title]=\"\r\n resolve(item.description) || resolve(item.displayName)\r\n \"\r\n (click)=\"pickStep(item)\"\n (keydown.enter)=\"pickStep(item)\"\n >\n <ng-template fExternalItemPreview>\n <ng-container\n *ngTemplateOutlet=\"\n stepDragPreview;\n context: { $implicit: item }\n \"\n />\n </ng-template>\n <mt-avatar\n shape=\"square\"\n [icon]=\"iconFor(item)\"\n [style.--fp-avatar-color]=\"severityFor(item).color\"\r\n [style.--fp-avatar-bg]=\"severityFor(item).bg\"\r\n />\r\n <div class=\"min-w-0 flex-1\">\r\n <div\r\n class=\"truncate text-[13px] font-semibold text-(--p-text-color)\"\r\n >\r\n {{ resolve(item.displayName) }}\r\n </div>\r\n @if (item.description) {\r\n <div\r\n class=\"line-clamp-2 text-[11.5px] leading-[1.45] text-(--p-text-muted-color)\"\r\n >\r\n {{ resolve(item.description) }}\r\n </div>\r\n }\r\n </div>\r\n <mt-icon\r\n class=\"flex-none text-(--p-text-muted-color)/40 transition-colors group-hover/item:text-(--p-text-muted-color) [&_svg]:size-4\"\r\n icon=\"general.dots-grid\"\r\n />\r\n </div>\r\n }\r\n @if (searchResults().length === 0) {\r\n <ng-container *ngTemplateOutlet=\"emptyRow\" />\r\n }\r\n } @else if (view() === \"triggers\") {\n @for (option of visibleTriggerOptions(); track option.key) {\n <div\n fExternalItem\n [fData]=\"triggerPaletteItem(option)\"\n [fExternalItemId]=\"triggerPaletteItemKey(option, $index)\"\n [fPreviewMatchSize]=\"false\"\n class=\"group/item !flex w-full cursor-grab touch-none select-none items-center gap-3 rounded-lg px-2.5 py-2.5 text-start transition-colors hover:bg-(--p-surface-100) active:cursor-grabbing focus-visible:bg-(--p-surface-100) focus-visible:outline-none\"\n role=\"button\"\n tabindex=\"0\"\n [attr.title]=\"\n resolve(option.description) || resolve(option.displayName)\n \"\n (click)=\"pickTrigger(option)\"\n (keydown.enter)=\"pickTrigger(option)\"\n >\n <ng-template fExternalItemPreview>\n <ng-container\n *ngTemplateOutlet=\"\n triggerDragPreview;\n context: { $implicit: option }\n \"\n />\n </ng-template>\n <mt-avatar\n shape=\"square\"\n [icon]=\"triggerIconFor(option)\"\n [style.--fp-avatar-color]=\"triggerOptionVars(option).color\"\n [style.--fp-avatar-bg]=\"triggerOptionVars(option).bg\"\r\n />\r\n <span class=\"min-w-0 flex-1\">\r\n <span\r\n class=\"block truncate text-[13px] font-semibold text-(--p-text-color)\"\r\n >{{ resolve(option.displayName) }}</span\r\n >\r\n @if (option.description) {\r\n <span\r\n class=\"block line-clamp-2 text-[11.5px] leading-[1.45] text-(--p-text-muted-color)\"\r\n >{{ resolve(option.description) }}</span\r\n >\n }\n </span>\n <mt-icon\n class=\"flex-none text-(--p-text-muted-color)/40 transition-colors group-hover/item:text-(--p-text-muted-color) [&_svg]:size-4\"\n icon=\"general.dots-grid\"\n />\n </div>\n }\n @if (visibleTriggerOptions().length === 0) {\n <ng-container *ngTemplateOutlet=\"emptyRow\" />\n }\n } @else if (view() === \"category\") {\n @for (item of activeItems(); track paletteItemKey(item, $index)) {\r\n <div\r\n fExternalItem\n [fData]=\"item\"\n [fExternalItemId]=\"paletteItemKey(item, $index)\"\n [fPreviewMatchSize]=\"false\"\n class=\"group/item !flex w-full cursor-grab touch-none select-none items-center gap-3 rounded-lg px-2.5 py-2.5 transition-colors hover:bg-(--p-surface-100) active:cursor-grabbing focus-visible:bg-(--p-surface-100) focus-visible:outline-none\"\n role=\"button\"\r\n tabindex=\"0\"\r\n [attr.title]=\"\r\n resolve(item.description) || resolve(item.displayName)\r\n \"\r\n (click)=\"pickStep(item)\"\n (keydown.enter)=\"pickStep(item)\"\n >\n <ng-template fExternalItemPreview>\n <ng-container\n *ngTemplateOutlet=\"\n stepDragPreview;\n context: { $implicit: item }\n \"\n />\n </ng-template>\n <mt-avatar\n shape=\"square\"\n [icon]=\"iconFor(item)\"\n [style.--fp-avatar-color]=\"severityFor(item).color\"\r\n [style.--fp-avatar-bg]=\"severityFor(item).bg\"\r\n />\r\n <div class=\"min-w-0 flex-1\">\r\n <div\r\n class=\"truncate text-[13px] font-semibold text-(--p-text-color)\"\r\n >\r\n {{ resolve(item.displayName) }}\r\n </div>\r\n @if (item.description) {\r\n <div\r\n class=\"line-clamp-2 text-[11.5px] leading-[1.45] text-(--p-text-muted-color)\"\r\n >\r\n {{ resolve(item.description) }}\r\n </div>\r\n }\r\n </div>\r\n <mt-icon\r\n class=\"flex-none text-(--p-text-muted-color)/40 transition-colors group-hover/item:text-(--p-text-muted-color) [&_svg]:size-4\"\r\n icon=\"general.dots-grid\"\r\n />\r\n </div>\r\n }\r\n } @else {\r\n @for (group of groups(); track group.category) {\r\n <div\n role=\"button\"\n tabindex=\"0\"\n class=\"flex w-full cursor-pointer items-center gap-3 rounded-lg px-2.5 py-2.5 text-start transition-colors hover:bg-(--p-surface-100) focus-visible:bg-(--p-surface-100) focus-visible:outline-none\"\n (click)=\"openCategory(group.category)\"\n (keydown.enter)=\"openCategory(group.category)\"\n (keydown.space)=\"$event.preventDefault(); openCategory(group.category)\"\n >\n <mt-avatar\r\n shape=\"square\"\r\n [icon]=\"group.icon\"\r\n [style.--fp-avatar-color]=\"group.color\"\r\n [style.--fp-avatar-bg]=\"group.bg\"\r\n />\r\n <span class=\"min-w-0 flex-1\">\r\n <span\r\n class=\"block truncate text-[13px] font-semibold text-(--p-text-color)\"\r\n >{{ group.label }}</span\r\n >\r\n <span\r\n class=\"block line-clamp-2 text-[11.5px] leading-[1.45] text-(--p-text-muted-color)\"\r\n >{{ group.description }}</span\r\n >\r\n </span>\r\n <mt-icon\n class=\"flex-none text-(--p-text-muted-color) [&_svg]:size-4 rtl:rotate-180\"\n icon=\"arrow.chevron-right\"\n />\n </div>\n }\n\n @if (groups().length === 0) {\n <ng-container *ngTemplateOutlet=\"emptyRow\" />\n } @else {\n <div class=\"mx-2.5 my-1.5 h-px bg-(--p-content-border-color)\"></div>\n <div\n role=\"button\"\n tabindex=\"0\"\n class=\"flex w-full cursor-pointer items-center gap-3 rounded-lg px-2.5 py-2.5 text-start transition-colors hover:bg-(--p-surface-100) focus-visible:bg-(--p-surface-100) focus-visible:outline-none\"\n (click)=\"openTriggers()\"\n (keydown.enter)=\"openTriggers()\"\n (keydown.space)=\"$event.preventDefault(); openTriggers()\"\n >\n <mt-avatar\r\n shape=\"square\"\r\n icon=\"general.zap-fast\"\r\n [style.--fp-avatar-color]=\"triggerVars().color\"\r\n [style.--fp-avatar-bg]=\"triggerVars().bg\"\r\n />\r\n <span class=\"min-w-0 flex-1\">\r\n <span\r\n class=\"block truncate text-[13px] font-semibold text-(--p-text-color)\"\r\n >{{ \"flowplus.palette.addAnotherTrigger\" | transloco }}</span\r\n >\r\n <span\r\n class=\"block line-clamp-2 text-[11.5px] leading-[1.45] text-(--p-text-muted-color)\"\r\n >{{\r\n \"flowplus.palette.addAnotherTriggerDesc\" | transloco\r\n }}</span\r\n >\r\n </span>\r\n <mt-icon\n class=\"flex-none text-(--p-text-muted-color) [&_svg]:size-4 rtl:rotate-180\"\n icon=\"arrow.chevron-right\"\n />\n </div>\n }\n }\r\n </div>\r\n </aside>\r\n}\r\n\r\n<ng-template #emptyRow>\r\n <div\r\n class=\"flex flex-col items-center justify-center px-6 py-8 text-center text-(--p-text-muted-color)\"\r\n >\r\n <p>{{ \"flowplus.common.empty\" | transloco }}</p>\r\n </div>\n</ng-template>\n\n<ng-template #stepDragPreview let-item>\n <div\n class=\"pointer-events-none w-[280px] overflow-hidden rounded-xl border border-[color-mix(in_srgb,var(--fp-avatar-color)_28%,var(--p-content-border-color))] bg-(--p-content-background)/95 text-(--p-text-color) shadow-[0_18px_36px_rgba(15,23,42,0.18),0_2px_8px_rgba(15,23,42,0.08)] ring-1 ring-white/60 backdrop-blur-md dark:ring-white/10\"\n [style.--fp-avatar-color]=\"severityFor(item).color\"\n [style.--fp-avatar-bg]=\"severityFor(item).bg\"\n >\n <div class=\"flex items-start gap-3 p-3\">\n <span\n class=\"grid h-11 w-11 flex-none place-items-center rounded-xl bg-[var(--fp-avatar-bg)] text-[var(--fp-avatar-color)] shadow-inner ring-1 ring-[color-mix(in_srgb,var(--fp-avatar-color)_24%,transparent)] [&_.p-avatar]:bg-transparent [&_.p-avatar]:text-inherit [&_mt-icon]:size-5 [&_svg]:size-5\"\n >\n <mt-avatar size=\"normal\" shape=\"square\" [icon]=\"iconFor(item)\" />\n </span>\n <span class=\"min-w-0 flex-1\">\n <span class=\"block truncate text-[13.5px] font-bold leading-5\">\n {{ resolve(item.displayName) }}\n </span>\n @if (item.description) {\n <span\n class=\"mt-0.5 block line-clamp-2 text-[11.5px] leading-[1.45] text-(--p-text-muted-color)\"\n >\n {{ resolve(item.description) }}\n </span>\n }\n </span>\n </div>\n <div\n class=\"flex items-center justify-between border-t border-(--p-content-border-color)/70 bg-[color-mix(in_srgb,var(--fp-avatar-color)_7%,transparent)] px-3 py-2\"\n >\n <span\n class=\"max-w-[150px] truncate text-[10.5px] font-semibold text-(--p-text-muted-color)\"\n >\n {{ item.category || item.type }}\n </span>\n <span\n class=\"inline-flex items-center gap-1 rounded-full bg-(--p-content-background) px-2 py-0.5 text-[10.5px] font-semibold text-[var(--fp-avatar-color)] shadow-sm\"\n >\n <mt-icon icon=\"general.plus\" class=\"[&_svg]:size-3\" />\n Drop to add\n </span>\n </div>\n </div>\n</ng-template>\n\n<ng-template #triggerDragPreview let-option>\n <div\n class=\"pointer-events-none w-[280px] overflow-hidden rounded-xl border border-[color-mix(in_srgb,rgb(var(--fp-app-action))_30%,var(--p-content-border-color))] bg-(--p-content-background)/95 text-(--p-text-color) shadow-[0_18px_36px_rgba(245,158,11,0.20),0_2px_8px_rgba(15,23,42,0.08)] ring-1 ring-white/60 backdrop-blur-md dark:ring-white/10\"\n >\n <div class=\"flex items-start gap-3 p-3\">\n <span\n class=\"grid h-11 w-11 flex-none place-items-center rounded-xl bg-[color-mix(in_srgb,rgb(var(--fp-app-action))_16%,transparent)] text-[rgb(var(--fp-app-action))] shadow-inner ring-1 ring-[color-mix(in_srgb,rgb(var(--fp-app-action))_24%,transparent)] [&_.p-avatar]:bg-transparent [&_.p-avatar]:text-inherit [&_mt-icon]:size-5 [&_svg]:size-5\"\n >\n <mt-avatar\n size=\"normal\"\n shape=\"square\"\n [icon]=\"triggerIconFor(option)\"\n />\n </span>\n <span class=\"min-w-0 flex-1\">\n <span class=\"block truncate text-[13.5px] font-bold leading-5\">\n {{ resolve(option.displayName) }}\n </span>\n @if (option.description) {\n <span\n class=\"mt-0.5 block line-clamp-2 text-[11.5px] leading-[1.45] text-(--p-text-muted-color)\"\n >\n {{ resolve(option.description) }}\n </span>\n }\n </span>\n </div>\n <div\n class=\"flex items-center justify-between border-t border-(--p-content-border-color)/70 bg-[color-mix(in_srgb,rgb(var(--fp-app-action))_8%,transparent)] px-3 py-2\"\n >\n <span\n class=\"max-w-[150px] truncate text-[10.5px] font-semibold text-(--p-text-muted-color)\"\n >\n Trigger\n </span>\n <span\n class=\"inline-flex items-center gap-1 rounded-full bg-(--p-content-background) px-2 py-0.5 text-[10.5px] font-semibold text-[rgb(var(--fp-app-action))] shadow-sm\"\n >\n <mt-icon icon=\"general.plus\" class=\"[&_svg]:size-3\" />\n Drop to add\n </span>\n </div>\n </div>\n</ng-template>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: TranslocoModule }, { kind: "component", type: Avatar, selector: "mt-avatar", inputs: ["label", "icon", "image", "styleClass", "size", "shape", "badge", "badgeSize", "badgeSeverity"], outputs: ["onImageError"] }, { kind: "component", type: Button, selector: "mt-button", inputs: ["icon", "label", "tooltip", "class", "type", "styleClass", "severity", "badge", "variant", "badgeSeverity", "size", "iconPos", "autofocus", "fluid", "raised", "rounded", "text", "plain", "outlined", "link", "disabled", "loading", "pInputs"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: Icon, selector: "mt-icon", inputs: ["icon"] }, { kind: "component", type: TextField, selector: "mt-text-field", inputs: ["field", "hint", "label", "placeholder", "class", "type", "readonly", "pInputs", "required", "maxLength", "icon", "iconPosition"] }, { kind: "directive", type: Tooltip, selector: "[mtTooltip]" }, { kind: "directive", type:
21432
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: PaletteComponent, isStandalone: true, selector: "fp-palette", outputs: { closeRequested: "closeRequested", openRequested: "openRequested", add: "add", addTrigger: "addTrigger", addNote: "addNote" }, host: { listeners: { "document:pointerdown": "onDocumentPointerDown($event)" }, classAttribute: "block absolute top-0 bottom-0 start-0 w-11 z-[5] overflow-visible bg-(--p-surface-200) dark:bg-(--p-surface-800) border-e border-(--p-content-border-color)" }, ngImport: i0, template: "<div\r\n class=\"flex h-full w-11 flex-col items-center gap-2 bg-transparent py-3\"\r\n role=\"toolbar\"\r\n [attr.aria-label]=\"'flowplus.palette.rail' | transloco\"\r\n>\r\n <mt-button\n variant=\"text\"\n severity=\"secondary\"\n size=\"small\"\n [icon]=\"isExpanded() ? 'general.x-close' : 'general.plus'\"\n [styleClass]=\"\n isExpanded()\n ? 'fp-palette-rail-button is-active'\n : 'fp-palette-rail-button'\n \"\n [attr.aria-label]=\"'flowplus.palette.toggle' | transloco\"\n [mtTooltip]=\"'flowplus.palette.toggle' | transloco\"\n tooltipPosition=\"right\"\n appendTo=\"body\"\n [showDelay]=\"220\"\n [hideDelay]=\"80\"\n (onClick)=\"isExpanded() ? closeDrawer() : openDrawer()\"\n />\n\n <mt-button\n variant=\"text\"\n severity=\"secondary\"\n size=\"small\"\n icon=\"general.search-md\"\n [styleClass]=\"\n isExpanded()\n ? 'fp-palette-rail-button is-active'\n : 'fp-palette-rail-button'\n \"\n [attr.aria-label]=\"'flowplus.palette.search' | transloco\"\n [mtTooltip]=\"'flowplus.palette.search' | transloco\"\n tooltipPosition=\"right\"\n appendTo=\"body\"\n [showDelay]=\"220\"\n [hideDelay]=\"80\"\n (onClick)=\"openDrawer()\"\n />\n\n <mt-button\n variant=\"text\"\n severity=\"secondary\"\n size=\"small\"\n icon=\"communication.annotation-plus\"\n styleClass=\"fp-palette-rail-button\"\n [attr.aria-label]=\"'flowplus.canvas.addNote' | transloco\"\n [mtTooltip]=\"'flowplus.canvas.addNote' | transloco\"\n tooltipPosition=\"right\"\n appendTo=\"body\"\n [showDelay]=\"220\"\n [hideDelay]=\"80\"\n (onClick)=\"addCanvasNote()\"\n />\n</div>\n\r\n@if (isExpanded()) {\r\n <aside\r\n class=\"absolute start-11 top-0 bottom-10 z-[6] flex w-80 min-h-0 flex-col overflow-hidden border-e border-(--p-content-border-color) bg-(--p-surface-50) shadow-lg animate-[fp-palette-slide-in_280ms_cubic-bezier(0,0,0.2,1)_both] rtl:animate-[fp-palette-slide-in-rtl_280ms_cubic-bezier(0,0,0.2,1)_both]\"\r\n role=\"complementary\"\r\n [attr.aria-label]=\"'flowplus.palette.title' | transloco\"\r\n >\r\n <header class=\"flex flex-col gap-2.5 px-3 pt-3 pb-2.5\">\r\n <div class=\"flex items-center gap-2\">\r\n @if (!searching() && view() !== \"root\") {\r\n <mt-button\n variant=\"text\"\n severity=\"secondary\"\n size=\"small\"\n icon=\"arrow.chevron-left\"\n styleClass=\"fp-palette-back-button\"\n (onClick)=\"back()\"\n [attr.aria-label]=\"'flowplus.palette.back' | transloco\"\n [mtTooltip]=\"'flowplus.palette.back' | transloco\"\n tooltipPosition=\"right\"\n appendTo=\"body\"\n [showDelay]=\"220\"\n [hideDelay]=\"80\"\n />\n }\r\n <h3\r\n class=\"min-w-0 flex-1 truncate text-[13.5px] font-bold text-(--p-text-color)\"\r\n >\r\n {{ headerTitle() }}\r\n </h3>\r\n </div>\r\n\r\n <mt-text-field\r\n [(ngModel)]=\"searchModel\"\r\n (ngModelChange)=\"onSearchChange($event)\"\r\n [placeholder]=\"'flowplus.palette.search' | transloco\"\r\n icon=\"general.search-md\"\r\n />\r\n </header>\r\n\r\n <div\r\n class=\"fp-scroll fp-pal-list flex flex-1 flex-col gap-0.5 overflow-y-auto px-2.5 pt-1 pb-6\"\r\n role=\"list\"\r\n >\r\n @if (searching() && view() !== \"triggers\") {\n @for (item of searchResults(); track paletteItemKey(item, $index)) {\r\n <div\r\n fExternalItem\n [fData]=\"item\"\n [fExternalItemId]=\"paletteItemKey(item, $index)\"\n [fPreviewMatchSize]=\"false\"\n class=\"group/item !flex w-full cursor-grab touch-none select-none items-center gap-3 rounded-lg px-2.5 py-2.5 transition-colors hover:bg-(--p-surface-100) active:cursor-grabbing focus-visible:bg-(--p-surface-100) focus-visible:outline-none\"\n role=\"button\"\r\n tabindex=\"0\"\r\n [attr.title]=\"\r\n resolve(item.description) || resolve(item.displayName)\r\n \"\r\n (click)=\"pickStep(item)\"\n (keydown.enter)=\"pickStep(item)\"\n >\n <ng-template fExternalItemPreview>\n <ng-container\n *ngTemplateOutlet=\"\n stepDragPreview;\n context: { $implicit: item }\n \"\n />\n </ng-template>\n <mt-avatar\n shape=\"square\"\n [icon]=\"iconFor(item)\"\n [style.--fp-avatar-color]=\"severityFor(item).color\"\r\n [style.--fp-avatar-bg]=\"severityFor(item).bg\"\r\n />\r\n <div class=\"min-w-0 flex-1\">\r\n <div\r\n class=\"truncate text-[13px] font-semibold text-(--p-text-color)\"\r\n >\r\n {{ resolve(item.displayName) }}\r\n </div>\r\n @if (item.description) {\r\n <div\r\n class=\"line-clamp-2 text-[11.5px] leading-[1.45] text-(--p-text-muted-color)\"\r\n >\r\n {{ resolve(item.description) }}\r\n </div>\r\n }\r\n </div>\r\n <mt-icon\r\n class=\"flex-none text-(--p-text-muted-color)/40 transition-colors group-hover/item:text-(--p-text-muted-color) [&_svg]:size-4\"\r\n icon=\"general.dots-grid\"\r\n />\r\n </div>\r\n }\r\n @if (searchResults().length === 0) {\r\n <ng-container *ngTemplateOutlet=\"emptyRow\" />\r\n }\r\n } @else if (view() === \"triggers\") {\n @for (option of visibleTriggerOptions(); track option.key) {\n <div\n fExternalItem\n [fData]=\"triggerPaletteItem(option)\"\n [fExternalItemId]=\"triggerPaletteItemKey(option, $index)\"\n [fPreviewMatchSize]=\"false\"\n class=\"group/item !flex w-full cursor-grab touch-none select-none items-center gap-3 rounded-lg px-2.5 py-2.5 text-start transition-colors hover:bg-(--p-surface-100) active:cursor-grabbing focus-visible:bg-(--p-surface-100) focus-visible:outline-none\"\n role=\"button\"\n tabindex=\"0\"\n [attr.title]=\"\n resolve(option.description) || resolve(option.displayName)\n \"\n (click)=\"pickTrigger(option)\"\n (keydown.enter)=\"pickTrigger(option)\"\n >\n <ng-template fExternalItemPreview>\n <ng-container\n *ngTemplateOutlet=\"\n triggerDragPreview;\n context: { $implicit: option }\n \"\n />\n </ng-template>\n <mt-avatar\n shape=\"square\"\n [icon]=\"triggerIconFor(option)\"\n [style.--fp-avatar-color]=\"triggerOptionVars(option).color\"\n [style.--fp-avatar-bg]=\"triggerOptionVars(option).bg\"\r\n />\r\n <span class=\"min-w-0 flex-1\">\r\n <span\r\n class=\"block truncate text-[13px] font-semibold text-(--p-text-color)\"\r\n >{{ resolve(option.displayName) }}</span\r\n >\r\n @if (option.description) {\r\n <span\r\n class=\"block line-clamp-2 text-[11.5px] leading-[1.45] text-(--p-text-muted-color)\"\r\n >{{ resolve(option.description) }}</span\r\n >\n }\n </span>\n <mt-icon\n class=\"flex-none text-(--p-text-muted-color)/40 transition-colors group-hover/item:text-(--p-text-muted-color) [&_svg]:size-4\"\n icon=\"general.dots-grid\"\n />\n </div>\n }\n @if (visibleTriggerOptions().length === 0) {\n <ng-container *ngTemplateOutlet=\"emptyRow\" />\n }\n } @else if (view() === \"category\") {\n @for (item of activeItems(); track paletteItemKey(item, $index)) {\r\n <div\r\n fExternalItem\n [fData]=\"item\"\n [fExternalItemId]=\"paletteItemKey(item, $index)\"\n [fPreviewMatchSize]=\"false\"\n class=\"group/item !flex w-full cursor-grab touch-none select-none items-center gap-3 rounded-lg px-2.5 py-2.5 transition-colors hover:bg-(--p-surface-100) active:cursor-grabbing focus-visible:bg-(--p-surface-100) focus-visible:outline-none\"\n role=\"button\"\r\n tabindex=\"0\"\r\n [attr.title]=\"\r\n resolve(item.description) || resolve(item.displayName)\r\n \"\r\n (click)=\"pickStep(item)\"\n (keydown.enter)=\"pickStep(item)\"\n >\n <ng-template fExternalItemPreview>\n <ng-container\n *ngTemplateOutlet=\"\n stepDragPreview;\n context: { $implicit: item }\n \"\n />\n </ng-template>\n <mt-avatar\n shape=\"square\"\n [icon]=\"iconFor(item)\"\n [style.--fp-avatar-color]=\"severityFor(item).color\"\r\n [style.--fp-avatar-bg]=\"severityFor(item).bg\"\r\n />\r\n <div class=\"min-w-0 flex-1\">\r\n <div\r\n class=\"truncate text-[13px] font-semibold text-(--p-text-color)\"\r\n >\r\n {{ resolve(item.displayName) }}\r\n </div>\r\n @if (item.description) {\r\n <div\r\n class=\"line-clamp-2 text-[11.5px] leading-[1.45] text-(--p-text-muted-color)\"\r\n >\r\n {{ resolve(item.description) }}\r\n </div>\r\n }\r\n </div>\r\n <mt-icon\r\n class=\"flex-none text-(--p-text-muted-color)/40 transition-colors group-hover/item:text-(--p-text-muted-color) [&_svg]:size-4\"\r\n icon=\"general.dots-grid\"\r\n />\r\n </div>\r\n }\r\n } @else {\r\n @for (group of groups(); track group.category) {\r\n <div\n role=\"button\"\n tabindex=\"0\"\n class=\"flex w-full cursor-pointer items-center gap-3 rounded-lg px-2.5 py-2.5 text-start transition-colors hover:bg-(--p-surface-100) focus-visible:bg-(--p-surface-100) focus-visible:outline-none\"\n (click)=\"openCategory(group.category)\"\n (keydown.enter)=\"openCategory(group.category)\"\n (keydown.space)=\"$event.preventDefault(); openCategory(group.category)\"\n >\n <mt-avatar\r\n shape=\"square\"\r\n [icon]=\"group.icon\"\r\n [style.--fp-avatar-color]=\"group.color\"\r\n [style.--fp-avatar-bg]=\"group.bg\"\r\n />\r\n <span class=\"min-w-0 flex-1\">\r\n <span\r\n class=\"block truncate text-[13px] font-semibold text-(--p-text-color)\"\r\n >{{ group.label }}</span\r\n >\r\n <span\r\n class=\"block line-clamp-2 text-[11.5px] leading-[1.45] text-(--p-text-muted-color)\"\r\n >{{ group.description }}</span\r\n >\r\n </span>\r\n <mt-icon\n class=\"flex-none text-(--p-text-muted-color) [&_svg]:size-4 rtl:rotate-180\"\n icon=\"arrow.chevron-right\"\n />\n </div>\n }\n\n @if (groups().length === 0) {\n <ng-container *ngTemplateOutlet=\"emptyRow\" />\n } @else {\n <div class=\"mx-2.5 my-1.5 h-px bg-(--p-content-border-color)\"></div>\n <div\n role=\"button\"\n tabindex=\"0\"\n class=\"flex w-full cursor-pointer items-center gap-3 rounded-lg px-2.5 py-2.5 text-start transition-colors hover:bg-(--p-surface-100) focus-visible:bg-(--p-surface-100) focus-visible:outline-none\"\n (click)=\"openTriggers()\"\n (keydown.enter)=\"openTriggers()\"\n (keydown.space)=\"$event.preventDefault(); openTriggers()\"\n >\n <mt-avatar\r\n shape=\"square\"\r\n icon=\"general.zap-fast\"\r\n [style.--fp-avatar-color]=\"triggerVars().color\"\r\n [style.--fp-avatar-bg]=\"triggerVars().bg\"\r\n />\r\n <span class=\"min-w-0 flex-1\">\r\n <span\r\n class=\"block truncate text-[13px] font-semibold text-(--p-text-color)\"\r\n >{{ \"flowplus.palette.addAnotherTrigger\" | transloco }}</span\r\n >\r\n <span\r\n class=\"block line-clamp-2 text-[11.5px] leading-[1.45] text-(--p-text-muted-color)\"\r\n >{{\r\n \"flowplus.palette.addAnotherTriggerDesc\" | transloco\r\n }}</span\r\n >\r\n </span>\r\n <mt-icon\n class=\"flex-none text-(--p-text-muted-color) [&_svg]:size-4 rtl:rotate-180\"\n icon=\"arrow.chevron-right\"\n />\n </div>\n }\n }\r\n </div>\r\n </aside>\r\n}\r\n\r\n<ng-template #emptyRow>\r\n <div\r\n class=\"flex flex-col items-center justify-center px-6 py-8 text-center text-(--p-text-muted-color)\"\r\n >\r\n <p>{{ \"flowplus.common.empty\" | transloco }}</p>\r\n </div>\n</ng-template>\n\n<ng-template #stepDragPreview let-item>\n <div\n class=\"pointer-events-none w-[280px] overflow-hidden rounded-xl border border-[color-mix(in_srgb,var(--fp-avatar-color)_28%,var(--p-content-border-color))] bg-(--p-content-background)/95 text-(--p-text-color) shadow-[0_18px_36px_rgba(15,23,42,0.18),0_2px_8px_rgba(15,23,42,0.08)] ring-1 ring-white/60 backdrop-blur-md dark:ring-white/10\"\n [style.--fp-avatar-color]=\"severityFor(item).color\"\n [style.--fp-avatar-bg]=\"severityFor(item).bg\"\n >\n <div class=\"flex items-start gap-3 p-3\">\n <span\n class=\"grid h-11 w-11 flex-none place-items-center rounded-xl bg-[var(--fp-avatar-bg)] text-[var(--fp-avatar-color)] shadow-inner ring-1 ring-[color-mix(in_srgb,var(--fp-avatar-color)_24%,transparent)] [&_.p-avatar]:bg-transparent [&_.p-avatar]:text-inherit [&_mt-icon]:size-5 [&_svg]:size-5\"\n >\n <mt-avatar size=\"normal\" shape=\"square\" [icon]=\"iconFor(item)\" />\n </span>\n <span class=\"min-w-0 flex-1\">\n <span class=\"block truncate text-[13.5px] font-bold leading-5\">\n {{ resolve(item.displayName) }}\n </span>\n @if (item.description) {\n <span\n class=\"mt-0.5 block line-clamp-2 text-[11.5px] leading-[1.45] text-(--p-text-muted-color)\"\n >\n {{ resolve(item.description) }}\n </span>\n }\n </span>\n </div>\n <div\n class=\"flex items-center justify-between border-t border-(--p-content-border-color)/70 bg-[color-mix(in_srgb,var(--fp-avatar-color)_7%,transparent)] px-3 py-2\"\n >\n <span\n class=\"max-w-[150px] truncate text-[10.5px] font-semibold text-(--p-text-muted-color)\"\n >\n {{ item.category || item.type }}\n </span>\n <span\n class=\"inline-flex items-center gap-1 rounded-full bg-(--p-content-background) px-2 py-0.5 text-[10.5px] font-semibold text-[var(--fp-avatar-color)] shadow-sm\"\n >\n <mt-icon icon=\"general.plus\" class=\"[&_svg]:size-3\" />\n Drop to add\n </span>\n </div>\n </div>\n</ng-template>\n\n<ng-template #triggerDragPreview let-option>\n <div\n class=\"pointer-events-none w-[280px] overflow-hidden rounded-xl border border-[color-mix(in_srgb,rgb(var(--fp-app-action))_30%,var(--p-content-border-color))] bg-(--p-content-background)/95 text-(--p-text-color) shadow-[0_18px_36px_rgba(245,158,11,0.20),0_2px_8px_rgba(15,23,42,0.08)] ring-1 ring-white/60 backdrop-blur-md dark:ring-white/10\"\n >\n <div class=\"flex items-start gap-3 p-3\">\n <span\n class=\"grid h-11 w-11 flex-none place-items-center rounded-xl bg-[color-mix(in_srgb,rgb(var(--fp-app-action))_16%,transparent)] text-[rgb(var(--fp-app-action))] shadow-inner ring-1 ring-[color-mix(in_srgb,rgb(var(--fp-app-action))_24%,transparent)] [&_.p-avatar]:bg-transparent [&_.p-avatar]:text-inherit [&_mt-icon]:size-5 [&_svg]:size-5\"\n >\n <mt-avatar\n size=\"normal\"\n shape=\"square\"\n [icon]=\"triggerIconFor(option)\"\n />\n </span>\n <span class=\"min-w-0 flex-1\">\n <span class=\"block truncate text-[13.5px] font-bold leading-5\">\n {{ resolve(option.displayName) }}\n </span>\n @if (option.description) {\n <span\n class=\"mt-0.5 block line-clamp-2 text-[11.5px] leading-[1.45] text-(--p-text-muted-color)\"\n >\n {{ resolve(option.description) }}\n </span>\n }\n </span>\n </div>\n <div\n class=\"flex items-center justify-between border-t border-(--p-content-border-color)/70 bg-[color-mix(in_srgb,rgb(var(--fp-app-action))_8%,transparent)] px-3 py-2\"\n >\n <span\n class=\"max-w-[150px] truncate text-[10.5px] font-semibold text-(--p-text-muted-color)\"\n >\n Trigger\n </span>\n <span\n class=\"inline-flex items-center gap-1 rounded-full bg-(--p-content-background) px-2 py-0.5 text-[10.5px] font-semibold text-[rgb(var(--fp-app-action))] shadow-sm\"\n >\n <mt-icon icon=\"general.plus\" class=\"[&_svg]:size-3\" />\n Drop to add\n </span>\n </div>\n </div>\n</ng-template>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: TranslocoModule }, { kind: "component", type: Avatar, selector: "mt-avatar", inputs: ["label", "icon", "image", "styleClass", "size", "shape", "badge", "badgeSize", "badgeSeverity"], outputs: ["onImageError"] }, { kind: "component", type: Button, selector: "mt-button", inputs: ["icon", "label", "tooltip", "class", "type", "styleClass", "severity", "badge", "variant", "badgeSeverity", "size", "iconPos", "autofocus", "fluid", "raised", "rounded", "text", "plain", "outlined", "link", "disabled", "loading", "pInputs"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "component", type: Icon, selector: "mt-icon", inputs: ["icon"] }, { kind: "component", type: TextField, selector: "mt-text-field", inputs: ["field", "hint", "label", "placeholder", "class", "type", "readonly", "pInputs", "required", "maxLength", "icon", "iconPosition"] }, { kind: "directive", type: Tooltip, selector: "[mtTooltip]" }, { kind: "directive", type:
20733
21433
  // Native Foblex external-item drag (clone preview, like the official
20734
21434
  // add-node-from-palette example).
20735
21435
  FExternalItem, selector: "[fExternalItem]", inputs: ["fExternalItemId", "fData", "fDisabled", "fPreview", "fPreviewMatchSize", "fPlaceholder"], outputs: ["fPreviewChange", "fPlaceholderChange"] }, { kind: "directive", type: FExternalItemPreview, selector: "ng-template[fExternalItemPreview]" }, { kind: "pipe", type: i2.TranslocoPipe, name: "transloco" }] });
@@ -20751,7 +21451,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
20751
21451
  FExternalItemPreview,
20752
21452
  ], host: {
20753
21453
  class: 'block absolute top-0 bottom-0 start-0 w-11 z-[5] overflow-visible bg-(--p-surface-200) dark:bg-(--p-surface-800) border-e border-(--p-content-border-color)',
20754
- }, template: "<div\r\n class=\"flex h-full w-11 flex-col items-center gap-2 bg-transparent py-3\"\r\n role=\"toolbar\"\r\n [attr.aria-label]=\"'flowplus.palette.rail' | transloco\"\r\n>\r\n <mt-button\n variant=\"text\"\n severity=\"secondary\"\n size=\"small\"\n [icon]=\"isExpanded() ? 'general.x-close' : 'general.plus'\"\n [styleClass]=\"\n isExpanded()\n ? 'fp-palette-rail-button is-active'\n : 'fp-palette-rail-button'\n \"\n [attr.aria-label]=\"'flowplus.palette.toggle' | transloco\"\n [mtTooltip]=\"'flowplus.palette.toggle' | transloco\"\n tooltipPosition=\"right\"\n appendTo=\"body\"\n [showDelay]=\"220\"\n [hideDelay]=\"80\"\n (onClick)=\"isExpanded() ? closeDrawer() : openDrawer()\"\n />\n\n <mt-button\n variant=\"text\"\n severity=\"secondary\"\n size=\"small\"\n icon=\"general.search-md\"\n [styleClass]=\"\n isExpanded()\n ? 'fp-palette-rail-button is-active'\n : 'fp-palette-rail-button'\n \"\n [attr.aria-label]=\"'flowplus.palette.search' | transloco\"\n [mtTooltip]=\"'flowplus.palette.search' | transloco\"\n tooltipPosition=\"right\"\n appendTo=\"body\"\n [showDelay]=\"220\"\n [hideDelay]=\"80\"\n (onClick)=\"openDrawer()\"\n />\n</div>\r\n\r\n@if (isExpanded()) {\r\n <aside\r\n class=\"absolute start-11 top-0 bottom-10 z-[6] flex w-80 min-h-0 flex-col overflow-hidden border-e border-(--p-content-border-color) bg-(--p-surface-50) shadow-lg animate-[fp-palette-slide-in_280ms_cubic-bezier(0,0,0.2,1)_both] rtl:animate-[fp-palette-slide-in-rtl_280ms_cubic-bezier(0,0,0.2,1)_both]\"\r\n role=\"complementary\"\r\n [attr.aria-label]=\"'flowplus.palette.title' | transloco\"\r\n >\r\n <header class=\"flex flex-col gap-2.5 px-3 pt-3 pb-2.5\">\r\n <div class=\"flex items-center gap-2\">\r\n @if (!searching() && view() !== \"root\") {\r\n <mt-button\n variant=\"text\"\n severity=\"secondary\"\n size=\"small\"\n icon=\"arrow.chevron-left\"\n styleClass=\"fp-palette-back-button\"\n (onClick)=\"back()\"\n [attr.aria-label]=\"'flowplus.palette.back' | transloco\"\n [mtTooltip]=\"'flowplus.palette.back' | transloco\"\n tooltipPosition=\"right\"\n appendTo=\"body\"\n [showDelay]=\"220\"\n [hideDelay]=\"80\"\n />\n }\r\n <h3\r\n class=\"min-w-0 flex-1 truncate text-[13.5px] font-bold text-(--p-text-color)\"\r\n >\r\n {{ headerTitle() }}\r\n </h3>\r\n </div>\r\n\r\n <mt-text-field\r\n [(ngModel)]=\"searchModel\"\r\n (ngModelChange)=\"onSearchChange($event)\"\r\n [placeholder]=\"'flowplus.palette.search' | transloco\"\r\n icon=\"general.search-md\"\r\n />\r\n </header>\r\n\r\n <div\r\n class=\"fp-scroll fp-pal-list flex flex-1 flex-col gap-0.5 overflow-y-auto px-2.5 pt-1 pb-6\"\r\n role=\"list\"\r\n >\r\n @if (searching() && view() !== \"triggers\") {\n @for (item of searchResults(); track paletteItemKey(item, $index)) {\r\n <div\r\n fExternalItem\n [fData]=\"item\"\n [fExternalItemId]=\"paletteItemKey(item, $index)\"\n [fPreviewMatchSize]=\"false\"\n class=\"group/item !flex w-full cursor-grab touch-none select-none items-center gap-3 rounded-lg px-2.5 py-2.5 transition-colors hover:bg-(--p-surface-100) active:cursor-grabbing focus-visible:bg-(--p-surface-100) focus-visible:outline-none\"\n role=\"button\"\r\n tabindex=\"0\"\r\n [attr.title]=\"\r\n resolve(item.description) || resolve(item.displayName)\r\n \"\r\n (click)=\"pickStep(item)\"\n (keydown.enter)=\"pickStep(item)\"\n >\n <ng-template fExternalItemPreview>\n <ng-container\n *ngTemplateOutlet=\"\n stepDragPreview;\n context: { $implicit: item }\n \"\n />\n </ng-template>\n <mt-avatar\n shape=\"square\"\n [icon]=\"iconFor(item)\"\n [style.--fp-avatar-color]=\"severityFor(item).color\"\r\n [style.--fp-avatar-bg]=\"severityFor(item).bg\"\r\n />\r\n <div class=\"min-w-0 flex-1\">\r\n <div\r\n class=\"truncate text-[13px] font-semibold text-(--p-text-color)\"\r\n >\r\n {{ resolve(item.displayName) }}\r\n </div>\r\n @if (item.description) {\r\n <div\r\n class=\"line-clamp-2 text-[11.5px] leading-[1.45] text-(--p-text-muted-color)\"\r\n >\r\n {{ resolve(item.description) }}\r\n </div>\r\n }\r\n </div>\r\n <mt-icon\r\n class=\"flex-none text-(--p-text-muted-color)/40 transition-colors group-hover/item:text-(--p-text-muted-color) [&_svg]:size-4\"\r\n icon=\"general.dots-grid\"\r\n />\r\n </div>\r\n }\r\n @if (searchResults().length === 0) {\r\n <ng-container *ngTemplateOutlet=\"emptyRow\" />\r\n }\r\n } @else if (view() === \"triggers\") {\n @for (option of visibleTriggerOptions(); track option.key) {\n <div\n fExternalItem\n [fData]=\"triggerPaletteItem(option)\"\n [fExternalItemId]=\"triggerPaletteItemKey(option, $index)\"\n [fPreviewMatchSize]=\"false\"\n class=\"group/item !flex w-full cursor-grab touch-none select-none items-center gap-3 rounded-lg px-2.5 py-2.5 text-start transition-colors hover:bg-(--p-surface-100) active:cursor-grabbing focus-visible:bg-(--p-surface-100) focus-visible:outline-none\"\n role=\"button\"\n tabindex=\"0\"\n [attr.title]=\"\n resolve(option.description) || resolve(option.displayName)\n \"\n (click)=\"pickTrigger(option)\"\n (keydown.enter)=\"pickTrigger(option)\"\n >\n <ng-template fExternalItemPreview>\n <ng-container\n *ngTemplateOutlet=\"\n triggerDragPreview;\n context: { $implicit: option }\n \"\n />\n </ng-template>\n <mt-avatar\n shape=\"square\"\n [icon]=\"triggerIconFor(option)\"\n [style.--fp-avatar-color]=\"triggerOptionVars(option).color\"\n [style.--fp-avatar-bg]=\"triggerOptionVars(option).bg\"\r\n />\r\n <span class=\"min-w-0 flex-1\">\r\n <span\r\n class=\"block truncate text-[13px] font-semibold text-(--p-text-color)\"\r\n >{{ resolve(option.displayName) }}</span\r\n >\r\n @if (option.description) {\r\n <span\r\n class=\"block line-clamp-2 text-[11.5px] leading-[1.45] text-(--p-text-muted-color)\"\r\n >{{ resolve(option.description) }}</span\r\n >\n }\n </span>\n <mt-icon\n class=\"flex-none text-(--p-text-muted-color)/40 transition-colors group-hover/item:text-(--p-text-muted-color) [&_svg]:size-4\"\n icon=\"general.dots-grid\"\n />\n </div>\n }\n @if (visibleTriggerOptions().length === 0) {\n <ng-container *ngTemplateOutlet=\"emptyRow\" />\n }\n } @else if (view() === \"category\") {\n @for (item of activeItems(); track paletteItemKey(item, $index)) {\r\n <div\r\n fExternalItem\n [fData]=\"item\"\n [fExternalItemId]=\"paletteItemKey(item, $index)\"\n [fPreviewMatchSize]=\"false\"\n class=\"group/item !flex w-full cursor-grab touch-none select-none items-center gap-3 rounded-lg px-2.5 py-2.5 transition-colors hover:bg-(--p-surface-100) active:cursor-grabbing focus-visible:bg-(--p-surface-100) focus-visible:outline-none\"\n role=\"button\"\r\n tabindex=\"0\"\r\n [attr.title]=\"\r\n resolve(item.description) || resolve(item.displayName)\r\n \"\r\n (click)=\"pickStep(item)\"\n (keydown.enter)=\"pickStep(item)\"\n >\n <ng-template fExternalItemPreview>\n <ng-container\n *ngTemplateOutlet=\"\n stepDragPreview;\n context: { $implicit: item }\n \"\n />\n </ng-template>\n <mt-avatar\n shape=\"square\"\n [icon]=\"iconFor(item)\"\n [style.--fp-avatar-color]=\"severityFor(item).color\"\r\n [style.--fp-avatar-bg]=\"severityFor(item).bg\"\r\n />\r\n <div class=\"min-w-0 flex-1\">\r\n <div\r\n class=\"truncate text-[13px] font-semibold text-(--p-text-color)\"\r\n >\r\n {{ resolve(item.displayName) }}\r\n </div>\r\n @if (item.description) {\r\n <div\r\n class=\"line-clamp-2 text-[11.5px] leading-[1.45] text-(--p-text-muted-color)\"\r\n >\r\n {{ resolve(item.description) }}\r\n </div>\r\n }\r\n </div>\r\n <mt-icon\r\n class=\"flex-none text-(--p-text-muted-color)/40 transition-colors group-hover/item:text-(--p-text-muted-color) [&_svg]:size-4\"\r\n icon=\"general.dots-grid\"\r\n />\r\n </div>\r\n }\r\n } @else {\r\n @for (group of groups(); track group.category) {\r\n <div\n role=\"button\"\n tabindex=\"0\"\n class=\"flex w-full cursor-pointer items-center gap-3 rounded-lg px-2.5 py-2.5 text-start transition-colors hover:bg-(--p-surface-100) focus-visible:bg-(--p-surface-100) focus-visible:outline-none\"\n (click)=\"openCategory(group.category)\"\n (keydown.enter)=\"openCategory(group.category)\"\n (keydown.space)=\"$event.preventDefault(); openCategory(group.category)\"\n >\n <mt-avatar\r\n shape=\"square\"\r\n [icon]=\"group.icon\"\r\n [style.--fp-avatar-color]=\"group.color\"\r\n [style.--fp-avatar-bg]=\"group.bg\"\r\n />\r\n <span class=\"min-w-0 flex-1\">\r\n <span\r\n class=\"block truncate text-[13px] font-semibold text-(--p-text-color)\"\r\n >{{ group.label }}</span\r\n >\r\n <span\r\n class=\"block line-clamp-2 text-[11.5px] leading-[1.45] text-(--p-text-muted-color)\"\r\n >{{ group.description }}</span\r\n >\r\n </span>\r\n <mt-icon\n class=\"flex-none text-(--p-text-muted-color) [&_svg]:size-4 rtl:rotate-180\"\n icon=\"arrow.chevron-right\"\n />\n </div>\n }\n\n @if (groups().length === 0) {\n <ng-container *ngTemplateOutlet=\"emptyRow\" />\n } @else {\n <div class=\"mx-2.5 my-1.5 h-px bg-(--p-content-border-color)\"></div>\n <div\n role=\"button\"\n tabindex=\"0\"\n class=\"flex w-full cursor-pointer items-center gap-3 rounded-lg px-2.5 py-2.5 text-start transition-colors hover:bg-(--p-surface-100) focus-visible:bg-(--p-surface-100) focus-visible:outline-none\"\n (click)=\"openTriggers()\"\n (keydown.enter)=\"openTriggers()\"\n (keydown.space)=\"$event.preventDefault(); openTriggers()\"\n >\n <mt-avatar\r\n shape=\"square\"\r\n icon=\"general.zap-fast\"\r\n [style.--fp-avatar-color]=\"triggerVars().color\"\r\n [style.--fp-avatar-bg]=\"triggerVars().bg\"\r\n />\r\n <span class=\"min-w-0 flex-1\">\r\n <span\r\n class=\"block truncate text-[13px] font-semibold text-(--p-text-color)\"\r\n >{{ \"flowplus.palette.addAnotherTrigger\" | transloco }}</span\r\n >\r\n <span\r\n class=\"block line-clamp-2 text-[11.5px] leading-[1.45] text-(--p-text-muted-color)\"\r\n >{{\r\n \"flowplus.palette.addAnotherTriggerDesc\" | transloco\r\n }}</span\r\n >\r\n </span>\r\n <mt-icon\n class=\"flex-none text-(--p-text-muted-color) [&_svg]:size-4 rtl:rotate-180\"\n icon=\"arrow.chevron-right\"\n />\n </div>\n }\n }\r\n </div>\r\n </aside>\r\n}\r\n\r\n<ng-template #emptyRow>\r\n <div\r\n class=\"flex flex-col items-center justify-center px-6 py-8 text-center text-(--p-text-muted-color)\"\r\n >\r\n <p>{{ \"flowplus.common.empty\" | transloco }}</p>\r\n </div>\n</ng-template>\n\n<ng-template #stepDragPreview let-item>\n <div\n class=\"pointer-events-none w-[280px] overflow-hidden rounded-xl border border-[color-mix(in_srgb,var(--fp-avatar-color)_28%,var(--p-content-border-color))] bg-(--p-content-background)/95 text-(--p-text-color) shadow-[0_18px_36px_rgba(15,23,42,0.18),0_2px_8px_rgba(15,23,42,0.08)] ring-1 ring-white/60 backdrop-blur-md dark:ring-white/10\"\n [style.--fp-avatar-color]=\"severityFor(item).color\"\n [style.--fp-avatar-bg]=\"severityFor(item).bg\"\n >\n <div class=\"flex items-start gap-3 p-3\">\n <span\n class=\"grid h-11 w-11 flex-none place-items-center rounded-xl bg-[var(--fp-avatar-bg)] text-[var(--fp-avatar-color)] shadow-inner ring-1 ring-[color-mix(in_srgb,var(--fp-avatar-color)_24%,transparent)] [&_.p-avatar]:bg-transparent [&_.p-avatar]:text-inherit [&_mt-icon]:size-5 [&_svg]:size-5\"\n >\n <mt-avatar size=\"normal\" shape=\"square\" [icon]=\"iconFor(item)\" />\n </span>\n <span class=\"min-w-0 flex-1\">\n <span class=\"block truncate text-[13.5px] font-bold leading-5\">\n {{ resolve(item.displayName) }}\n </span>\n @if (item.description) {\n <span\n class=\"mt-0.5 block line-clamp-2 text-[11.5px] leading-[1.45] text-(--p-text-muted-color)\"\n >\n {{ resolve(item.description) }}\n </span>\n }\n </span>\n </div>\n <div\n class=\"flex items-center justify-between border-t border-(--p-content-border-color)/70 bg-[color-mix(in_srgb,var(--fp-avatar-color)_7%,transparent)] px-3 py-2\"\n >\n <span\n class=\"max-w-[150px] truncate text-[10.5px] font-semibold text-(--p-text-muted-color)\"\n >\n {{ item.category || item.type }}\n </span>\n <span\n class=\"inline-flex items-center gap-1 rounded-full bg-(--p-content-background) px-2 py-0.5 text-[10.5px] font-semibold text-[var(--fp-avatar-color)] shadow-sm\"\n >\n <mt-icon icon=\"general.plus\" class=\"[&_svg]:size-3\" />\n Drop to add\n </span>\n </div>\n </div>\n</ng-template>\n\n<ng-template #triggerDragPreview let-option>\n <div\n class=\"pointer-events-none w-[280px] overflow-hidden rounded-xl border border-[color-mix(in_srgb,rgb(var(--fp-app-action))_30%,var(--p-content-border-color))] bg-(--p-content-background)/95 text-(--p-text-color) shadow-[0_18px_36px_rgba(245,158,11,0.20),0_2px_8px_rgba(15,23,42,0.08)] ring-1 ring-white/60 backdrop-blur-md dark:ring-white/10\"\n >\n <div class=\"flex items-start gap-3 p-3\">\n <span\n class=\"grid h-11 w-11 flex-none place-items-center rounded-xl bg-[color-mix(in_srgb,rgb(var(--fp-app-action))_16%,transparent)] text-[rgb(var(--fp-app-action))] shadow-inner ring-1 ring-[color-mix(in_srgb,rgb(var(--fp-app-action))_24%,transparent)] [&_.p-avatar]:bg-transparent [&_.p-avatar]:text-inherit [&_mt-icon]:size-5 [&_svg]:size-5\"\n >\n <mt-avatar\n size=\"normal\"\n shape=\"square\"\n [icon]=\"triggerIconFor(option)\"\n />\n </span>\n <span class=\"min-w-0 flex-1\">\n <span class=\"block truncate text-[13.5px] font-bold leading-5\">\n {{ resolve(option.displayName) }}\n </span>\n @if (option.description) {\n <span\n class=\"mt-0.5 block line-clamp-2 text-[11.5px] leading-[1.45] text-(--p-text-muted-color)\"\n >\n {{ resolve(option.description) }}\n </span>\n }\n </span>\n </div>\n <div\n class=\"flex items-center justify-between border-t border-(--p-content-border-color)/70 bg-[color-mix(in_srgb,rgb(var(--fp-app-action))_8%,transparent)] px-3 py-2\"\n >\n <span\n class=\"max-w-[150px] truncate text-[10.5px] font-semibold text-(--p-text-muted-color)\"\n >\n Trigger\n </span>\n <span\n class=\"inline-flex items-center gap-1 rounded-full bg-(--p-content-background) px-2 py-0.5 text-[10.5px] font-semibold text-[rgb(var(--fp-app-action))] shadow-sm\"\n >\n <mt-icon icon=\"general.plus\" class=\"[&_svg]:size-3\" />\n Drop to add\n </span>\n </div>\n </div>\n</ng-template>\n" }]
21454
+ }, template: "<div\r\n class=\"flex h-full w-11 flex-col items-center gap-2 bg-transparent py-3\"\r\n role=\"toolbar\"\r\n [attr.aria-label]=\"'flowplus.palette.rail' | transloco\"\r\n>\r\n <mt-button\n variant=\"text\"\n severity=\"secondary\"\n size=\"small\"\n [icon]=\"isExpanded() ? 'general.x-close' : 'general.plus'\"\n [styleClass]=\"\n isExpanded()\n ? 'fp-palette-rail-button is-active'\n : 'fp-palette-rail-button'\n \"\n [attr.aria-label]=\"'flowplus.palette.toggle' | transloco\"\n [mtTooltip]=\"'flowplus.palette.toggle' | transloco\"\n tooltipPosition=\"right\"\n appendTo=\"body\"\n [showDelay]=\"220\"\n [hideDelay]=\"80\"\n (onClick)=\"isExpanded() ? closeDrawer() : openDrawer()\"\n />\n\n <mt-button\n variant=\"text\"\n severity=\"secondary\"\n size=\"small\"\n icon=\"general.search-md\"\n [styleClass]=\"\n isExpanded()\n ? 'fp-palette-rail-button is-active'\n : 'fp-palette-rail-button'\n \"\n [attr.aria-label]=\"'flowplus.palette.search' | transloco\"\n [mtTooltip]=\"'flowplus.palette.search' | transloco\"\n tooltipPosition=\"right\"\n appendTo=\"body\"\n [showDelay]=\"220\"\n [hideDelay]=\"80\"\n (onClick)=\"openDrawer()\"\n />\n\n <mt-button\n variant=\"text\"\n severity=\"secondary\"\n size=\"small\"\n icon=\"communication.annotation-plus\"\n styleClass=\"fp-palette-rail-button\"\n [attr.aria-label]=\"'flowplus.canvas.addNote' | transloco\"\n [mtTooltip]=\"'flowplus.canvas.addNote' | transloco\"\n tooltipPosition=\"right\"\n appendTo=\"body\"\n [showDelay]=\"220\"\n [hideDelay]=\"80\"\n (onClick)=\"addCanvasNote()\"\n />\n</div>\n\r\n@if (isExpanded()) {\r\n <aside\r\n class=\"absolute start-11 top-0 bottom-10 z-[6] flex w-80 min-h-0 flex-col overflow-hidden border-e border-(--p-content-border-color) bg-(--p-surface-50) shadow-lg animate-[fp-palette-slide-in_280ms_cubic-bezier(0,0,0.2,1)_both] rtl:animate-[fp-palette-slide-in-rtl_280ms_cubic-bezier(0,0,0.2,1)_both]\"\r\n role=\"complementary\"\r\n [attr.aria-label]=\"'flowplus.palette.title' | transloco\"\r\n >\r\n <header class=\"flex flex-col gap-2.5 px-3 pt-3 pb-2.5\">\r\n <div class=\"flex items-center gap-2\">\r\n @if (!searching() && view() !== \"root\") {\r\n <mt-button\n variant=\"text\"\n severity=\"secondary\"\n size=\"small\"\n icon=\"arrow.chevron-left\"\n styleClass=\"fp-palette-back-button\"\n (onClick)=\"back()\"\n [attr.aria-label]=\"'flowplus.palette.back' | transloco\"\n [mtTooltip]=\"'flowplus.palette.back' | transloco\"\n tooltipPosition=\"right\"\n appendTo=\"body\"\n [showDelay]=\"220\"\n [hideDelay]=\"80\"\n />\n }\r\n <h3\r\n class=\"min-w-0 flex-1 truncate text-[13.5px] font-bold text-(--p-text-color)\"\r\n >\r\n {{ headerTitle() }}\r\n </h3>\r\n </div>\r\n\r\n <mt-text-field\r\n [(ngModel)]=\"searchModel\"\r\n (ngModelChange)=\"onSearchChange($event)\"\r\n [placeholder]=\"'flowplus.palette.search' | transloco\"\r\n icon=\"general.search-md\"\r\n />\r\n </header>\r\n\r\n <div\r\n class=\"fp-scroll fp-pal-list flex flex-1 flex-col gap-0.5 overflow-y-auto px-2.5 pt-1 pb-6\"\r\n role=\"list\"\r\n >\r\n @if (searching() && view() !== \"triggers\") {\n @for (item of searchResults(); track paletteItemKey(item, $index)) {\r\n <div\r\n fExternalItem\n [fData]=\"item\"\n [fExternalItemId]=\"paletteItemKey(item, $index)\"\n [fPreviewMatchSize]=\"false\"\n class=\"group/item !flex w-full cursor-grab touch-none select-none items-center gap-3 rounded-lg px-2.5 py-2.5 transition-colors hover:bg-(--p-surface-100) active:cursor-grabbing focus-visible:bg-(--p-surface-100) focus-visible:outline-none\"\n role=\"button\"\r\n tabindex=\"0\"\r\n [attr.title]=\"\r\n resolve(item.description) || resolve(item.displayName)\r\n \"\r\n (click)=\"pickStep(item)\"\n (keydown.enter)=\"pickStep(item)\"\n >\n <ng-template fExternalItemPreview>\n <ng-container\n *ngTemplateOutlet=\"\n stepDragPreview;\n context: { $implicit: item }\n \"\n />\n </ng-template>\n <mt-avatar\n shape=\"square\"\n [icon]=\"iconFor(item)\"\n [style.--fp-avatar-color]=\"severityFor(item).color\"\r\n [style.--fp-avatar-bg]=\"severityFor(item).bg\"\r\n />\r\n <div class=\"min-w-0 flex-1\">\r\n <div\r\n class=\"truncate text-[13px] font-semibold text-(--p-text-color)\"\r\n >\r\n {{ resolve(item.displayName) }}\r\n </div>\r\n @if (item.description) {\r\n <div\r\n class=\"line-clamp-2 text-[11.5px] leading-[1.45] text-(--p-text-muted-color)\"\r\n >\r\n {{ resolve(item.description) }}\r\n </div>\r\n }\r\n </div>\r\n <mt-icon\r\n class=\"flex-none text-(--p-text-muted-color)/40 transition-colors group-hover/item:text-(--p-text-muted-color) [&_svg]:size-4\"\r\n icon=\"general.dots-grid\"\r\n />\r\n </div>\r\n }\r\n @if (searchResults().length === 0) {\r\n <ng-container *ngTemplateOutlet=\"emptyRow\" />\r\n }\r\n } @else if (view() === \"triggers\") {\n @for (option of visibleTriggerOptions(); track option.key) {\n <div\n fExternalItem\n [fData]=\"triggerPaletteItem(option)\"\n [fExternalItemId]=\"triggerPaletteItemKey(option, $index)\"\n [fPreviewMatchSize]=\"false\"\n class=\"group/item !flex w-full cursor-grab touch-none select-none items-center gap-3 rounded-lg px-2.5 py-2.5 text-start transition-colors hover:bg-(--p-surface-100) active:cursor-grabbing focus-visible:bg-(--p-surface-100) focus-visible:outline-none\"\n role=\"button\"\n tabindex=\"0\"\n [attr.title]=\"\n resolve(option.description) || resolve(option.displayName)\n \"\n (click)=\"pickTrigger(option)\"\n (keydown.enter)=\"pickTrigger(option)\"\n >\n <ng-template fExternalItemPreview>\n <ng-container\n *ngTemplateOutlet=\"\n triggerDragPreview;\n context: { $implicit: option }\n \"\n />\n </ng-template>\n <mt-avatar\n shape=\"square\"\n [icon]=\"triggerIconFor(option)\"\n [style.--fp-avatar-color]=\"triggerOptionVars(option).color\"\n [style.--fp-avatar-bg]=\"triggerOptionVars(option).bg\"\r\n />\r\n <span class=\"min-w-0 flex-1\">\r\n <span\r\n class=\"block truncate text-[13px] font-semibold text-(--p-text-color)\"\r\n >{{ resolve(option.displayName) }}</span\r\n >\r\n @if (option.description) {\r\n <span\r\n class=\"block line-clamp-2 text-[11.5px] leading-[1.45] text-(--p-text-muted-color)\"\r\n >{{ resolve(option.description) }}</span\r\n >\n }\n </span>\n <mt-icon\n class=\"flex-none text-(--p-text-muted-color)/40 transition-colors group-hover/item:text-(--p-text-muted-color) [&_svg]:size-4\"\n icon=\"general.dots-grid\"\n />\n </div>\n }\n @if (visibleTriggerOptions().length === 0) {\n <ng-container *ngTemplateOutlet=\"emptyRow\" />\n }\n } @else if (view() === \"category\") {\n @for (item of activeItems(); track paletteItemKey(item, $index)) {\r\n <div\r\n fExternalItem\n [fData]=\"item\"\n [fExternalItemId]=\"paletteItemKey(item, $index)\"\n [fPreviewMatchSize]=\"false\"\n class=\"group/item !flex w-full cursor-grab touch-none select-none items-center gap-3 rounded-lg px-2.5 py-2.5 transition-colors hover:bg-(--p-surface-100) active:cursor-grabbing focus-visible:bg-(--p-surface-100) focus-visible:outline-none\"\n role=\"button\"\r\n tabindex=\"0\"\r\n [attr.title]=\"\r\n resolve(item.description) || resolve(item.displayName)\r\n \"\r\n (click)=\"pickStep(item)\"\n (keydown.enter)=\"pickStep(item)\"\n >\n <ng-template fExternalItemPreview>\n <ng-container\n *ngTemplateOutlet=\"\n stepDragPreview;\n context: { $implicit: item }\n \"\n />\n </ng-template>\n <mt-avatar\n shape=\"square\"\n [icon]=\"iconFor(item)\"\n [style.--fp-avatar-color]=\"severityFor(item).color\"\r\n [style.--fp-avatar-bg]=\"severityFor(item).bg\"\r\n />\r\n <div class=\"min-w-0 flex-1\">\r\n <div\r\n class=\"truncate text-[13px] font-semibold text-(--p-text-color)\"\r\n >\r\n {{ resolve(item.displayName) }}\r\n </div>\r\n @if (item.description) {\r\n <div\r\n class=\"line-clamp-2 text-[11.5px] leading-[1.45] text-(--p-text-muted-color)\"\r\n >\r\n {{ resolve(item.description) }}\r\n </div>\r\n }\r\n </div>\r\n <mt-icon\r\n class=\"flex-none text-(--p-text-muted-color)/40 transition-colors group-hover/item:text-(--p-text-muted-color) [&_svg]:size-4\"\r\n icon=\"general.dots-grid\"\r\n />\r\n </div>\r\n }\r\n } @else {\r\n @for (group of groups(); track group.category) {\r\n <div\n role=\"button\"\n tabindex=\"0\"\n class=\"flex w-full cursor-pointer items-center gap-3 rounded-lg px-2.5 py-2.5 text-start transition-colors hover:bg-(--p-surface-100) focus-visible:bg-(--p-surface-100) focus-visible:outline-none\"\n (click)=\"openCategory(group.category)\"\n (keydown.enter)=\"openCategory(group.category)\"\n (keydown.space)=\"$event.preventDefault(); openCategory(group.category)\"\n >\n <mt-avatar\r\n shape=\"square\"\r\n [icon]=\"group.icon\"\r\n [style.--fp-avatar-color]=\"group.color\"\r\n [style.--fp-avatar-bg]=\"group.bg\"\r\n />\r\n <span class=\"min-w-0 flex-1\">\r\n <span\r\n class=\"block truncate text-[13px] font-semibold text-(--p-text-color)\"\r\n >{{ group.label }}</span\r\n >\r\n <span\r\n class=\"block line-clamp-2 text-[11.5px] leading-[1.45] text-(--p-text-muted-color)\"\r\n >{{ group.description }}</span\r\n >\r\n </span>\r\n <mt-icon\n class=\"flex-none text-(--p-text-muted-color) [&_svg]:size-4 rtl:rotate-180\"\n icon=\"arrow.chevron-right\"\n />\n </div>\n }\n\n @if (groups().length === 0) {\n <ng-container *ngTemplateOutlet=\"emptyRow\" />\n } @else {\n <div class=\"mx-2.5 my-1.5 h-px bg-(--p-content-border-color)\"></div>\n <div\n role=\"button\"\n tabindex=\"0\"\n class=\"flex w-full cursor-pointer items-center gap-3 rounded-lg px-2.5 py-2.5 text-start transition-colors hover:bg-(--p-surface-100) focus-visible:bg-(--p-surface-100) focus-visible:outline-none\"\n (click)=\"openTriggers()\"\n (keydown.enter)=\"openTriggers()\"\n (keydown.space)=\"$event.preventDefault(); openTriggers()\"\n >\n <mt-avatar\r\n shape=\"square\"\r\n icon=\"general.zap-fast\"\r\n [style.--fp-avatar-color]=\"triggerVars().color\"\r\n [style.--fp-avatar-bg]=\"triggerVars().bg\"\r\n />\r\n <span class=\"min-w-0 flex-1\">\r\n <span\r\n class=\"block truncate text-[13px] font-semibold text-(--p-text-color)\"\r\n >{{ \"flowplus.palette.addAnotherTrigger\" | transloco }}</span\r\n >\r\n <span\r\n class=\"block line-clamp-2 text-[11.5px] leading-[1.45] text-(--p-text-muted-color)\"\r\n >{{\r\n \"flowplus.palette.addAnotherTriggerDesc\" | transloco\r\n }}</span\r\n >\r\n </span>\r\n <mt-icon\n class=\"flex-none text-(--p-text-muted-color) [&_svg]:size-4 rtl:rotate-180\"\n icon=\"arrow.chevron-right\"\n />\n </div>\n }\n }\r\n </div>\r\n </aside>\r\n}\r\n\r\n<ng-template #emptyRow>\r\n <div\r\n class=\"flex flex-col items-center justify-center px-6 py-8 text-center text-(--p-text-muted-color)\"\r\n >\r\n <p>{{ \"flowplus.common.empty\" | transloco }}</p>\r\n </div>\n</ng-template>\n\n<ng-template #stepDragPreview let-item>\n <div\n class=\"pointer-events-none w-[280px] overflow-hidden rounded-xl border border-[color-mix(in_srgb,var(--fp-avatar-color)_28%,var(--p-content-border-color))] bg-(--p-content-background)/95 text-(--p-text-color) shadow-[0_18px_36px_rgba(15,23,42,0.18),0_2px_8px_rgba(15,23,42,0.08)] ring-1 ring-white/60 backdrop-blur-md dark:ring-white/10\"\n [style.--fp-avatar-color]=\"severityFor(item).color\"\n [style.--fp-avatar-bg]=\"severityFor(item).bg\"\n >\n <div class=\"flex items-start gap-3 p-3\">\n <span\n class=\"grid h-11 w-11 flex-none place-items-center rounded-xl bg-[var(--fp-avatar-bg)] text-[var(--fp-avatar-color)] shadow-inner ring-1 ring-[color-mix(in_srgb,var(--fp-avatar-color)_24%,transparent)] [&_.p-avatar]:bg-transparent [&_.p-avatar]:text-inherit [&_mt-icon]:size-5 [&_svg]:size-5\"\n >\n <mt-avatar size=\"normal\" shape=\"square\" [icon]=\"iconFor(item)\" />\n </span>\n <span class=\"min-w-0 flex-1\">\n <span class=\"block truncate text-[13.5px] font-bold leading-5\">\n {{ resolve(item.displayName) }}\n </span>\n @if (item.description) {\n <span\n class=\"mt-0.5 block line-clamp-2 text-[11.5px] leading-[1.45] text-(--p-text-muted-color)\"\n >\n {{ resolve(item.description) }}\n </span>\n }\n </span>\n </div>\n <div\n class=\"flex items-center justify-between border-t border-(--p-content-border-color)/70 bg-[color-mix(in_srgb,var(--fp-avatar-color)_7%,transparent)] px-3 py-2\"\n >\n <span\n class=\"max-w-[150px] truncate text-[10.5px] font-semibold text-(--p-text-muted-color)\"\n >\n {{ item.category || item.type }}\n </span>\n <span\n class=\"inline-flex items-center gap-1 rounded-full bg-(--p-content-background) px-2 py-0.5 text-[10.5px] font-semibold text-[var(--fp-avatar-color)] shadow-sm\"\n >\n <mt-icon icon=\"general.plus\" class=\"[&_svg]:size-3\" />\n Drop to add\n </span>\n </div>\n </div>\n</ng-template>\n\n<ng-template #triggerDragPreview let-option>\n <div\n class=\"pointer-events-none w-[280px] overflow-hidden rounded-xl border border-[color-mix(in_srgb,rgb(var(--fp-app-action))_30%,var(--p-content-border-color))] bg-(--p-content-background)/95 text-(--p-text-color) shadow-[0_18px_36px_rgba(245,158,11,0.20),0_2px_8px_rgba(15,23,42,0.08)] ring-1 ring-white/60 backdrop-blur-md dark:ring-white/10\"\n >\n <div class=\"flex items-start gap-3 p-3\">\n <span\n class=\"grid h-11 w-11 flex-none place-items-center rounded-xl bg-[color-mix(in_srgb,rgb(var(--fp-app-action))_16%,transparent)] text-[rgb(var(--fp-app-action))] shadow-inner ring-1 ring-[color-mix(in_srgb,rgb(var(--fp-app-action))_24%,transparent)] [&_.p-avatar]:bg-transparent [&_.p-avatar]:text-inherit [&_mt-icon]:size-5 [&_svg]:size-5\"\n >\n <mt-avatar\n size=\"normal\"\n shape=\"square\"\n [icon]=\"triggerIconFor(option)\"\n />\n </span>\n <span class=\"min-w-0 flex-1\">\n <span class=\"block truncate text-[13.5px] font-bold leading-5\">\n {{ resolve(option.displayName) }}\n </span>\n @if (option.description) {\n <span\n class=\"mt-0.5 block line-clamp-2 text-[11.5px] leading-[1.45] text-(--p-text-muted-color)\"\n >\n {{ resolve(option.description) }}\n </span>\n }\n </span>\n </div>\n <div\n class=\"flex items-center justify-between border-t border-(--p-content-border-color)/70 bg-[color-mix(in_srgb,rgb(var(--fp-app-action))_8%,transparent)] px-3 py-2\"\n >\n <span\n class=\"max-w-[150px] truncate text-[10.5px] font-semibold text-(--p-text-muted-color)\"\n >\n Trigger\n </span>\n <span\n class=\"inline-flex items-center gap-1 rounded-full bg-(--p-content-background) px-2 py-0.5 text-[10.5px] font-semibold text-[rgb(var(--fp-app-action))] shadow-sm\"\n >\n <mt-icon icon=\"general.plus\" class=\"[&_svg]:size-3\" />\n Drop to add\n </span>\n </div>\n </div>\n</ng-template>\n" }]
20755
21455
  }], ctorParameters: () => [], propDecorators: { onDocumentPointerDown: [{
20756
21456
  type: HostListener,
20757
21457
  args: ['document:pointerdown', ['$event']]
@@ -20763,6 +21463,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
20763
21463
  type: Output
20764
21464
  }], addTrigger: [{
20765
21465
  type: Output
21466
+ }], addNote: [{
21467
+ type: Output
20766
21468
  }] } });
20767
21469
 
20768
21470
  class BuilderTopbarComponent {
@@ -20973,6 +21675,9 @@ const TRIGGER_COLUMN_X = 40;
20973
21675
  const TRIGGER_COLUMN_TOP = 60;
20974
21676
  const TRIGGER_ROW_GAP = 200;
20975
21677
  const STEP_COLUMN_OFFSET_X = 360;
21678
+ const CANVAS_NOTE_AUTOLAYOUT_PADDING = 48;
21679
+ const TRIGGER_NOTE_WIDTH = 180;
21680
+ const TRIGGER_NOTE_HEIGHT = 132;
20976
21681
  class WorkflowBuilderPageComponent {
20977
21682
  store = inject(FlowplusWorkflowFacade);
20978
21683
  /** The Foblex canvas adapter — used to drive real fit/center/select. */
@@ -21691,24 +22396,33 @@ class WorkflowBuilderPageComponent {
21691
22396
  englishName = item.displayName;
21692
22397
  }
21693
22398
  const beforeIds = new Set(this.store.triggers().map((t) => t.id));
22399
+ const layout = {
22400
+ x: event.position.x,
22401
+ y: event.position.y,
22402
+ };
22403
+ const config = {
22404
+ ui: { layout },
22405
+ };
21694
22406
  this.afterDispatch(this.store.createTrigger({
21695
22407
  type: triggerType,
21696
22408
  enabled: true,
21697
22409
  name: englishName,
22410
+ configJson: JSON.stringify(config),
21698
22411
  metadata: {
21699
22412
  triggerKey,
21700
22413
  catalogKey,
21701
- layout: {
21702
- x: event.position.x,
21703
- y: event.position.y,
21704
- },
22414
+ config,
22415
+ configJson: JSON.stringify(config),
22416
+ layout,
21705
22417
  },
21706
22418
  }), () => {
21707
22419
  const created = this.store
21708
22420
  .triggers()
21709
22421
  .find((t) => t.id > 0 && !beforeIds.has(t.id));
21710
- if (created)
22422
+ if (created) {
22423
+ this.store.setTriggerPosition(created.id, layout.x, layout.y);
21711
22424
  this.canvas()?.focusTrigger(created.id);
22425
+ }
21712
22426
  });
21713
22427
  }
21714
22428
  /**
@@ -21870,6 +22584,20 @@ class WorkflowBuilderPageComponent {
21870
22584
  this.clearTransientAddIntent();
21871
22585
  this.store.setPaletteOpen(false);
21872
22586
  }
22587
+ onAddCanvasNote() {
22588
+ this.clearTransientAddIntent();
22589
+ const note = this.store.addCanvasNote(this.canvas()?.flowCenter() ?? { x: 160, y: 160 });
22590
+ this.canvas()?.focusNote(note.id);
22591
+ }
22592
+ onCanvasNoteUpdate(event) {
22593
+ this.store.updateCanvasNote(event.noteId, event.patch);
22594
+ }
22595
+ onCanvasNoteDuplicate(event) {
22596
+ this.store.duplicateCanvasNote(event.noteId);
22597
+ }
22598
+ onCanvasNoteDelete(event) {
22599
+ this.store.deleteCanvasNote(event.noteId);
22600
+ }
21873
22601
  onCanvasBackgroundClick() {
21874
22602
  // Click on empty canvas background closes the editor modal + palette drawer
21875
22603
  // so the user has the full canvas back.
@@ -21933,6 +22661,7 @@ class WorkflowBuilderPageComponent {
21933
22661
  const s = this.store.builderState();
21934
22662
  if (!s.workflowId)
21935
22663
  return;
22664
+ const noteMembers = this.canvasNoteAutoLayoutMembers();
21936
22665
  // Triggers are virtual nodes the Dagre engine doesn't see. Place them in
21937
22666
  // a clean left column (entry points, left→right flow) so they never sit
21938
22667
  // on top of step nodes.
@@ -21955,19 +22684,109 @@ class WorkflowBuilderPageComponent {
21955
22684
  connections: s.connections,
21956
22685
  nodeSize: (step) => nodeSizeById.get(step.id) ?? null,
21957
22686
  });
22687
+ const nextNodePositions = layout.nodes.map((n) => ({
22688
+ stepId: n.stepId,
22689
+ x: n.x + stepOffsetX,
22690
+ y: n.y,
22691
+ }));
22692
+ const notePositions = this.canvasNoteAutoLayoutPositions(noteMembers, nextNodePositions, triggerPositions);
21958
22693
  this.canvas()?.suspendPositionSync();
21959
22694
  this.store.setLayoutPositions({
21960
22695
  triggers: triggerPositions,
21961
- nodes: layout.nodes.map((n) => ({
21962
- stepId: n.stepId,
21963
- x: n.x + stepOffsetX,
21964
- y: n.y,
21965
- })),
22696
+ nodes: nextNodePositions,
22697
+ notes: notePositions,
21966
22698
  });
21967
22699
  // Positions just changed — snap the viewport to the arranged graph once
21968
22700
  // it re-renders.
21969
22701
  this.canvas()?.armInitialFit();
21970
22702
  }
22703
+ canvasNoteAutoLayoutMembers() {
22704
+ const itemRects = this.currentCanvasItemRects();
22705
+ const members = new Map();
22706
+ for (const note of this.store.canvasNotes()) {
22707
+ const noteRect = {
22708
+ id: note.id,
22709
+ x: note.x,
22710
+ y: note.y,
22711
+ width: note.width,
22712
+ height: note.height,
22713
+ };
22714
+ const overlapped = itemRects
22715
+ .filter((rect) => rectsOverlap(noteRect, rect))
22716
+ .map((rect) => rect.id);
22717
+ if (overlapped.length)
22718
+ members.set(note.id, overlapped);
22719
+ }
22720
+ return members;
22721
+ }
22722
+ canvasNoteAutoLayoutPositions(members, nodes, triggers) {
22723
+ if (!members.size)
22724
+ return [];
22725
+ const nextRects = this.nextCanvasItemRects(nodes, triggers);
22726
+ const notes = this.store.canvasNotes();
22727
+ const updates = [];
22728
+ for (const note of notes) {
22729
+ const ids = members.get(note.id);
22730
+ if (!ids?.length)
22731
+ continue;
22732
+ const rects = ids
22733
+ .map((id) => nextRects.get(id))
22734
+ .filter((rect) => rect != null);
22735
+ const bounds = boundsForRects(rects);
22736
+ if (!bounds)
22737
+ continue;
22738
+ updates.push({
22739
+ noteId: note.id,
22740
+ x: bounds.x - CANVAS_NOTE_AUTOLAYOUT_PADDING,
22741
+ y: bounds.y - CANVAS_NOTE_AUTOLAYOUT_PADDING,
22742
+ width: bounds.width + CANVAS_NOTE_AUTOLAYOUT_PADDING * 2,
22743
+ height: bounds.height + CANVAS_NOTE_AUTOLAYOUT_PADDING * 2,
22744
+ });
22745
+ }
22746
+ return updates;
22747
+ }
22748
+ currentCanvasItemRects() {
22749
+ return [
22750
+ ...this.store.nodeVms().map((node) => ({
22751
+ id: `step:${node.stepId}`,
22752
+ x: node.x,
22753
+ y: node.y,
22754
+ width: node.width ?? 250,
22755
+ height: node.height ?? 116,
22756
+ })),
22757
+ ...this.store.triggerNodeVms().map((trigger) => ({
22758
+ id: `trigger:${trigger.triggerId}`,
22759
+ x: trigger.x,
22760
+ y: trigger.y,
22761
+ width: TRIGGER_NOTE_WIDTH,
22762
+ height: TRIGGER_NOTE_HEIGHT,
22763
+ })),
22764
+ ];
22765
+ }
22766
+ nextCanvasItemRects(nodes, triggers) {
22767
+ const currentNodes = new Map(this.store.nodeVms().map((node) => [node.stepId, node]));
22768
+ const rects = new Map();
22769
+ for (const node of nodes) {
22770
+ const vm = currentNodes.get(node.stepId);
22771
+ rects.set(`step:${node.stepId}`, {
22772
+ id: `step:${node.stepId}`,
22773
+ x: node.x,
22774
+ y: node.y,
22775
+ width: vm?.width ?? 250,
22776
+ height: vm?.height ?? 116,
22777
+ });
22778
+ }
22779
+ for (const trigger of triggers) {
22780
+ rects.set(`trigger:${trigger.triggerId}`, {
22781
+ id: `trigger:${trigger.triggerId}`,
22782
+ x: trigger.x,
22783
+ y: trigger.y,
22784
+ width: TRIGGER_NOTE_WIDTH,
22785
+ height: TRIGGER_NOTE_HEIGHT,
22786
+ });
22787
+ }
22788
+ return rects;
22789
+ }
21971
22790
  deleteSelection() {
21972
22791
  const sel = this.store.selection();
21973
22792
  for (const stepId of sel.stepIds) {
@@ -21978,6 +22797,8 @@ class WorkflowBuilderPageComponent {
21978
22797
  }
21979
22798
  for (const connId of sel.connectionIds)
21980
22799
  this.store.deleteConnection(connId);
22800
+ for (const noteId of sel.canvasNoteIds)
22801
+ this.store.deleteCanvasNote(noteId);
21981
22802
  }
21982
22803
  /* -------- node hover-bar actions (Phase 3) -------- */
21983
22804
  onNodeQuickAdd(e) {
@@ -22216,7 +23037,7 @@ class WorkflowBuilderPageComponent {
22216
23037
  this.store.setInspectorOpen(true);
22217
23038
  }
22218
23039
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: WorkflowBuilderPageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
22219
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: WorkflowBuilderPageComponent, isStandalone: true, selector: "fp-workflow-builder-page", viewQueries: [{ propertyName: "canvas", first: true, predicate: FlowCanvasComponent, descendants: true, isSignal: true }], ngImport: i0, template: "<div\n class=\"relative flex h-full min-h-full w-full flex-col overflow-hidden bg-(--p-surface-50) text-(--p-text-color)\"\n id=\"fp-builder-root\"\n>\n <fp-builder-topbar\n (validate)=\"onValidate()\"\n (testRun)=\"onOpenTestRun()\"\n (publish)=\"onPublish()\"\n (unpublish)=\"onUnpublish()\"\n (openSettings)=\"onOpenSettings()\"\n (toggleNodeFinder)=\"onTogglePalette()\"\n />\n\n <div\n class=\"relative min-h-0 flex-1 overflow-hidden\"\n id=\"fp-workspace-root\"\n [class.is-bottom-open]=\"store.ui().bottomPanelOpen\"\n [class.is-palette-expanded]=\"store.ui().paletteOpen\"\n >\n @if (store.busy()) {\n <div\n class=\"pointer-events-none absolute inset-x-0 top-0 z-[7] h-0.5 overflow-hidden bg-(--p-primary-color)/15\"\n role=\"status\"\n aria-label=\"Working...\"\n >\n <div\n class=\"h-full w-[30%] bg-(--p-primary-color) animate-[fp-busy-bar-slide_1.1s_cubic-bezier(0.4,0,0.2,1)_infinite]\"\n ></div>\n </div>\n }\n\n <fp-palette\n (openRequested)=\"onOpenPaletteFromRail()\"\n (closeRequested)=\"onClosePalette()\"\n (add)=\"onPaletteAdd($event)\"\n (addTrigger)=\"onPaletteAddTrigger($event)\"\n />\n\n <main class=\"absolute inset-0 overflow-hidden bg-(--p-surface-50)\">\n <fp-flow-canvas\n (paletteDrop)=\"onPaletteDrop($event)\"\n (connectionCreate)=\"onConnectionCreate($event)\"\n (connectionReassign)=\"onConnectionReassign($event)\"\n (triggerStartConnect)=\"onTriggerStartConnect($event)\"\n (triggerStartReassign)=\"onTriggerStartReassign($event)\"\n (triggerStartDisconnect)=\"onTriggerStartDisconnect($event)\"\n (connectionQuickAdd)=\"onConnectionQuickAdd($event)\"\n (quickAddPick)=\"onQuickAddPick($event)\"\n (autoLayoutRequested)=\"autoLayout()\"\n (nodeQuickAdd)=\"onNodeQuickAdd($event)\"\n (nodePortPlusClick)=\"onNodePortPlusClick($event)\"\n (nodeDuplicate)=\"onNodeDuplicate($event)\"\n (nodeRemove)=\"onNodeRemove($event)\"\n (nodeOpenDetails)=\"onNodeOpenDetails($event)\"\n (edgeInsertStep)=\"onEdgeInsertStep($event)\"\n (edgeRemove)=\"onEdgeRemove($event)\"\n (edgeEditFormula)=\"onEdgeEditFormula($event)\"\n (edgeOpenDetails)=\"onEdgeOpenDetails($event)\"\n (assignNodeToConnection)=\"onAssignNodeToConnection($event)\"\n (openChildWorkflow)=\"onOpenChildWorkflow($event)\"\n (starterAddTrigger)=\"onStarterAddTrigger($event)\"\n (triggerOpenDetails)=\"onTriggerOpenDetails($event)\"\n (triggerExecute)=\"onTriggerExecute($event)\"\n (triggerToggleEnabled)=\"onTriggerToggleEnabled($event)\"\n (triggerDelete)=\"onTriggerDelete($event)\"\n (requestAddStep)=\"onRequestAddStep($event)\"\n (canvasBackgroundClick)=\"onCanvasBackgroundClick()\"\n />\n\n @if (store.loading()) {\n <div\n class=\"pointer-events-none absolute inset-0 z-[7] flex flex-col items-center justify-center gap-3.5 bg-(--p-surface-50)/70 text-[12.5px] text-(--p-text-muted-color) backdrop-blur-[2px] animate-[fp-fade-in_240ms_ease-out]\"\n role=\"status\"\n aria-live=\"polite\"\n >\n <div\n class=\"h-9 w-9 rounded-full border-[3px] border-(--p-primary-color)/20 border-t-(--p-primary-color) animate-[fp-loading-spin_720ms_linear_infinite]\"\n aria-hidden=\"true\"\n ></div>\n <span>{{ \"flowplus.common.loading\" | transloco }}</span>\n </div>\n }\n </main>\n\n <fp-bottom-panel\n [class.h-96]=\"store.ui().bottomPanelOpen\"\n [class.h-10]=\"!store.ui().bottomPanelOpen\"\n (focus)=\"onFocusIssue($event)\"\n />\n </div>\n\n <fp-command-palette\n [open]=\"commandPaletteOpen()\"\n [actions]=\"commandPaletteActions()\"\n (pick)=\"onCommandPalettePick($event)\"\n (dismiss)=\"closeCommandPalette()\"\n />\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: TranslocoModule }, { kind: "component", type: BuilderTopbarComponent, selector: "fp-builder-topbar", outputs: ["validate", "testRun", "publish", "unpublish", "openSettings", "toggleNodeFinder"] }, { kind: "component", type: PaletteComponent, selector: "fp-palette", outputs: ["closeRequested", "openRequested", "add", "addTrigger"] }, { kind: "component", type: FlowCanvasComponent, selector: "fp-flow-canvas", outputs: ["paletteDrop", "connectionCreate", "connectionReassign", "triggerStartConnect", "triggerStartReassign", "triggerStartDisconnect", "connectionQuickAdd", "quickAddPick", "autoLayoutRequested", "loaded", "nodeQuickAdd", "nodePortPlusClick", "nodeDuplicate", "nodeRemove", "nodeOpenDetails", "edgeInsertStep", "edgeRemove", "edgeEditFormula", "edgeOpenDetails", "assignNodeToConnection", "openChildWorkflow", "starterAddTrigger", "triggerOpenDetails", "triggerExecute", "triggerToggleEnabled", "triggerDelete", "canvasBackgroundClick", "requestAddStep"] }, { kind: "component", type: BottomPanelComponent, selector: "fp-bottom-panel", outputs: ["focus"] }, { kind: "component", type: CommandPaletteComponent, selector: "fp-command-palette", inputs: ["open", "actions"], outputs: ["pick", "dismiss"] }, { kind: "pipe", type: i2.TranslocoPipe, name: "transloco" }] });
23040
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.8", type: WorkflowBuilderPageComponent, isStandalone: true, selector: "fp-workflow-builder-page", viewQueries: [{ propertyName: "canvas", first: true, predicate: FlowCanvasComponent, descendants: true, isSignal: true }], ngImport: i0, template: "<div\n class=\"relative flex h-full min-h-full w-full flex-col overflow-hidden bg-(--p-surface-50) text-(--p-text-color)\"\n id=\"fp-builder-root\"\n>\n <fp-builder-topbar\n (validate)=\"onValidate()\"\n (testRun)=\"onOpenTestRun()\"\n (publish)=\"onPublish()\"\n (unpublish)=\"onUnpublish()\"\n (openSettings)=\"onOpenSettings()\"\n (toggleNodeFinder)=\"onTogglePalette()\"\n />\n\n <div\n class=\"relative min-h-0 flex-1 overflow-hidden\"\n id=\"fp-workspace-root\"\n [class.is-bottom-open]=\"store.ui().bottomPanelOpen\"\n [class.is-palette-expanded]=\"store.ui().paletteOpen\"\n >\n @if (store.busy()) {\n <div\n class=\"pointer-events-none absolute inset-x-0 top-0 z-[7] h-0.5 overflow-hidden bg-(--p-primary-color)/15\"\n role=\"status\"\n aria-label=\"Working...\"\n >\n <div\n class=\"h-full w-[30%] bg-(--p-primary-color) animate-[fp-busy-bar-slide_1.1s_cubic-bezier(0.4,0,0.2,1)_infinite]\"\n ></div>\n </div>\n }\n\n <fp-palette\n (openRequested)=\"onOpenPaletteFromRail()\"\n (closeRequested)=\"onClosePalette()\"\n (add)=\"onPaletteAdd($event)\"\n (addTrigger)=\"onPaletteAddTrigger($event)\"\n (addNote)=\"onAddCanvasNote()\"\n />\n\n <main class=\"absolute inset-0 overflow-hidden bg-(--p-surface-50)\">\n <fp-flow-canvas\n (paletteDrop)=\"onPaletteDrop($event)\"\n (connectionCreate)=\"onConnectionCreate($event)\"\n (connectionReassign)=\"onConnectionReassign($event)\"\n (triggerStartConnect)=\"onTriggerStartConnect($event)\"\n (triggerStartReassign)=\"onTriggerStartReassign($event)\"\n (triggerStartDisconnect)=\"onTriggerStartDisconnect($event)\"\n (connectionQuickAdd)=\"onConnectionQuickAdd($event)\"\n (quickAddPick)=\"onQuickAddPick($event)\"\n (autoLayoutRequested)=\"autoLayout()\"\n (nodeQuickAdd)=\"onNodeQuickAdd($event)\"\n (nodePortPlusClick)=\"onNodePortPlusClick($event)\"\n (nodeDuplicate)=\"onNodeDuplicate($event)\"\n (nodeRemove)=\"onNodeRemove($event)\"\n (nodeOpenDetails)=\"onNodeOpenDetails($event)\"\n (edgeInsertStep)=\"onEdgeInsertStep($event)\"\n (edgeRemove)=\"onEdgeRemove($event)\"\n (edgeEditFormula)=\"onEdgeEditFormula($event)\"\n (edgeOpenDetails)=\"onEdgeOpenDetails($event)\"\n (assignNodeToConnection)=\"onAssignNodeToConnection($event)\"\n (openChildWorkflow)=\"onOpenChildWorkflow($event)\"\n (starterAddTrigger)=\"onStarterAddTrigger($event)\"\n (triggerOpenDetails)=\"onTriggerOpenDetails($event)\"\n (triggerExecute)=\"onTriggerExecute($event)\"\n (triggerToggleEnabled)=\"onTriggerToggleEnabled($event)\"\n (triggerDelete)=\"onTriggerDelete($event)\"\n (requestAddStep)=\"onRequestAddStep($event)\"\n (noteUpdate)=\"onCanvasNoteUpdate($event)\"\n (noteDuplicate)=\"onCanvasNoteDuplicate($event)\"\n (noteDelete)=\"onCanvasNoteDelete($event)\"\n (canvasBackgroundClick)=\"onCanvasBackgroundClick()\"\n />\n\n @if (store.loading()) {\n <div\n class=\"pointer-events-none absolute inset-0 z-[7] flex flex-col items-center justify-center gap-3.5 bg-(--p-surface-50)/70 text-[12.5px] text-(--p-text-muted-color) backdrop-blur-[2px] animate-[fp-fade-in_240ms_ease-out]\"\n role=\"status\"\n aria-live=\"polite\"\n >\n <div\n class=\"h-9 w-9 rounded-full border-[3px] border-(--p-primary-color)/20 border-t-(--p-primary-color) animate-[fp-loading-spin_720ms_linear_infinite]\"\n aria-hidden=\"true\"\n ></div>\n <span>{{ \"flowplus.common.loading\" | transloco }}</span>\n </div>\n }\n </main>\n\n <fp-bottom-panel\n [class.h-96]=\"store.ui().bottomPanelOpen\"\n [class.h-10]=\"!store.ui().bottomPanelOpen\"\n (focus)=\"onFocusIssue($event)\"\n />\n </div>\n\n <fp-command-palette\n [open]=\"commandPaletteOpen()\"\n [actions]=\"commandPaletteActions()\"\n (pick)=\"onCommandPalettePick($event)\"\n (dismiss)=\"closeCommandPalette()\"\n />\n</div>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: TranslocoModule }, { kind: "component", type: BuilderTopbarComponent, selector: "fp-builder-topbar", outputs: ["validate", "testRun", "publish", "unpublish", "openSettings", "toggleNodeFinder"] }, { kind: "component", type: PaletteComponent, selector: "fp-palette", outputs: ["closeRequested", "openRequested", "add", "addTrigger", "addNote"] }, { kind: "component", type: FlowCanvasComponent, selector: "fp-flow-canvas", outputs: ["paletteDrop", "connectionCreate", "connectionReassign", "triggerStartConnect", "triggerStartReassign", "triggerStartDisconnect", "connectionQuickAdd", "quickAddPick", "autoLayoutRequested", "loaded", "nodeQuickAdd", "nodePortPlusClick", "nodeDuplicate", "nodeRemove", "nodeOpenDetails", "edgeInsertStep", "edgeRemove", "edgeEditFormula", "edgeOpenDetails", "assignNodeToConnection", "openChildWorkflow", "starterAddTrigger", "triggerOpenDetails", "triggerExecute", "triggerToggleEnabled", "triggerDelete", "canvasBackgroundClick", "requestAddStep", "noteUpdate", "noteDuplicate", "noteDelete"] }, { kind: "component", type: BottomPanelComponent, selector: "fp-bottom-panel", outputs: ["focus"] }, { kind: "component", type: CommandPaletteComponent, selector: "fp-command-palette", inputs: ["open", "actions"], outputs: ["pick", "dismiss"] }, { kind: "pipe", type: i2.TranslocoPipe, name: "transloco" }] });
22220
23041
  }
22221
23042
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: WorkflowBuilderPageComponent, decorators: [{
22222
23043
  type: Component,
@@ -22228,7 +23049,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
22228
23049
  FlowCanvasComponent,
22229
23050
  BottomPanelComponent,
22230
23051
  CommandPaletteComponent,
22231
- ], template: "<div\n class=\"relative flex h-full min-h-full w-full flex-col overflow-hidden bg-(--p-surface-50) text-(--p-text-color)\"\n id=\"fp-builder-root\"\n>\n <fp-builder-topbar\n (validate)=\"onValidate()\"\n (testRun)=\"onOpenTestRun()\"\n (publish)=\"onPublish()\"\n (unpublish)=\"onUnpublish()\"\n (openSettings)=\"onOpenSettings()\"\n (toggleNodeFinder)=\"onTogglePalette()\"\n />\n\n <div\n class=\"relative min-h-0 flex-1 overflow-hidden\"\n id=\"fp-workspace-root\"\n [class.is-bottom-open]=\"store.ui().bottomPanelOpen\"\n [class.is-palette-expanded]=\"store.ui().paletteOpen\"\n >\n @if (store.busy()) {\n <div\n class=\"pointer-events-none absolute inset-x-0 top-0 z-[7] h-0.5 overflow-hidden bg-(--p-primary-color)/15\"\n role=\"status\"\n aria-label=\"Working...\"\n >\n <div\n class=\"h-full w-[30%] bg-(--p-primary-color) animate-[fp-busy-bar-slide_1.1s_cubic-bezier(0.4,0,0.2,1)_infinite]\"\n ></div>\n </div>\n }\n\n <fp-palette\n (openRequested)=\"onOpenPaletteFromRail()\"\n (closeRequested)=\"onClosePalette()\"\n (add)=\"onPaletteAdd($event)\"\n (addTrigger)=\"onPaletteAddTrigger($event)\"\n />\n\n <main class=\"absolute inset-0 overflow-hidden bg-(--p-surface-50)\">\n <fp-flow-canvas\n (paletteDrop)=\"onPaletteDrop($event)\"\n (connectionCreate)=\"onConnectionCreate($event)\"\n (connectionReassign)=\"onConnectionReassign($event)\"\n (triggerStartConnect)=\"onTriggerStartConnect($event)\"\n (triggerStartReassign)=\"onTriggerStartReassign($event)\"\n (triggerStartDisconnect)=\"onTriggerStartDisconnect($event)\"\n (connectionQuickAdd)=\"onConnectionQuickAdd($event)\"\n (quickAddPick)=\"onQuickAddPick($event)\"\n (autoLayoutRequested)=\"autoLayout()\"\n (nodeQuickAdd)=\"onNodeQuickAdd($event)\"\n (nodePortPlusClick)=\"onNodePortPlusClick($event)\"\n (nodeDuplicate)=\"onNodeDuplicate($event)\"\n (nodeRemove)=\"onNodeRemove($event)\"\n (nodeOpenDetails)=\"onNodeOpenDetails($event)\"\n (edgeInsertStep)=\"onEdgeInsertStep($event)\"\n (edgeRemove)=\"onEdgeRemove($event)\"\n (edgeEditFormula)=\"onEdgeEditFormula($event)\"\n (edgeOpenDetails)=\"onEdgeOpenDetails($event)\"\n (assignNodeToConnection)=\"onAssignNodeToConnection($event)\"\n (openChildWorkflow)=\"onOpenChildWorkflow($event)\"\n (starterAddTrigger)=\"onStarterAddTrigger($event)\"\n (triggerOpenDetails)=\"onTriggerOpenDetails($event)\"\n (triggerExecute)=\"onTriggerExecute($event)\"\n (triggerToggleEnabled)=\"onTriggerToggleEnabled($event)\"\n (triggerDelete)=\"onTriggerDelete($event)\"\n (requestAddStep)=\"onRequestAddStep($event)\"\n (canvasBackgroundClick)=\"onCanvasBackgroundClick()\"\n />\n\n @if (store.loading()) {\n <div\n class=\"pointer-events-none absolute inset-0 z-[7] flex flex-col items-center justify-center gap-3.5 bg-(--p-surface-50)/70 text-[12.5px] text-(--p-text-muted-color) backdrop-blur-[2px] animate-[fp-fade-in_240ms_ease-out]\"\n role=\"status\"\n aria-live=\"polite\"\n >\n <div\n class=\"h-9 w-9 rounded-full border-[3px] border-(--p-primary-color)/20 border-t-(--p-primary-color) animate-[fp-loading-spin_720ms_linear_infinite]\"\n aria-hidden=\"true\"\n ></div>\n <span>{{ \"flowplus.common.loading\" | transloco }}</span>\n </div>\n }\n </main>\n\n <fp-bottom-panel\n [class.h-96]=\"store.ui().bottomPanelOpen\"\n [class.h-10]=\"!store.ui().bottomPanelOpen\"\n (focus)=\"onFocusIssue($event)\"\n />\n </div>\n\n <fp-command-palette\n [open]=\"commandPaletteOpen()\"\n [actions]=\"commandPaletteActions()\"\n (pick)=\"onCommandPalettePick($event)\"\n (dismiss)=\"closeCommandPalette()\"\n />\n</div>\n" }]
23052
+ ], template: "<div\n class=\"relative flex h-full min-h-full w-full flex-col overflow-hidden bg-(--p-surface-50) text-(--p-text-color)\"\n id=\"fp-builder-root\"\n>\n <fp-builder-topbar\n (validate)=\"onValidate()\"\n (testRun)=\"onOpenTestRun()\"\n (publish)=\"onPublish()\"\n (unpublish)=\"onUnpublish()\"\n (openSettings)=\"onOpenSettings()\"\n (toggleNodeFinder)=\"onTogglePalette()\"\n />\n\n <div\n class=\"relative min-h-0 flex-1 overflow-hidden\"\n id=\"fp-workspace-root\"\n [class.is-bottom-open]=\"store.ui().bottomPanelOpen\"\n [class.is-palette-expanded]=\"store.ui().paletteOpen\"\n >\n @if (store.busy()) {\n <div\n class=\"pointer-events-none absolute inset-x-0 top-0 z-[7] h-0.5 overflow-hidden bg-(--p-primary-color)/15\"\n role=\"status\"\n aria-label=\"Working...\"\n >\n <div\n class=\"h-full w-[30%] bg-(--p-primary-color) animate-[fp-busy-bar-slide_1.1s_cubic-bezier(0.4,0,0.2,1)_infinite]\"\n ></div>\n </div>\n }\n\n <fp-palette\n (openRequested)=\"onOpenPaletteFromRail()\"\n (closeRequested)=\"onClosePalette()\"\n (add)=\"onPaletteAdd($event)\"\n (addTrigger)=\"onPaletteAddTrigger($event)\"\n (addNote)=\"onAddCanvasNote()\"\n />\n\n <main class=\"absolute inset-0 overflow-hidden bg-(--p-surface-50)\">\n <fp-flow-canvas\n (paletteDrop)=\"onPaletteDrop($event)\"\n (connectionCreate)=\"onConnectionCreate($event)\"\n (connectionReassign)=\"onConnectionReassign($event)\"\n (triggerStartConnect)=\"onTriggerStartConnect($event)\"\n (triggerStartReassign)=\"onTriggerStartReassign($event)\"\n (triggerStartDisconnect)=\"onTriggerStartDisconnect($event)\"\n (connectionQuickAdd)=\"onConnectionQuickAdd($event)\"\n (quickAddPick)=\"onQuickAddPick($event)\"\n (autoLayoutRequested)=\"autoLayout()\"\n (nodeQuickAdd)=\"onNodeQuickAdd($event)\"\n (nodePortPlusClick)=\"onNodePortPlusClick($event)\"\n (nodeDuplicate)=\"onNodeDuplicate($event)\"\n (nodeRemove)=\"onNodeRemove($event)\"\n (nodeOpenDetails)=\"onNodeOpenDetails($event)\"\n (edgeInsertStep)=\"onEdgeInsertStep($event)\"\n (edgeRemove)=\"onEdgeRemove($event)\"\n (edgeEditFormula)=\"onEdgeEditFormula($event)\"\n (edgeOpenDetails)=\"onEdgeOpenDetails($event)\"\n (assignNodeToConnection)=\"onAssignNodeToConnection($event)\"\n (openChildWorkflow)=\"onOpenChildWorkflow($event)\"\n (starterAddTrigger)=\"onStarterAddTrigger($event)\"\n (triggerOpenDetails)=\"onTriggerOpenDetails($event)\"\n (triggerExecute)=\"onTriggerExecute($event)\"\n (triggerToggleEnabled)=\"onTriggerToggleEnabled($event)\"\n (triggerDelete)=\"onTriggerDelete($event)\"\n (requestAddStep)=\"onRequestAddStep($event)\"\n (noteUpdate)=\"onCanvasNoteUpdate($event)\"\n (noteDuplicate)=\"onCanvasNoteDuplicate($event)\"\n (noteDelete)=\"onCanvasNoteDelete($event)\"\n (canvasBackgroundClick)=\"onCanvasBackgroundClick()\"\n />\n\n @if (store.loading()) {\n <div\n class=\"pointer-events-none absolute inset-0 z-[7] flex flex-col items-center justify-center gap-3.5 bg-(--p-surface-50)/70 text-[12.5px] text-(--p-text-muted-color) backdrop-blur-[2px] animate-[fp-fade-in_240ms_ease-out]\"\n role=\"status\"\n aria-live=\"polite\"\n >\n <div\n class=\"h-9 w-9 rounded-full border-[3px] border-(--p-primary-color)/20 border-t-(--p-primary-color) animate-[fp-loading-spin_720ms_linear_infinite]\"\n aria-hidden=\"true\"\n ></div>\n <span>{{ \"flowplus.common.loading\" | transloco }}</span>\n </div>\n }\n </main>\n\n <fp-bottom-panel\n [class.h-96]=\"store.ui().bottomPanelOpen\"\n [class.h-10]=\"!store.ui().bottomPanelOpen\"\n (focus)=\"onFocusIssue($event)\"\n />\n </div>\n\n <fp-command-palette\n [open]=\"commandPaletteOpen()\"\n [actions]=\"commandPaletteActions()\"\n (pick)=\"onCommandPalettePick($event)\"\n (dismiss)=\"closeCommandPalette()\"\n />\n</div>\n" }]
22232
23053
  }], ctorParameters: () => [], propDecorators: { canvas: [{ type: i0.ViewChild, args: [i0.forwardRef(() => FlowCanvasComponent), { isSignal: true }] }] } });
22233
23054
  function readObject(value) {
22234
23055
  return value && typeof value === 'object' && !Array.isArray(value)
@@ -22333,6 +23154,27 @@ function uniqueStrings(values) {
22333
23154
  }
22334
23155
  return out;
22335
23156
  }
23157
+ function rectsOverlap(a, b) {
23158
+ return !(a.x + a.width < b.x ||
23159
+ b.x + b.width < a.x ||
23160
+ a.y + a.height < b.y ||
23161
+ b.y + b.height < a.y);
23162
+ }
23163
+ function boundsForRects(rects) {
23164
+ if (!rects.length)
23165
+ return null;
23166
+ const minX = Math.min(...rects.map((rect) => rect.x));
23167
+ const minY = Math.min(...rects.map((rect) => rect.y));
23168
+ const maxX = Math.max(...rects.map((rect) => rect.x + rect.width));
23169
+ const maxY = Math.max(...rects.map((rect) => rect.y + rect.height));
23170
+ return {
23171
+ id: 'bounds',
23172
+ x: minX,
23173
+ y: minY,
23174
+ width: maxX - minX,
23175
+ height: maxY - minY,
23176
+ };
23177
+ }
22336
23178
  function readString$1(record, key) {
22337
23179
  const value = record[key];
22338
23180
  return typeof value === 'string' && value.trim() ? value : null;
@@ -23166,5 +24008,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
23166
24008
  * Generated bundle index. Do not edit.
23167
24009
  */
23168
24010
 
23169
- export { APP_STATES, ApplyAutomationExecutionDetail, ApplyBuilderCommand, ApplyBuilderSnapshot, AutomationBuilderCatalogApiService, AutomationDesignApiService, AutomationExecutionApiService, AutomationExecutionsPageComponent, BottomPanelComponent, BuilderTopbarComponent, ClearAutomationRuntimeState, ClearCommandHistory, ClearConflict, ClearPendingOperation, ClearSelection, ClearWorkflowBuilder, CommitStepUpdates, CommitWorkflowMetadata, ConnectionCreated, ConnectionCreationFailed, ContextPickerComponent, ContextPillButtonComponent, ContextPillComponent, CreateConnection, CreateStep, CreateTrigger, CreateWorkflow, DagreFlowLayoutEngine, DeleteConnection, DeleteStep, DeleteTrigger, DeleteWorkflow, DuplicateWorkflow, EndpointBuilder, FLOWPLUS_FORM_DESIGNER_ADAPTER, FLOWPLUS_WORKFLOW_CONFIG, FLOWPLUS_WORKFLOW_DEFAULT_RUNTIME, FLOWPLUS_WORKFLOW_DEFAULT_STATE, FLOWPLUS_WORKFLOW_NAVIGATION, FLOWPLUS_WORKFLOW_ROUTES, FLOW_NODE_BASE_HEIGHT, FLOW_NODE_CARD_WIDTH, FLOW_NODE_MULTI_OUTPUT_GAP, FLOW_NODE_OUTPUT_SIZE, FlowCanvasComponent, FlowNodeComponent, FlowplusWorkflowActionKey, FlowplusWorkflowFacade, FlowplusWorkflowNavigationService, FlowplusWorkflowOverlayService, FlowplusWorkflowState, InspectorShellComponent, KeyboardShortcutsService, LOCAL_FALLBACK_CATALOG, LoadAutomationExecution, LoadContextCatalog, LoadContextCatalogForStep, LoadLatestAutomationExecution, LoadLayout, LoadTriggers, LoadWorkflowBuilder, LoadWorkflowCatalog, LoadWorkflowList, MarkLayoutDirty, MoveStep, NODE_COLOR_TOKEN, NODE_DEFAULT_ICON, NODE_MT_ICON, NoopFlowplusFormDesignerAdapter, PaletteComponent, PaletteDragSourceDirective, PollAutomationExecution, ProblemsPanelComponent, ProcessCreateDialogComponent, PublishWorkflow, RedoBuilderCommand, ReloadWorkflowBuilder, RunAutomationTrigger, RunWorkflowTest, SaveLayout, SelectConnection, SelectRuntimeNodeRun, SelectStep, SelectTrigger, SetActiveInspectorTab, SetBottomPanelOpen, SetBottomPanelTab, SetCanvasViewport, SetContextCatalog, SetCreateDialogOpen, SetInspectorOpen, SetLayout, SetLayoutAutosavePaused, SetMinimap, SetPaletteOpen, SetPaletteSearch, SetPendingOperation, SetReadonly, SetSelectedRuntimeTrigger, SetSelection, SetSelectionFromCanvas, SetStudioFilter, SetValidation, SetWorkflowCatalog, SetWorkflowDefinition, StepCreated, StepCreationFailed, TRIGGER_MT_ICON, TestRunPanelComponent, UnconfiguredFormDesignerAdapter, UndoBuilderCommand, UnpublishWorkflow, UpdateConnection, UpdateStep, UpdateTrigger, UpdateWorkflowMetadata, UpdateWorkflowResources, UpdateWorkflowVariables, ValidateWorkflow, WorkflowBuilderPageComponent, WorkflowCatalogApiService, WorkflowContextApiService, WorkflowDebugApiService, WorkflowDefinitionApiService, WorkflowFormApiService, WorkflowRunDebuggerPageComponent, WorkflowRuntimeApiService, WorkflowStudioPageComponent, WorkflowValidationApiService, applyBuilderSnapshot, avatarSeverityForStep, avatarSeverityForTrigger, canonicalTriggerTypeForStarterKind, coerceTranslatable, colorVarFor, computeBranchLanes, computeSaveStatus, connectionCanvasId, derivePortsForStep, extractMessage, flowNodeCardHeightForOutputs, flowNodeLayoutHeightForOutputs, flowNodeOutputCenterY, flowNodeOutputStackHeight, fromWorkflowConnectionDomain, fromWorkflowStepDomain, hasOtherPending, iconFor, indexValidationByConnectionId, indexValidationByStepId, indexValidationByTriggerId, inputPortId, isEntityDirty, mapHttpError, mtIconForStep, mtIconForTrigger, newClientMutationId, nextOperationId, nodeCanvasId, outputPortId, parseConnectionId, parseNodeId, parsePortId, parseTriggerOutputPortId, parseTriggerStartConnectionId, patchLayoutPosition, patchTriggerLayoutPosition, provideFlowplusFormDesignerAdapter, provideFlowplusWorkflow, provideFlowplusWorkflowNavigation, provideFlowplusWorkflowNavigationDefaults, readRecord, removeStepWithEdges, resolveTranslatable, resolveTriggerStartNodeKey, resolveTriggerStartStep, resolveTriggerStartStepId, resolveWorkflowTriggerKey, savingOrSaved, tempNodeCanvasId, toWorkflowConnectionDomain, toWorkflowDefinitionDomain, toWorkflowStepDomain, toWorkflowTriggerDomain, triggerCanvasId, triggerOutputPortId, triggerStartConnectionCanvasId, triggerStarterKindFor, unwrap, unwrapApiData, upsertConnection, upsertStep };
24011
+ export { APP_STATES, AddCanvasNote, ApplyAutomationExecutionDetail, ApplyBuilderCommand, ApplyBuilderSnapshot, AutomationBuilderCatalogApiService, AutomationDesignApiService, AutomationExecutionApiService, AutomationExecutionsPageComponent, BottomPanelComponent, BuilderTopbarComponent, ClearAutomationRuntimeState, ClearCommandHistory, ClearConflict, ClearPendingOperation, ClearSelection, ClearWorkflowBuilder, CommitStepUpdates, CommitWorkflowMetadata, ConnectionCreated, ConnectionCreationFailed, ContextPickerComponent, ContextPillButtonComponent, ContextPillComponent, CreateConnection, CreateStep, CreateTrigger, CreateWorkflow, DagreFlowLayoutEngine, DeleteCanvasNote, DeleteConnection, DeleteStep, DeleteTrigger, DeleteWorkflow, DuplicateCanvasNote, DuplicateWorkflow, EndpointBuilder, FLOWPLUS_FORM_DESIGNER_ADAPTER, FLOWPLUS_WORKFLOW_CONFIG, FLOWPLUS_WORKFLOW_DEFAULT_RUNTIME, FLOWPLUS_WORKFLOW_DEFAULT_STATE, FLOWPLUS_WORKFLOW_NAVIGATION, FLOWPLUS_WORKFLOW_ROUTES, FLOW_NODE_BASE_HEIGHT, FLOW_NODE_CARD_WIDTH, FLOW_NODE_MULTI_OUTPUT_GAP, FLOW_NODE_OUTPUT_SIZE, FlowCanvasComponent, FlowNodeComponent, FlowplusWorkflowActionKey, FlowplusWorkflowFacade, FlowplusWorkflowNavigationService, FlowplusWorkflowOverlayService, FlowplusWorkflowState, InspectorShellComponent, KeyboardShortcutsService, LOCAL_FALLBACK_CATALOG, LoadAutomationExecution, LoadContextCatalog, LoadContextCatalogForStep, LoadLatestAutomationExecution, LoadLayout, LoadTriggers, LoadWorkflowBuilder, LoadWorkflowCatalog, LoadWorkflowList, MarkLayoutDirty, MoveStep, NODE_COLOR_TOKEN, NODE_DEFAULT_ICON, NODE_MT_ICON, NoopFlowplusFormDesignerAdapter, PaletteComponent, PaletteDragSourceDirective, PollAutomationExecution, ProblemsPanelComponent, ProcessCreateDialogComponent, PublishWorkflow, RedoBuilderCommand, ReloadWorkflowBuilder, RunAutomationTrigger, RunWorkflowTest, SaveLayout, SelectCanvasNote, SelectConnection, SelectRuntimeNodeRun, SelectStep, SelectTrigger, SetActiveInspectorTab, SetBottomPanelOpen, SetBottomPanelTab, SetCanvasViewport, SetContextCatalog, SetCreateDialogOpen, SetInspectorOpen, SetLayout, SetLayoutAutosavePaused, SetMinimap, SetPaletteOpen, SetPaletteSearch, SetPendingOperation, SetReadonly, SetSelectedRuntimeTrigger, SetSelection, SetSelectionFromCanvas, SetStudioFilter, SetValidation, SetWorkflowCatalog, SetWorkflowDefinition, StepCreated, StepCreationFailed, TRIGGER_MT_ICON, TestRunPanelComponent, UnconfiguredFormDesignerAdapter, UndoBuilderCommand, UnpublishWorkflow, UpdateCanvasNote, UpdateConnection, UpdateStep, UpdateTrigger, UpdateWorkflowMetadata, UpdateWorkflowResources, UpdateWorkflowVariables, ValidateWorkflow, WorkflowBuilderPageComponent, WorkflowCatalogApiService, WorkflowContextApiService, WorkflowDebugApiService, WorkflowDefinitionApiService, WorkflowFormApiService, WorkflowRunDebuggerPageComponent, WorkflowRuntimeApiService, WorkflowStudioPageComponent, WorkflowValidationApiService, applyBuilderSnapshot, avatarSeverityForStep, avatarSeverityForTrigger, canonicalTriggerTypeForStarterKind, coerceTranslatable, colorVarFor, computeBranchLanes, computeSaveStatus, connectionCanvasId, derivePortsForStep, extractMessage, flowNodeCardHeightForOutputs, flowNodeLayoutHeightForOutputs, flowNodeOutputCenterY, flowNodeOutputStackHeight, fromWorkflowConnectionDomain, fromWorkflowStepDomain, hasOtherPending, iconFor, indexValidationByConnectionId, indexValidationByStepId, indexValidationByTriggerId, inputPortId, isEntityDirty, mapHttpError, mtIconForStep, mtIconForTrigger, newClientMutationId, nextOperationId, nodeCanvasId, outputPortId, parseConnectionId, parseNodeId, parsePortId, parseTriggerOutputPortId, parseTriggerStartConnectionId, patchLayoutPosition, patchTriggerLayoutPosition, provideFlowplusFormDesignerAdapter, provideFlowplusWorkflow, provideFlowplusWorkflowNavigation, provideFlowplusWorkflowNavigationDefaults, readRecord, removeStepWithEdges, resolveTranslatable, resolveTriggerStartNodeKey, resolveTriggerStartStep, resolveTriggerStartStepId, resolveWorkflowTriggerKey, savingOrSaved, tempNodeCanvasId, toWorkflowConnectionDomain, toWorkflowDefinitionDomain, toWorkflowStepDomain, toWorkflowTriggerDomain, triggerCanvasId, triggerOutputPortId, triggerStartConnectionCanvasId, triggerStarterKindFor, unwrap, unwrapApiData, upsertConnection, upsertStep };
23170
24012
  //# sourceMappingURL=masterteam-flowplus-workflow.mjs.map