@masterteam/flowplus-workflow 0.0.3 → 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.
|
@@ -1860,6 +1860,19 @@ function workflowTriggerToAutomationTriggerRequest(trigger, existing) {
|
|
|
1860
1860
|
...parseJsonObject$4(trigger.configJson),
|
|
1861
1861
|
...readRecord$2(metadata, 'config'),
|
|
1862
1862
|
};
|
|
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
|
+
}
|
|
1863
1876
|
const startNodeKey = readString$d(metadata, 'startNodeKey') ?? readString$d(config, 'startNodeKey');
|
|
1864
1877
|
if (startNodeKey)
|
|
1865
1878
|
config['startNodeKey'] = startNodeKey;
|
|
@@ -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) ??
|
|
@@ -8482,6 +8495,7 @@ let FlowplusWorkflowState = class FlowplusWorkflowState {
|
|
|
8482
8495
|
}).pipe(catchError(() => EMPTY));
|
|
8483
8496
|
}
|
|
8484
8497
|
triggerTimers = new Map();
|
|
8498
|
+
triggerCommitSeq = new Map();
|
|
8485
8499
|
updateTrigger(ctx, action) {
|
|
8486
8500
|
const state = ctx.getState();
|
|
8487
8501
|
ctx.patchState({
|
|
@@ -8512,18 +8526,24 @@ let FlowplusWorkflowState = class FlowplusWorkflowState {
|
|
|
8512
8526
|
}
|
|
8513
8527
|
const patch = ctxState.patch;
|
|
8514
8528
|
this.triggerTimers.delete(triggerId);
|
|
8529
|
+
const seq = (this.triggerCommitSeq.get(triggerId) ?? 0) + 1;
|
|
8530
|
+
this.triggerCommitSeq.set(triggerId, seq);
|
|
8515
8531
|
handleApiRequest({
|
|
8516
8532
|
ctx,
|
|
8517
8533
|
key: FlowplusWorkflowActionKey.UpdateTrigger,
|
|
8518
8534
|
request$: this.defApi.updateTrigger(triggerId, patch),
|
|
8519
8535
|
onSuccess: (server, s) => {
|
|
8536
|
+
const isLatest = this.triggerCommitSeq.get(triggerId) === seq;
|
|
8520
8537
|
const stillEditing = this.triggerTimers.has(triggerId);
|
|
8521
8538
|
const hasPending = hasOtherPending(s.builder.pendingOperations);
|
|
8522
8539
|
const hasQueuedTriggerUpdates = this.triggerTimers.size > 0;
|
|
8540
|
+
const applyServer = isLatest && !stillEditing;
|
|
8523
8541
|
return {
|
|
8524
8542
|
builder: {
|
|
8525
8543
|
...s.builder,
|
|
8526
|
-
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),
|
|
8527
8547
|
dirty: {
|
|
8528
8548
|
...s.builder.dirty,
|
|
8529
8549
|
triggers: hasPending || hasQueuedTriggerUpdates,
|
|
@@ -12857,6 +12877,19 @@ function toPolicy(main, advanced, advancedEmptyState) {
|
|
|
12857
12877
|
};
|
|
12858
12878
|
}
|
|
12859
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
|
+
];
|
|
12860
12893
|
class AutomationSmartEditorComponent {
|
|
12861
12894
|
step = input(null, ...(ngDevMode ? [{ debugName: "step" }] : /* istanbul ignore next */ []));
|
|
12862
12895
|
trigger = input(null, ...(ngDevMode ? [{ debugName: "trigger" }] : /* istanbul ignore next */ []));
|
|
@@ -13017,10 +13050,26 @@ class AutomationSmartEditorComponent {
|
|
|
13017
13050
|
{ value: 'hmac', label: 'HMAC' },
|
|
13018
13051
|
{ value: 'sharedSecret', label: 'Shared secret' },
|
|
13019
13052
|
];
|
|
13020
|
-
scheduleModeOptions =
|
|
13021
|
-
|
|
13022
|
-
|
|
13023
|
-
|
|
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 */ []));
|
|
13024
13073
|
ifOperatorOptions = [
|
|
13025
13074
|
{ value: 'equals', label: 'Equals' },
|
|
13026
13075
|
{ value: 'notEquals', label: 'Not equals' },
|
|
@@ -13105,6 +13154,11 @@ class AutomationSmartEditorComponent {
|
|
|
13105
13154
|
label: humanize$1(policy),
|
|
13106
13155
|
})),
|
|
13107
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 */ []));
|
|
13108
13162
|
formOptions = computed(() => [
|
|
13109
13163
|
{ value: '', label: 'Select backend form' },
|
|
13110
13164
|
...this.forms().map((form) => ({
|
|
@@ -13391,6 +13445,14 @@ class AutomationSmartEditorComponent {
|
|
|
13391
13445
|
onConfigFieldChange(key, value) {
|
|
13392
13446
|
this.patchConfig({ ...this.config(), [key]: coerceFieldValue(value) });
|
|
13393
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
|
+
}
|
|
13394
13456
|
onSubworkflowTargetChange(automationId) {
|
|
13395
13457
|
const automation = this.subworkflowAutomations().find((item) => String(item.automationId) === String(automationId));
|
|
13396
13458
|
this.patchConfig({
|
|
@@ -14196,7 +14258,7 @@ class AutomationSmartEditorComponent {
|
|
|
14196
14258
|
.subscribe((result) => this.subworkflowAutomations.set(result.items ?? []));
|
|
14197
14259
|
}
|
|
14198
14260
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: AutomationSmartEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
14199
|
-
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"] }] });
|
|
14200
14262
|
}
|
|
14201
14263
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: AutomationSmartEditorComponent, decorators: [{
|
|
14202
14264
|
type: Component,
|
|
@@ -14212,7 +14274,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
|
|
|
14212
14274
|
SelectField,
|
|
14213
14275
|
ToggleField,
|
|
14214
14276
|
...DATA_PILL_DRAG,
|
|
14215
|
-
], 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" }]
|
|
14216
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 }] }] } });
|
|
14217
14279
|
function schemaFieldsFrom$1(schema, locale) {
|
|
14218
14280
|
const raw = asRecord$4(schema);
|
|
@@ -14368,6 +14430,7 @@ function normalizeSchemaType(type, format, key = '') {
|
|
|
14368
14430
|
}
|
|
14369
14431
|
}
|
|
14370
14432
|
function coerceFieldValue(value) {
|
|
14433
|
+
value = selectScalarValue(value);
|
|
14371
14434
|
if (value instanceof Date)
|
|
14372
14435
|
return value.toISOString();
|
|
14373
14436
|
if (typeof value !== 'string')
|
|
@@ -14392,6 +14455,29 @@ function coerceFieldValue(value) {
|
|
|
14392
14455
|
return false;
|
|
14393
14456
|
return value;
|
|
14394
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
|
+
}
|
|
14395
14481
|
function isDateLikeConfigKey(key) {
|
|
14396
14482
|
const normalized = key.trim().toLowerCase();
|
|
14397
14483
|
return (normalized.includes('date') ||
|
|
@@ -22310,24 +22396,33 @@ class WorkflowBuilderPageComponent {
|
|
|
22310
22396
|
englishName = item.displayName;
|
|
22311
22397
|
}
|
|
22312
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
|
+
};
|
|
22313
22406
|
this.afterDispatch(this.store.createTrigger({
|
|
22314
22407
|
type: triggerType,
|
|
22315
22408
|
enabled: true,
|
|
22316
22409
|
name: englishName,
|
|
22410
|
+
configJson: JSON.stringify(config),
|
|
22317
22411
|
metadata: {
|
|
22318
22412
|
triggerKey,
|
|
22319
22413
|
catalogKey,
|
|
22320
|
-
|
|
22321
|
-
|
|
22322
|
-
|
|
22323
|
-
},
|
|
22414
|
+
config,
|
|
22415
|
+
configJson: JSON.stringify(config),
|
|
22416
|
+
layout,
|
|
22324
22417
|
},
|
|
22325
22418
|
}), () => {
|
|
22326
22419
|
const created = this.store
|
|
22327
22420
|
.triggers()
|
|
22328
22421
|
.find((t) => t.id > 0 && !beforeIds.has(t.id));
|
|
22329
|
-
if (created)
|
|
22422
|
+
if (created) {
|
|
22423
|
+
this.store.setTriggerPosition(created.id, layout.x, layout.y);
|
|
22330
22424
|
this.canvas()?.focusTrigger(created.id);
|
|
22425
|
+
}
|
|
22331
22426
|
});
|
|
22332
22427
|
}
|
|
22333
22428
|
/**
|