@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.
- package/assets/flowplus-workflow.css +1 -1
- package/assets/i18n/ar.json +8 -1
- package/assets/i18n/en.json +8 -1
- package/fesm2022/masterteam-flowplus-workflow.mjs +1162 -320
- package/fesm2022/masterteam-flowplus-workflow.mjs.map +1 -1
- package/package.json +4 -4
- package/types/masterteam-flowplus-workflow.d.ts +134 -6
|
@@ -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$
|
|
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$
|
|
1252
|
+
.map((item) => readString$e(item, 'key'))
|
|
1253
1253
|
.filter((key) => !!key?.trim())
|
|
1254
1254
|
.map((key) => switchCaseRouteKey$3(key));
|
|
1255
|
-
const defaultKey = readString$
|
|
1256
|
-
readString$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
1780
|
-
const projectId = readString$
|
|
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$
|
|
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$
|
|
1803
|
+
const configJson = readString$d(metadata, 'configJson') ?? stringifyJson$1(mergedConfig) ?? '{}';
|
|
1804
1804
|
const positionJson = layoutToJson(step.layout) ??
|
|
1805
1805
|
layoutToJson(existing?.layout) ??
|
|
1806
|
-
readString$
|
|
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$
|
|
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$
|
|
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
|
|
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:
|
|
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$
|
|
1913
|
+
const sourceNodeKey = readString$d(metadata, 'sourceNodeKey') ??
|
|
1901
1914
|
index.nodeKeyByStepId.get(sourceStepId) ??
|
|
1902
1915
|
existing?.sourceStepKey ??
|
|
1903
1916
|
'';
|
|
1904
|
-
const targetNodeKey = readString$
|
|
1917
|
+
const targetNodeKey = readString$d(metadata, 'targetNodeKey') ??
|
|
1905
1918
|
index.nodeKeyByStepId.get(targetStepId) ??
|
|
1906
1919
|
existing?.targetStepKey ??
|
|
1907
1920
|
'';
|
|
1908
|
-
const sourcePortKey = readString$
|
|
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$
|
|
1925
|
+
const sourceOutputKey = readString$d(connectionMetadata, 'sourceOutputKey') ??
|
|
1913
1926
|
sourcePortKey ??
|
|
1914
1927
|
existing?.sourcePortKey ??
|
|
1915
1928
|
readSelectedActionKey(connection) ??
|
|
1916
1929
|
'';
|
|
1917
|
-
const explicitConditionJson = readString$
|
|
1930
|
+
const explicitConditionJson = readString$d(connectionMetadata, 'conditionJson') ??
|
|
1918
1931
|
readExpressionText(connection);
|
|
1919
1932
|
const preservedConditionJson = sourceOutputChanged
|
|
1920
1933
|
? null
|
|
1921
|
-
: (readString$
|
|
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$
|
|
1928
|
-
(sourceOutputChanged ? null : readString$
|
|
1940
|
+
const routeType = normalizeAutomationRouteType(readString$d(connectionMetadata, 'routeType') ??
|
|
1941
|
+
(sourceOutputChanged ? null : readString$d(existingMetadata, 'routeType')), sourceOutputKey, conditionJson);
|
|
1929
1942
|
return {
|
|
1930
|
-
routeId: readString$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
2017
|
-
readString$
|
|
2029
|
+
const triggerType = readString$d(raw, 'type') ??
|
|
2030
|
+
readString$d(raw, 'triggerType') ??
|
|
2018
2031
|
'ManualTrigger';
|
|
2019
2032
|
const triggerKey = trigger.key ||
|
|
2020
|
-
readString$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
2248
|
-
icon: readString$
|
|
2249
|
-
readString$
|
|
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$
|
|
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$
|
|
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$
|
|
2371
|
+
.map((item) => readString$d(item, 'key'))
|
|
2359
2372
|
.filter((key) => !!key?.trim())
|
|
2360
2373
|
.map((key) => switchCaseRouteKey$2(key));
|
|
2361
|
-
const defaultKey = readString$
|
|
2362
|
-
readString$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
2539
|
-
readString$
|
|
2540
|
-
readString$
|
|
2541
|
-
readString$
|
|
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$
|
|
2603
|
-
readString$
|
|
2604
|
-
readString$
|
|
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$
|
|
2610
|
-
readString$
|
|
2611
|
-
readString$
|
|
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$
|
|
2617
|
-
readString$
|
|
2618
|
-
readString$
|
|
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$
|
|
2651
|
-
readString$
|
|
2652
|
-
readString$
|
|
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$
|
|
2661
|
-
const ar = readString$
|
|
2662
|
-
const display = readString$
|
|
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$
|
|
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$
|
|
2756
|
-
const primaryModuleKey = readString$
|
|
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$
|
|
2775
|
+
key: readString$c(raw, 'key'),
|
|
2763
2776
|
name: toTranslatableText(raw['name']),
|
|
2764
2777
|
description: toTranslatableText(raw['description']),
|
|
2765
|
-
status: readString$
|
|
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$
|
|
2772
|
-
operationType: readString$
|
|
2773
|
-
commandName: readString$
|
|
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$
|
|
2779
|
-
triggerType: readString$
|
|
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$
|
|
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$
|
|
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$
|
|
2798
|
-
typeKey: readString$
|
|
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$
|
|
2822
|
+
initiationPropertyMappings: readString$c(raw, 'initiationPropertyMappings'),
|
|
2810
2823
|
targetProcessSchemaId: readNullableNumber(raw, 'targetProcessSchemaId'),
|
|
2811
2824
|
targetProcessSchemaName: toTranslatableText(raw['targetProcessSchemaName']),
|
|
2812
|
-
expectedHttpMethod: readString$
|
|
2813
|
-
workflowKey: readString$
|
|
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$
|
|
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$
|
|
2825
|
-
schemaJson: readString$
|
|
2826
|
-
authenticationPolicyJson: readString$
|
|
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$
|
|
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$
|
|
2843
|
-
typeKey: readString$
|
|
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$
|
|
2848
|
-
targetType: readString$
|
|
2849
|
-
targetValue: readString$
|
|
2850
|
-
groupSelection: readString$
|
|
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$
|
|
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$
|
|
2877
|
-
readString$
|
|
2878
|
-
readString$
|
|
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$
|
|
2889
|
-
targetStepKey: readString$
|
|
2890
|
-
sourcePortKey: readString$
|
|
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$
|
|
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$
|
|
2900
|
-
expression: readString$
|
|
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$
|
|
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$
|
|
2960
|
+
const inputSchema = parseJsonSchema(readString$c(raw, 'inputSchemaJson')) ??
|
|
2948
2961
|
schemaFromPluginFields(raw['inputFields']);
|
|
2949
|
-
const outputSchema = parseJsonSchema(readString$
|
|
2962
|
+
const outputSchema = parseJsonSchema(readString$c(raw, 'outputSchemaJson')) ??
|
|
2950
2963
|
schemaFromPluginFields(raw['outputFields']);
|
|
2951
2964
|
return {
|
|
2952
2965
|
...spreadAs(raw),
|
|
2953
|
-
pluginId: readString$
|
|
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$
|
|
2959
|
-
outputSchemaJson: readString$
|
|
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$
|
|
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$
|
|
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$
|
|
2998
|
+
parseJsonSchema(readString$c(raw, 'configSchemaJson'));
|
|
2986
2999
|
return {
|
|
2987
3000
|
...spreadAs(raw),
|
|
2988
|
-
appCode: readString$
|
|
2989
|
-
actionKey: readString$
|
|
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$
|
|
3005
|
+
category: readString$c(raw, 'category'),
|
|
2993
3006
|
configSchema: configSchema ?? null,
|
|
2994
|
-
configSchemaJson: readString$
|
|
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$
|
|
3044
|
+
testRunId: readString$c(raw, 'testRunId') ??
|
|
3032
3045
|
`workflow_${readNullableNumber(raw, 'workflowId') ?? 'test'}`,
|
|
3033
|
-
status: readString$
|
|
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$
|
|
3252
|
-
readString$
|
|
3253
|
-
readString$
|
|
3254
|
-
readString$
|
|
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$
|
|
3266
|
-
typeKey: readString$
|
|
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$
|
|
3270
|
-
color: readString$
|
|
3271
|
-
icon: readString$
|
|
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$
|
|
3288
|
+
routingBehavior: readString$c(raw, 'routingBehavior'),
|
|
3276
3289
|
routesThroughSelectedActionConnection: readNullableBoolean(raw, 'routesThroughSelectedActionConnection') ??
|
|
3277
3290
|
undefined,
|
|
3278
3291
|
isTerminal: readNullableBoolean(raw, 'isTerminal') ??
|
|
3279
|
-
readString$
|
|
3292
|
+
readString$c(raw, 'routingBehavior') === 'Terminal',
|
|
3280
3293
|
isRouteable: readNullableBoolean(raw, 'isRouteable') ??
|
|
3281
3294
|
readNullableBoolean(raw, 'canRoute') ??
|
|
3282
3295
|
readNullableBoolean(raw, 'routesThroughSelectedActionConnection') ??
|
|
3283
|
-
readString$
|
|
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$
|
|
3304
|
-
type: readString$
|
|
3305
|
-
propertyKey: readString$
|
|
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$
|
|
3330
|
+
key: readString$c(property, 'key'),
|
|
3318
3331
|
name: toTranslatableText(property['name']),
|
|
3319
|
-
viewType: readString$
|
|
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$
|
|
3334
|
-
serviceUrl: readString$
|
|
3335
|
-
url: readString$
|
|
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$
|
|
3340
|
-
readString$
|
|
3341
|
-
failureBehavior: readString$
|
|
3342
|
-
readString$
|
|
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$
|
|
3349
|
-
body: readString$
|
|
3350
|
-
bodyType: readString$
|
|
3351
|
-
bodyKind: readString$
|
|
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$
|
|
3371
|
-
inputSchemaJson: readString$
|
|
3372
|
-
outputSchemaJson: readString$
|
|
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$
|
|
3378
|
-
readString$
|
|
3379
|
-
failureBehavior: readString$
|
|
3380
|
-
readString$
|
|
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$
|
|
3408
|
-
actionKey: readString$
|
|
3409
|
-
configJson: readString$
|
|
3410
|
-
config: parseJsonObject$3(readString$
|
|
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$
|
|
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$
|
|
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$
|
|
3505
|
-
readString$
|
|
3506
|
-
readString$
|
|
3507
|
-
readString$
|
|
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$
|
|
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$
|
|
3568
|
-
targetPath: readString$
|
|
3569
|
-
jsonPath: readString$
|
|
3570
|
-
contextPath: readString$
|
|
3571
|
-
writePolicy: readString$
|
|
3572
|
-
valueType: readString$
|
|
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$
|
|
3594
|
+
field: readString$c(raw, 'pluginInputFieldName') ?? '',
|
|
3582
3595
|
source: 'Property',
|
|
3583
3596
|
value: readNullableNumber(raw, 'requestPropertyId'),
|
|
3584
3597
|
};
|
|
3585
3598
|
}
|
|
3586
|
-
if (readString$
|
|
3599
|
+
if (readString$c(raw, 'contextPath')) {
|
|
3587
3600
|
return {
|
|
3588
|
-
field: readString$
|
|
3601
|
+
field: readString$c(raw, 'pluginInputFieldName') ?? '',
|
|
3589
3602
|
source: 'Context',
|
|
3590
|
-
value: readString$
|
|
3603
|
+
value: readString$c(raw, 'contextPath') ?? '',
|
|
3591
3604
|
};
|
|
3592
3605
|
}
|
|
3593
|
-
if (readString$
|
|
3606
|
+
if (readString$c(raw, 'transformExpression')) {
|
|
3594
3607
|
return {
|
|
3595
|
-
field: readString$
|
|
3608
|
+
field: readString$c(raw, 'pluginInputFieldName') ?? '',
|
|
3596
3609
|
source: 'Expression',
|
|
3597
|
-
value: readString$
|
|
3610
|
+
value: readString$c(raw, 'transformExpression') ?? '',
|
|
3598
3611
|
};
|
|
3599
3612
|
}
|
|
3600
3613
|
return {
|
|
3601
|
-
field: readString$
|
|
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$
|
|
3610
|
-
targetPath: readString$
|
|
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$
|
|
3630
|
+
field: readString$c(raw, 'fieldKey') ?? '',
|
|
3618
3631
|
source: 'Property',
|
|
3619
3632
|
value: readNullableNumber(raw, 'requestPropertyId'),
|
|
3620
3633
|
};
|
|
3621
3634
|
}
|
|
3622
|
-
if (readString$
|
|
3635
|
+
if (readString$c(raw, 'contextPath')) {
|
|
3623
3636
|
return {
|
|
3624
|
-
field: readString$
|
|
3637
|
+
field: readString$c(raw, 'fieldKey') ?? '',
|
|
3625
3638
|
source: 'Context',
|
|
3626
|
-
value: readString$
|
|
3639
|
+
value: readString$c(raw, 'contextPath') ?? '',
|
|
3627
3640
|
};
|
|
3628
3641
|
}
|
|
3629
|
-
if (readString$
|
|
3642
|
+
if (readString$c(raw, 'transformExpression')) {
|
|
3630
3643
|
return {
|
|
3631
|
-
field: readString$
|
|
3644
|
+
field: readString$c(raw, 'fieldKey') ?? '',
|
|
3632
3645
|
source: 'Expression',
|
|
3633
|
-
value: readString$
|
|
3646
|
+
value: readString$c(raw, 'transformExpression') ?? '',
|
|
3634
3647
|
};
|
|
3635
3648
|
}
|
|
3636
3649
|
return {
|
|
3637
|
-
field: readString$
|
|
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$
|
|
3646
|
-
targetPath: readString$
|
|
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$
|
|
3670
|
+
parentPath: readString$c(raw, 'parentPath') ??
|
|
3658
3671
|
String(readNullableNumber(raw, 'parentRequestPropertyId') ?? ''),
|
|
3659
|
-
childPath: readString$
|
|
3672
|
+
childPath: readString$c(raw, 'childPath') ??
|
|
3660
3673
|
String(readNullableNumber(raw, 'subRequestPropertyId') ?? ''),
|
|
3661
|
-
direction: readString$
|
|
3674
|
+
direction: readString$c(raw, 'direction') ??
|
|
3662
3675
|
(readNullableBoolean(raw, 'reverseMapping') ? 'Both' : 'In'),
|
|
3663
3676
|
required: readNullableBoolean(raw, 'required') ?? undefined,
|
|
3664
|
-
transform: readString$
|
|
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$
|
|
3671
|
-
childPath: readString$
|
|
3672
|
-
direction: readString$
|
|
3673
|
-
transform: readString$
|
|
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$
|
|
3737
|
+
const source = readString$c(raw, 'source') ?? 'Context';
|
|
3725
3738
|
return {
|
|
3726
3739
|
id: 0,
|
|
3727
|
-
pluginInputFieldName: readString$
|
|
3740
|
+
pluginInputFieldName: readString$c(raw, 'field') ?? '',
|
|
3728
3741
|
requestPropertyId: source === 'Property' ? readNullableNumber(raw, 'value') : undefined,
|
|
3729
|
-
contextPath: source === 'Context' ? (readString$
|
|
3742
|
+
contextPath: source === 'Context' ? (readString$c(raw, 'value') ?? '') : undefined,
|
|
3730
3743
|
staticValue: source === 'Static' ? raw['value'] : undefined,
|
|
3731
|
-
transformExpression: source === 'Expression' ? (readString$
|
|
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$
|
|
3746
|
-
contextPath: readString$
|
|
3747
|
-
transformExpression: readString$
|
|
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$
|
|
3764
|
-
contextPath: readString$
|
|
3776
|
+
fieldKey: readString$c(raw, 'field') ?? '',
|
|
3777
|
+
contextPath: readString$c(raw, 'targetPath') ?? readString$c(raw, 'contextPath') ?? '',
|
|
3765
3778
|
};
|
|
3766
3779
|
}
|
|
3767
|
-
const source = readString$
|
|
3780
|
+
const source = readString$c(raw, 'source') ?? 'Context';
|
|
3768
3781
|
return {
|
|
3769
|
-
fieldKey: readString$
|
|
3782
|
+
fieldKey: readString$c(raw, 'field') ?? '',
|
|
3770
3783
|
requestPropertyId: source === 'Property' ? readNullableNumber(raw, 'value') : undefined,
|
|
3771
|
-
contextPath: source === 'Context' ? (readString$
|
|
3784
|
+
contextPath: source === 'Context' ? (readString$c(raw, 'value') ?? '') : undefined,
|
|
3772
3785
|
staticValue: source === 'Static' ? raw['value'] : undefined,
|
|
3773
|
-
transformExpression: source === 'Expression' ? (readString$
|
|
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$
|
|
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$
|
|
3838
|
+
const key = readString$c(raw, 'name');
|
|
3826
3839
|
if (!key)
|
|
3827
3840
|
continue;
|
|
3828
3841
|
properties[key] = {
|
|
3829
|
-
type: readString$
|
|
3830
|
-
description: readString$
|
|
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$
|
|
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$
|
|
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$
|
|
3875
|
+
const key = readString$c(raw, 'key');
|
|
3863
3876
|
if (!key)
|
|
3864
3877
|
continue;
|
|
3865
3878
|
properties[key] = {
|
|
3866
|
-
type: readString$
|
|
3867
|
-
description: readString$
|
|
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$
|
|
3876
|
-
const ar = readString$
|
|
3877
|
-
const display = readString$
|
|
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$
|
|
3893
|
-
readString$
|
|
3894
|
-
readString$
|
|
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$
|
|
3917
|
-
value: readString$
|
|
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$
|
|
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$
|
|
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$
|
|
4934
|
-
message: readString$
|
|
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$
|
|
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$
|
|
5105
|
-
buildCursor(readString$
|
|
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$
|
|
5132
|
+
parseJsonValue$1(readString$a(raw, 'triggerSummaryJson'));
|
|
5120
5133
|
return {
|
|
5121
5134
|
...detail,
|
|
5122
5135
|
...summary,
|
|
5123
|
-
triggerSummaryJson: readString$
|
|
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$
|
|
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$
|
|
5159
|
+
automationName: readString$a(raw, 'automationName') ?? null,
|
|
5147
5160
|
revisionNumber: readNumber$2(raw, 'revisionNumber') ?? null,
|
|
5148
|
-
status: String(readString$
|
|
5149
|
-
triggerType: readString$
|
|
5150
|
-
triggerKey: readString$
|
|
5151
|
-
createdAtUtc: readString$
|
|
5152
|
-
startedAtUtc: readString$
|
|
5153
|
-
readString$
|
|
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$
|
|
5168
|
+
completedAtUtc: readString$a(raw, 'completedAtUtc') ?? null,
|
|
5156
5169
|
durationMs: readNumber$2(raw, 'durationMs') ?? null,
|
|
5157
|
-
correlationId: readString$
|
|
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$
|
|
5166
|
-
nodeType: String(readString$
|
|
5167
|
-
status: String(readString$
|
|
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$
|
|
5177
|
-
errorMessage: readString$
|
|
5178
|
-
readString$
|
|
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$
|
|
5191
|
-
waitType: readString$
|
|
5192
|
-
status: readString$
|
|
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$
|
|
5201
|
-
severity: readString$
|
|
5202
|
-
occurredAtUtc: readString$
|
|
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$
|
|
5206
|
-
data: raw['data'] ?? parseJsonValue$1(readString$
|
|
5207
|
-
dataJson: readString$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
5256
|
-
readString$
|
|
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$
|
|
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 &&
|
|
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 &&
|
|
8487
|
-
|
|
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
|
|
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:
|
|
10759
|
+
version: layoutForNotes?.version ?? null,
|
|
10357
10760
|
nodes: Array.from(nodesByStepId.values()),
|
|
10358
|
-
connections:
|
|
10359
|
-
viewport:
|
|
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
|
-
|
|
12633
|
-
|
|
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
|
-
|
|
19964
|
-
|
|
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
|
-
|
|
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
|
-
|
|
20139
|
-
|
|
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
|
-
|
|
21702
|
-
|
|
21703
|
-
|
|
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:
|
|
21962
|
-
|
|
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
|