@masterteam/flowplus-workflow 0.0.6 → 0.0.7
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.
|
@@ -20283,7 +20283,7 @@ class AutomationSmartEditorComponent {
|
|
|
20283
20283
|
.subscribe((result) => this.subworkflowAutomations.set(result.items ?? []));
|
|
20284
20284
|
}
|
|
20285
20285
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: AutomationSmartEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
20286
|
-
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]=\"true\"\n (dataDropEvent)=\"insertExpressionDrop($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\n class=\"text-[12px] font-semibold text-(--p-text-muted-color)\"\n >\n First node connected\n </div>\n <div\n class=\"truncate text-[14px] font-semibold text-(--p-text-color)\"\n >\n {{ startConnection().label }}\n </div>\n <div\n class=\"flex flex-wrap items-center gap-2 text-[12px] text-(--p-text-muted-color)\"\n >\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\n class=\"m-0 text-[12px] leading-5 text-(--p-text-muted-color)\"\n >\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\n class=\"flex flex-wrap items-center gap-2 text-[12px] text-(--p-text-muted-color)\"\n >\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\n class=\"rounded-lg border border-dashed border-(--p-content-border-color) bg-(--p-surface-50) p-3\"\n >\n <div class=\"text-[14px] font-semibold text-(--p-text-color)\">\n No first node connected\n </div>\n <p\n class=\"m-0 mt-1 text-[12px] leading-5 text-(--p-text-muted-color)\"\n >\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 (\n hasTriggerPayloadSchema() && triggerPayloadSchema();\n as schema\n ) {\n <div\n class=\"rounded-md border border-surface-200 bg-surface-0 p-3\"\n >\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\n class=\"mt-3 rounded-md border border-surface-200 bg-surface-0 p-3\"\n >\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\n 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\n class=\"self-end\"\n size=\"small\"\n variant=\"outlined\"\n label=\"Copy\"\n (onClick)=\"copyWebhookUrl()\"\n />\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>{{\n (setup.requiredHeaders ?? []).join(\", \") || \"-\"\n }}</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\n class=\"mt-3 rounded-lg border border-(--p-content-border-color)\"\n >\n <summary\n class=\"cursor-pointer px-3 py-2 text-[12px] font-semibold\"\n >\n Sample request\n </summary>\n <pre class=\"fp-ae-code\">{{\n schemaText(setup.sampleRequest)\n }}</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 [required]=\"isAuthFieldRequired('mode')\"\n [options]=\"authModeOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-text-field\n [ngModel]=\"authPolicy()['signatureHeaderName'] ?? ''\"\n (ngModelChange)=\"\n onAuthFieldChange('signatureHeaderName', $event)\n \"\n label=\"Signature header\"\n hint=\"Header name that carries the webhook signature when the policy requires signed requests.\"\n [required]=\"isAuthFieldRequired('signatureHeaderName')\"\n />\n <mt-text-field\n [ngModel]=\"authPolicy()['timestampHeaderName'] ?? ''\"\n (ngModelChange)=\"\n onAuthFieldChange('timestampHeaderName', $event)\n \"\n label=\"Timestamp header\"\n hint=\"Header name that carries the request timestamp for replay protection.\"\n [required]=\"isAuthFieldRequired('timestampHeaderName')\"\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 [required]=\"true\"\n [options]=\"formOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-select-field\n [ngModel]=\"\n selectedFormVersionId() || formBinding()?.formVersionId || ''\n \"\n (ngModelChange)=\"onFormVersionChange($event)\"\n label=\"Form version\"\n hint=\"Persist the exact backend formVersionId. Do not type or generate IDs manually.\"\n [required]=\"true\"\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\n size=\"small\"\n severity=\"primary\"\n label=\"Save binding\"\n (onClick)=\"saveFormBinding()\"\n />\n @if (formBinding()) {\n <span\n class=\"rounded-lg bg-(--p-surface-100) px-2 py-1 font-mono text-[11px] text-(--p-text-muted-color)\"\n >\n {{ formBinding()!.formVersionId }}\n </span>\n }\n </div>\n @if (formSchema(); as schema) {\n <details\n class=\"mt-3 rounded-lg border border-(--p-content-border-color)\"\n >\n <summary\n class=\"cursor-pointer px-3 py-2 text-[12px] font-semibold\"\n >\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 [required]=\"true\"\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 [required]=\"isConfigFieldRequired('timezone')\"\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 [required]=\"showScheduleCron()\"\n />\n }\n @if (showScheduleInterval()) {\n <mt-number-field\n [ngModel]=\"numberValue(config()['intervalSeconds'])\"\n (ngModelChange)=\"\n onConfigFieldChange('intervalSeconds', $event)\n \"\n label=\"Interval seconds\"\n hint=\"Repeat interval in seconds.\"\n [required]=\"showScheduleInterval()\"\n [min]=\"0\"\n />\n }\n @if (showScheduleOnce()) {\n <mt-date-field\n class=\"md:col-span-2\"\n [ngModel]=\"\n config()['runAt'] ??\n config()['runAtUtc'] ??\n config()['oneTimeAt'] ??\n config()['at'] ??\n ''\n \"\n (ngModelChange)=\"onConfigFieldChange('runAt', $event)\"\n label=\"Run at UTC\"\n hint=\"UTC date/time for the one-time schedule.\"\n [required]=\"showScheduleOnce()\"\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 [required]=\"isConfigFieldRequired('startDate')\"\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 [required]=\"isConfigFieldRequired('misfirePolicy')\"\n [options]=\"misfirePolicyOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n </div>\n <div class=\"mt-3 flex gap-2\">\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n label=\"Validate\"\n (onClick)=\"validateSchedule()\"\n />\n <mt-button\n size=\"small\"\n severity=\"primary\"\n label=\"Preview next fire\"\n (onClick)=\"previewSchedule()\"\n />\n </div>\n @if (schedulePreview(); as preview) {\n <div\n class=\"mt-3 rounded-lg bg-(--p-surface-100) px-3 py-2 text-[12px]\"\n >\n <strong class=\"text-emerald-600\">Valid</strong>\n <span class=\"ms-2\"\n >Next fire: {{ preview.nextFireAtUtc ?? \"-\" }}</span\n >\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\n *ngTemplateOutlet=\"\n mapEditor;\n context: {\n objectKey: 'fields',\n rows: fieldsRows(),\n keyLabel: 'Field',\n valueLabel: 'Value',\n addLabel: 'Add field',\n required: isConfigFieldRequired('fields'),\n }\n \"\n />\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]=\"ifConditionFieldText('left')\"\n (focusin)=\"setExpressionTarget('config:left')\"\n (ngModelChange)=\"onIfConditionFieldChange('left', $event)\"\n label=\"Left\"\n hint=\"Left-side value or expression to compare.\"\n [required]=\"isConfigFieldRequired('left')\"\n />\n <mt-select-field\n [ngModel]=\"ifConditionFieldValue('operator') || 'equals'\"\n (ngModelChange)=\"onIfConditionFieldChange('operator', $event)\"\n label=\"Operator\"\n hint=\"Comparison operator used by the backend condition evaluator.\"\n [required]=\"isConfigFieldRequired('operator')\"\n [options]=\"ifOperatorOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-text-field\n [ngModel]=\"ifConditionFieldText('right')\"\n (focusin)=\"setExpressionTarget('config:right')\"\n (ngModelChange)=\"onIfConditionFieldChange('right', $event)\"\n label=\"Right\"\n hint=\"Right-side value or expression to compare against.\"\n [required]=\"isConfigFieldRequired('right')\"\n />\n </div>\n <div\n class=\"mt-3 rounded-md border border-surface-200 bg-surface-0 p-3\"\n >\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\n class=\"rounded-md bg-(--p-surface-100) px-2 py-1 font-mono text-[11px]\"\n >{{ key }}</span\n >\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]=\"httpMethod()\"\n (ngModelChange)=\"onHttpMethodChange($event)\"\n label=\"Method\"\n hint=\"HTTP method used for the outbound request.\"\n [required]=\"isConfigFieldRequired('method')\"\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 [required]=\"isConfigFieldRequired('url')\"\n />\n </div>\n <div class=\"mt-3\">\n <ng-container\n *ngTemplateOutlet=\"\n mapEditor;\n context: {\n objectKey: 'headers',\n rows: headerRows(),\n keyLabel: 'Header',\n valueLabel: 'Value',\n addLabel: 'Add header',\n }\n \"\n />\n </div>\n <div class=\"mt-3\">\n <ng-container\n *ngTemplateOutlet=\"\n mapEditor;\n context: {\n objectKey: 'query',\n rows: queryRows(),\n keyLabel: 'Query param',\n valueLabel: 'Value',\n addLabel: 'Add query param',\n }\n \"\n />\n </div>\n @if (httpMethodAllowsPayload()) {\n @if (httpSupportsBodyMode()) {\n <mt-select-field\n class=\"mt-3\"\n [ngModel]=\"httpBodyMode()\"\n (ngModelChange)=\"onHttpBodyModeChange($event)\"\n label=\"Payload mode\"\n hint=\"How the request payload should be sent by the backend HTTP runtime.\"\n [required]=\"isConfigFieldRequired(httpBodyModeConfigKey())\"\n [options]=\"httpBodyModeOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n }\n @if (httpBodyEnabled()) {\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=\"Request payload\"\n hint=\"Payload sent by this HTTP method. Use JSON or expressions when the backend schema allows it.\"\n [required]=\"isConfigFieldRequired('body')\"\n rows=\"6\"\n />\n }\n } @else {\n <div\n class=\"mt-3 rounded-md border border-(--p-content-border-color) bg-(--p-surface-50) px-3 py-2 text-[12px] leading-5 text-(--p-text-muted-color)\"\n >\n {{ httpMethod() }} requests do not send a payload. Put request\n data in query parameters or headers.\n </div>\n }\n @if (\n supportsConfigKey(\"timeoutSeconds\") ||\n supportsConfigKey(\"responseHandling\")\n ) {\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)=\"\n onConfigFieldChange('timeoutSeconds', $event)\n \"\n label=\"Timeout seconds\"\n hint=\"Request timeout when exposed by the backend schema.\"\n [required]=\"isConfigFieldRequired('timeoutSeconds')\"\n [min]=\"0\"\n />\n }\n @if (supportsConfigKey(\"responseHandling\")) {\n <mt-text-field\n [ngModel]=\"config()['responseHandling'] ?? ''\"\n (ngModelChange)=\"\n onConfigFieldChange('responseHandling', $event)\n \"\n label=\"Response handling\"\n hint=\"Backend-supported response handling mode or expression.\"\n [required]=\"isConfigFieldRequired('responseHandling')\"\n />\n }\n </div>\n }\n @if (sectionInMain(\"credentials\")) {\n <div\n class=\"mt-3 rounded-md border border-surface-200 bg-surface-0 p-3\"\n >\n <div class=\"fp-ae-label mb-2\">Credential</div>\n <ng-container *ngTemplateOutlet=\"credentialSelector\" />\n </div>\n }\n <div\n class=\"mt-3 rounded-md border border-(--p-content-border-color) bg-(--p-surface-50) p-3\"\n >\n <div class=\"flex items-center justify-between gap-3\">\n <div class=\"min-w-0\">\n <div class=\"fp-ae-label\">Configuration check</div>\n <p class=\"fp-ae-copy m-0 mt-1\">\n Runs backend dry-run validation for method, URL,\n expressions, and request policy before execution.\n </p>\n </div>\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n severity=\"secondary\"\n label=\"Check request\"\n [disabled]=\"httpCheckState() === 'loading'\"\n (onClick)=\"checkHttpRequest()\"\n />\n </div>\n @if (httpLocalIssues().length > 0) {\n <div\n class=\"mt-2 rounded-md border border-[rgb(var(--fp-warning))]/25 bg-[rgb(var(--fp-warning))]/10 px-2.5 py-2 text-[12px] text-(--p-text-color)\"\n >\n {{ httpLocalIssues()[0] }}\n </div>\n } @else if (httpCheckError(); as error) {\n <div\n class=\"mt-2 rounded-md border border-[rgb(var(--fp-error))]/25 bg-[rgb(var(--fp-error))]/10 px-2.5 py-2 text-[12px] text-(--p-text-color)\"\n >\n {{ error }}\n </div>\n } @else if (httpCheckResult(); as result) {\n <div\n class=\"mt-2 rounded-md border border-[rgb(var(--fp-commit))]/25 bg-[rgb(var(--fp-commit))]/10 px-2.5 py-2 text-[12px] text-(--p-text-color)\"\n >\n Backend check\n {{\n result.success || result.canStart\n ? \"passed\"\n : \"completed with issues\"\n }}.\n </div>\n }\n </div>\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]=\"waitMode()\"\n (ngModelChange)=\"onWaitModeChange($event)\"\n label=\"Mode\"\n hint=\"Choose how execution should pause.\"\n [required]=\"isConfigFieldRequired('mode')\"\n [options]=\"waitModeOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n @if (showWaitDuration()) {\n <mt-number-field\n [ngModel]=\"numberValue(config()['durationSeconds'])\"\n (ngModelChange)=\"\n onConfigFieldChange('durationSeconds', $event)\n \"\n label=\"Wait duration\"\n hint=\"Seconds to pause before continuing.\"\n [required]=\"isConfigFieldRequired('durationSeconds')\"\n [min]=\"1\"\n />\n }\n @if (showWaitUntilDate()) {\n <mt-date-field\n [ngModel]=\"waitUntilDateValue()\"\n (ngModelChange)=\"onWaitUntilDateChange($event)\"\n label=\"Resume at\"\n hint=\"Date and time when execution should continue.\"\n [required]=\"isConfigFieldRequired('untilDate')\"\n [showTime]=\"true\"\n [showClear]=\"true\"\n [pInputs]=\"dateTimePickerInputs\"\n />\n }\n </div>\n @if (showWaitResumePayloadSchema()) {\n <mt-textarea-field\n class=\"mt-3 w-full font-mono\"\n [ngModel]=\"waitResumePayloadSchemaText()\"\n (focusin)=\"setExpressionTarget('config:resumePayloadSchema')\"\n (ngModelChange)=\"onWaitResumePayloadSchemaChange($event)\"\n label=\"Resume payload schema\"\n hint=\"Optional JSON schema for data sent back when this wait is resumed.\"\n [required]=\"isConfigFieldRequired('resumePayloadSchema')\"\n rows=\"5\"\n />\n }\n </section>\n }\n }\n @case (\"HumanTask\") {\n @if (sectionInMain(\"humanTask\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Human task</div>\n @if (\n assignmentOptions()?.providerStatus &&\n assignmentOptions()?.providerStatus !== \"Available\"\n ) {\n <p class=\"fp-ae-copy\">\n Assignment provider status:\n {{ assignmentOptions()?.providerStatus }}. The backend provider\n is the source of truth for available assignees.\n </p>\n }\n <div\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 Task details\n </h3>\n <div class=\"grid gap-3 p-4 xl:grid-cols-3\">\n <mt-text-field\n [ngModel]=\"config()['title'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:title')\"\n (ngModelChange)=\"onConfigFieldChange('title', $event)\"\n label=\"Task title\"\n hint=\"Title shown to the assigned user while the execution waits.\"\n [required]=\"isConfigFieldRequired('title')\"\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 submit this task.\"\n [required]=\"isConfigFieldRequired('assignment')\"\n [options]=\"assignmentSelectOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-text-field\n [ngModel]=\"config()['priority'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('priority', $event)\"\n label=\"Priority\"\n hint=\"Optional task priority passed to backend task orchestration.\"\n [required]=\"isConfigFieldRequired('priority')\"\n />\n <mt-textarea-field\n class=\"xl:col-span-3\"\n [ngModel]=\"config()['description'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:description')\"\n (ngModelChange)=\"onConfigFieldChange('description', $event)\"\n label=\"Task description\"\n hint=\"Instructions shown with the HumanTask form.\"\n [required]=\"isConfigFieldRequired('description')\"\n rows=\"4\"\n />\n </div>\n </div>\n\n <div\n class=\"mt-3 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 Task behavior\n </h3>\n <div class=\"grid gap-3 p-4 xl:grid-cols-4\">\n <mt-date-field\n [ngModel]=\"\n config()['dueDateUtc'] ?? config()['dueDate'] ?? ''\n \"\n (ngModelChange)=\"onConfigFieldChange('dueDateUtc', $event)\"\n label=\"Due date\"\n hint=\"Optional backend due timestamp for the waiting task.\"\n [required]=\"isConfigFieldRequired('dueDateUtc')\"\n [showTime]=\"true\"\n [showClear]=\"true\"\n [pInputs]=\"dateTimePickerInputs\"\n />\n <mt-number-field\n [ngModel]=\"numberValue(config()['dueInSeconds'])\"\n (ngModelChange)=\"onConfigFieldChange('dueInSeconds', $event)\"\n label=\"Due in seconds\"\n hint=\"Relative due duration when the backend should calculate the timestamp.\"\n [required]=\"isConfigFieldRequired('dueInSeconds')\"\n [min]=\"0\"\n />\n <mt-text-field\n [ngModel]=\"config()['contextOutputPath'] ?? 'task'\"\n (ngModelChange)=\"\n onConfigFieldChange('contextOutputPath', $event)\n \"\n label=\"Context output path\"\n hint=\"Where submitted task values are written in execution context.\"\n [required]=\"isConfigFieldRequired('contextOutputPath')\"\n />\n <mt-select-field\n [ngModel]=\"config()['cancelBehavior'] ?? 'RouteCancelled'\"\n (ngModelChange)=\"\n onConfigFieldChange('cancelBehavior', $event)\n \"\n label=\"Cancel behavior\"\n hint=\"Route cancelled when users cancel, or fail this node.\"\n [required]=\"isConfigFieldRequired('cancelBehavior')\"\n [options]=\"humanTaskCancelBehaviorOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <div\n class=\"flex min-h-[4.25rem] items-end rounded-md border border-surface-200 bg-surface-0 px-3 py-2\"\n >\n <mt-toggle-field\n size=\"small\"\n label=\"Allow draft save\"\n labelPosition=\"end\"\n [ngModel]=\"config()['saveDraft'] !== false\"\n [required]=\"isConfigFieldRequired('saveDraft')\"\n (ngModelChange)=\"\n onConfigFieldChange('saveDraft', $event === true)\n \"\n hint=\"Expose backend draft save for this task when supported.\"\n />\n </div>\n @if (supportsConfigKey(\"submitBehavior\")) {\n <mt-text-field\n [ngModel]=\"config()['submitBehavior'] ?? ''\"\n (ngModelChange)=\"\n onConfigFieldChange('submitBehavior', $event)\n \"\n label=\"Submit behavior\"\n hint=\"Backend submit behavior when exposed by the node schema.\"\n [required]=\"isConfigFieldRequired('submitBehavior')\"\n />\n }\n </div>\n </div>\n\n <div class=\"mt-3 flex gap-2\">\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n label=\"Validate assignment\"\n (onClick)=\"validateAssignment()\"\n />\n </div>\n @if (assignmentValidation(); as result) {\n <div\n class=\"mt-3 rounded-lg bg-(--p-surface-100) px-3 py-2 text-[12px]\"\n >\n {{\n result.isValid === false\n ? \"Assignment invalid\"\n : \"Assignment accepted by helper\"\n }}\n @for (issue of resultIssues(result); track $index) {\n <div class=\"mt-1 text-[11px] text-(--p-text-muted-color)\">\n {{ issue }}\n </div>\n }\n </div>\n }\n </section>\n }\n\n @if (sectionInMain(\"formBinding\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Task input form</div>\n <mt-radio-cards-field\n [ngModel]=\"humanTaskFormSource()\"\n (ngModelChange)=\"onHumanTaskFormSourceChange($event)\"\n label=\"Form source\"\n hint=\"HumanTask requires either its own node form binding or an explicit reused FormSubmitTrigger payload.\"\n [required]=\"isConfigFieldRequired('formSource')\"\n [options]=\"humanTaskFormSourceOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n size=\"small\"\n >\n <ng-template #option let-item>\n <div class=\"w-full min-w-0\">\n <div class=\"text-[13px] font-semibold leading-5\">\n {{ item.label }}\n </div>\n <p\n class=\"mt-1 text-[12px] leading-5 text-(--p-text-muted-color)\"\n >\n {{ item.description }}\n </p>\n </div>\n </ng-template>\n </mt-radio-cards-field>\n\n @if (humanTaskUsesExplicitFormBinding()) {\n <div\n class=\"mt-3 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 HumanTask input binding\n </h3>\n <div class=\"grid gap-3 p-4 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 for this HumanTask node.\"\n [required]=\"humanTaskUsesExplicitFormBinding()\"\n [options]=\"formOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-select-field\n [ngModel]=\"\n selectedFormVersionId() ||\n formBinding()?.formVersionId ||\n ''\n \"\n (ngModelChange)=\"onFormVersionChange($event)\"\n label=\"Form version\"\n hint=\"Persist the exact backend formVersionId for the node binding.\"\n [required]=\"humanTaskUsesExplicitFormBinding()\"\n [options]=\"formVersionOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n </div>\n <div class=\"border-t border-surface-200 px-4 py-3\">\n @if (humanTaskFormSummary(); as summary) {\n <div\n class=\"flex flex-wrap items-center gap-2 text-[12px] text-(--p-text-muted-color)\"\n >\n @if (summary.formLabel) {\n <span class=\"rounded-md bg-(--p-surface-100) px-2 py-1\">\n {{ summary.formLabel }}\n </span>\n }\n @if (summary.formVersionId) {\n <span\n class=\"rounded-md bg-(--p-surface-100) px-2 py-1 font-mono text-[11px]\"\n >\n {{ summary.formVersionId }}\n </span>\n }\n <span class=\"rounded-md bg-(--p-surface-100) px-2 py-1\">\n {{ summary.fieldCount }} fields\n </span>\n <span class=\"rounded-md bg-(--p-surface-100) px-2 py-1\">\n {{ summary.requiredCount }} required\n </span>\n <span class=\"rounded-md bg-(--p-surface-100) px-2 py-1\">\n {{ summary.writableCount }} writable\n </span>\n </div>\n }\n\n @if (humanTaskHasCustomInputMapping()) {\n <div\n class=\"mt-3 rounded-md 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 Existing custom input mapping will be preserved unless\n field prefill options are changed.\n </div>\n }\n\n @if (humanTaskFormFields().length > 0) {\n <div\n class=\"mt-3 overflow-hidden rounded-md border border-surface-200 bg-surface-0\"\n >\n <div\n class=\"flex flex-wrap items-center justify-between gap-2 border-b border-surface-200 bg-surface-50 px-3 py-2\"\n >\n <div class=\"text-[13px] font-semibold text-color\">\n FormBuilder fields\n </div>\n <div class=\"flex flex-wrap gap-2\">\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n severity=\"secondary\"\n label=\"Select all\"\n (onClick)=\"selectAllHumanTaskPrefillFields()\"\n />\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n severity=\"secondary\"\n label=\"Clear\"\n (onClick)=\"clearHumanTaskPrefillFields()\"\n />\n </div>\n </div>\n <div class=\"divide-y divide-surface-200\">\n @for (field of humanTaskFormFields(); track field.key) {\n <div\n class=\"grid gap-3 px-3 py-3 md:grid-cols-[minmax(0,1fr)_auto] md:items-center\"\n >\n <div class=\"min-w-0\">\n <div class=\"flex flex-wrap items-center gap-2\">\n <span\n class=\"truncate text-[13px] font-semibold text-(--p-text-color)\"\n >\n {{ field.label }}\n </span>\n <span\n class=\"rounded-md bg-(--p-surface-100) px-2 py-0.5 font-mono text-[11px] text-(--p-text-muted-color)\"\n >\n {{ field.key }}\n </span>\n @if (field.required) {\n <span\n class=\"rounded-md bg-[rgb(var(--fp-danger))]/10 px-2 py-0.5 text-[11px] font-semibold text-[rgb(var(--fp-danger))]\"\n >\n Required\n </span>\n }\n @if (field.read === true) {\n <span\n class=\"rounded-md bg-(--p-surface-100) px-2 py-0.5 text-[11px] text-(--p-text-muted-color)\"\n >\n Read\n </span>\n }\n @if (field.write === true) {\n <span\n class=\"rounded-md bg-(--p-surface-100) px-2 py-0.5 text-[11px] text-(--p-text-muted-color)\"\n >\n Write\n </span>\n }\n </div>\n @if (field.description) {\n <p\n class=\"m-0 mt-1 text-[12px] leading-5 text-(--p-text-muted-color)\"\n >\n {{ field.description }}\n </p>\n }\n </div>\n <mt-toggle-field\n size=\"small\"\n label=\"Prefill\"\n labelPosition=\"end\"\n [ngModel]=\"\n humanTaskPrefillFieldKeySet().has(field.key)\n \"\n (ngModelChange)=\"\n onHumanTaskPrefillFieldToggle(\n field.key,\n $event === true\n )\n \"\n />\n </div>\n }\n </div>\n <div\n class=\"border-t border-surface-200 bg-surface-50 px-3 py-2 text-[12px] leading-5 text-(--p-text-muted-color)\"\n >\n Selected fields are saved as\n <span class=\"font-mono\"\n >inputMappingJson.fieldKeys</span\n >\n so backend can prefill matching upstream values.\n </div>\n </div>\n } @else if (formSchema()) {\n <div\n class=\"mt-3 rounded-md border border-surface-200 bg-surface-50 px-3 py-2 text-[12px] leading-5 text-(--p-text-muted-color)\"\n >\n No FormBuilder fields were returned for this form schema.\n </div>\n }\n\n @if (formSchema()?.requiresSnapshotOnPublish) {\n <div\n class=\"mt-3 rounded-md border border-surface-200 bg-surface-50 px-3 py-2 text-[12px] leading-5 text-(--p-text-muted-color)\"\n >\n Backend requires this form version snapshot during\n publish.\n </div>\n }\n </div>\n <div class=\"border-t border-surface-200 px-4 py-3\">\n <div class=\"flex flex-wrap items-center gap-2\">\n <mt-button\n size=\"small\"\n severity=\"primary\"\n label=\"Save task binding\"\n (onClick)=\"saveFormBinding()\"\n />\n @if (formBinding()) {\n <span\n class=\"rounded-lg bg-(--p-surface-100) px-2 py-1 font-mono text-[11px] text-(--p-text-muted-color)\"\n >\n {{ formBinding()!.formVersionId }}\n </span>\n }\n </div>\n </div>\n </div>\n } @else {\n <div class=\"mt-3 grid gap-3 md:grid-cols-2\">\n <mt-select-field\n [ngModel]=\"config()['triggerKey'] ?? ''\"\n (ngModelChange)=\"onHumanTaskReuseTriggerChange($event)\"\n label=\"Trigger to reuse\"\n hint=\"Explicitly select the FormSubmitTrigger whose submitted form payload is reused.\"\n [options]=\"triggerOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n </div>\n @if (humanTaskReuseFormBinding()) {\n <div\n class=\"mt-3 rounded-md border border-surface-200 bg-surface-0 p-3\"\n >\n <div class=\"fp-ae-label mb-2\">Reused trigger form</div>\n @if (humanTaskFormSummary(); as summary) {\n <div\n class=\"flex flex-wrap items-center gap-2 text-[12px] text-(--p-text-muted-color)\"\n >\n @if (summary.formLabel) {\n <span class=\"rounded-md bg-(--p-surface-100) px-2 py-1\">\n {{ summary.formLabel }}\n </span>\n }\n @if (summary.formVersionId) {\n <span\n class=\"rounded-md bg-(--p-surface-100) px-2 py-1 font-mono text-[11px]\"\n >\n {{ summary.formVersionId }}\n </span>\n }\n <span class=\"rounded-md bg-(--p-surface-100) px-2 py-1\">\n {{ summary.fieldCount }} fields\n </span>\n <span class=\"rounded-md bg-(--p-surface-100) px-2 py-1\">\n {{ summary.requiredCount }} required\n </span>\n </div>\n }\n </div>\n }\n }\n\n @if (formSchema(); as schema) {\n <details\n class=\"mt-3 rounded-lg border border-(--p-content-border-color)\"\n >\n <summary\n class=\"cursor-pointer px-3 py-2 text-[12px] font-semibold\"\n >\n Form schema preview\n </summary>\n <pre class=\"fp-ae-code\">{{ schemaText(schema.schema) }}</pre>\n </details>\n }\n\n <div\n class=\"mt-3 rounded-md border border-surface-200 bg-surface-0 p-3\"\n >\n <div class=\"fp-ae-label mb-2\">HumanTask routes</div>\n <div class=\"flex flex-wrap gap-1.5\">\n @for (key of routeOutputKeys(); track key) {\n <span\n class=\"rounded-md bg-(--p-surface-100) px-2 py-1 font-mono text-[11px]\"\n >{{ key }}</span\n >\n }\n </div>\n </div>\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 (\n assignmentOptions()?.providerStatus &&\n assignmentOptions()?.providerStatus !== \"Available\"\n ) {\n <p class=\"fp-ae-copy\">\n Assignment provider status:\n {{ assignmentOptions()?.providerStatus }}. The backend provider\n 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 [required]=\"isConfigFieldRequired('title')\"\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 [required]=\"isConfigFieldRequired('assignment')\"\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 [required]=\"isConfigFieldRequired('priority')\"\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 [required]=\"isConfigFieldRequired('message')\"\n rows=\"4\"\n />\n @if (\n humanApprovalSupportsConfig(\"dueDate\") ||\n humanApprovalSupportsConfig(\"dueInSeconds\") ||\n humanApprovalSupportsConfig(\"timeoutSeconds\") ||\n humanApprovalSupportsConfig(\"expiresAt\") ||\n humanApprovalSupportsConfig(\"commentsRequired\") ||\n humanApprovalSupportsConfig(\"allowReturn\")\n ) {\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 [required]=\"isConfigFieldRequired('dueDate')\"\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)=\"\n onConfigFieldChange('dueInSeconds', $event)\n \"\n label=\"Due in seconds\"\n hint=\"Relative approval due duration when supported by the backend schema.\"\n [required]=\"isConfigFieldRequired('dueInSeconds')\"\n [min]=\"0\"\n />\n }\n @if (humanApprovalSupportsConfig(\"timeoutSeconds\")) {\n <mt-number-field\n [ngModel]=\"numberValue(config()['timeoutSeconds'])\"\n (ngModelChange)=\"\n onConfigFieldChange('timeoutSeconds', $event)\n \"\n label=\"Timeout seconds\"\n hint=\"Approval timeout emitted according to backend approval runtime support.\"\n [required]=\"isConfigFieldRequired('timeoutSeconds')\"\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 [required]=\"isConfigFieldRequired('expiresAt')\"\n [showTime]=\"true\"\n [showClear]=\"true\"\n [pInputs]=\"dateTimePickerInputs\"\n />\n }\n @if (humanApprovalSupportsConfig(\"commentsRequired\")) {\n <div\n class=\"flex min-h-[4.25rem] items-end rounded-md border border-surface-200 bg-surface-0 px-3 py-2\"\n >\n <mt-toggle-field\n size=\"small\"\n label=\"Comments required\"\n labelPosition=\"end\"\n [ngModel]=\"config()['commentsRequired'] === true\"\n [required]=\"isConfigFieldRequired('commentsRequired')\"\n (ngModelChange)=\"\n onConfigFieldChange('commentsRequired', $event === true)\n \"\n hint=\"Require approver comments when the backend supports this flag.\"\n />\n </div>\n }\n @if (humanApprovalSupportsConfig(\"allowReturn\")) {\n <div\n class=\"flex min-h-[4.25rem] items-end rounded-md border border-surface-200 bg-surface-0 px-3 py-2\"\n >\n <mt-toggle-field\n size=\"small\"\n label=\"Allow return for changes\"\n labelPosition=\"end\"\n [ngModel]=\"config()['allowReturn'] !== false\"\n [required]=\"isConfigFieldRequired('allowReturn')\"\n (ngModelChange)=\"\n onConfigFieldChange('allowReturn', $event === true)\n \"\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\n class=\"overflow-hidden rounded-md border border-surface-200 bg-surface-0\"\n >\n @if (selectedApprovalDecisionRows().length > 0) {\n <div\n 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 >\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 (\n decision of selectedApprovalDecisionRows();\n track decision.value\n ) {\n <div\n 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 >\n <div class=\"min-w-0\">\n <div\n class=\"text-[11px] font-semibold text-(--p-text-muted-color) xl:hidden\"\n >\n Decision label\n </div>\n <div\n class=\"truncate text-[13px] font-semibold text-(--p-text-color)\"\n >\n {{ decision.label }}\n </div>\n </div>\n <div class=\"min-w-0\">\n <div\n class=\"text-[11px] font-semibold text-(--p-text-muted-color) xl:hidden\"\n >\n Canonical value\n </div>\n <div\n class=\"truncate font-mono text-[12px] text-(--p-text-color)\"\n >\n {{ decision.value }}\n </div>\n </div>\n <div class=\"min-w-0\">\n <div\n class=\"text-[11px] font-semibold text-(--p-text-muted-color) xl:hidden\"\n >\n Route output key\n </div>\n <div\n class=\"truncate font-mono text-[12px] text-(--p-text-muted-color)\"\n >\n {{ decision.routeOutputKey }}\n </div>\n </div>\n <div>\n <div\n class=\"text-[11px] font-semibold text-(--p-text-muted-color) xl:hidden\"\n >\n Routes\n </div>\n <div class=\"text-[13px] text-(--p-text-color)\">\n {{ decision.routeCount }}\n </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\n class=\"px-3 py-4 text-[12px] text-(--p-text-muted-color)\"\n >\n Add at least one backend-supported approval decision.\n </div>\n }\n </div>\n @if (approvalDecisionIssues().length > 0) {\n <div\n 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 >\n @for (issue of approvalDecisionIssues(); track $index) {\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 [required]=\"isConfigFieldRequired('allowedDecisions')\"\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 (\n humanApprovalSupportsConfig(\"payload\") ||\n humanApprovalSupportsConfig(\"context\") ||\n humanApprovalSupportsConfig(\"metadata\")\n ) {\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 [required]=\"isConfigFieldRequired('payload')\"\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 [required]=\"isConfigFieldRequired('context')\"\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 [required]=\"isConfigFieldRequired('metadata')\"\n rows=\"4\"\n />\n }\n </div>\n }\n <div class=\"mt-3 flex gap-2\">\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n label=\"Validate assignment\"\n (onClick)=\"validateAssignment()\"\n />\n </div>\n @if (assignmentValidation(); as result) {\n <div\n class=\"mt-3 rounded-lg bg-(--p-surface-100) px-3 py-2 text-[12px]\"\n >\n {{\n result.isValid === false\n ? \"Assignment invalid\"\n : \"Assignment accepted by helper\"\n }}\n @for (issue of resultIssues(result); track $index) {\n <div class=\"mt-1 text-[11px] text-(--p-text-muted-color)\">\n {{ issue }}\n </div>\n }\n </div>\n }\n </section>\n }\n }\n @case (\"ConnectorAction\") {\n @if (sectionInMain(\"humanReviewRequest\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">\n {{ humanReviewProviderLabel() }} human review\n </div>\n <div class=\"grid gap-3 md:grid-cols-2\">\n <mt-text-field\n [ngModel]=\"config()['channelType'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('channelType', $event)\"\n label=\"Channel\"\n [required]=\"isConfigFieldRequired('channelType')\"\n />\n </div>\n\n <div class=\"mt-4 grid gap-3 md:grid-cols-3\">\n <mt-text-field\n [ngModel]=\"humanReviewDestination()['recipient'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:destination:recipient')\"\n (ngModelChange)=\"\n onNestedConfigFieldChange('destination', 'recipient', $event)\n \"\n label=\"Recipient\"\n [required]=\"isConnectorDestinationRequired()\"\n />\n <mt-text-field\n [ngModel]=\"humanReviewDestination()['channelId'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:destination:channelId')\"\n (ngModelChange)=\"\n onNestedConfigFieldChange('destination', 'channelId', $event)\n \"\n label=\"Channel ID\"\n [required]=\"isConnectorDestinationRequired()\"\n />\n <mt-text-field\n [ngModel]=\"humanReviewDestination()['threadKey'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:destination:threadKey')\"\n (ngModelChange)=\"\n onNestedConfigFieldChange('destination', 'threadKey', $event)\n \"\n label=\"Thread key\"\n [required]=\"isConnectorDestinationRequired()\"\n />\n </div>\n\n <div class=\"mt-4 grid gap-3\">\n <mt-text-field\n [ngModel]=\"humanReviewMessage()['subject'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:message:subject')\"\n (ngModelChange)=\"\n onNestedConfigFieldChange('message', 'subject', $event)\n \"\n label=\"Subject\"\n [required]=\"isConnectorMessageRequired()\"\n />\n <mt-textarea-field\n class=\"w-full\"\n [ngModel]=\"humanReviewMessage()['body'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:message:body')\"\n (ngModelChange)=\"\n onNestedConfigFieldChange('message', 'body', $event)\n \"\n label=\"Message\"\n [required]=\"isConnectorMessageRequired()\"\n rows=\"5\"\n />\n <mt-text-field\n [ngModel]=\"humanReviewMessage()['templateKey'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:message:templateKey')\"\n (ngModelChange)=\"\n onNestedConfigFieldChange('message', 'templateKey', $event)\n \"\n label=\"Template key\"\n [required]=\"isConnectorMessageRequired()\"\n />\n </div>\n\n <div class=\"mt-4\">\n <div class=\"mb-2 flex items-center justify-between gap-3\">\n <div class=\"fp-ae-label\">Response options</div>\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n label=\"Add option\"\n (onClick)=\"addHumanReviewResponseOption()\"\n />\n </div>\n <div class=\"grid gap-2\">\n @for (\n option of humanReviewResponseOptions();\n track option.key + \":\" + $index\n ) {\n <div\n class=\"grid gap-2 rounded-md border border-(--p-content-border-color) bg-(--p-surface-50) p-2 md:grid-cols-[1fr_1fr_1fr_auto]\"\n >\n <mt-text-field\n [ngModel]=\"option.key\"\n (ngModelChange)=\"\n updateHumanReviewResponseOption($index, 'key', $event)\n \"\n label=\"Key\"\n [required]=\"isConfigFieldRequired('responseOptions')\"\n />\n <mt-text-field\n [ngModel]=\"option.label\"\n (ngModelChange)=\"\n updateHumanReviewResponseOption($index, 'label', $event)\n \"\n label=\"Label\"\n [required]=\"isConfigFieldRequired('responseOptions')\"\n />\n <mt-text-field\n [ngModel]=\"option.routeOutputKey\"\n (ngModelChange)=\"\n updateHumanReviewResponseOption(\n $index,\n 'routeOutputKey',\n $event\n )\n \"\n label=\"Route output\"\n [required]=\"isConfigFieldRequired('responseOptions')\"\n />\n <div class=\"flex items-end justify-end\">\n <mt-button\n size=\"small\"\n variant=\"text\"\n severity=\"secondary\"\n icon=\"general.x-close\"\n [disabled]=\"option.routeCount > 0\"\n (onClick)=\"removeHumanReviewResponseOption($index)\"\n [attr.aria-label]=\"'Remove response option'\"\n />\n </div>\n </div>\n }\n </div>\n </div>\n\n <div class=\"mt-4 grid gap-3 md:grid-cols-2\">\n <mt-select-field\n [ngModel]=\"selectedAssignmentOptionKey()\"\n (ngModelChange)=\"onAssignmentOptionChange($event)\"\n label=\"Assignment\"\n [required]=\"isConfigFieldRequired('assignment')\"\n [options]=\"assignmentSelectOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-text-field\n [ngModel]=\"config()['credentialRef'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('credentialRef', $event)\"\n label=\"Credential\"\n [required]=\"isConfigFieldRequired('credentialRef')\"\n />\n <mt-number-field\n [ngModel]=\"numberValue(config()['timeoutSeconds'])\"\n (ngModelChange)=\"onConfigFieldChange('timeoutSeconds', $event)\"\n label=\"Timeout seconds\"\n [required]=\"isConfigFieldRequired('timeoutSeconds')\"\n [min]=\"1\"\n />\n <mt-select-field\n [ngModel]=\"config()['failureBehavior'] ?? 'RouteFailure'\"\n (ngModelChange)=\"onConfigFieldChange('failureBehavior', $event)\"\n label=\"Failure behavior\"\n [required]=\"isConfigFieldRequired('failureBehavior')\"\n [options]=\"humanReviewFailureBehaviorOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n </div>\n\n <div class=\"mt-3 flex gap-2\">\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n label=\"Validate assignment\"\n (onClick)=\"validateAssignment()\"\n />\n </div>\n @if (assignmentValidation(); as result) {\n <div\n class=\"mt-3 rounded-lg bg-(--p-surface-100) px-3 py-2 text-[12px]\"\n >\n {{\n result.isValid === false\n ? \"Assignment invalid\"\n : \"Assignment accepted by helper\"\n }}\n @for (issue of resultIssues(result); track $index) {\n <div class=\"mt-1 text-[11px] text-(--p-text-muted-color)\">\n {{ issue }}\n </div>\n }\n </div>\n }\n\n <div\n class=\"mt-4 rounded-md border border-(--p-content-border-color) bg-(--p-surface-50) p-3\"\n >\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\n class=\"rounded-md bg-(--p-surface-100) px-2 py-1 font-mono text-[11px]\"\n >{{ key }}</span\n >\n }\n </div>\n </div>\n </section>\n }\n }\n @case (\"ConvertToFile\") {\n @if (sectionInMain(\"convertToFile\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">\n {{ convertToFileActionLabel() }}\n </div>\n @if (convertToFileUnavailableReason(); as reason) {\n <div\n class=\"mb-3 rounded-md border border-(--p-content-border-color) bg-(--p-surface-50) px-3 py-2 text-[12px] leading-5 text-(--p-text-muted-color)\"\n >\n {{ reason }}\n </div>\n }\n\n <div class=\"grid gap-3 md:grid-cols-2\">\n <mt-text-field\n [ngModel]=\"config()['fileName'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:fileName')\"\n (ngModelChange)=\"onConfigFieldChange('fileName', $event)\"\n label=\"File name\"\n [required]=\"isConfigFieldRequired('fileName')\"\n />\n @if (convertToFileIsBase64()) {\n <mt-text-field\n [ngModel]=\"config()['contentType'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('contentType', $event)\"\n label=\"Content type\"\n [required]=\"isConfigFieldRequired('contentType')\"\n />\n } @else {\n <mt-text-field\n [ngModel]=\"convertToFileContentType()\"\n [readonly]=\"true\"\n label=\"Content type\"\n />\n }\n </div>\n\n @if (\n convertToFileIsCsv() ||\n convertToFileIsJson() ||\n convertToFileIsSpreadsheet()\n ) {\n <div class=\"mt-4 grid gap-3 md:grid-cols-2\">\n <mt-select-field\n [ngModel]=\"convertToFileSourceMode()\"\n (ngModelChange)=\"onConvertToFileSourceModeChange($event)\"\n label=\"Data source\"\n [required]=\"isConfigFieldRequired('sourceExpression')\"\n [options]=\"convertToFileSourceOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n @if (convertToFileSourceMode() === \"expression\") {\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"convertToFileSourceExpression()\"\n (focusin)=\"setExpressionTarget('config:sourceExpression')\"\n (ngModelChange)=\"\n onConvertToFileSourceExpressionChange($event)\n \"\n label=\"Source expression\"\n [required]=\"isConfigFieldRequired('sourceExpression')\"\n />\n }\n @if (convertToFileIsCsv() || convertToFileIsSpreadsheet()) {\n <div\n class=\"rounded-md border border-(--p-content-border-color) bg-(--p-surface-50) p-3 md:col-span-2\"\n >\n <div class=\"mb-2 flex items-center justify-between gap-3\">\n <div class=\"fp-ae-label\">Columns</div>\n @if (convertToFileColumns().length) {\n <mt-button\n size=\"small\"\n variant=\"text\"\n severity=\"secondary\"\n label=\"Clear\"\n (onClick)=\"clearConvertToFileColumns()\"\n />\n }\n </div>\n <div class=\"mb-3 flex min-h-8 flex-wrap gap-1.5\">\n @if (convertToFileColumns().length) {\n @for (column of convertToFileColumns(); track column) {\n <span\n class=\"inline-flex items-center gap-1 rounded-md bg-(--p-surface-100) px-2 py-1 font-mono text-[11px]\"\n >\n {{ column }}\n <button\n type=\"button\"\n class=\"inline-flex h-4 w-4 items-center justify-center rounded-sm text-[10px] hover:bg-(--p-surface-200) focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--p-primary-color)\"\n (click)=\"removeConvertToFileColumn(column)\"\n [attr.aria-label]=\"'Remove column ' + column\"\n >\n x\n </button>\n </span>\n }\n } @else {\n <span\n class=\"rounded-md bg-(--p-content-background) px-2 py-1 text-[12px] text-(--p-text-muted-color)\"\n >\n All fields\n </span>\n }\n </div>\n <div class=\"grid items-end gap-2 md:grid-cols-[1fr_auto]\">\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"convertToFileColumnDraft()\"\n (ngModelChange)=\"\n onConvertToFileColumnDraftChange($event)\n \"\n (keyup.enter)=\"addConvertToFileColumns()\"\n label=\"Add column\"\n [required]=\"isConfigFieldRequired('columns')\"\n />\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n icon=\"general.plus\"\n label=\"Add\"\n [disabled]=\"!convertToFileColumnDraft().trim()\"\n (onClick)=\"addConvertToFileColumns()\"\n />\n </div>\n </div>\n }\n </div>\n }\n\n @if (convertToFileIsCsv()) {\n <div class=\"mt-4 grid gap-3 md:grid-cols-3\">\n <mt-toggle-field\n size=\"small\"\n labelPosition=\"end\"\n [ngModel]=\"config()['includeHeaders'] !== false\"\n (ngModelChange)=\"\n onConfigFieldChange('includeHeaders', $event === true)\n \"\n label=\"Include headers\"\n />\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"config()['delimiter'] ?? ','\"\n (ngModelChange)=\"onConfigFieldChange('delimiter', $event)\"\n label=\"Delimiter\"\n [required]=\"isConfigFieldRequired('delimiter')\"\n />\n <mt-text-field\n [ngModel]=\"config()['encoding'] ?? 'utf-8'\"\n [readonly]=\"true\"\n label=\"Encoding\"\n />\n </div>\n }\n\n @if (convertToFileIsJson()) {\n <div class=\"mt-4 grid gap-3 md:grid-cols-2\">\n <mt-toggle-field\n size=\"small\"\n labelPosition=\"end\"\n [ngModel]=\"config()['pretty'] !== false\"\n (ngModelChange)=\"\n onConfigFieldChange('pretty', $event === true)\n \"\n label=\"Pretty JSON\"\n />\n </div>\n }\n\n @if (convertToFileIsHtml()) {\n <div class=\"mt-4 grid gap-3\">\n <mt-text-field\n [ngModel]=\"config()['title'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:title')\"\n (ngModelChange)=\"onConfigFieldChange('title', $event)\"\n label=\"Title\"\n [required]=\"isConfigFieldRequired('title')\"\n />\n <mt-textarea-field\n class=\"w-full\"\n [ngModel]=\"config()['body'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:body')\"\n (ngModelChange)=\"onConfigFieldChange('body', $event)\"\n label=\"Body\"\n [required]=\"isConfigFieldRequired('body')\"\n rows=\"7\"\n />\n <mt-toggle-field\n size=\"small\"\n labelPosition=\"end\"\n [ngModel]=\"config()['sanitize'] !== false\"\n (ngModelChange)=\"\n onConfigFieldChange('sanitize', $event === true)\n \"\n label=\"Sanitize HTML\"\n />\n </div>\n }\n\n @if (convertToFileIsIcs()) {\n <div class=\"mt-4 grid gap-3 md:grid-cols-2\">\n <mt-text-field\n [ngModel]=\"config()['summary'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:summary')\"\n (ngModelChange)=\"onConfigFieldChange('summary', $event)\"\n label=\"Event summary\"\n [required]=\"isConfigFieldRequired('summary')\"\n />\n <mt-text-field\n [ngModel]=\"config()['timezone'] ?? 'UTC'\"\n (ngModelChange)=\"onConfigFieldChange('timezone', $event)\"\n label=\"Timezone\"\n [required]=\"isConfigFieldRequired('timezone')\"\n />\n <mt-date-field\n [ngModel]=\"config()['start'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('start', $event)\"\n label=\"Start\"\n [required]=\"isConfigFieldRequired('start')\"\n [showTime]=\"true\"\n [showClear]=\"true\"\n [pInputs]=\"dateTimePickerInputs\"\n />\n <mt-date-field\n [ngModel]=\"config()['end'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('end', $event)\"\n label=\"End\"\n [required]=\"isConfigFieldRequired('end')\"\n [showTime]=\"true\"\n [showClear]=\"true\"\n [pInputs]=\"dateTimePickerInputs\"\n />\n <mt-text-field\n [ngModel]=\"config()['location'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:location')\"\n (ngModelChange)=\"onConfigFieldChange('location', $event)\"\n label=\"Location\"\n [required]=\"isConfigFieldRequired('location')\"\n />\n <mt-textarea-field\n class=\"w-full\"\n [ngModel]=\"config()['description'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:description')\"\n (ngModelChange)=\"onConfigFieldChange('description', $event)\"\n label=\"Description\"\n [required]=\"isConfigFieldRequired('description')\"\n rows=\"4\"\n />\n </div>\n }\n\n @if (convertToFileIsSpreadsheet()) {\n <div\n class=\"mt-4 rounded-md border border-(--p-content-border-color) bg-(--p-surface-50) p-3\"\n >\n <div class=\"mb-3 flex items-center justify-between gap-3\">\n <div class=\"fp-ae-label\">Sheets</div>\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n icon=\"general.plus\"\n label=\"Add sheet\"\n (onClick)=\"addConvertToFileSheet()\"\n />\n </div>\n <div class=\"grid gap-3\">\n @for (sheet of convertToFileSheetRows(); track sheet.index) {\n <div\n class=\"rounded-md border border-(--p-content-border-color) bg-(--p-content-background) p-3\"\n >\n <div\n class=\"grid gap-3 md:grid-cols-[minmax(140px,0.45fr)_minmax(0,1fr)_minmax(180px,0.6fr)_auto]\"\n >\n <mt-text-field\n [ngModel]=\"sheet.name\"\n (ngModelChange)=\"\n updateConvertToFileSheet(\n sheet.index,\n 'name',\n $event\n )\n \"\n label=\"Sheet name\"\n [required]=\"isConfigFieldRequired('sheets')\"\n />\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"sheet.sourceExpression\"\n (focusin)=\"\n setExpressionTarget(\n 'config:sheets:' + sheet.index + ':source'\n )\n \"\n (ngModelChange)=\"\n updateConvertToFileSheet(\n sheet.index,\n 'sourceExpression',\n $event\n )\n \"\n label=\"Source\"\n [required]=\"isConfigFieldRequired('sheets')\"\n />\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"sheet.columnsText\"\n (ngModelChange)=\"\n updateConvertToFileSheet(\n sheet.index,\n 'columns',\n $event\n )\n \"\n label=\"Columns\"\n [required]=\"isConfigFieldRequired('sheets')\"\n />\n <div class=\"flex items-end justify-end\">\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n severity=\"danger\"\n icon=\"general.trash-01\"\n tooltip=\"Remove sheet\"\n (onClick)=\"removeConvertToFileSheet(sheet.index)\"\n />\n </div>\n </div>\n </div>\n } @empty {\n <div\n class=\"rounded-md bg-(--p-content-background) px-3 py-2 text-[12px] text-(--p-text-muted-color)\"\n >\n Single sheet uses the selected data source.\n </div>\n }\n </div>\n </div>\n }\n\n @if (convertToFileIsText()) {\n <div class=\"mt-4 grid gap-3\">\n <mt-textarea-field\n class=\"w-full\"\n [ngModel]=\"config()['content'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:content')\"\n (ngModelChange)=\"onConfigFieldChange('content', $event)\"\n label=\"Content\"\n [required]=\"isConfigFieldRequired('content')\"\n rows=\"7\"\n />\n <mt-text-field\n [ngModel]=\"config()['encoding'] ?? 'utf-8'\"\n [readonly]=\"true\"\n label=\"Encoding\"\n />\n </div>\n }\n\n @if (convertToFileIsBase64()) {\n <div class=\"mt-4 grid gap-3 md:grid-cols-2\">\n <mt-textarea-field\n class=\"w-full font-mono md:col-span-2\"\n [ngModel]=\"config()['base64Expression'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:base64Expression')\"\n (ngModelChange)=\"\n onConfigFieldChange('base64Expression', $event)\n \"\n label=\"Base64 source\"\n [required]=\"isConfigFieldRequired('base64Expression')\"\n rows=\"5\"\n />\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"config()['extension'] ?? convertToFileExtension()\"\n (ngModelChange)=\"onConfigFieldChange('extension', $event)\"\n label=\"Extension\"\n [required]=\"isConfigFieldRequired('extension')\"\n />\n </div>\n }\n </section>\n }\n }\n @case (\"DataTransform\") {\n @if (sectionInMain(\"dataTransform\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">\n {{ dataTransformActionLabel() }}\n </div>\n @if (dataTransformUnavailableReason(); as reason) {\n <div\n class=\"mb-3 rounded-md border border-(--p-content-border-color) bg-(--p-surface-50) px-3 py-2 text-[12px] leading-5 text-(--p-text-muted-color)\"\n >\n {{ reason }}\n </div>\n }\n\n @if (\n !dataTransformIsMerge() && dataTransformUsesKnownActionEditor()\n ) {\n <div class=\"grid gap-3\">\n <div class=\"grid gap-3 md:grid-cols-[minmax(220px,0.45fr)_1fr]\">\n <mt-select-field\n [ngModel]=\"dataTransformSourcePreset()\"\n (ngModelChange)=\"onDataTransformSourcePresetChange($event)\"\n [label]=\"dataTransformSourceLabel()\"\n hint=\"Choose which list this action should transform.\"\n [required]=\"isConfigFieldRequired('sourceExpression')\"\n [options]=\"dataTransformSourceOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n @if (showDataTransformSourceExpression()) {\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"dataTransformSourceExpression()\"\n (focusin)=\"setExpressionTarget('config:sourceExpression')\"\n (ngModelChange)=\"\n onDataTransformSourceExpressionChange($event)\n \"\n [label]=\"dataTransformSourceExpressionLabel()\"\n hint=\"Must resolve to an array of items.\"\n [required]=\"isConfigFieldRequired('sourceExpression')\"\n />\n } @else {\n <div\n class=\"rounded-md border border-surface-200 bg-surface-50 px-3 py-2\"\n >\n <div class=\"fp-ae-label mb-1\">Selected input</div>\n <div\n class=\"truncate font-mono text-sm text-(--p-text-color)\"\n >\n {{ dataTransformSourceSummary() }}\n </div>\n </div>\n }\n </div>\n <div class=\"flex flex-wrap gap-2\">\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n icon=\"general.search-sm\"\n label=\"Preview source\"\n [disabled]=\"!dataTransformSourceExpression()\"\n (onClick)=\"previewDataTransformSourceExpression()\"\n />\n </div>\n </div>\n }\n\n @if (expressionPreview(); as preview) {\n <div\n class=\"mt-3 rounded-md border border-(--p-content-border-color) bg-(--p-surface-50) p-3\"\n >\n <div class=\"fp-ae-label mb-2\">Source preview</div>\n <pre class=\"fp-ae-code\">{{ schemaText(preview) }}</pre>\n </div>\n }\n\n @if (dataTransformIsFilter()) {\n <div class=\"mt-4 grid gap-3\">\n @if (dataTransformUsesStructuredConditionEditor()) {\n <div class=\"fp-ae-label\">Keep item when</div>\n <div class=\"grid gap-3 md:grid-cols-[1fr_180px_1fr]\">\n <mt-text-field\n [ngModel]=\"dataTransformConditionFieldText('left')\"\n (focusin)=\"setExpressionTarget('config:condition:left')\"\n (ngModelChange)=\"\n onDataTransformConditionFieldChange('left', $event)\n \"\n label=\"Left value\"\n hint=\"Field, value, or expression from each item.\"\n [required]=\"isConfigFieldRequired('condition.left')\"\n />\n <mt-select-field\n [ngModel]=\"\n dataTransformConditionFieldValue('operator') || 'equals'\n \"\n (ngModelChange)=\"\n onDataTransformConditionFieldChange('operator', $event)\n \"\n label=\"Operator\"\n [required]=\"isConfigFieldRequired('condition.operator')\"\n [options]=\"ifOperatorOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-text-field\n [ngModel]=\"dataTransformConditionFieldText('right')\"\n (focusin)=\"setExpressionTarget('config:condition:right')\"\n (ngModelChange)=\"\n onDataTransformConditionFieldChange('right', $event)\n \"\n label=\"Right value\"\n hint=\"Value or expression to compare against.\"\n [required]=\"isConfigFieldRequired('condition.right')\"\n />\n </div>\n } @else {\n <mt-textarea-field\n class=\"w-full font-mono\"\n [ngModel]=\"dataTransformConditionText()\"\n (focusin)=\"setExpressionTarget('config:condition')\"\n (ngModelChange)=\"onDataTransformConditionChange($event)\"\n label=\"Keep item when\"\n hint=\"Condition expression or JSON supplied by the backend catalog.\"\n [required]=\"isConfigFieldRequired('condition')\"\n rows=\"4\"\n />\n }\n </div>\n }\n\n @if (dataTransformIsSort()) {\n <div class=\"mt-4 grid gap-3 md:grid-cols-2\">\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"config()['sortBy'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:sortBy')\"\n (ngModelChange)=\"onConfigFieldChange('sortBy', $event)\"\n label=\"Sort by field\"\n hint=\"Field path inside each item, for example createdAt.\"\n [required]=\"isConfigFieldRequired('sortBy')\"\n />\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"config()['sortExpression'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:sortExpression')\"\n (ngModelChange)=\"\n onConfigFieldChange('sortExpression', $event)\n \"\n label=\"Custom sort expression\"\n hint=\"Optional expression when a simple field path is not enough.\"\n [required]=\"isConfigFieldRequired('sortExpression')\"\n />\n <mt-select-field\n [ngModel]=\"config()['direction'] ?? 'asc'\"\n (ngModelChange)=\"onConfigFieldChange('direction', $event)\"\n label=\"Direction\"\n [required]=\"isConfigFieldRequired('direction')\"\n [options]=\"dataTransformDirectionOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-select-field\n [ngModel]=\"config()['valueType'] ?? 'string'\"\n (ngModelChange)=\"onConfigFieldChange('valueType', $event)\"\n label=\"Value type\"\n [required]=\"isConfigFieldRequired('valueType')\"\n [options]=\"dataTransformValueTypeOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n </div>\n }\n\n @if (dataTransformIsLimit()) {\n <div class=\"mt-4 grid gap-3 md:grid-cols-2\">\n <mt-number-field\n [ngModel]=\"numberValue(config()['offset']) ?? 0\"\n (ngModelChange)=\"onConfigFieldChange('offset', $event)\"\n label=\"Skip first\"\n [required]=\"isConfigFieldRequired('offset')\"\n [min]=\"0\"\n />\n <mt-number-field\n [ngModel]=\"numberValue(config()['limit'])\"\n (ngModelChange)=\"onConfigFieldChange('limit', $event)\"\n label=\"Take at most\"\n [required]=\"isConfigFieldRequired('limit')\"\n [min]=\"1\"\n />\n </div>\n }\n\n @if (dataTransformIsMapFields()) {\n <div class=\"mt-4 grid gap-3\">\n <mt-select-field\n [ngModel]=\"config()['mode'] ?? 'merge'\"\n (ngModelChange)=\"onConfigFieldChange('mode', $event)\"\n label=\"Field mode\"\n [required]=\"isConfigFieldRequired('mode')\"\n [options]=\"dataTransformMapModeOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <ng-container\n *ngTemplateOutlet=\"\n mapEditor;\n context: {\n objectKey: 'fields',\n rows: fieldsRows(),\n keyLabel: 'Output field',\n valueLabel: 'Value or expression',\n addLabel: 'Add mapped field',\n emptyText:\n 'No fields mapped yet. Add an output field to create or update item data.',\n keyHint: 'Name of the field in the output item.',\n valueHint:\n 'Use a fixed value or an expression from each item.',\n required: isConfigFieldRequired('fields'),\n }\n \"\n />\n </div>\n }\n\n @if (dataTransformIsAggregate()) {\n <div class=\"mt-4 grid gap-3\">\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"config()['groupBy'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:groupBy')\"\n (ngModelChange)=\"onConfigFieldChange('groupBy', $event)\"\n label=\"Group items by\"\n hint=\"Optional field path or expression used to group results.\"\n [required]=\"isConfigFieldRequired('groupBy')\"\n />\n <div class=\"rounded-md border border-surface-200 p-3\">\n <div\n class=\"mb-3 flex flex-wrap items-center justify-between gap-2\"\n >\n <div class=\"fp-ae-label\">Summary fields</div>\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n icon=\"general.plus\"\n label=\"Add summary\"\n (onClick)=\"addDataTransformAggregation()\"\n />\n </div>\n <div class=\"space-y-3\">\n @for (\n item of dataTransformAggregationRows();\n track item.index\n ) {\n <div\n class=\"grid gap-2 rounded-md bg-surface-50 p-3 md:grid-cols-[minmax(160px,0.8fr)_minmax(180px,0.9fr)_1fr_minmax(150px,0.7fr)_auto]\"\n >\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"item.key\"\n (ngModelChange)=\"\n updateDataTransformAggregation(\n item.index,\n 'key',\n $event\n )\n \"\n label=\"Output field\"\n hint=\"Name saved in the summary output.\"\n [required]=\"isConfigFieldRequired('aggregations')\"\n />\n <mt-select-field\n [ngModel]=\"item.operation\"\n (ngModelChange)=\"\n updateDataTransformAggregation(\n item.index,\n 'operation',\n $event\n )\n \"\n label=\"Calculation\"\n [required]=\"isConfigFieldRequired('aggregations')\"\n [options]=\"dataTransformAggregationOperationOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"item.field\"\n [readonly]=\"\n !dataTransformAggregationNeedsField(item.operation)\n \"\n (focusin)=\"\n setExpressionTarget(\n 'config:aggregations:' + item.index + ':field'\n )\n \"\n (ngModelChange)=\"\n updateDataTransformAggregation(\n item.index,\n 'field',\n $event\n )\n \"\n label=\"Field or expression\"\n [hint]=\"\n dataTransformAggregationFieldHint(item.operation)\n \"\n [required]=\"\n isConfigFieldRequired('aggregations') &&\n dataTransformAggregationNeedsField(item.operation)\n \"\n />\n <mt-select-field\n [ngModel]=\"item.valueType\"\n (ngModelChange)=\"\n updateDataTransformAggregation(\n item.index,\n 'valueType',\n $event\n )\n \"\n label=\"Value type\"\n [required]=\"isConfigFieldRequired('aggregations')\"\n [options]=\"dataTransformValueTypeOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-button\n class=\"self-end\"\n size=\"small\"\n severity=\"secondary\"\n variant=\"outlined\"\n label=\"Remove\"\n (onClick)=\"removeDataTransformAggregation(item.index)\"\n />\n </div>\n } @empty {\n <div\n class=\"rounded-md bg-(--p-content-background) px-3 py-2 text-[12px] text-(--p-text-muted-color)\"\n >\n No summary fields yet. Add a summary such as count,\n total, average, minimum, or maximum.\n </div>\n }\n </div>\n </div>\n </div>\n }\n\n @if (dataTransformIsMerge()) {\n <div class=\"mt-4 grid gap-3 md:grid-cols-2\">\n <mt-textarea-field\n class=\"w-full font-mono md:col-span-2\"\n [ngModel]=\"config()['leftExpression'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:leftExpression')\"\n (ngModelChange)=\"\n onConfigFieldChange('leftExpression', $event)\n \"\n label=\"First list\"\n hint=\"Expression that resolves to the first list of items.\"\n [required]=\"isConfigFieldRequired('leftExpression')\"\n rows=\"3\"\n />\n <mt-textarea-field\n class=\"w-full font-mono md:col-span-2\"\n [ngModel]=\"config()['rightExpression'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:rightExpression')\"\n (ngModelChange)=\"\n onConfigFieldChange('rightExpression', $event)\n \"\n label=\"Second list\"\n hint=\"Expression that resolves to the second list of items.\"\n [required]=\"isConfigFieldRequired('rightExpression')\"\n rows=\"3\"\n />\n <mt-select-field\n [ngModel]=\"config()['strategy'] ?? 'append'\"\n (ngModelChange)=\"onConfigFieldChange('strategy', $event)\"\n label=\"Merge strategy\"\n [required]=\"isConfigFieldRequired('strategy')\"\n [options]=\"dataTransformMergeStrategyOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n @if (dataTransformUsesKeyedMerge()) {\n <div class=\"grid gap-3 md:grid-cols-2\">\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"config()['leftKey'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('leftKey', $event)\"\n label=\"First list key\"\n [required]=\"isConfigFieldRequired('leftKey')\"\n />\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"config()['rightKey'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('rightKey', $event)\"\n label=\"Second list key\"\n [required]=\"isConfigFieldRequired('rightKey')\"\n />\n </div>\n }\n </div>\n }\n\n @if (!dataTransformUsesKnownActionEditor()) {\n <div class=\"mt-4 grid gap-3\">\n @for (field of dataTransformGenericFields(); track field.key) {\n @if (field.enumValues.length) {\n <mt-select-field\n [ngModel]=\"config()[field.key] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange(field.key, $event)\"\n [label]=\"field.label\"\n [hint]=\"field.description ?? ''\"\n [required]=\"isConfigFieldRequired(field.key)\"\n [options]=\"enumOptions(field.enumValues)\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n } @else if (field.type === \"number\") {\n <mt-number-field\n [ngModel]=\"numberValue(config()[field.key])\"\n (ngModelChange)=\"onConfigFieldChange(field.key, $event)\"\n [label]=\"field.label\"\n [hint]=\"field.description ?? ''\"\n [required]=\"isConfigFieldRequired(field.key)\"\n />\n } @else {\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"fieldText(field.key)\"\n (focusin)=\"setExpressionTarget('config:' + field.key)\"\n (ngModelChange)=\"onConfigFieldChange(field.key, $event)\"\n [label]=\"field.label\"\n [hint]=\"field.description ?? ''\"\n [required]=\"isConfigFieldRequired(field.key)\"\n />\n }\n } @empty {\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"dataTransformActionKey()\"\n [readonly]=\"true\"\n label=\"Action\"\n />\n }\n </div>\n }\n\n <div class=\"mt-4 grid gap-3 md:grid-cols-3\">\n <mt-number-field\n [ngModel]=\"numberValue(config()['maxOutputItems'])\"\n (ngModelChange)=\"onConfigFieldChange('maxOutputItems', $event)\"\n label=\"Output limit\"\n [required]=\"isConfigFieldRequired('maxOutputItems')\"\n [min]=\"1\"\n />\n <div\n class=\"rounded-md border border-(--p-content-border-color) bg-(--p-surface-50) p-3 md:col-span-2\"\n >\n <div class=\"fp-ae-label mb-2\">Output</div>\n <div class=\"flex flex-wrap gap-1.5\">\n @for (field of dataTransformOutputFieldLabels; track field) {\n <span\n class=\"rounded-md bg-(--p-surface-100) px-2 py-1 font-mono text-[11px]\"\n >{{ field }}</span\n >\n }\n </div>\n </div>\n </div>\n </section>\n }\n }\n @case (\"LoopOverItems\") {\n @if (sectionInMain(\"loopOverItems\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Loop over items</div>\n\n <div class=\"grid gap-3\">\n <div class=\"grid gap-3 md:grid-cols-[minmax(220px,0.45fr)_1fr]\">\n <mt-select-field\n [ngModel]=\"loopSourceMode()\"\n (ngModelChange)=\"onLoopSourceModeChange($event)\"\n label=\"List source\"\n hint=\"Choose an output array or type an array expression.\"\n [required]=\"isConfigFieldRequired('itemsExpression')\"\n [options]=\"loopSourceModeOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n @if (showLoopItemsExpression()) {\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"loopItemsExpression()\"\n (focusin)=\"setExpressionTarget('config:itemsExpression')\"\n (ngModelChange)=\"onLoopItemsExpressionChange($event)\"\n label=\"Array expression\"\n hint=\"Must resolve to an array.\"\n [required]=\"isConfigFieldRequired('itemsExpression')\"\n />\n } @else {\n <mt-select-field\n [ngModel]=\"loopSelectedOutputArray()\"\n (ngModelChange)=\"onLoopOutputArrayChange($event)\"\n label=\"Output array\"\n hint=\"Arrays exposed by previous connected steps.\"\n [required]=\"isConfigFieldRequired('itemsExpression')\"\n [options]=\"loopOutputArrayOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n }\n </div>\n <div class=\"flex flex-wrap gap-2\">\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n icon=\"general.search-sm\"\n label=\"Preview array\"\n [disabled]=\"!loopItemsExpression()\"\n (onClick)=\"previewLoopItemsExpression()\"\n />\n </div>\n </div>\n\n @if (expressionPreview(); as preview) {\n <div\n class=\"mt-3 rounded-md border border-(--p-content-border-color) bg-(--p-surface-50) p-3\"\n >\n <div class=\"fp-ae-label mb-2\">Expression preview</div>\n <pre class=\"fp-ae-code\">{{ schemaText(preview) }}</pre>\n </div>\n }\n\n <div class=\"mt-4 border-t border-surface-100 pt-4\">\n <div class=\"fp-ae-label mb-3\">Run settings</div>\n <div class=\"grid gap-3 md:grid-cols-2 xl:grid-cols-4\">\n <mt-number-field\n [ngModel]=\"\n numberValue(\n config()['maxItems'] ?? config()['maxIterations']\n )\n \"\n (ngModelChange)=\"onConfigFieldChange('maxItems', $event)\"\n label=\"Item limit\"\n hint=\"Optional maximum number of items to process.\"\n [required]=\"isConfigFieldRequired('maxItems')\"\n [min]=\"1\"\n />\n <mt-select-field\n [ngModel]=\"loopFailureBehavior()\"\n (ngModelChange)=\"onLoopFailureBehaviorChange($event)\"\n label=\"When an item fails\"\n [required]=\"isConfigFieldRequired('failureBehavior')\"\n [options]=\"loopFailureBehaviorOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-select-field\n [ngModel]=\"config()['emptyBehavior'] ?? 'done'\"\n (ngModelChange)=\"onConfigFieldChange('emptyBehavior', $event)\"\n label=\"When list is empty\"\n [required]=\"isConfigFieldRequired('emptyBehavior')\"\n [options]=\"loopEmptyBehaviorOptions\"\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()['collectResults'] === true\"\n (ngModelChange)=\"\n onConfigFieldChange('collectResults', $event === true)\n \"\n label=\"Save item results\"\n hint=\"Stores a bounded summary for the done route.\"\n />\n </div>\n </div>\n\n <details class=\"mt-4 border-t border-surface-100 pt-3\">\n <summary\n class=\"cursor-pointer text-sm font-semibold text-(--p-text-color)\"\n >\n Advanced runtime details\n </summary>\n <div class=\"mt-3 grid gap-3 md:grid-cols-3\">\n <div>\n <div class=\"fp-ae-label mb-1\">Current item token</div>\n <div\n class=\"rounded-md border border-surface-200 bg-surface-50 px-3 py-2 font-mono text-sm text-(--p-text-color)\"\n >\n $item.json\n </div>\n </div>\n <div>\n <div class=\"fp-ae-label mb-1\">Loop context token</div>\n <div\n class=\"rounded-md border border-surface-200 bg-surface-50 px-3 py-2 font-mono text-sm text-(--p-text-color)\"\n >\n $loop.current\n </div>\n </div>\n <mt-number-field\n [ngModel]=\"numberValue(config()['concurrency']) ?? 1\"\n (ngModelChange)=\"onLoopConcurrencyChange($event)\"\n label=\"Concurrency\"\n hint=\"This deployment supports 1.\"\n [required]=\"isConfigFieldRequired('concurrency')\"\n [min]=\"1\"\n [max]=\"1\"\n />\n </div>\n </details>\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\n does 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 [required]=\"isConfigFieldRequired('targetModule')\"\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 [required]=\"isConfigFieldRequired('operation')\"\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 [required]=\"isConfigFieldRequired('idempotencyKey')\"\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\n >{{ field.viewType ?? \"Value\" }}\n @if (field.required) {\n *\n }\n </strong>\n </div>\n }\n </div>\n </div>\n }\n <div class=\"mt-3\">\n <ng-container\n *ngTemplateOutlet=\"\n mapEditor;\n context: {\n objectKey: 'mapping',\n rows: mappingRows(),\n keyLabel: 'Module field',\n valueLabel: 'Expression / value',\n addLabel: 'Add module field',\n required: isConfigFieldRequired('mapping'),\n }\n \"\n />\n </div>\n <div class=\"mt-3 flex gap-2\">\n <mt-button\n size=\"small\"\n severity=\"primary\"\n label=\"Validate mapping\"\n (onClick)=\"validateCommitMapping()\"\n />\n </div>\n @if (commitValidation(); as result) {\n <div\n class=\"mt-3 rounded-lg bg-(--p-surface-100) px-3 py-2 text-[12px]\"\n >\n {{\n result.isValid === false\n ? \"Mapping invalid\"\n : \"Mapping accepted by helper\"\n }}\n @for (issue of resultIssues(result); track $index) {\n <div class=\"mt-1 text-[11px] text-(--p-text-muted-color)\">\n {{ issue }}\n </div>\n }\n </div>\n }\n @if (sectionInMain(\"credentials\")) {\n <div\n class=\"mt-3 rounded-md border border-surface-200 bg-surface-0 p-3\"\n >\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 [required]=\"isConfigFieldRequired('statusCode')\"\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 [required]=\"isConfigFieldRequired('responseMode')\"\n [options]=\"responseModeOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n </div>\n <div class=\"mt-3\">\n <ng-container\n *ngTemplateOutlet=\"\n mapEditor;\n context: {\n objectKey: 'headers',\n rows: headerRows(),\n keyLabel: 'Header',\n valueLabel: 'Value',\n addLabel: 'Add header',\n }\n \"\n />\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 [required]=\"isConfigFieldRequired('body')\"\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 [required]=\"isConfigFieldRequired('targetAutomationId')\"\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 [required]=\"isConfigFieldRequired('revisionMode')\"\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)=\"\n onConfigFieldChange('specificRevisionId', $event)\n \"\n label=\"Specific revision id\"\n hint=\"Required only when revision mode is Specific revision.\"\n [required]=\"isConfigFieldRequired('specificRevisionId')\"\n />\n }\n <mt-toggle-field\n size=\"small\"\n labelPosition=\"end\"\n [ngModel]=\"config()['waitForCompletion'] !== false\"\n (ngModelChange)=\"\n onConfigFieldChange('waitForCompletion', $event === true)\n \"\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)=\"\n onConfigFieldChange('inputMappingJson', $event)\n \"\n label=\"Input mapping JSON\"\n hint=\"JSON object or expression map sent into the child automation input.\"\n [required]=\"isConfigFieldRequired('inputMappingJson')\"\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)=\"\n onConfigFieldChange('outputMappingJson', $event)\n \"\n label=\"Output mapping JSON\"\n hint=\"JSON object or expression map for child output/status returned to the parent.\"\n [required]=\"isConfigFieldRequired('outputMappingJson')\"\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 [required]=\"isConfigFieldRequired('targetAutomationId')\"\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 [required]=\"isConfigFieldRequired('revisionMode')\"\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)=\"\n onConfigFieldChange('specificRevisionId', $event)\n \"\n label=\"Specific revision id\"\n hint=\"Required only when revision mode is Specific revision.\"\n [required]=\"isConfigFieldRequired('specificRevisionId')\"\n />\n }\n <mt-toggle-field\n size=\"small\"\n labelPosition=\"end\"\n [ngModel]=\"config()['waitForCompletion'] !== false\"\n (ngModelChange)=\"\n onConfigFieldChange('waitForCompletion', $event === true)\n \"\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)=\"\n onConfigFieldChange('inputMappingJson', $event)\n \"\n label=\"Input mapping JSON\"\n hint=\"JSON object or expression map sent into the child automation input.\"\n [required]=\"isConfigFieldRequired('inputMappingJson')\"\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)=\"\n onConfigFieldChange('outputMappingJson', $event)\n \"\n label=\"Output mapping JSON\"\n hint=\"JSON object or expression map for child output/status returned to the parent.\"\n [required]=\"isConfigFieldRequired('outputMappingJson')\"\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\n class=\"flex items-center justify-between gap-3 border-b border-surface-200 bg-surface-50 px-4 py-3\"\n >\n <div class=\"fp-ae-section-title !m-0\">Parallel start</div>\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n icon=\"general.plus\"\n label=\"Add branch\"\n (onClick)=\"addParallelBranch()\"\n />\n </div>\n <div class=\"space-y-3 p-4\">\n @for (\n branch of parallelBranches();\n track branch.key;\n let i = $index\n ) {\n <div\n class=\"rounded-md border border-surface-200 bg-surface-0 p-3\"\n >\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 [required]=\"isConfigFieldRequired('branches')\"\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 [required]=\"isConfigFieldRequired('branches')\"\n />\n <mt-text-field\n [ngModel]=\"branch.description\"\n (ngModelChange)=\"\n updateParallelBranch(i, 'description', $event)\n \"\n label=\"Description\"\n hint=\"Optional branch note for reviewers.\"\n [required]=\"false\"\n />\n <div class=\"flex items-end gap-1\">\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n icon=\"arrow.arrow-up\"\n tooltip=\"Move up\"\n (onClick)=\"moveParallelBranch(i, -1)\"\n />\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n icon=\"arrow.arrow-down\"\n tooltip=\"Move down\"\n (onClick)=\"moveParallelBranch(i, 1)\"\n />\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n severity=\"danger\"\n icon=\"general.trash-01\"\n tooltip=\"Remove branch\"\n (onClick)=\"removeParallelBranch(i)\"\n />\n </div>\n </div>\n <div\n class=\"mt-3 flex flex-wrap items-center gap-1.5 text-[11px]\"\n >\n <span\n 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)\"\n >{{ branch.key }}</span\n >\n <span\n 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)\"\n >{{ branch.routeCount }} connected route{{\n branch.routeCount === 1 ? \"\" : \"s\"\n }}</span\n >\n </div>\n </div>\n }\n @if (parallelBranches().length === 0) {\n <p class=\"fp-ae-copy\">\n Add at least two stable branch keys. Backend validation blocks\n publish until branches are valid.\n </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 [required]=\"isConfigFieldRequired('joinPolicy')\"\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 [required]=\"isConfigFieldRequired('threshold')\"\n [min]=\"1\"\n />\n }\n <mt-select-field\n [ngModel]=\"config()['aggregationStrategy'] ?? 'MergeObjects'\"\n (ngModelChange)=\"\n onConfigFieldChange('aggregationStrategy', $event)\n \"\n label=\"Aggregation strategy\"\n hint=\"How backend joins branch outputs into the join node output.\"\n [required]=\"isConfigFieldRequired('aggregationStrategy')\"\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)=\"\n onConfigFieldChange('outputTargetPath', $event)\n \"\n label=\"Output target path\"\n hint=\"Optional context path where joined output should be written.\"\n [required]=\"isConfigFieldRequired('outputTargetPath')\"\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\n class=\"grid items-end gap-3 md:grid-cols-[minmax(0,1fr)_minmax(280px,0.7fr)]\"\n >\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 [required]=\"isConfigFieldRequired('mode')\"\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)=\"\n onConfigFieldChange('firstMatch', $event === true)\n \"\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]=\"\n showSwitchExpression()\n ? 'font-mono'\n : 'font-mono md:col-span-2'\n \"\n [ngModel]=\"config()['sourceValue'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:sourceValue')\"\n (ngModelChange)=\"\n onConfigFieldChange('sourceValue', $event)\n \"\n label=\"Source value\"\n hint=\"Value or expression used for value matching.\"\n [required]=\"isConfigFieldRequired('sourceValue')\"\n />\n }\n @if (showSwitchExpression()) {\n <mt-text-field\n [class]=\"\n showSwitchSourceValue()\n ? 'font-mono'\n : 'font-mono md:col-span-2'\n \"\n [ngModel]=\"config()['expression'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:expression')\"\n (ngModelChange)=\"\n onConfigFieldChange('expression', $event)\n \"\n label=\"Expression\"\n hint=\"Expression evaluated when mode is expression or rules.\"\n [required]=\"isConfigFieldRequired('expression')\"\n />\n }\n </div>\n }\n <div\n class=\"grid items-end gap-3 md:grid-cols-[minmax(0,1fr)_minmax(280px,0.7fr)]\"\n >\n @if (showSwitchDefaultOutputKey()) {\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"config()['defaultOutputKey'] ?? 'default'\"\n (ngModelChange)=\"\n onConfigFieldChange('defaultOutputKey', $event)\n \"\n label=\"Default output key\"\n hint=\"Stable default route key used when no case matches.\"\n [required]=\"isConfigFieldRequired('defaultOutputKey')\"\n />\n }\n <mt-toggle-field\n [class]=\"\n showSwitchDefaultOutputKey()\n ? 'self-end pb-1'\n : 'self-end pb-1 md:col-span-2'\n \"\n size=\"small\"\n labelPosition=\"end\"\n [ngModel]=\"config()['includeDefaultOutput'] !== false\"\n (ngModelChange)=\"\n onConfigFieldChange('includeDefaultOutput', $event === true)\n \"\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\n class=\"flex items-center justify-between gap-3 border-b border-surface-200 bg-surface-50 px-4 py-3\"\n >\n <div class=\"fp-ae-section-title !m-0\">Switch cases</div>\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n icon=\"general.plus\"\n label=\"Add case\"\n (onClick)=\"addSwitchCase()\"\n />\n </div>\n <div class=\"space-y-3 p-4\">\n @for (item of switchCases(); track item.key; let i = $index) {\n <div\n class=\"rounded-md border border-surface-200 bg-surface-0 p-3\"\n >\n <div\n class=\"flex flex-wrap items-start justify-between gap-3 border-b border-surface-100 pb-3\"\n >\n <div class=\"min-w-0\">\n <div class=\"flex flex-wrap items-center gap-1.5\">\n <span\n class=\"text-sm font-semibold text-(--p-text-color)\"\n >Case {{ i + 1 }}</span\n >\n <span\n class=\"inline-flex h-7 items-center rounded-md border border-surface-200 bg-surface-50 px-2 font-mono text-[11px] font-semibold text-(--p-text-color)\"\n >{{ item.routeKey }}</span\n >\n <span\n class=\"inline-flex h-7 items-center rounded-md border border-surface-200 bg-surface-0 px-2 text-[11px] font-medium text-(--p-text-muted-color)\"\n >{{ item.routeCount }} connected route{{\n item.routeCount === 1 ? \"\" : \"s\"\n }}</span\n >\n </div>\n <div\n class=\"mt-1 truncate text-xs font-medium text-(--p-text-muted-color)\"\n >\n {{ item.label || item.routeKey }}\n </div>\n </div>\n <div class=\"flex shrink-0 items-center gap-1\">\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n icon=\"arrow.arrow-up\"\n tooltip=\"Move up\"\n (onClick)=\"moveSwitchCase(i, -1)\"\n />\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n icon=\"arrow.arrow-down\"\n tooltip=\"Move down\"\n (onClick)=\"moveSwitchCase(i, 1)\"\n />\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n severity=\"danger\"\n icon=\"general.trash-01\"\n tooltip=\"Remove case\"\n (onClick)=\"removeSwitchCase(i)\"\n />\n </div>\n </div>\n <div class=\"mt-3 grid gap-3 md:grid-cols-2\">\n <mt-text-field\n [ngModel]=\"item.label\"\n (ngModelChange)=\"updateSwitchCase(i, 'label', $event)\"\n label=\"Case name\"\n hint=\"Shown in route labels.\"\n [required]=\"isConfigFieldRequired('cases')\"\n />\n @if (showSwitchCaseValue()) {\n <mt-text-field\n [ngModel]=\"item.value\"\n (focusin)=\"\n setExpressionTarget(\n 'config:cases:' + item.key + ':value'\n )\n \"\n (ngModelChange)=\"updateSwitchCase(i, 'value', $event)\"\n label=\"Match value\"\n hint=\"Compared with the source value.\"\n [required]=\"isConfigFieldRequired('cases')\"\n />\n }\n @if (showSwitchCaseCondition()) {\n <mt-text-field\n class=\"font-mono md:col-span-2\"\n [ngModel]=\"item.condition\"\n (focusin)=\"\n setExpressionTarget(\n 'config:cases:' + item.key + ':condition'\n )\n \"\n (ngModelChange)=\"\n updateSwitchCase(i, 'condition', $event)\n \"\n label=\"Rule condition\"\n hint=\"When true, this case is selected.\"\n [required]=\"isConfigFieldRequired('cases')\"\n />\n }\n </div>\n <details\n class=\"mt-3 rounded-md border border-surface-200 bg-surface-50 px-3 py-2 text-xs text-(--p-text-muted-color)\"\n >\n <summary class=\"cursor-pointer font-medium\">\n Advanced route settings\n </summary>\n <div class=\"mt-3 grid gap-3 md:grid-cols-2\">\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"item.key\"\n (ngModelChange)=\"updateSwitchCase(i, 'key', $event)\"\n label=\"Route key\"\n hint=\"Stable output key for connected routes.\"\n [required]=\"isConfigFieldRequired('cases')\"\n />\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"item.expression\"\n (focusin)=\"\n setExpressionTarget(\n 'config:cases:' + item.key + ':expression'\n )\n \"\n (ngModelChange)=\"\n updateSwitchCase(i, 'expression', $event)\n \"\n label=\"Advanced condition expression\"\n hint=\"Optional true or false expression for this case.\"\n [required]=\"false\"\n />\n </div>\n <div\n class=\"mt-3 inline-flex h-7 items-center rounded-md border border-surface-200 bg-surface-0 px-2 font-medium\"\n >\n Visual order {{ i + 1 }}\n </div>\n </details>\n </div>\n }\n @if (switchCases().length === 0) {\n <p class=\"fp-ae-copy\">\n Add a case for each route this switch can select.\n </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 (\n supportsConfigKey(\"status\") ||\n supportsConfigKey(\"message\") ||\n supportsConfigKey(\"output\")\n ) {\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 Stop nodes use their backend defaults and do not expose extra\n configuration.\n </p>\n }\n </section>\n }\n }\n }\n\n @if (sectionInAdvanced(\"policy\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">\n Retry, timeout, and failure handling\n </div>\n <div class=\"grid gap-3 md:grid-cols-3\">\n <mt-number-field\n [ngModel]=\"policyNumberValue('timeoutPolicyJson', 'timeoutSeconds')\"\n (ngModelChange)=\"\n updatePolicyNumber('timeoutPolicyJson', 'timeoutSeconds', $event)\n \"\n label=\"Step timeout (seconds)\"\n hint=\"How long this step may run before Automation Engine marks it timed out.\"\n [min]=\"1\"\n />\n <mt-number-field\n [ngModel]=\"policyNumberValue('retryPolicyJson', 'maxAttempts')\"\n (ngModelChange)=\"\n updatePolicyNumber('retryPolicyJson', 'maxAttempts', $event)\n \"\n label=\"Max attempts\"\n hint=\"Total tries including the first run. Use 1 for no retry.\"\n [min]=\"1\"\n />\n <mt-select-field\n [ngModel]=\"errorPolicyMode()\"\n (ngModelChange)=\"updateErrorPolicyMode($event)\"\n label=\"When this step fails\"\n hint=\"Choose whether failures pause for recovery, use the failure path, or fail the workflow.\"\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 count, or\n failure rule here.\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(\"mapping\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Custom data handoff</div>\n <div class=\"grid gap-3 md:grid-cols-2\">\n <div>\n <div class=\"fp-ae-label mb-2\">Inputs sent to this step</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)\">{{\n row.key\n }}</span>\n <mt-text-field\n [ngModel]=\"valueText(row.value)\"\n (focusin)=\"\n setExpressionTarget('config:inputMapping:' + row.key)\n \"\n (ngModelChange)=\"\n updateJsonField('inputMappingJson', row.key, $event)\n \"\n hint=\"Value this step receives from the workflow context.\"\n />\n </label>\n }\n </div>\n <div>\n <div class=\"fp-ae-label mb-2\">Outputs saved for next steps</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)\">{{\n row.key\n }}</span>\n <mt-text-field\n [ngModel]=\"valueText(row.value)\"\n (focusin)=\"\n setExpressionTarget('config:outputMapping:' + row.key)\n \"\n (ngModelChange)=\"\n updateJsonField('outputMappingJson', row.key, $event)\n \"\n hint=\"Value this step exposes to the rest of the workflow.\"\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 Default handoff is active. This step receives the current workflow\n item and exposes its normal output to the next connected step. Add a\n custom handoff only when the business data must be reshaped.\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.\n Triggers 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\">\n Config schema\n </summary>\n <pre class=\"fp-ae-code\">{{ schemaText(configSchema()) }}</pre>\n </details>\n @if (hasTriggerPayloadSchema() && triggerPayloadSchema(); as schema) {\n <details\n class=\"rounded-lg border border-(--p-content-border-color)\"\n >\n <summary\n class=\"cursor-pointer px-3 py-2 text-[12px] font-semibold\"\n >\n Payload schema\n </summary>\n <pre class=\"fp-ae-code\">{{ schemaText(schema) }}</pre>\n </details>\n }\n @if (authPolicySchema()) {\n <details\n class=\"rounded-lg border border-(--p-content-border-color)\"\n >\n <summary\n class=\"cursor-pointer px-3 py-2 text-[12px] font-semibold\"\n >\n Auth policy schema\n </summary>\n <pre class=\"fp-ae-code\">{{ schemaText(authPolicySchema()) }}</pre>\n </details>\n }\n @if (triggerPayloadSample(); as sample) {\n <details\n class=\"rounded-lg border border-(--p-content-border-color)\"\n >\n <summary\n class=\"cursor-pointer px-3 py-2 text-[12px] font-semibold\"\n >\n Payload or request sample\n </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\">\n Config schema\n </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\">\n Input schema\n </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\">\n Output schema\n </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\n class=\"rounded-md bg-(--p-surface-100) px-2 py-1 font-mono text-[11px]\"\n >{{ key }}</span\n >\n }\n @if (routeOutputKeys().length === 0) {\n <span class=\"text-[12px] text-(--p-text-muted-color)\"\n >No outgoing route keys</span\n >\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]=\"\n field.type === 'object' || field.type === 'array'\n \"\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\"\n [hint]=\"field.description ?? ''\"\n [required]=\"isConfigFieldRequired(field.key)\"\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)=\"\n onConfigFieldChange(field.key, $event === true)\n \"\n [label]=\"field.label\"\n [hint]=\"field.description ?? ''\"\n [required]=\"isConfigFieldRequired(field.key)\"\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\"\n [hint]=\"field.description ?? ''\"\n [required]=\"isConfigFieldRequired(field.key)\"\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\"\n [hint]=\"field.description ?? ''\"\n [required]=\"isConfigFieldRequired(field.key)\"\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\"\n [hint]=\"field.description ?? ''\"\n [required]=\"isConfigFieldRequired(field.key)\"\n rows=\"5\"\n />\n } @else {\n <mt-text-field\n [ngModel]=\"fieldText(field.key)\"\n (focusin)=\"\n field.expressionEnabled &&\n setExpressionTarget('config:' + field.key)\n \"\n (ngModelChange)=\"onConfigFieldChange(field.key, $event)\"\n [label]=\"field.label\"\n [hint]=\"field.description ?? ''\"\n [required]=\"isConfigFieldRequired(field.key)\"\n />\n }\n </div>\n }\n </div>\n </details>\n }\n </div>\n</div>\n\n<ng-template #credentialSelector>\n @if (activeCredentialProvider(); as provider) {\n <div\n class=\"mb-4 rounded-lg border border-(--p-content-border-color) bg-(--p-content-background) p-4 shadow-[0_1px_2px_rgba(15,23,42,0.05)]\"\n >\n <div class=\"mb-2 text-[12px] font-bold text-(--p-text-color)\">\n Credential\n </div>\n <div class=\"flex flex-wrap items-center gap-2\">\n @if (provider.connectAvailable) {\n <button\n type=\"button\"\n class=\"inline-flex min-h-10 max-w-full items-center gap-2 rounded-md border border-(--p-content-border-color) bg-(--p-content-background) px-3 py-2 text-[13px] font-bold text-(--p-text-color) shadow-sm transition-[border-color,box-shadow,background] hover:border-(--p-primary-color) hover:shadow-[0_2px_7px_rgba(15,23,42,0.10)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--p-primary-color)\"\n [disabled]=\"credentialOauthState() === 'loading'\"\n [attr.aria-busy]=\"\n credentialOauthState() === 'loading' ? 'true' : null\n \"\n (click)=\"connectCredentialProvider()\"\n >\n <fp-automation-node-icon\n class=\"text-[22px]\"\n [itemKey]=\"provider.providerKey\"\n [displayName]=\"provider.displayName\"\n [metadata]=\"credentialProviderIconMetadata(provider)\"\n />\n <span class=\"truncate\">{{ credentialConnectLabel() }}</span>\n </button>\n @if (provider.manualSetupAvailable) {\n <span class=\"text-[12px] text-(--p-text-muted-color)\">or</span>\n <button\n type=\"button\"\n class=\"rounded-md px-1 py-1 text-[12px] font-semibold text-(--p-text-color) underline decoration-(--p-text-muted-color) underline-offset-4 transition-colors hover:text-(--p-primary-color) focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--p-primary-color)\"\n (click)=\"showManualCredentialSetup()\"\n >\n setup manually\n </button>\n }\n } @else if (provider.manualSetupAvailable) {\n <button\n type=\"button\"\n class=\"inline-flex min-h-10 max-w-full items-center gap-2 rounded-md border border-(--p-content-border-color) bg-(--p-content-background) px-3 py-2 text-[13px] font-bold text-(--p-text-color) shadow-sm transition-[border-color,box-shadow,background] hover:border-(--p-primary-color) hover:shadow-[0_2px_7px_rgba(15,23,42,0.10)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--p-primary-color)\"\n (click)=\"showManualCredentialSetup()\"\n >\n <fp-automation-node-icon\n class=\"text-[22px]\"\n [itemKey]=\"provider.providerKey\"\n [displayName]=\"provider.displayName\"\n [metadata]=\"credentialProviderIconMetadata(provider)\"\n />\n <span class=\"truncate\"\n >Set up\n {{ provider.displayName || humanReviewProviderLabel() }}\n credential</span\n >\n </button>\n }\n </div>\n <div\n class=\"mt-2 flex flex-wrap items-center gap-1.5 text-[11px] text-(--p-text-muted-color)\"\n >\n @if (provider.authModes?.length) {\n @for (mode of provider.authModes; track mode) {\n <span\n class=\"rounded-full bg-(--p-surface-100) px-2 py-0.5 font-semibold\"\n >\n {{ mode }}\n </span>\n }\n }\n @if (provider.requiredScopes?.length) {\n <span class=\"truncate\">\n Scopes: {{ provider.requiredScopes.join(\", \") }}\n </span>\n }\n </div>\n @if (provider.setupInstructions) {\n <p class=\"m-0 mt-2 text-[11.5px] leading-5 text-(--p-text-muted-color)\">\n {{ provider.setupInstructions }}\n </p>\n }\n </div>\n }\n\n @if (manualCredentialOpen()) {\n <div\n class=\"mb-3 rounded-md border border-(--p-content-border-color) bg-(--p-content-background) p-3\"\n >\n <div class=\"grid gap-3 md:grid-cols-2\">\n <mt-text-field\n [ngModel]=\"manualCredentialDisplayName()\"\n (ngModelChange)=\"onManualCredentialDisplayNameChange($event)\"\n label=\"Credential name\"\n />\n <mt-select-field\n [ngModel]=\"manualCredentialType()\"\n (ngModelChange)=\"onManualCredentialTypeChange($event)\"\n label=\"Credential type\"\n [options]=\"credentialTypeOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n </div>\n <div class=\"mt-3 grid gap-3 md:grid-cols-2\">\n @for (field of manualCredentialFields(); track field.key) {\n @if (manualCredentialFieldIsToggle(field)) {\n <mt-toggle-field\n [ngModel]=\"manualCredentialFieldValue(field.key) === 'true'\"\n (ngModelChange)=\"onManualCredentialFieldChange(field.key, $event)\"\n [label]=\"\n manualCredentialFieldLabel(field) + (field.required ? ' *' : '')\n \"\n size=\"small\"\n />\n } @else {\n <mt-text-field\n [ngModel]=\"manualCredentialFieldValue(field.key)\"\n (ngModelChange)=\"onManualCredentialFieldChange(field.key, $event)\"\n [label]=\"\n manualCredentialFieldLabel(field) + (field.required ? ' *' : '')\n \"\n [placeholder]=\"field.placeholder ?? ''\"\n [type]=\"manualCredentialFieldInputType(field)\"\n />\n }\n }\n </div>\n @if (credentialManualError(); as error) {\n <div\n class=\"mt-3 rounded-md border border-(--p-content-border-color) bg-(--p-surface-50) px-3 py-2 text-[12px] leading-5 text-(--p-text-muted-color)\"\n >\n {{ error }}\n </div>\n }\n <div class=\"mt-3 flex justify-end gap-2\">\n <mt-button\n size=\"small\"\n variant=\"text\"\n label=\"Cancel\"\n (onClick)=\"cancelManualCredentialSetup()\"\n />\n <mt-button\n size=\"small\"\n severity=\"primary\"\n label=\"Save credential\"\n [loading]=\"credentialManualState() === 'loading'\"\n [disabled]=\"credentialManualState() === 'loading'\"\n (onClick)=\"createManualCredential()\"\n />\n </div>\n </div>\n }\n\n @if (credentialOptions().length) {\n <div class=\"space-y-2\">\n <mt-select-field\n [ngModel]=\"selectedCredentialRef()\"\n (ngModelChange)=\"selectCredential($event)\"\n label=\"Saved credential\"\n [options]=\"credentialOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n [showClear]=\"true\"\n />\n @for (credential of credentials(); track credential.credentialRef) {\n @if (credential.credentialRef === selectedCredentialRef()) {\n <div\n class=\"rounded-md border border-(--p-content-border-color) bg-(--p-surface-50) px-3 py-2\"\n >\n <div class=\"flex items-center justify-between gap-3\">\n <div class=\"min-w-0\">\n <div class=\"truncate text-[12px] font-semibold\">\n {{ credential.displayName ?? credential.credentialRef }}\n </div>\n <div\n class=\"truncate font-mono text-[11px] text-(--p-text-muted-color)\"\n >\n {{ credential.credentialRef }} /\n {{\n credential.status ??\n (credential.resolved ? \"Resolved\" : \"Unresolved\")\n }}\n </div>\n </div>\n <div class=\"flex flex-none items-center gap-2\">\n <mt-button\n size=\"small\"\n variant=\"text\"\n label=\"Test\"\n [loading]=\"\n credentialTestingRef() === credential.credentialRef\n \"\n [disabled]=\"\n credentialTestingRef() === credential.credentialRef\n \"\n (onClick)=\"testCredential(credential.credentialRef)\"\n />\n @if (activeCredentialProvider()?.connectAvailable) {\n <mt-button\n size=\"small\"\n variant=\"text\"\n label=\"Reconnect\"\n [disabled]=\"credentialOauthState() === 'loading'\"\n (onClick)=\"connectCredentialProvider()\"\n />\n }\n <mt-button\n size=\"small\"\n variant=\"text\"\n severity=\"danger\"\n label=\"Revoke\"\n [loading]=\"credentialRevokeRef() === credential.credentialRef\"\n [disabled]=\"\n credentialRevokeRef() === credential.credentialRef\n \"\n (onClick)=\"revokeCredential(credential.credentialRef)\"\n />\n </div>\n </div>\n @if (credential.maskedFields; as maskedFields) {\n <div\n class=\"mt-2 flex flex-wrap gap-1.5 text-[11px] text-(--p-text-muted-color)\"\n >\n @for (field of maskedFields | keyvalue; track field.key) {\n <span\n class=\"rounded-md bg-(--p-content-background) px-2 py-1 font-mono\"\n >\n {{ field.key }}: {{ field.value }}\n </span>\n }\n </div>\n }\n </div>\n }\n }\n </div>\n } @else if (sectionHasAdvancedEmptyState(\"credentials\")) {\n <p class=\"fp-ae-copy\">\n No saved credential exists for this provider. Connect the provider or set\n it up manually before publishing.\n </p>\n }\n @if (credentialTest(); as test) {\n <div class=\"mt-3 rounded-md bg-(--p-surface-100) px-3 py-2 text-[12px]\">\n {{ test.status ?? (test.succeeded ? \"Succeeded\" : \"Failed\") }}\n @if (test.message) {\n <span>- {{ test.message }}</span>\n }\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 let-emptyText=\"emptyText\"\n let-keyHint=\"keyHint\"\n let-valueHint=\"valueHint\"\n let-required=\"required\"\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)=\"\n updateObjectRow(objectKey, row.key, $event, row.value)\n \"\n [label]=\"keyLabel\"\n [hint]=\"\n keyHint || 'Configuration key saved to the backend JSON object.'\n \"\n [required]=\"required === true\"\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]=\"\n valueHint ||\n 'Configuration value. Use expressions when this field should resolve at runtime.'\n \"\n [required]=\"required === true\"\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 } @empty {\n @if (emptyText) {\n <div\n class=\"rounded-md bg-(--p-content-background) px-3 py-2 text-[12px] text-(--p-text-muted-color)\"\n >\n {{ emptyText }}\n </div>\n }\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.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i1$1.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: RadioCardsField, selector: "mt-radio-cards-field", inputs: ["circle", "label", "hint", "readonly", "required", "color", "size", "optionLabel", "optionValue", "options"] }, { 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: "component", type: AutomationNodeIconComponent, selector: "fp-automation-node-icon", inputs: ["type", "itemKey", "icon", "iconKey", "category", "displayName", "description", "metadata", "defaultConfig", "visualKey"] }, { kind: "directive", type: DropDataDirective, selector: "[fpDropData]", inputs: ["fpDropAutoInsert"], outputs: ["dataDrop", "dataDropEvent"] }, { kind: "pipe", type: i1.KeyValuePipe, name: "keyvalue" }] });
|
|
20286
|
+
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]=\"true\"\n (dataDropEvent)=\"insertExpressionDrop($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\n class=\"text-[12px] font-semibold text-(--p-text-muted-color)\"\n >\n First node connected\n </div>\n <div\n class=\"truncate text-[14px] font-semibold text-(--p-text-color)\"\n >\n {{ startConnection().label }}\n </div>\n <div\n class=\"flex flex-wrap items-center gap-2 text-[12px] text-(--p-text-muted-color)\"\n >\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\n class=\"m-0 text-[12px] leading-5 text-(--p-text-muted-color)\"\n >\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\n class=\"flex flex-wrap items-center gap-2 text-[12px] text-(--p-text-muted-color)\"\n >\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\n class=\"rounded-lg border border-dashed border-(--p-content-border-color) bg-(--p-surface-50) p-3\"\n >\n <div class=\"text-[14px] font-semibold text-(--p-text-color)\">\n No first node connected\n </div>\n <p\n class=\"m-0 mt-1 text-[12px] leading-5 text-(--p-text-muted-color)\"\n >\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 (\n hasTriggerPayloadSchema() && triggerPayloadSchema();\n as schema\n ) {\n <div\n class=\"rounded-md border border-surface-200 bg-surface-0 p-3\"\n >\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\n class=\"mt-3 rounded-md border border-surface-200 bg-surface-0 p-3\"\n >\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\n 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\n class=\"self-end\"\n size=\"small\"\n variant=\"outlined\"\n label=\"Copy\"\n (onClick)=\"copyWebhookUrl()\"\n />\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>{{\n (setup.requiredHeaders ?? []).join(\", \") || \"-\"\n }}</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\n class=\"mt-3 rounded-lg border border-(--p-content-border-color)\"\n >\n <summary\n class=\"cursor-pointer px-3 py-2 text-[12px] font-semibold\"\n >\n Sample request\n </summary>\n <pre class=\"fp-ae-code\">{{\n schemaText(setup.sampleRequest)\n }}</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 [required]=\"isAuthFieldRequired('mode')\"\n [options]=\"authModeOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-text-field\n [ngModel]=\"authPolicy()['signatureHeaderName'] ?? ''\"\n (ngModelChange)=\"\n onAuthFieldChange('signatureHeaderName', $event)\n \"\n label=\"Signature header\"\n hint=\"Header name that carries the webhook signature when the policy requires signed requests.\"\n [required]=\"isAuthFieldRequired('signatureHeaderName')\"\n />\n <mt-text-field\n [ngModel]=\"authPolicy()['timestampHeaderName'] ?? ''\"\n (ngModelChange)=\"\n onAuthFieldChange('timestampHeaderName', $event)\n \"\n label=\"Timestamp header\"\n hint=\"Header name that carries the request timestamp for replay protection.\"\n [required]=\"isAuthFieldRequired('timestampHeaderName')\"\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 [required]=\"true\"\n [options]=\"formOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-select-field\n [ngModel]=\"\n selectedFormVersionId() || formBinding()?.formVersionId || ''\n \"\n (ngModelChange)=\"onFormVersionChange($event)\"\n label=\"Form version\"\n hint=\"Persist the exact backend formVersionId. Do not type or generate IDs manually.\"\n [required]=\"true\"\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\n size=\"small\"\n severity=\"primary\"\n label=\"Save binding\"\n (onClick)=\"saveFormBinding()\"\n />\n @if (formBinding()) {\n <span\n class=\"rounded-lg bg-(--p-surface-100) px-2 py-1 font-mono text-[11px] text-(--p-text-muted-color)\"\n >\n {{ formBinding()!.formVersionId }}\n </span>\n }\n </div>\n @if (formSchema(); as schema) {\n <details\n class=\"mt-3 rounded-lg border border-(--p-content-border-color)\"\n >\n <summary\n class=\"cursor-pointer px-3 py-2 text-[12px] font-semibold\"\n >\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 [required]=\"true\"\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 [required]=\"isConfigFieldRequired('timezone')\"\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 [required]=\"showScheduleCron()\"\n />\n }\n @if (showScheduleInterval()) {\n <mt-number-field\n [ngModel]=\"numberValue(config()['intervalSeconds'])\"\n (ngModelChange)=\"\n onConfigFieldChange('intervalSeconds', $event)\n \"\n label=\"Interval seconds\"\n hint=\"Repeat interval in seconds.\"\n [required]=\"showScheduleInterval()\"\n [min]=\"0\"\n />\n }\n @if (showScheduleOnce()) {\n <mt-date-field\n class=\"md:col-span-2\"\n [ngModel]=\"\n config()['runAt'] ??\n config()['runAtUtc'] ??\n config()['oneTimeAt'] ??\n config()['at'] ??\n ''\n \"\n (ngModelChange)=\"onConfigFieldChange('runAt', $event)\"\n label=\"Run at UTC\"\n hint=\"UTC date/time for the one-time schedule.\"\n [required]=\"showScheduleOnce()\"\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 [required]=\"isConfigFieldRequired('startDate')\"\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 [required]=\"isConfigFieldRequired('misfirePolicy')\"\n [options]=\"misfirePolicyOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n </div>\n <div class=\"mt-3 flex gap-2\">\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n label=\"Validate\"\n (onClick)=\"validateSchedule()\"\n />\n <mt-button\n size=\"small\"\n severity=\"primary\"\n label=\"Preview next fire\"\n (onClick)=\"previewSchedule()\"\n />\n </div>\n @if (schedulePreview(); as preview) {\n <div\n class=\"mt-3 rounded-lg bg-(--p-surface-100) px-3 py-2 text-[12px]\"\n >\n <strong class=\"text-emerald-600\">Valid</strong>\n <span class=\"ms-2\"\n >Next fire: {{ preview.nextFireAtUtc ?? \"-\" }}</span\n >\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\n *ngTemplateOutlet=\"\n mapEditor;\n context: {\n objectKey: 'fields',\n rows: fieldsRows(),\n keyLabel: 'Field',\n valueLabel: 'Value',\n addLabel: 'Add field',\n required: isConfigFieldRequired('fields'),\n }\n \"\n />\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]=\"ifConditionFieldText('left')\"\n (focusin)=\"setExpressionTarget('config:left')\"\n (ngModelChange)=\"onIfConditionFieldChange('left', $event)\"\n label=\"Left\"\n hint=\"Left-side value or expression to compare.\"\n [required]=\"isConfigFieldRequired('left')\"\n />\n <mt-select-field\n [ngModel]=\"ifConditionFieldValue('operator') || 'equals'\"\n (ngModelChange)=\"onIfConditionFieldChange('operator', $event)\"\n label=\"Operator\"\n hint=\"Comparison operator used by the backend condition evaluator.\"\n [required]=\"isConfigFieldRequired('operator')\"\n [options]=\"ifOperatorOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-text-field\n [ngModel]=\"ifConditionFieldText('right')\"\n (focusin)=\"setExpressionTarget('config:right')\"\n (ngModelChange)=\"onIfConditionFieldChange('right', $event)\"\n label=\"Right\"\n hint=\"Right-side value or expression to compare against.\"\n [required]=\"isConfigFieldRequired('right')\"\n />\n </div>\n <div\n class=\"mt-3 rounded-md border border-surface-200 bg-surface-0 p-3\"\n >\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\n class=\"rounded-md bg-(--p-surface-100) px-2 py-1 font-mono text-[11px]\"\n >{{ key }}</span\n >\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]=\"httpMethod()\"\n (ngModelChange)=\"onHttpMethodChange($event)\"\n label=\"Method\"\n hint=\"HTTP method used for the outbound request.\"\n [required]=\"isConfigFieldRequired('method')\"\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 [required]=\"isConfigFieldRequired('url')\"\n />\n </div>\n <div class=\"mt-3\">\n <ng-container\n *ngTemplateOutlet=\"\n mapEditor;\n context: {\n objectKey: 'headers',\n rows: headerRows(),\n keyLabel: 'Header',\n valueLabel: 'Value',\n addLabel: 'Add header',\n }\n \"\n />\n </div>\n <div class=\"mt-3\">\n <ng-container\n *ngTemplateOutlet=\"\n mapEditor;\n context: {\n objectKey: 'query',\n rows: queryRows(),\n keyLabel: 'Query param',\n valueLabel: 'Value',\n addLabel: 'Add query param',\n }\n \"\n />\n </div>\n @if (httpMethodAllowsPayload()) {\n @if (httpSupportsBodyMode()) {\n <mt-select-field\n class=\"mt-3\"\n [ngModel]=\"httpBodyMode()\"\n (ngModelChange)=\"onHttpBodyModeChange($event)\"\n label=\"Payload mode\"\n hint=\"How the request payload should be sent by the backend HTTP runtime.\"\n [required]=\"isConfigFieldRequired(httpBodyModeConfigKey())\"\n [options]=\"httpBodyModeOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n }\n @if (httpBodyEnabled()) {\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=\"Request payload\"\n hint=\"Payload sent by this HTTP method. Use JSON or expressions when the backend schema allows it.\"\n [required]=\"isConfigFieldRequired('body')\"\n rows=\"6\"\n />\n }\n } @else {\n <div\n class=\"mt-3 rounded-md border border-(--p-content-border-color) bg-(--p-surface-50) px-3 py-2 text-[12px] leading-5 text-(--p-text-muted-color)\"\n >\n {{ httpMethod() }} requests do not send a payload. Put request\n data in query parameters or headers.\n </div>\n }\n @if (\n supportsConfigKey(\"timeoutSeconds\") ||\n supportsConfigKey(\"responseHandling\")\n ) {\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)=\"\n onConfigFieldChange('timeoutSeconds', $event)\n \"\n label=\"Timeout seconds\"\n hint=\"Request timeout when exposed by the backend schema.\"\n [required]=\"isConfigFieldRequired('timeoutSeconds')\"\n [min]=\"0\"\n />\n }\n @if (supportsConfigKey(\"responseHandling\")) {\n <mt-text-field\n [ngModel]=\"config()['responseHandling'] ?? ''\"\n (ngModelChange)=\"\n onConfigFieldChange('responseHandling', $event)\n \"\n label=\"Response handling\"\n hint=\"Backend-supported response handling mode or expression.\"\n [required]=\"isConfigFieldRequired('responseHandling')\"\n />\n }\n </div>\n }\n @if (sectionInMain(\"credentials\")) {\n <div\n class=\"mt-3 rounded-md border border-surface-200 bg-surface-0 p-3\"\n >\n <div class=\"fp-ae-label mb-2\">Credential</div>\n <ng-container *ngTemplateOutlet=\"credentialSelector\" />\n </div>\n }\n <div\n class=\"mt-3 rounded-md border border-(--p-content-border-color) bg-(--p-surface-50) p-3\"\n >\n <div class=\"flex items-center justify-between gap-3\">\n <div class=\"min-w-0\">\n <div class=\"fp-ae-label\">Configuration check</div>\n <p class=\"fp-ae-copy m-0 mt-1\">\n Runs backend dry-run validation for method, URL,\n expressions, and request policy before execution.\n </p>\n </div>\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n severity=\"secondary\"\n label=\"Check request\"\n [disabled]=\"httpCheckState() === 'loading'\"\n (onClick)=\"checkHttpRequest()\"\n />\n </div>\n @if (httpLocalIssues().length > 0) {\n <div\n class=\"mt-2 rounded-md border border-[rgb(var(--fp-warning))]/25 bg-[rgb(var(--fp-warning))]/10 px-2.5 py-2 text-[12px] text-(--p-text-color)\"\n >\n {{ httpLocalIssues()[0] }}\n </div>\n } @else if (httpCheckError(); as error) {\n <div\n class=\"mt-2 rounded-md border border-[rgb(var(--fp-error))]/25 bg-[rgb(var(--fp-error))]/10 px-2.5 py-2 text-[12px] text-(--p-text-color)\"\n >\n {{ error }}\n </div>\n } @else if (httpCheckResult(); as result) {\n <div\n class=\"mt-2 rounded-md border border-[rgb(var(--fp-commit))]/25 bg-[rgb(var(--fp-commit))]/10 px-2.5 py-2 text-[12px] text-(--p-text-color)\"\n >\n Backend check\n {{\n result.success || result.canStart\n ? \"passed\"\n : \"completed with issues\"\n }}.\n </div>\n }\n </div>\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]=\"waitMode()\"\n (ngModelChange)=\"onWaitModeChange($event)\"\n label=\"Mode\"\n hint=\"Choose how execution should pause.\"\n [required]=\"isConfigFieldRequired('mode')\"\n [options]=\"waitModeOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n @if (showWaitDuration()) {\n <mt-number-field\n [ngModel]=\"numberValue(config()['durationSeconds'])\"\n (ngModelChange)=\"\n onConfigFieldChange('durationSeconds', $event)\n \"\n label=\"Wait duration\"\n hint=\"Seconds to pause before continuing.\"\n [required]=\"isConfigFieldRequired('durationSeconds')\"\n [min]=\"1\"\n />\n }\n @if (showWaitUntilDate()) {\n <mt-date-field\n [ngModel]=\"waitUntilDateValue()\"\n (ngModelChange)=\"onWaitUntilDateChange($event)\"\n label=\"Resume at\"\n hint=\"Date and time when execution should continue.\"\n [required]=\"isConfigFieldRequired('untilDate')\"\n [showTime]=\"true\"\n [showClear]=\"true\"\n [pInputs]=\"dateTimePickerInputs\"\n />\n }\n </div>\n @if (showWaitResumePayloadSchema()) {\n <mt-textarea-field\n class=\"mt-3 w-full font-mono\"\n [ngModel]=\"waitResumePayloadSchemaText()\"\n (focusin)=\"setExpressionTarget('config:resumePayloadSchema')\"\n (ngModelChange)=\"onWaitResumePayloadSchemaChange($event)\"\n label=\"Resume payload schema\"\n hint=\"Optional JSON schema for data sent back when this wait is resumed.\"\n [required]=\"isConfigFieldRequired('resumePayloadSchema')\"\n rows=\"5\"\n />\n }\n </section>\n }\n }\n @case (\"HumanTask\") {\n @if (sectionInMain(\"humanTask\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Human task</div>\n @if (\n assignmentOptions()?.providerStatus &&\n assignmentOptions()?.providerStatus !== \"Available\"\n ) {\n <p class=\"fp-ae-copy\">\n Assignment provider status:\n {{ assignmentOptions()?.providerStatus }}. The backend provider\n is the source of truth for available assignees.\n </p>\n }\n <div\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 Task details\n </h3>\n <div class=\"grid gap-3 p-4 xl:grid-cols-3\">\n <mt-text-field\n [ngModel]=\"config()['title'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:title')\"\n (ngModelChange)=\"onConfigFieldChange('title', $event)\"\n label=\"Task title\"\n hint=\"Title shown to the assigned user while the execution waits.\"\n [required]=\"isConfigFieldRequired('title')\"\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 submit this task.\"\n [required]=\"isConfigFieldRequired('assignment')\"\n [options]=\"assignmentSelectOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-text-field\n [ngModel]=\"config()['priority'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('priority', $event)\"\n label=\"Priority\"\n hint=\"Optional task priority passed to backend task orchestration.\"\n [required]=\"isConfigFieldRequired('priority')\"\n />\n <mt-textarea-field\n class=\"xl:col-span-3\"\n [ngModel]=\"config()['description'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:description')\"\n (ngModelChange)=\"onConfigFieldChange('description', $event)\"\n label=\"Task description\"\n hint=\"Instructions shown with the HumanTask form.\"\n [required]=\"isConfigFieldRequired('description')\"\n rows=\"4\"\n />\n </div>\n </div>\n\n <div\n class=\"mt-3 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 Task behavior\n </h3>\n <div class=\"grid gap-3 p-4 xl:grid-cols-4\">\n <mt-date-field\n [ngModel]=\"\n config()['dueDateUtc'] ?? config()['dueDate'] ?? ''\n \"\n (ngModelChange)=\"onConfigFieldChange('dueDateUtc', $event)\"\n label=\"Due date\"\n hint=\"Optional backend due timestamp for the waiting task.\"\n [required]=\"isConfigFieldRequired('dueDateUtc')\"\n [showTime]=\"true\"\n [showClear]=\"true\"\n [pInputs]=\"dateTimePickerInputs\"\n />\n <mt-number-field\n [ngModel]=\"numberValue(config()['dueInSeconds'])\"\n (ngModelChange)=\"onConfigFieldChange('dueInSeconds', $event)\"\n label=\"Due in seconds\"\n hint=\"Relative due duration when the backend should calculate the timestamp.\"\n [required]=\"isConfigFieldRequired('dueInSeconds')\"\n [min]=\"0\"\n />\n <mt-text-field\n [ngModel]=\"config()['contextOutputPath'] ?? 'task'\"\n (ngModelChange)=\"\n onConfigFieldChange('contextOutputPath', $event)\n \"\n label=\"Context output path\"\n hint=\"Where submitted task values are written in execution context.\"\n [required]=\"isConfigFieldRequired('contextOutputPath')\"\n />\n <mt-select-field\n [ngModel]=\"config()['cancelBehavior'] ?? 'RouteCancelled'\"\n (ngModelChange)=\"\n onConfigFieldChange('cancelBehavior', $event)\n \"\n label=\"Cancel behavior\"\n hint=\"Route cancelled when users cancel, or fail this node.\"\n [required]=\"isConfigFieldRequired('cancelBehavior')\"\n [options]=\"humanTaskCancelBehaviorOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <div\n class=\"flex min-h-[4.25rem] items-end rounded-md border border-surface-200 bg-surface-0 px-3 py-2\"\n >\n <mt-toggle-field\n size=\"small\"\n label=\"Allow draft save\"\n labelPosition=\"end\"\n [ngModel]=\"config()['saveDraft'] !== false\"\n [required]=\"isConfigFieldRequired('saveDraft')\"\n (ngModelChange)=\"\n onConfigFieldChange('saveDraft', $event === true)\n \"\n hint=\"Expose backend draft save for this task when supported.\"\n />\n </div>\n @if (supportsConfigKey(\"submitBehavior\")) {\n <mt-text-field\n [ngModel]=\"config()['submitBehavior'] ?? ''\"\n (ngModelChange)=\"\n onConfigFieldChange('submitBehavior', $event)\n \"\n label=\"Submit behavior\"\n hint=\"Backend submit behavior when exposed by the node schema.\"\n [required]=\"isConfigFieldRequired('submitBehavior')\"\n />\n }\n </div>\n </div>\n\n <div class=\"mt-3 flex gap-2\">\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n label=\"Validate assignment\"\n (onClick)=\"validateAssignment()\"\n />\n </div>\n @if (assignmentValidation(); as result) {\n <div\n class=\"mt-3 rounded-lg bg-(--p-surface-100) px-3 py-2 text-[12px]\"\n >\n {{\n result.isValid === false\n ? \"Assignment invalid\"\n : \"Assignment accepted by helper\"\n }}\n @for (issue of resultIssues(result); track $index) {\n <div class=\"mt-1 text-[11px] text-(--p-text-muted-color)\">\n {{ issue }}\n </div>\n }\n </div>\n }\n </section>\n }\n\n @if (sectionInMain(\"formBinding\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Task input form</div>\n <mt-radio-cards-field\n [ngModel]=\"humanTaskFormSource()\"\n (ngModelChange)=\"onHumanTaskFormSourceChange($event)\"\n label=\"Form source\"\n hint=\"HumanTask requires either its own node form binding or an explicit reused FormSubmitTrigger payload.\"\n [required]=\"isConfigFieldRequired('formSource')\"\n [options]=\"humanTaskFormSourceOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n size=\"small\"\n >\n <ng-template #option let-item>\n <div class=\"w-full min-w-0\">\n <div class=\"text-[13px] font-semibold leading-5\">\n {{ item.label }}\n </div>\n <p\n class=\"mt-1 text-[12px] leading-5 text-(--p-text-muted-color)\"\n >\n {{ item.description }}\n </p>\n </div>\n </ng-template>\n </mt-radio-cards-field>\n\n @if (humanTaskUsesExplicitFormBinding()) {\n <div\n class=\"mt-3 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 HumanTask input binding\n </h3>\n <div class=\"grid gap-3 p-4 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 for this HumanTask node.\"\n [required]=\"humanTaskUsesExplicitFormBinding()\"\n [options]=\"formOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-select-field\n [ngModel]=\"\n selectedFormVersionId() ||\n formBinding()?.formVersionId ||\n ''\n \"\n (ngModelChange)=\"onFormVersionChange($event)\"\n label=\"Form version\"\n hint=\"Persist the exact backend formVersionId for the node binding.\"\n [required]=\"humanTaskUsesExplicitFormBinding()\"\n [options]=\"formVersionOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n </div>\n <div class=\"border-t border-surface-200 px-4 py-3\">\n @if (humanTaskFormSummary(); as summary) {\n <div\n class=\"flex flex-wrap items-center gap-2 text-[12px] text-(--p-text-muted-color)\"\n >\n @if (summary.formLabel) {\n <span class=\"rounded-md bg-(--p-surface-100) px-2 py-1\">\n {{ summary.formLabel }}\n </span>\n }\n @if (summary.formVersionId) {\n <span\n class=\"rounded-md bg-(--p-surface-100) px-2 py-1 font-mono text-[11px]\"\n >\n {{ summary.formVersionId }}\n </span>\n }\n <span class=\"rounded-md bg-(--p-surface-100) px-2 py-1\">\n {{ summary.fieldCount }} fields\n </span>\n <span class=\"rounded-md bg-(--p-surface-100) px-2 py-1\">\n {{ summary.requiredCount }} required\n </span>\n <span class=\"rounded-md bg-(--p-surface-100) px-2 py-1\">\n {{ summary.writableCount }} writable\n </span>\n </div>\n }\n\n @if (humanTaskHasCustomInputMapping()) {\n <div\n class=\"mt-3 rounded-md 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 Existing custom input mapping will be preserved unless\n field prefill options are changed.\n </div>\n }\n\n @if (humanTaskFormFields().length > 0) {\n <div\n class=\"mt-3 overflow-hidden rounded-md border border-surface-200 bg-surface-0\"\n >\n <div\n class=\"flex flex-wrap items-center justify-between gap-2 border-b border-surface-200 bg-surface-50 px-3 py-2\"\n >\n <div class=\"text-[13px] font-semibold text-color\">\n FormBuilder fields\n </div>\n <div class=\"flex flex-wrap gap-2\">\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n severity=\"secondary\"\n label=\"Select all\"\n (onClick)=\"selectAllHumanTaskPrefillFields()\"\n />\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n severity=\"secondary\"\n label=\"Clear\"\n (onClick)=\"clearHumanTaskPrefillFields()\"\n />\n </div>\n </div>\n <div class=\"divide-y divide-surface-200\">\n @for (field of humanTaskFormFields(); track field.key) {\n <div\n class=\"grid gap-3 px-3 py-3 md:grid-cols-[minmax(0,1fr)_auto] md:items-center\"\n >\n <div class=\"min-w-0\">\n <div class=\"flex flex-wrap items-center gap-2\">\n <span\n class=\"truncate text-[13px] font-semibold text-(--p-text-color)\"\n >\n {{ field.label }}\n </span>\n <span\n class=\"rounded-md bg-(--p-surface-100) px-2 py-0.5 font-mono text-[11px] text-(--p-text-muted-color)\"\n >\n {{ field.key }}\n </span>\n @if (field.required) {\n <span\n class=\"rounded-md bg-[rgb(var(--fp-danger))]/10 px-2 py-0.5 text-[11px] font-semibold text-[rgb(var(--fp-danger))]\"\n >\n Required\n </span>\n }\n @if (field.read === true) {\n <span\n class=\"rounded-md bg-(--p-surface-100) px-2 py-0.5 text-[11px] text-(--p-text-muted-color)\"\n >\n Read\n </span>\n }\n @if (field.write === true) {\n <span\n class=\"rounded-md bg-(--p-surface-100) px-2 py-0.5 text-[11px] text-(--p-text-muted-color)\"\n >\n Write\n </span>\n }\n </div>\n @if (field.description) {\n <p\n class=\"m-0 mt-1 text-[12px] leading-5 text-(--p-text-muted-color)\"\n >\n {{ field.description }}\n </p>\n }\n </div>\n <mt-toggle-field\n size=\"small\"\n label=\"Prefill\"\n labelPosition=\"end\"\n [ngModel]=\"\n humanTaskPrefillFieldKeySet().has(field.key)\n \"\n (ngModelChange)=\"\n onHumanTaskPrefillFieldToggle(\n field.key,\n $event === true\n )\n \"\n />\n </div>\n }\n </div>\n <div\n class=\"border-t border-surface-200 bg-surface-50 px-3 py-2 text-[12px] leading-5 text-(--p-text-muted-color)\"\n >\n Selected fields are saved as\n <span class=\"font-mono\"\n >inputMappingJson.fieldKeys</span\n >\n so backend can prefill matching upstream values.\n </div>\n </div>\n } @else if (formSchema()) {\n <div\n class=\"mt-3 rounded-md border border-surface-200 bg-surface-50 px-3 py-2 text-[12px] leading-5 text-(--p-text-muted-color)\"\n >\n No FormBuilder fields were returned for this form schema.\n </div>\n }\n\n @if (formSchema()?.requiresSnapshotOnPublish) {\n <div\n class=\"mt-3 rounded-md border border-surface-200 bg-surface-50 px-3 py-2 text-[12px] leading-5 text-(--p-text-muted-color)\"\n >\n Backend requires this form version snapshot during\n publish.\n </div>\n }\n </div>\n <div class=\"border-t border-surface-200 px-4 py-3\">\n <div class=\"flex flex-wrap items-center gap-2\">\n <mt-button\n size=\"small\"\n severity=\"primary\"\n label=\"Save task binding\"\n (onClick)=\"saveFormBinding()\"\n />\n @if (formBinding()) {\n <span\n class=\"rounded-lg bg-(--p-surface-100) px-2 py-1 font-mono text-[11px] text-(--p-text-muted-color)\"\n >\n {{ formBinding()!.formVersionId }}\n </span>\n }\n </div>\n </div>\n </div>\n } @else {\n <div class=\"mt-3 grid gap-3 md:grid-cols-2\">\n <mt-select-field\n [ngModel]=\"config()['triggerKey'] ?? ''\"\n (ngModelChange)=\"onHumanTaskReuseTriggerChange($event)\"\n label=\"Trigger to reuse\"\n hint=\"Explicitly select the FormSubmitTrigger whose submitted form payload is reused.\"\n [options]=\"triggerOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n </div>\n @if (humanTaskReuseFormBinding()) {\n <div\n class=\"mt-3 rounded-md border border-surface-200 bg-surface-0 p-3\"\n >\n <div class=\"fp-ae-label mb-2\">Reused trigger form</div>\n @if (humanTaskFormSummary(); as summary) {\n <div\n class=\"flex flex-wrap items-center gap-2 text-[12px] text-(--p-text-muted-color)\"\n >\n @if (summary.formLabel) {\n <span class=\"rounded-md bg-(--p-surface-100) px-2 py-1\">\n {{ summary.formLabel }}\n </span>\n }\n @if (summary.formVersionId) {\n <span\n class=\"rounded-md bg-(--p-surface-100) px-2 py-1 font-mono text-[11px]\"\n >\n {{ summary.formVersionId }}\n </span>\n }\n <span class=\"rounded-md bg-(--p-surface-100) px-2 py-1\">\n {{ summary.fieldCount }} fields\n </span>\n <span class=\"rounded-md bg-(--p-surface-100) px-2 py-1\">\n {{ summary.requiredCount }} required\n </span>\n </div>\n }\n </div>\n }\n }\n\n @if (formSchema(); as schema) {\n <details\n class=\"mt-3 rounded-lg border border-(--p-content-border-color)\"\n >\n <summary\n class=\"cursor-pointer px-3 py-2 text-[12px] font-semibold\"\n >\n Form schema preview\n </summary>\n <pre class=\"fp-ae-code\">{{ schemaText(schema.schema) }}</pre>\n </details>\n }\n\n <div\n class=\"mt-3 rounded-md border border-surface-200 bg-surface-0 p-3\"\n >\n <div class=\"fp-ae-label mb-2\">HumanTask routes</div>\n <div class=\"flex flex-wrap gap-1.5\">\n @for (key of routeOutputKeys(); track key) {\n <span\n class=\"rounded-md bg-(--p-surface-100) px-2 py-1 font-mono text-[11px]\"\n >{{ key }}</span\n >\n }\n </div>\n </div>\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 (\n assignmentOptions()?.providerStatus &&\n assignmentOptions()?.providerStatus !== \"Available\"\n ) {\n <p class=\"fp-ae-copy\">\n Assignment provider status:\n {{ assignmentOptions()?.providerStatus }}. The backend provider\n 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 [required]=\"isConfigFieldRequired('title')\"\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 [required]=\"isConfigFieldRequired('assignment')\"\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 [required]=\"isConfigFieldRequired('priority')\"\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 [required]=\"isConfigFieldRequired('message')\"\n rows=\"4\"\n />\n @if (\n humanApprovalSupportsConfig(\"dueDate\") ||\n humanApprovalSupportsConfig(\"dueInSeconds\") ||\n humanApprovalSupportsConfig(\"timeoutSeconds\") ||\n humanApprovalSupportsConfig(\"expiresAt\") ||\n humanApprovalSupportsConfig(\"commentsRequired\") ||\n humanApprovalSupportsConfig(\"allowReturn\")\n ) {\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 [required]=\"isConfigFieldRequired('dueDate')\"\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)=\"\n onConfigFieldChange('dueInSeconds', $event)\n \"\n label=\"Due in seconds\"\n hint=\"Relative approval due duration when supported by the backend schema.\"\n [required]=\"isConfigFieldRequired('dueInSeconds')\"\n [min]=\"0\"\n />\n }\n @if (humanApprovalSupportsConfig(\"timeoutSeconds\")) {\n <mt-number-field\n [ngModel]=\"numberValue(config()['timeoutSeconds'])\"\n (ngModelChange)=\"\n onConfigFieldChange('timeoutSeconds', $event)\n \"\n label=\"Timeout seconds\"\n hint=\"Approval timeout emitted according to backend approval runtime support.\"\n [required]=\"isConfigFieldRequired('timeoutSeconds')\"\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 [required]=\"isConfigFieldRequired('expiresAt')\"\n [showTime]=\"true\"\n [showClear]=\"true\"\n [pInputs]=\"dateTimePickerInputs\"\n />\n }\n @if (humanApprovalSupportsConfig(\"commentsRequired\")) {\n <div\n class=\"flex min-h-[4.25rem] items-end rounded-md border border-surface-200 bg-surface-0 px-3 py-2\"\n >\n <mt-toggle-field\n size=\"small\"\n label=\"Comments required\"\n labelPosition=\"end\"\n [ngModel]=\"config()['commentsRequired'] === true\"\n [required]=\"isConfigFieldRequired('commentsRequired')\"\n (ngModelChange)=\"\n onConfigFieldChange('commentsRequired', $event === true)\n \"\n hint=\"Require approver comments when the backend supports this flag.\"\n />\n </div>\n }\n @if (humanApprovalSupportsConfig(\"allowReturn\")) {\n <div\n class=\"flex min-h-[4.25rem] items-end rounded-md border border-surface-200 bg-surface-0 px-3 py-2\"\n >\n <mt-toggle-field\n size=\"small\"\n label=\"Allow return for changes\"\n labelPosition=\"end\"\n [ngModel]=\"config()['allowReturn'] !== false\"\n [required]=\"isConfigFieldRequired('allowReturn')\"\n (ngModelChange)=\"\n onConfigFieldChange('allowReturn', $event === true)\n \"\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\n class=\"overflow-hidden rounded-md border border-surface-200 bg-surface-0\"\n >\n @if (selectedApprovalDecisionRows().length > 0) {\n <div\n 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 >\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 (\n decision of selectedApprovalDecisionRows();\n track decision.value\n ) {\n <div\n 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 >\n <div class=\"min-w-0\">\n <div\n class=\"text-[11px] font-semibold text-(--p-text-muted-color) xl:hidden\"\n >\n Decision label\n </div>\n <div\n class=\"truncate text-[13px] font-semibold text-(--p-text-color)\"\n >\n {{ decision.label }}\n </div>\n </div>\n <div class=\"min-w-0\">\n <div\n class=\"text-[11px] font-semibold text-(--p-text-muted-color) xl:hidden\"\n >\n Canonical value\n </div>\n <div\n class=\"truncate font-mono text-[12px] text-(--p-text-color)\"\n >\n {{ decision.value }}\n </div>\n </div>\n <div class=\"min-w-0\">\n <div\n class=\"text-[11px] font-semibold text-(--p-text-muted-color) xl:hidden\"\n >\n Route output key\n </div>\n <div\n class=\"truncate font-mono text-[12px] text-(--p-text-muted-color)\"\n >\n {{ decision.routeOutputKey }}\n </div>\n </div>\n <div>\n <div\n class=\"text-[11px] font-semibold text-(--p-text-muted-color) xl:hidden\"\n >\n Routes\n </div>\n <div class=\"text-[13px] text-(--p-text-color)\">\n {{ decision.routeCount }}\n </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\n class=\"px-3 py-4 text-[12px] text-(--p-text-muted-color)\"\n >\n Add at least one backend-supported approval decision.\n </div>\n }\n </div>\n @if (approvalDecisionIssues().length > 0) {\n <div\n 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 >\n @for (issue of approvalDecisionIssues(); track $index) {\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 [required]=\"isConfigFieldRequired('allowedDecisions')\"\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 (\n humanApprovalSupportsConfig(\"payload\") ||\n humanApprovalSupportsConfig(\"context\") ||\n humanApprovalSupportsConfig(\"metadata\")\n ) {\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 [required]=\"isConfigFieldRequired('payload')\"\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 [required]=\"isConfigFieldRequired('context')\"\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 [required]=\"isConfigFieldRequired('metadata')\"\n rows=\"4\"\n />\n }\n </div>\n }\n <div class=\"mt-3 flex gap-2\">\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n label=\"Validate assignment\"\n (onClick)=\"validateAssignment()\"\n />\n </div>\n @if (assignmentValidation(); as result) {\n <div\n class=\"mt-3 rounded-lg bg-(--p-surface-100) px-3 py-2 text-[12px]\"\n >\n {{\n result.isValid === false\n ? \"Assignment invalid\"\n : \"Assignment accepted by helper\"\n }}\n @for (issue of resultIssues(result); track $index) {\n <div class=\"mt-1 text-[11px] text-(--p-text-muted-color)\">\n {{ issue }}\n </div>\n }\n </div>\n }\n </section>\n }\n }\n @case (\"ConnectorAction\") {\n @if (sectionInMain(\"humanReviewRequest\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">\n {{ humanReviewProviderLabel() }} human review\n </div>\n <div class=\"grid gap-3 md:grid-cols-2\">\n <mt-text-field\n [ngModel]=\"config()['channelType'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('channelType', $event)\"\n label=\"Channel\"\n [required]=\"isConfigFieldRequired('channelType')\"\n />\n </div>\n\n <div class=\"mt-4 grid gap-3 md:grid-cols-3\">\n <mt-text-field\n [ngModel]=\"humanReviewDestination()['recipient'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:destination:recipient')\"\n (ngModelChange)=\"\n onNestedConfigFieldChange('destination', 'recipient', $event)\n \"\n label=\"Recipient\"\n [required]=\"isConnectorDestinationRequired()\"\n />\n <mt-text-field\n [ngModel]=\"humanReviewDestination()['channelId'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:destination:channelId')\"\n (ngModelChange)=\"\n onNestedConfigFieldChange('destination', 'channelId', $event)\n \"\n label=\"Channel ID\"\n [required]=\"isConnectorDestinationRequired()\"\n />\n <mt-text-field\n [ngModel]=\"humanReviewDestination()['threadKey'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:destination:threadKey')\"\n (ngModelChange)=\"\n onNestedConfigFieldChange('destination', 'threadKey', $event)\n \"\n label=\"Thread key\"\n [required]=\"isConnectorDestinationRequired()\"\n />\n </div>\n\n <div class=\"mt-4 grid gap-3\">\n <mt-text-field\n [ngModel]=\"humanReviewMessage()['subject'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:message:subject')\"\n (ngModelChange)=\"\n onNestedConfigFieldChange('message', 'subject', $event)\n \"\n label=\"Subject\"\n [required]=\"isConnectorMessageRequired()\"\n />\n <mt-textarea-field\n class=\"w-full\"\n [ngModel]=\"humanReviewMessage()['body'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:message:body')\"\n (ngModelChange)=\"\n onNestedConfigFieldChange('message', 'body', $event)\n \"\n label=\"Message\"\n [required]=\"isConnectorMessageRequired()\"\n rows=\"5\"\n />\n <mt-text-field\n [ngModel]=\"humanReviewMessage()['templateKey'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:message:templateKey')\"\n (ngModelChange)=\"\n onNestedConfigFieldChange('message', 'templateKey', $event)\n \"\n label=\"Template key\"\n [required]=\"isConnectorMessageRequired()\"\n />\n </div>\n\n <div class=\"mt-4\">\n <div class=\"mb-2 flex items-center justify-between gap-3\">\n <div class=\"fp-ae-label\">Response options</div>\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n label=\"Add option\"\n (onClick)=\"addHumanReviewResponseOption()\"\n />\n </div>\n <div class=\"grid gap-2\">\n @for (\n option of humanReviewResponseOptions();\n track option.key + \":\" + $index\n ) {\n <div\n class=\"grid gap-2 rounded-md border border-(--p-content-border-color) bg-(--p-surface-50) p-2 md:grid-cols-[1fr_1fr_1fr_auto]\"\n >\n <mt-text-field\n [ngModel]=\"option.key\"\n (ngModelChange)=\"\n updateHumanReviewResponseOption($index, 'key', $event)\n \"\n label=\"Key\"\n [required]=\"isConfigFieldRequired('responseOptions')\"\n />\n <mt-text-field\n [ngModel]=\"option.label\"\n (ngModelChange)=\"\n updateHumanReviewResponseOption($index, 'label', $event)\n \"\n label=\"Label\"\n [required]=\"isConfigFieldRequired('responseOptions')\"\n />\n <mt-text-field\n [ngModel]=\"option.routeOutputKey\"\n (ngModelChange)=\"\n updateHumanReviewResponseOption(\n $index,\n 'routeOutputKey',\n $event\n )\n \"\n label=\"Route output\"\n [required]=\"isConfigFieldRequired('responseOptions')\"\n />\n <div class=\"flex items-end justify-end\">\n <mt-button\n size=\"small\"\n variant=\"text\"\n severity=\"secondary\"\n icon=\"general.x-close\"\n [disabled]=\"option.routeCount > 0\"\n (onClick)=\"removeHumanReviewResponseOption($index)\"\n [attr.aria-label]=\"'Remove response option'\"\n />\n </div>\n </div>\n }\n </div>\n </div>\n\n <div class=\"mt-4 grid gap-3 md:grid-cols-2\">\n <mt-select-field\n [ngModel]=\"selectedAssignmentKey()\"\n (ngModelChange)=\"onAssignmentOptionChange($event)\"\n label=\"Assignment\"\n [required]=\"isConfigFieldRequired('assignment')\"\n [options]=\"assignmentSelectOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-text-field\n [ngModel]=\"config()['credentialRef'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('credentialRef', $event)\"\n label=\"Credential\"\n [required]=\"isConfigFieldRequired('credentialRef')\"\n />\n <mt-number-field\n [ngModel]=\"numberValue(config()['timeoutSeconds'])\"\n (ngModelChange)=\"onConfigFieldChange('timeoutSeconds', $event)\"\n label=\"Timeout seconds\"\n [required]=\"isConfigFieldRequired('timeoutSeconds')\"\n [min]=\"1\"\n />\n <mt-select-field\n [ngModel]=\"config()['failureBehavior'] ?? 'RouteFailure'\"\n (ngModelChange)=\"onConfigFieldChange('failureBehavior', $event)\"\n label=\"Failure behavior\"\n [required]=\"isConfigFieldRequired('failureBehavior')\"\n [options]=\"humanReviewFailureBehaviorOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n </div>\n\n <div class=\"mt-3 flex gap-2\">\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n label=\"Validate assignment\"\n (onClick)=\"validateAssignment()\"\n />\n </div>\n @if (assignmentValidation(); as result) {\n <div\n class=\"mt-3 rounded-lg bg-(--p-surface-100) px-3 py-2 text-[12px]\"\n >\n {{\n result.isValid === false\n ? \"Assignment invalid\"\n : \"Assignment accepted by helper\"\n }}\n @for (issue of resultIssues(result); track $index) {\n <div class=\"mt-1 text-[11px] text-(--p-text-muted-color)\">\n {{ issue }}\n </div>\n }\n </div>\n }\n\n <div\n class=\"mt-4 rounded-md border border-(--p-content-border-color) bg-(--p-surface-50) p-3\"\n >\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\n class=\"rounded-md bg-(--p-surface-100) px-2 py-1 font-mono text-[11px]\"\n >{{ key }}</span\n >\n }\n </div>\n </div>\n </section>\n }\n }\n @case (\"ConvertToFile\") {\n @if (sectionInMain(\"convertToFile\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">\n {{ convertToFileActionLabel() }}\n </div>\n @if (convertToFileUnavailableReason(); as reason) {\n <div\n class=\"mb-3 rounded-md border border-(--p-content-border-color) bg-(--p-surface-50) px-3 py-2 text-[12px] leading-5 text-(--p-text-muted-color)\"\n >\n {{ reason }}\n </div>\n }\n\n <div class=\"grid gap-3 md:grid-cols-2\">\n <mt-text-field\n [ngModel]=\"config()['fileName'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:fileName')\"\n (ngModelChange)=\"onConfigFieldChange('fileName', $event)\"\n label=\"File name\"\n [required]=\"isConfigFieldRequired('fileName')\"\n />\n @if (convertToFileIsBase64()) {\n <mt-text-field\n [ngModel]=\"config()['contentType'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('contentType', $event)\"\n label=\"Content type\"\n [required]=\"isConfigFieldRequired('contentType')\"\n />\n } @else {\n <mt-text-field\n [ngModel]=\"convertToFileContentType()\"\n [readonly]=\"true\"\n label=\"Content type\"\n />\n }\n </div>\n\n @if (\n convertToFileIsCsv() ||\n convertToFileIsJson() ||\n convertToFileIsSpreadsheet()\n ) {\n <div class=\"mt-4 grid gap-3 md:grid-cols-2\">\n <mt-select-field\n [ngModel]=\"convertToFileSourceMode()\"\n (ngModelChange)=\"onConvertToFileSourceModeChange($event)\"\n label=\"Data source\"\n [required]=\"isConfigFieldRequired('sourceExpression')\"\n [options]=\"convertToFileSourceOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n @if (convertToFileSourceMode() === \"expression\") {\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"convertToFileSourceExpression()\"\n (focusin)=\"setExpressionTarget('config:sourceExpression')\"\n (ngModelChange)=\"\n onConvertToFileSourceExpressionChange($event)\n \"\n label=\"Source expression\"\n [required]=\"isConfigFieldRequired('sourceExpression')\"\n />\n }\n @if (convertToFileIsCsv() || convertToFileIsSpreadsheet()) {\n <div\n class=\"rounded-md border border-(--p-content-border-color) bg-(--p-surface-50) p-3 md:col-span-2\"\n >\n <div class=\"mb-2 flex items-center justify-between gap-3\">\n <div class=\"fp-ae-label\">Columns</div>\n @if (convertToFileColumns().length) {\n <mt-button\n size=\"small\"\n variant=\"text\"\n severity=\"secondary\"\n label=\"Clear\"\n (onClick)=\"clearConvertToFileColumns()\"\n />\n }\n </div>\n <div class=\"mb-3 flex min-h-8 flex-wrap gap-1.5\">\n @if (convertToFileColumns().length) {\n @for (column of convertToFileColumns(); track column) {\n <span\n class=\"inline-flex items-center gap-1 rounded-md bg-(--p-surface-100) px-2 py-1 font-mono text-[11px]\"\n >\n {{ column }}\n <button\n type=\"button\"\n class=\"inline-flex h-4 w-4 items-center justify-center rounded-sm text-[10px] hover:bg-(--p-surface-200) focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--p-primary-color)\"\n (click)=\"removeConvertToFileColumn(column)\"\n [attr.aria-label]=\"'Remove column ' + column\"\n >\n x\n </button>\n </span>\n }\n } @else {\n <span\n class=\"rounded-md bg-(--p-content-background) px-2 py-1 text-[12px] text-(--p-text-muted-color)\"\n >\n All fields\n </span>\n }\n </div>\n <div class=\"grid items-end gap-2 md:grid-cols-[1fr_auto]\">\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"convertToFileColumnDraft()\"\n (ngModelChange)=\"\n onConvertToFileColumnDraftChange($event)\n \"\n (keyup.enter)=\"addConvertToFileColumns()\"\n label=\"Add column\"\n [required]=\"isConfigFieldRequired('columns')\"\n />\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n icon=\"general.plus\"\n label=\"Add\"\n [disabled]=\"!convertToFileColumnDraft().trim()\"\n (onClick)=\"addConvertToFileColumns()\"\n />\n </div>\n </div>\n }\n </div>\n }\n\n @if (convertToFileIsCsv()) {\n <div class=\"mt-4 grid gap-3 md:grid-cols-3\">\n <mt-toggle-field\n size=\"small\"\n labelPosition=\"end\"\n [ngModel]=\"config()['includeHeaders'] !== false\"\n (ngModelChange)=\"\n onConfigFieldChange('includeHeaders', $event === true)\n \"\n label=\"Include headers\"\n />\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"config()['delimiter'] ?? ','\"\n (ngModelChange)=\"onConfigFieldChange('delimiter', $event)\"\n label=\"Delimiter\"\n [required]=\"isConfigFieldRequired('delimiter')\"\n />\n <mt-text-field\n [ngModel]=\"config()['encoding'] ?? 'utf-8'\"\n [readonly]=\"true\"\n label=\"Encoding\"\n />\n </div>\n }\n\n @if (convertToFileIsJson()) {\n <div class=\"mt-4 grid gap-3 md:grid-cols-2\">\n <mt-toggle-field\n size=\"small\"\n labelPosition=\"end\"\n [ngModel]=\"config()['pretty'] !== false\"\n (ngModelChange)=\"\n onConfigFieldChange('pretty', $event === true)\n \"\n label=\"Pretty JSON\"\n />\n </div>\n }\n\n @if (convertToFileIsHtml()) {\n <div class=\"mt-4 grid gap-3\">\n <mt-text-field\n [ngModel]=\"config()['title'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:title')\"\n (ngModelChange)=\"onConfigFieldChange('title', $event)\"\n label=\"Title\"\n [required]=\"isConfigFieldRequired('title')\"\n />\n <mt-textarea-field\n class=\"w-full\"\n [ngModel]=\"config()['body'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:body')\"\n (ngModelChange)=\"onConfigFieldChange('body', $event)\"\n label=\"Body\"\n [required]=\"isConfigFieldRequired('body')\"\n rows=\"7\"\n />\n <mt-toggle-field\n size=\"small\"\n labelPosition=\"end\"\n [ngModel]=\"config()['sanitize'] !== false\"\n (ngModelChange)=\"\n onConfigFieldChange('sanitize', $event === true)\n \"\n label=\"Sanitize HTML\"\n />\n </div>\n }\n\n @if (convertToFileIsIcs()) {\n <div class=\"mt-4 grid gap-3 md:grid-cols-2\">\n <mt-text-field\n [ngModel]=\"config()['summary'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:summary')\"\n (ngModelChange)=\"onConfigFieldChange('summary', $event)\"\n label=\"Event summary\"\n [required]=\"isConfigFieldRequired('summary')\"\n />\n <mt-text-field\n [ngModel]=\"config()['timezone'] ?? 'UTC'\"\n (ngModelChange)=\"onConfigFieldChange('timezone', $event)\"\n label=\"Timezone\"\n [required]=\"isConfigFieldRequired('timezone')\"\n />\n <mt-date-field\n [ngModel]=\"config()['start'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('start', $event)\"\n label=\"Start\"\n [required]=\"isConfigFieldRequired('start')\"\n [showTime]=\"true\"\n [showClear]=\"true\"\n [pInputs]=\"dateTimePickerInputs\"\n />\n <mt-date-field\n [ngModel]=\"config()['end'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('end', $event)\"\n label=\"End\"\n [required]=\"isConfigFieldRequired('end')\"\n [showTime]=\"true\"\n [showClear]=\"true\"\n [pInputs]=\"dateTimePickerInputs\"\n />\n <mt-text-field\n [ngModel]=\"config()['location'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:location')\"\n (ngModelChange)=\"onConfigFieldChange('location', $event)\"\n label=\"Location\"\n [required]=\"isConfigFieldRequired('location')\"\n />\n <mt-textarea-field\n class=\"w-full\"\n [ngModel]=\"config()['description'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:description')\"\n (ngModelChange)=\"onConfigFieldChange('description', $event)\"\n label=\"Description\"\n [required]=\"isConfigFieldRequired('description')\"\n rows=\"4\"\n />\n </div>\n }\n\n @if (convertToFileIsSpreadsheet()) {\n <div\n class=\"mt-4 rounded-md border border-(--p-content-border-color) bg-(--p-surface-50) p-3\"\n >\n <div class=\"mb-3 flex items-center justify-between gap-3\">\n <div class=\"fp-ae-label\">Sheets</div>\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n icon=\"general.plus\"\n label=\"Add sheet\"\n (onClick)=\"addConvertToFileSheet()\"\n />\n </div>\n <div class=\"grid gap-3\">\n @for (sheet of convertToFileSheetRows(); track sheet.index) {\n <div\n class=\"rounded-md border border-(--p-content-border-color) bg-(--p-content-background) p-3\"\n >\n <div\n class=\"grid gap-3 md:grid-cols-[minmax(140px,0.45fr)_minmax(0,1fr)_minmax(180px,0.6fr)_auto]\"\n >\n <mt-text-field\n [ngModel]=\"sheet.name\"\n (ngModelChange)=\"\n updateConvertToFileSheet(\n sheet.index,\n 'name',\n $event\n )\n \"\n label=\"Sheet name\"\n [required]=\"isConfigFieldRequired('sheets')\"\n />\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"sheet.sourceExpression\"\n (focusin)=\"\n setExpressionTarget(\n 'config:sheets:' + sheet.index + ':source'\n )\n \"\n (ngModelChange)=\"\n updateConvertToFileSheet(\n sheet.index,\n 'sourceExpression',\n $event\n )\n \"\n label=\"Source\"\n [required]=\"isConfigFieldRequired('sheets')\"\n />\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"sheet.columnsText\"\n (ngModelChange)=\"\n updateConvertToFileSheet(\n sheet.index,\n 'columns',\n $event\n )\n \"\n label=\"Columns\"\n [required]=\"isConfigFieldRequired('sheets')\"\n />\n <div class=\"flex items-end justify-end\">\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n severity=\"danger\"\n icon=\"general.trash-01\"\n tooltip=\"Remove sheet\"\n (onClick)=\"removeConvertToFileSheet(sheet.index)\"\n />\n </div>\n </div>\n </div>\n } @empty {\n <div\n class=\"rounded-md bg-(--p-content-background) px-3 py-2 text-[12px] text-(--p-text-muted-color)\"\n >\n Single sheet uses the selected data source.\n </div>\n }\n </div>\n </div>\n }\n\n @if (convertToFileIsText()) {\n <div class=\"mt-4 grid gap-3\">\n <mt-textarea-field\n class=\"w-full\"\n [ngModel]=\"config()['content'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:content')\"\n (ngModelChange)=\"onConfigFieldChange('content', $event)\"\n label=\"Content\"\n [required]=\"isConfigFieldRequired('content')\"\n rows=\"7\"\n />\n <mt-text-field\n [ngModel]=\"config()['encoding'] ?? 'utf-8'\"\n [readonly]=\"true\"\n label=\"Encoding\"\n />\n </div>\n }\n\n @if (convertToFileIsBase64()) {\n <div class=\"mt-4 grid gap-3 md:grid-cols-2\">\n <mt-textarea-field\n class=\"w-full font-mono md:col-span-2\"\n [ngModel]=\"config()['base64Expression'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:base64Expression')\"\n (ngModelChange)=\"\n onConfigFieldChange('base64Expression', $event)\n \"\n label=\"Base64 source\"\n [required]=\"isConfigFieldRequired('base64Expression')\"\n rows=\"5\"\n />\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"config()['extension'] ?? convertToFileExtension()\"\n (ngModelChange)=\"onConfigFieldChange('extension', $event)\"\n label=\"Extension\"\n [required]=\"isConfigFieldRequired('extension')\"\n />\n </div>\n }\n </section>\n }\n }\n @case (\"DataTransform\") {\n @if (sectionInMain(\"dataTransform\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">\n {{ dataTransformActionLabel() }}\n </div>\n @if (dataTransformUnavailableReason(); as reason) {\n <div\n class=\"mb-3 rounded-md border border-(--p-content-border-color) bg-(--p-surface-50) px-3 py-2 text-[12px] leading-5 text-(--p-text-muted-color)\"\n >\n {{ reason }}\n </div>\n }\n\n @if (\n !dataTransformIsMerge() && dataTransformUsesKnownActionEditor()\n ) {\n <div class=\"grid gap-3\">\n <div class=\"grid gap-3 md:grid-cols-[minmax(220px,0.45fr)_1fr]\">\n <mt-select-field\n [ngModel]=\"dataTransformSourcePreset()\"\n (ngModelChange)=\"onDataTransformSourcePresetChange($event)\"\n [label]=\"dataTransformSourceLabel()\"\n hint=\"Choose which list this action should transform.\"\n [required]=\"isConfigFieldRequired('sourceExpression')\"\n [options]=\"dataTransformSourceOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n @if (showDataTransformSourceExpression()) {\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"dataTransformSourceExpression()\"\n (focusin)=\"setExpressionTarget('config:sourceExpression')\"\n (ngModelChange)=\"\n onDataTransformSourceExpressionChange($event)\n \"\n [label]=\"dataTransformSourceExpressionLabel()\"\n hint=\"Must resolve to an array of items.\"\n [required]=\"isConfigFieldRequired('sourceExpression')\"\n />\n } @else {\n <div\n class=\"rounded-md border border-surface-200 bg-surface-50 px-3 py-2\"\n >\n <div class=\"fp-ae-label mb-1\">Selected input</div>\n <div\n class=\"truncate font-mono text-sm text-(--p-text-color)\"\n >\n {{ dataTransformSourceSummary() }}\n </div>\n </div>\n }\n </div>\n <div class=\"flex flex-wrap gap-2\">\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n icon=\"general.search-sm\"\n label=\"Preview source\"\n [disabled]=\"!dataTransformSourceExpression()\"\n (onClick)=\"previewDataTransformSourceExpression()\"\n />\n </div>\n </div>\n }\n\n @if (expressionPreview(); as preview) {\n <div\n class=\"mt-3 rounded-md border border-(--p-content-border-color) bg-(--p-surface-50) p-3\"\n >\n <div class=\"fp-ae-label mb-2\">Source preview</div>\n <pre class=\"fp-ae-code\">{{ schemaText(preview) }}</pre>\n </div>\n }\n\n @if (dataTransformIsFilter()) {\n <div class=\"mt-4 grid gap-3\">\n @if (dataTransformUsesStructuredConditionEditor()) {\n <div class=\"fp-ae-label\">Keep item when</div>\n <div class=\"grid gap-3 md:grid-cols-[1fr_180px_1fr]\">\n <mt-text-field\n [ngModel]=\"dataTransformConditionFieldText('left')\"\n (focusin)=\"setExpressionTarget('config:condition:left')\"\n (ngModelChange)=\"\n onDataTransformConditionFieldChange('left', $event)\n \"\n label=\"Left value\"\n hint=\"Field, value, or expression from each item.\"\n [required]=\"isConfigFieldRequired('condition.left')\"\n />\n <mt-select-field\n [ngModel]=\"\n dataTransformConditionFieldValue('operator') || 'equals'\n \"\n (ngModelChange)=\"\n onDataTransformConditionFieldChange('operator', $event)\n \"\n label=\"Operator\"\n [required]=\"isConfigFieldRequired('condition.operator')\"\n [options]=\"ifOperatorOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-text-field\n [ngModel]=\"dataTransformConditionFieldText('right')\"\n (focusin)=\"setExpressionTarget('config:condition:right')\"\n (ngModelChange)=\"\n onDataTransformConditionFieldChange('right', $event)\n \"\n label=\"Right value\"\n hint=\"Value or expression to compare against.\"\n [required]=\"isConfigFieldRequired('condition.right')\"\n />\n </div>\n } @else {\n <mt-textarea-field\n class=\"w-full font-mono\"\n [ngModel]=\"dataTransformConditionText()\"\n (focusin)=\"setExpressionTarget('config:condition')\"\n (ngModelChange)=\"onDataTransformConditionChange($event)\"\n label=\"Keep item when\"\n hint=\"Condition expression or JSON supplied by the backend catalog.\"\n [required]=\"isConfigFieldRequired('condition')\"\n rows=\"4\"\n />\n }\n </div>\n }\n\n @if (dataTransformIsSort()) {\n <div class=\"mt-4 grid gap-3 md:grid-cols-2\">\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"config()['sortBy'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:sortBy')\"\n (ngModelChange)=\"onConfigFieldChange('sortBy', $event)\"\n label=\"Sort by field\"\n hint=\"Field path inside each item, for example createdAt.\"\n [required]=\"isConfigFieldRequired('sortBy')\"\n />\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"config()['sortExpression'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:sortExpression')\"\n (ngModelChange)=\"\n onConfigFieldChange('sortExpression', $event)\n \"\n label=\"Custom sort expression\"\n hint=\"Optional expression when a simple field path is not enough.\"\n [required]=\"isConfigFieldRequired('sortExpression')\"\n />\n <mt-select-field\n [ngModel]=\"config()['direction'] ?? 'asc'\"\n (ngModelChange)=\"onConfigFieldChange('direction', $event)\"\n label=\"Direction\"\n [required]=\"isConfigFieldRequired('direction')\"\n [options]=\"dataTransformDirectionOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-select-field\n [ngModel]=\"config()['valueType'] ?? 'string'\"\n (ngModelChange)=\"onConfigFieldChange('valueType', $event)\"\n label=\"Value type\"\n [required]=\"isConfigFieldRequired('valueType')\"\n [options]=\"dataTransformValueTypeOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n </div>\n }\n\n @if (dataTransformIsLimit()) {\n <div class=\"mt-4 grid gap-3 md:grid-cols-2\">\n <mt-number-field\n [ngModel]=\"numberValue(config()['offset']) ?? 0\"\n (ngModelChange)=\"onConfigFieldChange('offset', $event)\"\n label=\"Skip first\"\n [required]=\"isConfigFieldRequired('offset')\"\n [min]=\"0\"\n />\n <mt-number-field\n [ngModel]=\"numberValue(config()['limit'])\"\n (ngModelChange)=\"onConfigFieldChange('limit', $event)\"\n label=\"Take at most\"\n [required]=\"isConfigFieldRequired('limit')\"\n [min]=\"1\"\n />\n </div>\n }\n\n @if (dataTransformIsMapFields()) {\n <div class=\"mt-4 grid gap-3\">\n <mt-select-field\n [ngModel]=\"config()['mode'] ?? 'merge'\"\n (ngModelChange)=\"onConfigFieldChange('mode', $event)\"\n label=\"Field mode\"\n [required]=\"isConfigFieldRequired('mode')\"\n [options]=\"dataTransformMapModeOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <ng-container\n *ngTemplateOutlet=\"\n mapEditor;\n context: {\n objectKey: 'fields',\n rows: fieldsRows(),\n keyLabel: 'Output field',\n valueLabel: 'Value or expression',\n addLabel: 'Add mapped field',\n emptyText:\n 'No fields mapped yet. Add an output field to create or update item data.',\n keyHint: 'Name of the field in the output item.',\n valueHint:\n 'Use a fixed value or an expression from each item.',\n required: isConfigFieldRequired('fields'),\n }\n \"\n />\n </div>\n }\n\n @if (dataTransformIsAggregate()) {\n <div class=\"mt-4 grid gap-3\">\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"config()['groupBy'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:groupBy')\"\n (ngModelChange)=\"onConfigFieldChange('groupBy', $event)\"\n label=\"Group items by\"\n hint=\"Optional field path or expression used to group results.\"\n [required]=\"isConfigFieldRequired('groupBy')\"\n />\n <div class=\"rounded-md border border-surface-200 p-3\">\n <div\n class=\"mb-3 flex flex-wrap items-center justify-between gap-2\"\n >\n <div class=\"fp-ae-label\">Summary fields</div>\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n icon=\"general.plus\"\n label=\"Add summary\"\n (onClick)=\"addDataTransformAggregation()\"\n />\n </div>\n <div class=\"space-y-3\">\n @for (\n item of dataTransformAggregationRows();\n track item.index\n ) {\n <div\n class=\"grid gap-2 rounded-md bg-surface-50 p-3 md:grid-cols-[minmax(160px,0.8fr)_minmax(180px,0.9fr)_1fr_minmax(150px,0.7fr)_auto]\"\n >\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"item.key\"\n (ngModelChange)=\"\n updateDataTransformAggregation(\n item.index,\n 'key',\n $event\n )\n \"\n label=\"Output field\"\n hint=\"Name saved in the summary output.\"\n [required]=\"isConfigFieldRequired('aggregations')\"\n />\n <mt-select-field\n [ngModel]=\"item.operation\"\n (ngModelChange)=\"\n updateDataTransformAggregation(\n item.index,\n 'operation',\n $event\n )\n \"\n label=\"Calculation\"\n [required]=\"isConfigFieldRequired('aggregations')\"\n [options]=\"dataTransformAggregationOperationOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"item.field\"\n [readonly]=\"\n !dataTransformAggregationNeedsField(item.operation)\n \"\n (focusin)=\"\n setExpressionTarget(\n 'config:aggregations:' + item.index + ':field'\n )\n \"\n (ngModelChange)=\"\n updateDataTransformAggregation(\n item.index,\n 'field',\n $event\n )\n \"\n label=\"Field or expression\"\n [hint]=\"\n dataTransformAggregationFieldHint(item.operation)\n \"\n [required]=\"\n isConfigFieldRequired('aggregations') &&\n dataTransformAggregationNeedsField(item.operation)\n \"\n />\n <mt-select-field\n [ngModel]=\"item.valueType\"\n (ngModelChange)=\"\n updateDataTransformAggregation(\n item.index,\n 'valueType',\n $event\n )\n \"\n label=\"Value type\"\n [required]=\"isConfigFieldRequired('aggregations')\"\n [options]=\"dataTransformValueTypeOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-button\n class=\"self-end\"\n size=\"small\"\n severity=\"secondary\"\n variant=\"outlined\"\n label=\"Remove\"\n (onClick)=\"removeDataTransformAggregation(item.index)\"\n />\n </div>\n } @empty {\n <div\n class=\"rounded-md bg-(--p-content-background) px-3 py-2 text-[12px] text-(--p-text-muted-color)\"\n >\n No summary fields yet. Add a summary such as count,\n total, average, minimum, or maximum.\n </div>\n }\n </div>\n </div>\n </div>\n }\n\n @if (dataTransformIsMerge()) {\n <div class=\"mt-4 grid gap-3 md:grid-cols-2\">\n <mt-textarea-field\n class=\"w-full font-mono md:col-span-2\"\n [ngModel]=\"config()['leftExpression'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:leftExpression')\"\n (ngModelChange)=\"\n onConfigFieldChange('leftExpression', $event)\n \"\n label=\"First list\"\n hint=\"Expression that resolves to the first list of items.\"\n [required]=\"isConfigFieldRequired('leftExpression')\"\n rows=\"3\"\n />\n <mt-textarea-field\n class=\"w-full font-mono md:col-span-2\"\n [ngModel]=\"config()['rightExpression'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:rightExpression')\"\n (ngModelChange)=\"\n onConfigFieldChange('rightExpression', $event)\n \"\n label=\"Second list\"\n hint=\"Expression that resolves to the second list of items.\"\n [required]=\"isConfigFieldRequired('rightExpression')\"\n rows=\"3\"\n />\n <mt-select-field\n [ngModel]=\"config()['strategy'] ?? 'append'\"\n (ngModelChange)=\"onConfigFieldChange('strategy', $event)\"\n label=\"Merge strategy\"\n [required]=\"isConfigFieldRequired('strategy')\"\n [options]=\"dataTransformMergeStrategyOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n @if (dataTransformUsesKeyedMerge()) {\n <div class=\"grid gap-3 md:grid-cols-2\">\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"config()['leftKey'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('leftKey', $event)\"\n label=\"First list key\"\n [required]=\"isConfigFieldRequired('leftKey')\"\n />\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"config()['rightKey'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('rightKey', $event)\"\n label=\"Second list key\"\n [required]=\"isConfigFieldRequired('rightKey')\"\n />\n </div>\n }\n </div>\n }\n\n @if (!dataTransformUsesKnownActionEditor()) {\n <div class=\"mt-4 grid gap-3\">\n @for (field of dataTransformGenericFields(); track field.key) {\n @if (field.enumValues.length) {\n <mt-select-field\n [ngModel]=\"config()[field.key] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange(field.key, $event)\"\n [label]=\"field.label\"\n [hint]=\"field.description ?? ''\"\n [required]=\"isConfigFieldRequired(field.key)\"\n [options]=\"enumOptions(field.enumValues)\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n } @else if (field.type === \"number\") {\n <mt-number-field\n [ngModel]=\"numberValue(config()[field.key])\"\n (ngModelChange)=\"onConfigFieldChange(field.key, $event)\"\n [label]=\"field.label\"\n [hint]=\"field.description ?? ''\"\n [required]=\"isConfigFieldRequired(field.key)\"\n />\n } @else {\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"fieldText(field.key)\"\n (focusin)=\"setExpressionTarget('config:' + field.key)\"\n (ngModelChange)=\"onConfigFieldChange(field.key, $event)\"\n [label]=\"field.label\"\n [hint]=\"field.description ?? ''\"\n [required]=\"isConfigFieldRequired(field.key)\"\n />\n }\n } @empty {\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"dataTransformActionKey()\"\n [readonly]=\"true\"\n label=\"Action\"\n />\n }\n </div>\n }\n\n <div class=\"mt-4 grid gap-3 md:grid-cols-3\">\n <mt-number-field\n [ngModel]=\"numberValue(config()['maxOutputItems'])\"\n (ngModelChange)=\"onConfigFieldChange('maxOutputItems', $event)\"\n label=\"Output limit\"\n [required]=\"isConfigFieldRequired('maxOutputItems')\"\n [min]=\"1\"\n />\n <div\n class=\"rounded-md border border-(--p-content-border-color) bg-(--p-surface-50) p-3 md:col-span-2\"\n >\n <div class=\"fp-ae-label mb-2\">Output</div>\n <div class=\"flex flex-wrap gap-1.5\">\n @for (field of dataTransformOutputFieldLabels; track field) {\n <span\n class=\"rounded-md bg-(--p-surface-100) px-2 py-1 font-mono text-[11px]\"\n >{{ field }}</span\n >\n }\n </div>\n </div>\n </div>\n </section>\n }\n }\n @case (\"LoopOverItems\") {\n @if (sectionInMain(\"loopOverItems\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Loop over items</div>\n\n <div class=\"grid gap-3\">\n <div class=\"grid gap-3 md:grid-cols-[minmax(220px,0.45fr)_1fr]\">\n <mt-select-field\n [ngModel]=\"loopSourceMode()\"\n (ngModelChange)=\"onLoopSourceModeChange($event)\"\n label=\"List source\"\n hint=\"Choose an output array or type an array expression.\"\n [required]=\"isConfigFieldRequired('itemsExpression')\"\n [options]=\"loopSourceModeOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n @if (showLoopItemsExpression()) {\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"loopItemsExpression()\"\n (focusin)=\"setExpressionTarget('config:itemsExpression')\"\n (ngModelChange)=\"onLoopItemsExpressionChange($event)\"\n label=\"Array expression\"\n hint=\"Must resolve to an array.\"\n [required]=\"isConfigFieldRequired('itemsExpression')\"\n />\n } @else {\n <mt-select-field\n [ngModel]=\"loopSelectedOutputArray()\"\n (ngModelChange)=\"onLoopOutputArrayChange($event)\"\n label=\"Output array\"\n hint=\"Arrays exposed by previous connected steps.\"\n [required]=\"isConfigFieldRequired('itemsExpression')\"\n [options]=\"loopOutputArrayOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n }\n </div>\n <div class=\"flex flex-wrap gap-2\">\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n icon=\"general.search-sm\"\n label=\"Preview array\"\n [disabled]=\"!loopItemsExpression()\"\n (onClick)=\"previewLoopItemsExpression()\"\n />\n </div>\n </div>\n\n @if (expressionPreview(); as preview) {\n <div\n class=\"mt-3 rounded-md border border-(--p-content-border-color) bg-(--p-surface-50) p-3\"\n >\n <div class=\"fp-ae-label mb-2\">Expression preview</div>\n <pre class=\"fp-ae-code\">{{ schemaText(preview) }}</pre>\n </div>\n }\n\n <div class=\"mt-4 border-t border-surface-100 pt-4\">\n <div class=\"fp-ae-label mb-3\">Run settings</div>\n <div class=\"grid gap-3 md:grid-cols-2 xl:grid-cols-4\">\n <mt-number-field\n [ngModel]=\"\n numberValue(\n config()['maxItems'] ?? config()['maxIterations']\n )\n \"\n (ngModelChange)=\"onConfigFieldChange('maxItems', $event)\"\n label=\"Item limit\"\n hint=\"Optional maximum number of items to process.\"\n [required]=\"isConfigFieldRequired('maxItems')\"\n [min]=\"1\"\n />\n <mt-select-field\n [ngModel]=\"loopFailureBehavior()\"\n (ngModelChange)=\"onLoopFailureBehaviorChange($event)\"\n label=\"When an item fails\"\n [required]=\"isConfigFieldRequired('failureBehavior')\"\n [options]=\"loopFailureBehaviorOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-select-field\n [ngModel]=\"config()['emptyBehavior'] ?? 'done'\"\n (ngModelChange)=\"onConfigFieldChange('emptyBehavior', $event)\"\n label=\"When list is empty\"\n [required]=\"isConfigFieldRequired('emptyBehavior')\"\n [options]=\"loopEmptyBehaviorOptions\"\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()['collectResults'] === true\"\n (ngModelChange)=\"\n onConfigFieldChange('collectResults', $event === true)\n \"\n label=\"Save item results\"\n hint=\"Stores a bounded summary for the done route.\"\n />\n </div>\n </div>\n\n <details class=\"mt-4 border-t border-surface-100 pt-3\">\n <summary\n class=\"cursor-pointer text-sm font-semibold text-(--p-text-color)\"\n >\n Advanced runtime details\n </summary>\n <div class=\"mt-3 grid gap-3 md:grid-cols-3\">\n <div>\n <div class=\"fp-ae-label mb-1\">Current item token</div>\n <div\n class=\"rounded-md border border-surface-200 bg-surface-50 px-3 py-2 font-mono text-sm text-(--p-text-color)\"\n >\n $item.json\n </div>\n </div>\n <div>\n <div class=\"fp-ae-label mb-1\">Loop context token</div>\n <div\n class=\"rounded-md border border-surface-200 bg-surface-50 px-3 py-2 font-mono text-sm text-(--p-text-color)\"\n >\n $loop.current\n </div>\n </div>\n <mt-number-field\n [ngModel]=\"numberValue(config()['concurrency']) ?? 1\"\n (ngModelChange)=\"onLoopConcurrencyChange($event)\"\n label=\"Concurrency\"\n hint=\"This deployment supports 1.\"\n [required]=\"isConfigFieldRequired('concurrency')\"\n [min]=\"1\"\n [max]=\"1\"\n />\n </div>\n </details>\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\n does 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 [required]=\"isConfigFieldRequired('targetModule')\"\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 [required]=\"isConfigFieldRequired('operation')\"\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 [required]=\"isConfigFieldRequired('idempotencyKey')\"\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\n >{{ field.viewType ?? \"Value\" }}\n @if (field.required) {\n *\n }\n </strong>\n </div>\n }\n </div>\n </div>\n }\n <div class=\"mt-3\">\n <ng-container\n *ngTemplateOutlet=\"\n mapEditor;\n context: {\n objectKey: 'mapping',\n rows: mappingRows(),\n keyLabel: 'Module field',\n valueLabel: 'Expression / value',\n addLabel: 'Add module field',\n required: isConfigFieldRequired('mapping'),\n }\n \"\n />\n </div>\n <div class=\"mt-3 flex gap-2\">\n <mt-button\n size=\"small\"\n severity=\"primary\"\n label=\"Validate mapping\"\n (onClick)=\"validateCommitMapping()\"\n />\n </div>\n @if (commitValidation(); as result) {\n <div\n class=\"mt-3 rounded-lg bg-(--p-surface-100) px-3 py-2 text-[12px]\"\n >\n {{\n result.isValid === false\n ? \"Mapping invalid\"\n : \"Mapping accepted by helper\"\n }}\n @for (issue of resultIssues(result); track $index) {\n <div class=\"mt-1 text-[11px] text-(--p-text-muted-color)\">\n {{ issue }}\n </div>\n }\n </div>\n }\n @if (sectionInMain(\"credentials\")) {\n <div\n class=\"mt-3 rounded-md border border-surface-200 bg-surface-0 p-3\"\n >\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 [required]=\"isConfigFieldRequired('statusCode')\"\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 [required]=\"isConfigFieldRequired('responseMode')\"\n [options]=\"responseModeOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n </div>\n <div class=\"mt-3\">\n <ng-container\n *ngTemplateOutlet=\"\n mapEditor;\n context: {\n objectKey: 'headers',\n rows: headerRows(),\n keyLabel: 'Header',\n valueLabel: 'Value',\n addLabel: 'Add header',\n }\n \"\n />\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 [required]=\"isConfigFieldRequired('body')\"\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 [required]=\"isConfigFieldRequired('targetAutomationId')\"\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 [required]=\"isConfigFieldRequired('revisionMode')\"\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)=\"\n onConfigFieldChange('specificRevisionId', $event)\n \"\n label=\"Specific revision id\"\n hint=\"Required only when revision mode is Specific revision.\"\n [required]=\"isConfigFieldRequired('specificRevisionId')\"\n />\n }\n <mt-toggle-field\n size=\"small\"\n labelPosition=\"end\"\n [ngModel]=\"config()['waitForCompletion'] !== false\"\n (ngModelChange)=\"\n onConfigFieldChange('waitForCompletion', $event === true)\n \"\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)=\"\n onConfigFieldChange('inputMappingJson', $event)\n \"\n label=\"Input mapping JSON\"\n hint=\"JSON object or expression map sent into the child automation input.\"\n [required]=\"isConfigFieldRequired('inputMappingJson')\"\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)=\"\n onConfigFieldChange('outputMappingJson', $event)\n \"\n label=\"Output mapping JSON\"\n hint=\"JSON object or expression map for child output/status returned to the parent.\"\n [required]=\"isConfigFieldRequired('outputMappingJson')\"\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 [required]=\"isConfigFieldRequired('targetAutomationId')\"\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 [required]=\"isConfigFieldRequired('revisionMode')\"\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)=\"\n onConfigFieldChange('specificRevisionId', $event)\n \"\n label=\"Specific revision id\"\n hint=\"Required only when revision mode is Specific revision.\"\n [required]=\"isConfigFieldRequired('specificRevisionId')\"\n />\n }\n <mt-toggle-field\n size=\"small\"\n labelPosition=\"end\"\n [ngModel]=\"config()['waitForCompletion'] !== false\"\n (ngModelChange)=\"\n onConfigFieldChange('waitForCompletion', $event === true)\n \"\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)=\"\n onConfigFieldChange('inputMappingJson', $event)\n \"\n label=\"Input mapping JSON\"\n hint=\"JSON object or expression map sent into the child automation input.\"\n [required]=\"isConfigFieldRequired('inputMappingJson')\"\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)=\"\n onConfigFieldChange('outputMappingJson', $event)\n \"\n label=\"Output mapping JSON\"\n hint=\"JSON object or expression map for child output/status returned to the parent.\"\n [required]=\"isConfigFieldRequired('outputMappingJson')\"\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\n class=\"flex items-center justify-between gap-3 border-b border-surface-200 bg-surface-50 px-4 py-3\"\n >\n <div class=\"fp-ae-section-title !m-0\">Parallel start</div>\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n icon=\"general.plus\"\n label=\"Add branch\"\n (onClick)=\"addParallelBranch()\"\n />\n </div>\n <div class=\"space-y-3 p-4\">\n @for (\n branch of parallelBranches();\n track branch.key;\n let i = $index\n ) {\n <div\n class=\"rounded-md border border-surface-200 bg-surface-0 p-3\"\n >\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 [required]=\"isConfigFieldRequired('branches')\"\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 [required]=\"isConfigFieldRequired('branches')\"\n />\n <mt-text-field\n [ngModel]=\"branch.description\"\n (ngModelChange)=\"\n updateParallelBranch(i, 'description', $event)\n \"\n label=\"Description\"\n hint=\"Optional branch note for reviewers.\"\n [required]=\"false\"\n />\n <div class=\"flex items-end gap-1\">\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n icon=\"arrow.arrow-up\"\n tooltip=\"Move up\"\n (onClick)=\"moveParallelBranch(i, -1)\"\n />\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n icon=\"arrow.arrow-down\"\n tooltip=\"Move down\"\n (onClick)=\"moveParallelBranch(i, 1)\"\n />\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n severity=\"danger\"\n icon=\"general.trash-01\"\n tooltip=\"Remove branch\"\n (onClick)=\"removeParallelBranch(i)\"\n />\n </div>\n </div>\n <div\n class=\"mt-3 flex flex-wrap items-center gap-1.5 text-[11px]\"\n >\n <span\n 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)\"\n >{{ branch.key }}</span\n >\n <span\n 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)\"\n >{{ branch.routeCount }} connected route{{\n branch.routeCount === 1 ? \"\" : \"s\"\n }}</span\n >\n </div>\n </div>\n }\n @if (parallelBranches().length === 0) {\n <p class=\"fp-ae-copy\">\n Add at least two stable branch keys. Backend validation blocks\n publish until branches are valid.\n </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 [required]=\"isConfigFieldRequired('joinPolicy')\"\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 [required]=\"isConfigFieldRequired('threshold')\"\n [min]=\"1\"\n />\n }\n <mt-select-field\n [ngModel]=\"config()['aggregationStrategy'] ?? 'MergeObjects'\"\n (ngModelChange)=\"\n onConfigFieldChange('aggregationStrategy', $event)\n \"\n label=\"Aggregation strategy\"\n hint=\"How backend joins branch outputs into the join node output.\"\n [required]=\"isConfigFieldRequired('aggregationStrategy')\"\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)=\"\n onConfigFieldChange('outputTargetPath', $event)\n \"\n label=\"Output target path\"\n hint=\"Optional context path where joined output should be written.\"\n [required]=\"isConfigFieldRequired('outputTargetPath')\"\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\n class=\"grid items-end gap-3 md:grid-cols-[minmax(0,1fr)_minmax(280px,0.7fr)]\"\n >\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 [required]=\"isConfigFieldRequired('mode')\"\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)=\"\n onConfigFieldChange('firstMatch', $event === true)\n \"\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]=\"\n showSwitchExpression()\n ? 'font-mono'\n : 'font-mono md:col-span-2'\n \"\n [ngModel]=\"config()['sourceValue'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:sourceValue')\"\n (ngModelChange)=\"\n onConfigFieldChange('sourceValue', $event)\n \"\n label=\"Source value\"\n hint=\"Value or expression used for value matching.\"\n [required]=\"isConfigFieldRequired('sourceValue')\"\n />\n }\n @if (showSwitchExpression()) {\n <mt-text-field\n [class]=\"\n showSwitchSourceValue()\n ? 'font-mono'\n : 'font-mono md:col-span-2'\n \"\n [ngModel]=\"config()['expression'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:expression')\"\n (ngModelChange)=\"\n onConfigFieldChange('expression', $event)\n \"\n label=\"Expression\"\n hint=\"Expression evaluated when mode is expression or rules.\"\n [required]=\"isConfigFieldRequired('expression')\"\n />\n }\n </div>\n }\n <div\n class=\"grid items-end gap-3 md:grid-cols-[minmax(0,1fr)_minmax(280px,0.7fr)]\"\n >\n @if (showSwitchDefaultOutputKey()) {\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"config()['defaultOutputKey'] ?? 'default'\"\n (ngModelChange)=\"\n onConfigFieldChange('defaultOutputKey', $event)\n \"\n label=\"Default output key\"\n hint=\"Stable default route key used when no case matches.\"\n [required]=\"isConfigFieldRequired('defaultOutputKey')\"\n />\n }\n <mt-toggle-field\n [class]=\"\n showSwitchDefaultOutputKey()\n ? 'self-end pb-1'\n : 'self-end pb-1 md:col-span-2'\n \"\n size=\"small\"\n labelPosition=\"end\"\n [ngModel]=\"config()['includeDefaultOutput'] !== false\"\n (ngModelChange)=\"\n onConfigFieldChange('includeDefaultOutput', $event === true)\n \"\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\n class=\"flex items-center justify-between gap-3 border-b border-surface-200 bg-surface-50 px-4 py-3\"\n >\n <div class=\"fp-ae-section-title !m-0\">Switch cases</div>\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n icon=\"general.plus\"\n label=\"Add case\"\n (onClick)=\"addSwitchCase()\"\n />\n </div>\n <div class=\"space-y-3 p-4\">\n @for (item of switchCases(); track item.key; let i = $index) {\n <div\n class=\"rounded-md border border-surface-200 bg-surface-0 p-3\"\n >\n <div\n class=\"flex flex-wrap items-start justify-between gap-3 border-b border-surface-100 pb-3\"\n >\n <div class=\"min-w-0\">\n <div class=\"flex flex-wrap items-center gap-1.5\">\n <span\n class=\"text-sm font-semibold text-(--p-text-color)\"\n >Case {{ i + 1 }}</span\n >\n <span\n class=\"inline-flex h-7 items-center rounded-md border border-surface-200 bg-surface-50 px-2 font-mono text-[11px] font-semibold text-(--p-text-color)\"\n >{{ item.routeKey }}</span\n >\n <span\n class=\"inline-flex h-7 items-center rounded-md border border-surface-200 bg-surface-0 px-2 text-[11px] font-medium text-(--p-text-muted-color)\"\n >{{ item.routeCount }} connected route{{\n item.routeCount === 1 ? \"\" : \"s\"\n }}</span\n >\n </div>\n <div\n class=\"mt-1 truncate text-xs font-medium text-(--p-text-muted-color)\"\n >\n {{ item.label || item.routeKey }}\n </div>\n </div>\n <div class=\"flex shrink-0 items-center gap-1\">\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n icon=\"arrow.arrow-up\"\n tooltip=\"Move up\"\n (onClick)=\"moveSwitchCase(i, -1)\"\n />\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n icon=\"arrow.arrow-down\"\n tooltip=\"Move down\"\n (onClick)=\"moveSwitchCase(i, 1)\"\n />\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n severity=\"danger\"\n icon=\"general.trash-01\"\n tooltip=\"Remove case\"\n (onClick)=\"removeSwitchCase(i)\"\n />\n </div>\n </div>\n <div class=\"mt-3 grid gap-3 md:grid-cols-2\">\n <mt-text-field\n [ngModel]=\"item.label\"\n (ngModelChange)=\"updateSwitchCase(i, 'label', $event)\"\n label=\"Case name\"\n hint=\"Shown in route labels.\"\n [required]=\"isConfigFieldRequired('cases')\"\n />\n @if (showSwitchCaseValue()) {\n <mt-text-field\n [ngModel]=\"item.value\"\n (focusin)=\"\n setExpressionTarget(\n 'config:cases:' + item.key + ':value'\n )\n \"\n (ngModelChange)=\"updateSwitchCase(i, 'value', $event)\"\n label=\"Match value\"\n hint=\"Compared with the source value.\"\n [required]=\"isConfigFieldRequired('cases')\"\n />\n }\n @if (showSwitchCaseCondition()) {\n <mt-text-field\n class=\"font-mono md:col-span-2\"\n [ngModel]=\"item.condition\"\n (focusin)=\"\n setExpressionTarget(\n 'config:cases:' + item.key + ':condition'\n )\n \"\n (ngModelChange)=\"\n updateSwitchCase(i, 'condition', $event)\n \"\n label=\"Rule condition\"\n hint=\"When true, this case is selected.\"\n [required]=\"isConfigFieldRequired('cases')\"\n />\n }\n </div>\n <details\n class=\"mt-3 rounded-md border border-surface-200 bg-surface-50 px-3 py-2 text-xs text-(--p-text-muted-color)\"\n >\n <summary class=\"cursor-pointer font-medium\">\n Advanced route settings\n </summary>\n <div class=\"mt-3 grid gap-3 md:grid-cols-2\">\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"item.key\"\n (ngModelChange)=\"updateSwitchCase(i, 'key', $event)\"\n label=\"Route key\"\n hint=\"Stable output key for connected routes.\"\n [required]=\"isConfigFieldRequired('cases')\"\n />\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"item.expression\"\n (focusin)=\"\n setExpressionTarget(\n 'config:cases:' + item.key + ':expression'\n )\n \"\n (ngModelChange)=\"\n updateSwitchCase(i, 'expression', $event)\n \"\n label=\"Advanced condition expression\"\n hint=\"Optional true or false expression for this case.\"\n [required]=\"false\"\n />\n </div>\n <div\n class=\"mt-3 inline-flex h-7 items-center rounded-md border border-surface-200 bg-surface-0 px-2 font-medium\"\n >\n Visual order {{ i + 1 }}\n </div>\n </details>\n </div>\n }\n @if (switchCases().length === 0) {\n <p class=\"fp-ae-copy\">\n Add a case for each route this switch can select.\n </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 (\n supportsConfigKey(\"status\") ||\n supportsConfigKey(\"message\") ||\n supportsConfigKey(\"output\")\n ) {\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 Stop nodes use their backend defaults and do not expose extra\n configuration.\n </p>\n }\n </section>\n }\n }\n }\n\n @if (sectionInAdvanced(\"policy\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">\n Retry, timeout, and failure handling\n </div>\n <div class=\"grid gap-3 md:grid-cols-3\">\n <mt-number-field\n [ngModel]=\"policyNumberValue('timeoutPolicyJson', 'timeoutSeconds')\"\n (ngModelChange)=\"\n updatePolicyNumber('timeoutPolicyJson', 'timeoutSeconds', $event)\n \"\n label=\"Step timeout (seconds)\"\n hint=\"How long this step may run before Automation Engine marks it timed out.\"\n [min]=\"1\"\n />\n <mt-number-field\n [ngModel]=\"policyNumberValue('retryPolicyJson', 'maxAttempts')\"\n (ngModelChange)=\"\n updatePolicyNumber('retryPolicyJson', 'maxAttempts', $event)\n \"\n label=\"Max attempts\"\n hint=\"Total tries including the first run. Use 1 for no retry.\"\n [min]=\"1\"\n />\n <mt-select-field\n [ngModel]=\"errorPolicyMode()\"\n (ngModelChange)=\"updateErrorPolicyMode($event)\"\n label=\"When this step fails\"\n hint=\"Choose whether failures pause for recovery, use the failure path, or fail the workflow.\"\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 count, or\n failure rule here.\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(\"mapping\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Custom data handoff</div>\n <div class=\"grid gap-3 md:grid-cols-2\">\n <div>\n <div class=\"fp-ae-label mb-2\">Inputs sent to this step</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)\">{{\n row.key\n }}</span>\n <mt-text-field\n [ngModel]=\"valueText(row.value)\"\n (focusin)=\"\n setExpressionTarget('config:inputMapping:' + row.key)\n \"\n (ngModelChange)=\"\n updateJsonField('inputMappingJson', row.key, $event)\n \"\n hint=\"Value this step receives from the workflow context.\"\n />\n </label>\n }\n </div>\n <div>\n <div class=\"fp-ae-label mb-2\">Outputs saved for next steps</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)\">{{\n row.key\n }}</span>\n <mt-text-field\n [ngModel]=\"valueText(row.value)\"\n (focusin)=\"\n setExpressionTarget('config:outputMapping:' + row.key)\n \"\n (ngModelChange)=\"\n updateJsonField('outputMappingJson', row.key, $event)\n \"\n hint=\"Value this step exposes to the rest of the workflow.\"\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 Default handoff is active. This step receives the current workflow\n item and exposes its normal output to the next connected step. Add a\n custom handoff only when the business data must be reshaped.\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.\n Triggers 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\">\n Config schema\n </summary>\n <pre class=\"fp-ae-code\">{{ schemaText(configSchema()) }}</pre>\n </details>\n @if (hasTriggerPayloadSchema() && triggerPayloadSchema(); as schema) {\n <details\n class=\"rounded-lg border border-(--p-content-border-color)\"\n >\n <summary\n class=\"cursor-pointer px-3 py-2 text-[12px] font-semibold\"\n >\n Payload schema\n </summary>\n <pre class=\"fp-ae-code\">{{ schemaText(schema) }}</pre>\n </details>\n }\n @if (authPolicySchema()) {\n <details\n class=\"rounded-lg border border-(--p-content-border-color)\"\n >\n <summary\n class=\"cursor-pointer px-3 py-2 text-[12px] font-semibold\"\n >\n Auth policy schema\n </summary>\n <pre class=\"fp-ae-code\">{{ schemaText(authPolicySchema()) }}</pre>\n </details>\n }\n @if (triggerPayloadSample(); as sample) {\n <details\n class=\"rounded-lg border border-(--p-content-border-color)\"\n >\n <summary\n class=\"cursor-pointer px-3 py-2 text-[12px] font-semibold\"\n >\n Payload or request sample\n </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\">\n Config schema\n </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\">\n Input schema\n </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\">\n Output schema\n </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\n class=\"rounded-md bg-(--p-surface-100) px-2 py-1 font-mono text-[11px]\"\n >{{ key }}</span\n >\n }\n @if (routeOutputKeys().length === 0) {\n <span class=\"text-[12px] text-(--p-text-muted-color)\"\n >No outgoing route keys</span\n >\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]=\"\n field.type === 'object' || field.type === 'array'\n \"\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\"\n [hint]=\"field.description ?? ''\"\n [required]=\"isConfigFieldRequired(field.key)\"\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)=\"\n onConfigFieldChange(field.key, $event === true)\n \"\n [label]=\"field.label\"\n [hint]=\"field.description ?? ''\"\n [required]=\"isConfigFieldRequired(field.key)\"\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\"\n [hint]=\"field.description ?? ''\"\n [required]=\"isConfigFieldRequired(field.key)\"\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\"\n [hint]=\"field.description ?? ''\"\n [required]=\"isConfigFieldRequired(field.key)\"\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\"\n [hint]=\"field.description ?? ''\"\n [required]=\"isConfigFieldRequired(field.key)\"\n rows=\"5\"\n />\n } @else {\n <mt-text-field\n [ngModel]=\"fieldText(field.key)\"\n (focusin)=\"\n field.expressionEnabled &&\n setExpressionTarget('config:' + field.key)\n \"\n (ngModelChange)=\"onConfigFieldChange(field.key, $event)\"\n [label]=\"field.label\"\n [hint]=\"field.description ?? ''\"\n [required]=\"isConfigFieldRequired(field.key)\"\n />\n }\n </div>\n }\n </div>\n </details>\n }\n </div>\n</div>\n\n<ng-template #credentialSelector>\n @if (activeCredentialProvider(); as provider) {\n <div\n class=\"mb-4 rounded-lg border border-(--p-content-border-color) bg-(--p-content-background) p-4 shadow-[0_1px_2px_rgba(15,23,42,0.05)]\"\n >\n <div class=\"mb-2 text-[12px] font-bold text-(--p-text-color)\">\n Credential\n </div>\n <div class=\"flex flex-wrap items-center gap-2\">\n @if (provider.connectAvailable) {\n <button\n type=\"button\"\n class=\"inline-flex min-h-10 max-w-full items-center gap-2 rounded-md border border-(--p-content-border-color) bg-(--p-content-background) px-3 py-2 text-[13px] font-bold text-(--p-text-color) shadow-sm transition-[border-color,box-shadow,background] hover:border-(--p-primary-color) hover:shadow-[0_2px_7px_rgba(15,23,42,0.10)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--p-primary-color)\"\n [disabled]=\"credentialOauthState() === 'loading'\"\n [attr.aria-busy]=\"\n credentialOauthState() === 'loading' ? 'true' : null\n \"\n (click)=\"connectCredentialProvider()\"\n >\n <fp-automation-node-icon\n class=\"text-[22px]\"\n [itemKey]=\"provider.providerKey\"\n [displayName]=\"provider.displayName\"\n [metadata]=\"credentialProviderIconMetadata(provider)\"\n />\n <span class=\"truncate\">{{ credentialConnectLabel() }}</span>\n </button>\n @if (provider.manualSetupAvailable) {\n <span class=\"text-[12px] text-(--p-text-muted-color)\">or</span>\n <button\n type=\"button\"\n class=\"rounded-md px-1 py-1 text-[12px] font-semibold text-(--p-text-color) underline decoration-(--p-text-muted-color) underline-offset-4 transition-colors hover:text-(--p-primary-color) focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--p-primary-color)\"\n (click)=\"showManualCredentialSetup()\"\n >\n setup manually\n </button>\n }\n } @else if (provider.manualSetupAvailable) {\n <button\n type=\"button\"\n class=\"inline-flex min-h-10 max-w-full items-center gap-2 rounded-md border border-(--p-content-border-color) bg-(--p-content-background) px-3 py-2 text-[13px] font-bold text-(--p-text-color) shadow-sm transition-[border-color,box-shadow,background] hover:border-(--p-primary-color) hover:shadow-[0_2px_7px_rgba(15,23,42,0.10)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--p-primary-color)\"\n (click)=\"showManualCredentialSetup()\"\n >\n <fp-automation-node-icon\n class=\"text-[22px]\"\n [itemKey]=\"provider.providerKey\"\n [displayName]=\"provider.displayName\"\n [metadata]=\"credentialProviderIconMetadata(provider)\"\n />\n <span class=\"truncate\"\n >Set up\n {{ provider.displayName || humanReviewProviderLabel() }}\n credential</span\n >\n </button>\n }\n </div>\n <div\n class=\"mt-2 flex flex-wrap items-center gap-1.5 text-[11px] text-(--p-text-muted-color)\"\n >\n @if (provider.authModes?.length) {\n @for (mode of provider.authModes; track mode) {\n <span\n class=\"rounded-full bg-(--p-surface-100) px-2 py-0.5 font-semibold\"\n >\n {{ mode }}\n </span>\n }\n }\n @if (provider.requiredScopes?.length) {\n <span class=\"truncate\">\n Scopes: {{ provider.requiredScopes.join(\", \") }}\n </span>\n }\n </div>\n @if (provider.setupInstructions) {\n <p class=\"m-0 mt-2 text-[11.5px] leading-5 text-(--p-text-muted-color)\">\n {{ provider.setupInstructions }}\n </p>\n }\n </div>\n }\n\n @if (manualCredentialOpen()) {\n <div\n class=\"mb-3 rounded-md border border-(--p-content-border-color) bg-(--p-content-background) p-3\"\n >\n <div class=\"grid gap-3 md:grid-cols-2\">\n <mt-text-field\n [ngModel]=\"manualCredentialDisplayName()\"\n (ngModelChange)=\"onManualCredentialDisplayNameChange($event)\"\n label=\"Credential name\"\n />\n <mt-select-field\n [ngModel]=\"manualCredentialType()\"\n (ngModelChange)=\"onManualCredentialTypeChange($event)\"\n label=\"Credential type\"\n [options]=\"credentialTypeOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n </div>\n <div class=\"mt-3 grid gap-3 md:grid-cols-2\">\n @for (field of manualCredentialFields(); track field.key) {\n @if (manualCredentialFieldIsToggle(field)) {\n <mt-toggle-field\n [ngModel]=\"manualCredentialFieldValue(field.key) === 'true'\"\n (ngModelChange)=\"onManualCredentialFieldChange(field.key, $event)\"\n [label]=\"\n manualCredentialFieldLabel(field) + (field.required ? ' *' : '')\n \"\n size=\"small\"\n />\n } @else {\n <mt-text-field\n [ngModel]=\"manualCredentialFieldValue(field.key)\"\n (ngModelChange)=\"onManualCredentialFieldChange(field.key, $event)\"\n [label]=\"\n manualCredentialFieldLabel(field) + (field.required ? ' *' : '')\n \"\n [placeholder]=\"field.placeholder ?? ''\"\n [type]=\"manualCredentialFieldInputType(field)\"\n />\n }\n }\n </div>\n @if (credentialManualError(); as error) {\n <div\n class=\"mt-3 rounded-md border border-(--p-content-border-color) bg-(--p-surface-50) px-3 py-2 text-[12px] leading-5 text-(--p-text-muted-color)\"\n >\n {{ error }}\n </div>\n }\n <div class=\"mt-3 flex justify-end gap-2\">\n <mt-button\n size=\"small\"\n variant=\"text\"\n label=\"Cancel\"\n (onClick)=\"cancelManualCredentialSetup()\"\n />\n <mt-button\n size=\"small\"\n severity=\"primary\"\n label=\"Save credential\"\n [loading]=\"credentialManualState() === 'loading'\"\n [disabled]=\"credentialManualState() === 'loading'\"\n (onClick)=\"createManualCredential()\"\n />\n </div>\n </div>\n }\n\n @if (credentialOptions().length) {\n <div class=\"space-y-2\">\n <mt-select-field\n [ngModel]=\"selectedCredentialRef()\"\n (ngModelChange)=\"selectCredential($event)\"\n label=\"Saved credential\"\n [options]=\"credentialOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n [showClear]=\"true\"\n />\n @for (credential of credentials(); track credential.credentialRef) {\n @if (credential.credentialRef === selectedCredentialRef()) {\n <div\n class=\"rounded-md border border-(--p-content-border-color) bg-(--p-surface-50) px-3 py-2\"\n >\n <div class=\"flex items-center justify-between gap-3\">\n <div class=\"min-w-0\">\n <div class=\"truncate text-[12px] font-semibold\">\n {{ credential.displayName ?? credential.credentialRef }}\n </div>\n <div\n class=\"truncate font-mono text-[11px] text-(--p-text-muted-color)\"\n >\n {{ credential.credentialRef }} /\n {{\n credential.status ??\n (credential.resolved ? \"Resolved\" : \"Unresolved\")\n }}\n </div>\n </div>\n <div class=\"flex flex-none items-center gap-2\">\n <mt-button\n size=\"small\"\n variant=\"text\"\n label=\"Test\"\n [loading]=\"\n credentialTestingRef() === credential.credentialRef\n \"\n [disabled]=\"\n credentialTestingRef() === credential.credentialRef\n \"\n (onClick)=\"testCredential(credential.credentialRef)\"\n />\n @if (activeCredentialProvider()?.connectAvailable) {\n <mt-button\n size=\"small\"\n variant=\"text\"\n label=\"Reconnect\"\n [disabled]=\"credentialOauthState() === 'loading'\"\n (onClick)=\"connectCredentialProvider()\"\n />\n }\n <mt-button\n size=\"small\"\n variant=\"text\"\n severity=\"danger\"\n label=\"Revoke\"\n [loading]=\"credentialRevokeRef() === credential.credentialRef\"\n [disabled]=\"\n credentialRevokeRef() === credential.credentialRef\n \"\n (onClick)=\"revokeCredential(credential.credentialRef)\"\n />\n </div>\n </div>\n @if (credential.maskedFields; as maskedFields) {\n <div\n class=\"mt-2 flex flex-wrap gap-1.5 text-[11px] text-(--p-text-muted-color)\"\n >\n @for (field of maskedFields | keyvalue; track field.key) {\n <span\n class=\"rounded-md bg-(--p-content-background) px-2 py-1 font-mono\"\n >\n {{ field.key }}: {{ field.value }}\n </span>\n }\n </div>\n }\n </div>\n }\n }\n </div>\n } @else if (sectionHasAdvancedEmptyState(\"credentials\")) {\n <p class=\"fp-ae-copy\">\n No saved credential exists for this provider. Connect the provider or set\n it up manually before publishing.\n </p>\n }\n @if (credentialTest(); as test) {\n <div class=\"mt-3 rounded-md bg-(--p-surface-100) px-3 py-2 text-[12px]\">\n {{ test.status ?? (test.succeeded ? \"Succeeded\" : \"Failed\") }}\n @if (test.message) {\n <span>- {{ test.message }}</span>\n }\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 let-emptyText=\"emptyText\"\n let-keyHint=\"keyHint\"\n let-valueHint=\"valueHint\"\n let-required=\"required\"\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)=\"\n updateObjectRow(objectKey, row.key, $event, row.value)\n \"\n [label]=\"keyLabel\"\n [hint]=\"\n keyHint || 'Configuration key saved to the backend JSON object.'\n \"\n [required]=\"required === true\"\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]=\"\n valueHint ||\n 'Configuration value. Use expressions when this field should resolve at runtime.'\n \"\n [required]=\"required === true\"\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 } @empty {\n @if (emptyText) {\n <div\n class=\"rounded-md bg-(--p-content-background) px-3 py-2 text-[12px] text-(--p-text-muted-color)\"\n >\n {{ emptyText }}\n </div>\n }\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.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i1$1.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: RadioCardsField, selector: "mt-radio-cards-field", inputs: ["circle", "label", "hint", "readonly", "required", "color", "size", "optionLabel", "optionValue", "options"] }, { 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: "component", type: AutomationNodeIconComponent, selector: "fp-automation-node-icon", inputs: ["type", "itemKey", "icon", "iconKey", "category", "displayName", "description", "metadata", "defaultConfig", "visualKey"] }, { kind: "directive", type: DropDataDirective, selector: "[fpDropData]", inputs: ["fpDropAutoInsert"], outputs: ["dataDrop", "dataDropEvent"] }, { kind: "pipe", type: i1.KeyValuePipe, name: "keyvalue" }] });
|
|
20287
20287
|
}
|
|
20288
20288
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImport: i0, type: AutomationSmartEditorComponent, decorators: [{
|
|
20289
20289
|
type: Component,
|
|
@@ -20301,7 +20301,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.8", ngImpor
|
|
|
20301
20301
|
ToggleField,
|
|
20302
20302
|
AutomationNodeIconComponent,
|
|
20303
20303
|
...DATA_PILL_DRAG,
|
|
20304
|
-
], 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]=\"true\"\n (dataDropEvent)=\"insertExpressionDrop($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\n class=\"text-[12px] font-semibold text-(--p-text-muted-color)\"\n >\n First node connected\n </div>\n <div\n class=\"truncate text-[14px] font-semibold text-(--p-text-color)\"\n >\n {{ startConnection().label }}\n </div>\n <div\n class=\"flex flex-wrap items-center gap-2 text-[12px] text-(--p-text-muted-color)\"\n >\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\n class=\"m-0 text-[12px] leading-5 text-(--p-text-muted-color)\"\n >\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\n class=\"flex flex-wrap items-center gap-2 text-[12px] text-(--p-text-muted-color)\"\n >\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\n class=\"rounded-lg border border-dashed border-(--p-content-border-color) bg-(--p-surface-50) p-3\"\n >\n <div class=\"text-[14px] font-semibold text-(--p-text-color)\">\n No first node connected\n </div>\n <p\n class=\"m-0 mt-1 text-[12px] leading-5 text-(--p-text-muted-color)\"\n >\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 (\n hasTriggerPayloadSchema() && triggerPayloadSchema();\n as schema\n ) {\n <div\n class=\"rounded-md border border-surface-200 bg-surface-0 p-3\"\n >\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\n class=\"mt-3 rounded-md border border-surface-200 bg-surface-0 p-3\"\n >\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\n 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\n class=\"self-end\"\n size=\"small\"\n variant=\"outlined\"\n label=\"Copy\"\n (onClick)=\"copyWebhookUrl()\"\n />\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>{{\n (setup.requiredHeaders ?? []).join(\", \") || \"-\"\n }}</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\n class=\"mt-3 rounded-lg border border-(--p-content-border-color)\"\n >\n <summary\n class=\"cursor-pointer px-3 py-2 text-[12px] font-semibold\"\n >\n Sample request\n </summary>\n <pre class=\"fp-ae-code\">{{\n schemaText(setup.sampleRequest)\n }}</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 [required]=\"isAuthFieldRequired('mode')\"\n [options]=\"authModeOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-text-field\n [ngModel]=\"authPolicy()['signatureHeaderName'] ?? ''\"\n (ngModelChange)=\"\n onAuthFieldChange('signatureHeaderName', $event)\n \"\n label=\"Signature header\"\n hint=\"Header name that carries the webhook signature when the policy requires signed requests.\"\n [required]=\"isAuthFieldRequired('signatureHeaderName')\"\n />\n <mt-text-field\n [ngModel]=\"authPolicy()['timestampHeaderName'] ?? ''\"\n (ngModelChange)=\"\n onAuthFieldChange('timestampHeaderName', $event)\n \"\n label=\"Timestamp header\"\n hint=\"Header name that carries the request timestamp for replay protection.\"\n [required]=\"isAuthFieldRequired('timestampHeaderName')\"\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 [required]=\"true\"\n [options]=\"formOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-select-field\n [ngModel]=\"\n selectedFormVersionId() || formBinding()?.formVersionId || ''\n \"\n (ngModelChange)=\"onFormVersionChange($event)\"\n label=\"Form version\"\n hint=\"Persist the exact backend formVersionId. Do not type or generate IDs manually.\"\n [required]=\"true\"\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\n size=\"small\"\n severity=\"primary\"\n label=\"Save binding\"\n (onClick)=\"saveFormBinding()\"\n />\n @if (formBinding()) {\n <span\n class=\"rounded-lg bg-(--p-surface-100) px-2 py-1 font-mono text-[11px] text-(--p-text-muted-color)\"\n >\n {{ formBinding()!.formVersionId }}\n </span>\n }\n </div>\n @if (formSchema(); as schema) {\n <details\n class=\"mt-3 rounded-lg border border-(--p-content-border-color)\"\n >\n <summary\n class=\"cursor-pointer px-3 py-2 text-[12px] font-semibold\"\n >\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 [required]=\"true\"\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 [required]=\"isConfigFieldRequired('timezone')\"\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 [required]=\"showScheduleCron()\"\n />\n }\n @if (showScheduleInterval()) {\n <mt-number-field\n [ngModel]=\"numberValue(config()['intervalSeconds'])\"\n (ngModelChange)=\"\n onConfigFieldChange('intervalSeconds', $event)\n \"\n label=\"Interval seconds\"\n hint=\"Repeat interval in seconds.\"\n [required]=\"showScheduleInterval()\"\n [min]=\"0\"\n />\n }\n @if (showScheduleOnce()) {\n <mt-date-field\n class=\"md:col-span-2\"\n [ngModel]=\"\n config()['runAt'] ??\n config()['runAtUtc'] ??\n config()['oneTimeAt'] ??\n config()['at'] ??\n ''\n \"\n (ngModelChange)=\"onConfigFieldChange('runAt', $event)\"\n label=\"Run at UTC\"\n hint=\"UTC date/time for the one-time schedule.\"\n [required]=\"showScheduleOnce()\"\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 [required]=\"isConfigFieldRequired('startDate')\"\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 [required]=\"isConfigFieldRequired('misfirePolicy')\"\n [options]=\"misfirePolicyOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n </div>\n <div class=\"mt-3 flex gap-2\">\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n label=\"Validate\"\n (onClick)=\"validateSchedule()\"\n />\n <mt-button\n size=\"small\"\n severity=\"primary\"\n label=\"Preview next fire\"\n (onClick)=\"previewSchedule()\"\n />\n </div>\n @if (schedulePreview(); as preview) {\n <div\n class=\"mt-3 rounded-lg bg-(--p-surface-100) px-3 py-2 text-[12px]\"\n >\n <strong class=\"text-emerald-600\">Valid</strong>\n <span class=\"ms-2\"\n >Next fire: {{ preview.nextFireAtUtc ?? \"-\" }}</span\n >\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\n *ngTemplateOutlet=\"\n mapEditor;\n context: {\n objectKey: 'fields',\n rows: fieldsRows(),\n keyLabel: 'Field',\n valueLabel: 'Value',\n addLabel: 'Add field',\n required: isConfigFieldRequired('fields'),\n }\n \"\n />\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]=\"ifConditionFieldText('left')\"\n (focusin)=\"setExpressionTarget('config:left')\"\n (ngModelChange)=\"onIfConditionFieldChange('left', $event)\"\n label=\"Left\"\n hint=\"Left-side value or expression to compare.\"\n [required]=\"isConfigFieldRequired('left')\"\n />\n <mt-select-field\n [ngModel]=\"ifConditionFieldValue('operator') || 'equals'\"\n (ngModelChange)=\"onIfConditionFieldChange('operator', $event)\"\n label=\"Operator\"\n hint=\"Comparison operator used by the backend condition evaluator.\"\n [required]=\"isConfigFieldRequired('operator')\"\n [options]=\"ifOperatorOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-text-field\n [ngModel]=\"ifConditionFieldText('right')\"\n (focusin)=\"setExpressionTarget('config:right')\"\n (ngModelChange)=\"onIfConditionFieldChange('right', $event)\"\n label=\"Right\"\n hint=\"Right-side value or expression to compare against.\"\n [required]=\"isConfigFieldRequired('right')\"\n />\n </div>\n <div\n class=\"mt-3 rounded-md border border-surface-200 bg-surface-0 p-3\"\n >\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\n class=\"rounded-md bg-(--p-surface-100) px-2 py-1 font-mono text-[11px]\"\n >{{ key }}</span\n >\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]=\"httpMethod()\"\n (ngModelChange)=\"onHttpMethodChange($event)\"\n label=\"Method\"\n hint=\"HTTP method used for the outbound request.\"\n [required]=\"isConfigFieldRequired('method')\"\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 [required]=\"isConfigFieldRequired('url')\"\n />\n </div>\n <div class=\"mt-3\">\n <ng-container\n *ngTemplateOutlet=\"\n mapEditor;\n context: {\n objectKey: 'headers',\n rows: headerRows(),\n keyLabel: 'Header',\n valueLabel: 'Value',\n addLabel: 'Add header',\n }\n \"\n />\n </div>\n <div class=\"mt-3\">\n <ng-container\n *ngTemplateOutlet=\"\n mapEditor;\n context: {\n objectKey: 'query',\n rows: queryRows(),\n keyLabel: 'Query param',\n valueLabel: 'Value',\n addLabel: 'Add query param',\n }\n \"\n />\n </div>\n @if (httpMethodAllowsPayload()) {\n @if (httpSupportsBodyMode()) {\n <mt-select-field\n class=\"mt-3\"\n [ngModel]=\"httpBodyMode()\"\n (ngModelChange)=\"onHttpBodyModeChange($event)\"\n label=\"Payload mode\"\n hint=\"How the request payload should be sent by the backend HTTP runtime.\"\n [required]=\"isConfigFieldRequired(httpBodyModeConfigKey())\"\n [options]=\"httpBodyModeOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n }\n @if (httpBodyEnabled()) {\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=\"Request payload\"\n hint=\"Payload sent by this HTTP method. Use JSON or expressions when the backend schema allows it.\"\n [required]=\"isConfigFieldRequired('body')\"\n rows=\"6\"\n />\n }\n } @else {\n <div\n class=\"mt-3 rounded-md border border-(--p-content-border-color) bg-(--p-surface-50) px-3 py-2 text-[12px] leading-5 text-(--p-text-muted-color)\"\n >\n {{ httpMethod() }} requests do not send a payload. Put request\n data in query parameters or headers.\n </div>\n }\n @if (\n supportsConfigKey(\"timeoutSeconds\") ||\n supportsConfigKey(\"responseHandling\")\n ) {\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)=\"\n onConfigFieldChange('timeoutSeconds', $event)\n \"\n label=\"Timeout seconds\"\n hint=\"Request timeout when exposed by the backend schema.\"\n [required]=\"isConfigFieldRequired('timeoutSeconds')\"\n [min]=\"0\"\n />\n }\n @if (supportsConfigKey(\"responseHandling\")) {\n <mt-text-field\n [ngModel]=\"config()['responseHandling'] ?? ''\"\n (ngModelChange)=\"\n onConfigFieldChange('responseHandling', $event)\n \"\n label=\"Response handling\"\n hint=\"Backend-supported response handling mode or expression.\"\n [required]=\"isConfigFieldRequired('responseHandling')\"\n />\n }\n </div>\n }\n @if (sectionInMain(\"credentials\")) {\n <div\n class=\"mt-3 rounded-md border border-surface-200 bg-surface-0 p-3\"\n >\n <div class=\"fp-ae-label mb-2\">Credential</div>\n <ng-container *ngTemplateOutlet=\"credentialSelector\" />\n </div>\n }\n <div\n class=\"mt-3 rounded-md border border-(--p-content-border-color) bg-(--p-surface-50) p-3\"\n >\n <div class=\"flex items-center justify-between gap-3\">\n <div class=\"min-w-0\">\n <div class=\"fp-ae-label\">Configuration check</div>\n <p class=\"fp-ae-copy m-0 mt-1\">\n Runs backend dry-run validation for method, URL,\n expressions, and request policy before execution.\n </p>\n </div>\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n severity=\"secondary\"\n label=\"Check request\"\n [disabled]=\"httpCheckState() === 'loading'\"\n (onClick)=\"checkHttpRequest()\"\n />\n </div>\n @if (httpLocalIssues().length > 0) {\n <div\n class=\"mt-2 rounded-md border border-[rgb(var(--fp-warning))]/25 bg-[rgb(var(--fp-warning))]/10 px-2.5 py-2 text-[12px] text-(--p-text-color)\"\n >\n {{ httpLocalIssues()[0] }}\n </div>\n } @else if (httpCheckError(); as error) {\n <div\n class=\"mt-2 rounded-md border border-[rgb(var(--fp-error))]/25 bg-[rgb(var(--fp-error))]/10 px-2.5 py-2 text-[12px] text-(--p-text-color)\"\n >\n {{ error }}\n </div>\n } @else if (httpCheckResult(); as result) {\n <div\n class=\"mt-2 rounded-md border border-[rgb(var(--fp-commit))]/25 bg-[rgb(var(--fp-commit))]/10 px-2.5 py-2 text-[12px] text-(--p-text-color)\"\n >\n Backend check\n {{\n result.success || result.canStart\n ? \"passed\"\n : \"completed with issues\"\n }}.\n </div>\n }\n </div>\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]=\"waitMode()\"\n (ngModelChange)=\"onWaitModeChange($event)\"\n label=\"Mode\"\n hint=\"Choose how execution should pause.\"\n [required]=\"isConfigFieldRequired('mode')\"\n [options]=\"waitModeOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n @if (showWaitDuration()) {\n <mt-number-field\n [ngModel]=\"numberValue(config()['durationSeconds'])\"\n (ngModelChange)=\"\n onConfigFieldChange('durationSeconds', $event)\n \"\n label=\"Wait duration\"\n hint=\"Seconds to pause before continuing.\"\n [required]=\"isConfigFieldRequired('durationSeconds')\"\n [min]=\"1\"\n />\n }\n @if (showWaitUntilDate()) {\n <mt-date-field\n [ngModel]=\"waitUntilDateValue()\"\n (ngModelChange)=\"onWaitUntilDateChange($event)\"\n label=\"Resume at\"\n hint=\"Date and time when execution should continue.\"\n [required]=\"isConfigFieldRequired('untilDate')\"\n [showTime]=\"true\"\n [showClear]=\"true\"\n [pInputs]=\"dateTimePickerInputs\"\n />\n }\n </div>\n @if (showWaitResumePayloadSchema()) {\n <mt-textarea-field\n class=\"mt-3 w-full font-mono\"\n [ngModel]=\"waitResumePayloadSchemaText()\"\n (focusin)=\"setExpressionTarget('config:resumePayloadSchema')\"\n (ngModelChange)=\"onWaitResumePayloadSchemaChange($event)\"\n label=\"Resume payload schema\"\n hint=\"Optional JSON schema for data sent back when this wait is resumed.\"\n [required]=\"isConfigFieldRequired('resumePayloadSchema')\"\n rows=\"5\"\n />\n }\n </section>\n }\n }\n @case (\"HumanTask\") {\n @if (sectionInMain(\"humanTask\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Human task</div>\n @if (\n assignmentOptions()?.providerStatus &&\n assignmentOptions()?.providerStatus !== \"Available\"\n ) {\n <p class=\"fp-ae-copy\">\n Assignment provider status:\n {{ assignmentOptions()?.providerStatus }}. The backend provider\n is the source of truth for available assignees.\n </p>\n }\n <div\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 Task details\n </h3>\n <div class=\"grid gap-3 p-4 xl:grid-cols-3\">\n <mt-text-field\n [ngModel]=\"config()['title'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:title')\"\n (ngModelChange)=\"onConfigFieldChange('title', $event)\"\n label=\"Task title\"\n hint=\"Title shown to the assigned user while the execution waits.\"\n [required]=\"isConfigFieldRequired('title')\"\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 submit this task.\"\n [required]=\"isConfigFieldRequired('assignment')\"\n [options]=\"assignmentSelectOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-text-field\n [ngModel]=\"config()['priority'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('priority', $event)\"\n label=\"Priority\"\n hint=\"Optional task priority passed to backend task orchestration.\"\n [required]=\"isConfigFieldRequired('priority')\"\n />\n <mt-textarea-field\n class=\"xl:col-span-3\"\n [ngModel]=\"config()['description'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:description')\"\n (ngModelChange)=\"onConfigFieldChange('description', $event)\"\n label=\"Task description\"\n hint=\"Instructions shown with the HumanTask form.\"\n [required]=\"isConfigFieldRequired('description')\"\n rows=\"4\"\n />\n </div>\n </div>\n\n <div\n class=\"mt-3 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 Task behavior\n </h3>\n <div class=\"grid gap-3 p-4 xl:grid-cols-4\">\n <mt-date-field\n [ngModel]=\"\n config()['dueDateUtc'] ?? config()['dueDate'] ?? ''\n \"\n (ngModelChange)=\"onConfigFieldChange('dueDateUtc', $event)\"\n label=\"Due date\"\n hint=\"Optional backend due timestamp for the waiting task.\"\n [required]=\"isConfigFieldRequired('dueDateUtc')\"\n [showTime]=\"true\"\n [showClear]=\"true\"\n [pInputs]=\"dateTimePickerInputs\"\n />\n <mt-number-field\n [ngModel]=\"numberValue(config()['dueInSeconds'])\"\n (ngModelChange)=\"onConfigFieldChange('dueInSeconds', $event)\"\n label=\"Due in seconds\"\n hint=\"Relative due duration when the backend should calculate the timestamp.\"\n [required]=\"isConfigFieldRequired('dueInSeconds')\"\n [min]=\"0\"\n />\n <mt-text-field\n [ngModel]=\"config()['contextOutputPath'] ?? 'task'\"\n (ngModelChange)=\"\n onConfigFieldChange('contextOutputPath', $event)\n \"\n label=\"Context output path\"\n hint=\"Where submitted task values are written in execution context.\"\n [required]=\"isConfigFieldRequired('contextOutputPath')\"\n />\n <mt-select-field\n [ngModel]=\"config()['cancelBehavior'] ?? 'RouteCancelled'\"\n (ngModelChange)=\"\n onConfigFieldChange('cancelBehavior', $event)\n \"\n label=\"Cancel behavior\"\n hint=\"Route cancelled when users cancel, or fail this node.\"\n [required]=\"isConfigFieldRequired('cancelBehavior')\"\n [options]=\"humanTaskCancelBehaviorOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <div\n class=\"flex min-h-[4.25rem] items-end rounded-md border border-surface-200 bg-surface-0 px-3 py-2\"\n >\n <mt-toggle-field\n size=\"small\"\n label=\"Allow draft save\"\n labelPosition=\"end\"\n [ngModel]=\"config()['saveDraft'] !== false\"\n [required]=\"isConfigFieldRequired('saveDraft')\"\n (ngModelChange)=\"\n onConfigFieldChange('saveDraft', $event === true)\n \"\n hint=\"Expose backend draft save for this task when supported.\"\n />\n </div>\n @if (supportsConfigKey(\"submitBehavior\")) {\n <mt-text-field\n [ngModel]=\"config()['submitBehavior'] ?? ''\"\n (ngModelChange)=\"\n onConfigFieldChange('submitBehavior', $event)\n \"\n label=\"Submit behavior\"\n hint=\"Backend submit behavior when exposed by the node schema.\"\n [required]=\"isConfigFieldRequired('submitBehavior')\"\n />\n }\n </div>\n </div>\n\n <div class=\"mt-3 flex gap-2\">\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n label=\"Validate assignment\"\n (onClick)=\"validateAssignment()\"\n />\n </div>\n @if (assignmentValidation(); as result) {\n <div\n class=\"mt-3 rounded-lg bg-(--p-surface-100) px-3 py-2 text-[12px]\"\n >\n {{\n result.isValid === false\n ? \"Assignment invalid\"\n : \"Assignment accepted by helper\"\n }}\n @for (issue of resultIssues(result); track $index) {\n <div class=\"mt-1 text-[11px] text-(--p-text-muted-color)\">\n {{ issue }}\n </div>\n }\n </div>\n }\n </section>\n }\n\n @if (sectionInMain(\"formBinding\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Task input form</div>\n <mt-radio-cards-field\n [ngModel]=\"humanTaskFormSource()\"\n (ngModelChange)=\"onHumanTaskFormSourceChange($event)\"\n label=\"Form source\"\n hint=\"HumanTask requires either its own node form binding or an explicit reused FormSubmitTrigger payload.\"\n [required]=\"isConfigFieldRequired('formSource')\"\n [options]=\"humanTaskFormSourceOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n size=\"small\"\n >\n <ng-template #option let-item>\n <div class=\"w-full min-w-0\">\n <div class=\"text-[13px] font-semibold leading-5\">\n {{ item.label }}\n </div>\n <p\n class=\"mt-1 text-[12px] leading-5 text-(--p-text-muted-color)\"\n >\n {{ item.description }}\n </p>\n </div>\n </ng-template>\n </mt-radio-cards-field>\n\n @if (humanTaskUsesExplicitFormBinding()) {\n <div\n class=\"mt-3 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 HumanTask input binding\n </h3>\n <div class=\"grid gap-3 p-4 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 for this HumanTask node.\"\n [required]=\"humanTaskUsesExplicitFormBinding()\"\n [options]=\"formOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-select-field\n [ngModel]=\"\n selectedFormVersionId() ||\n formBinding()?.formVersionId ||\n ''\n \"\n (ngModelChange)=\"onFormVersionChange($event)\"\n label=\"Form version\"\n hint=\"Persist the exact backend formVersionId for the node binding.\"\n [required]=\"humanTaskUsesExplicitFormBinding()\"\n [options]=\"formVersionOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n </div>\n <div class=\"border-t border-surface-200 px-4 py-3\">\n @if (humanTaskFormSummary(); as summary) {\n <div\n class=\"flex flex-wrap items-center gap-2 text-[12px] text-(--p-text-muted-color)\"\n >\n @if (summary.formLabel) {\n <span class=\"rounded-md bg-(--p-surface-100) px-2 py-1\">\n {{ summary.formLabel }}\n </span>\n }\n @if (summary.formVersionId) {\n <span\n class=\"rounded-md bg-(--p-surface-100) px-2 py-1 font-mono text-[11px]\"\n >\n {{ summary.formVersionId }}\n </span>\n }\n <span class=\"rounded-md bg-(--p-surface-100) px-2 py-1\">\n {{ summary.fieldCount }} fields\n </span>\n <span class=\"rounded-md bg-(--p-surface-100) px-2 py-1\">\n {{ summary.requiredCount }} required\n </span>\n <span class=\"rounded-md bg-(--p-surface-100) px-2 py-1\">\n {{ summary.writableCount }} writable\n </span>\n </div>\n }\n\n @if (humanTaskHasCustomInputMapping()) {\n <div\n class=\"mt-3 rounded-md 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 Existing custom input mapping will be preserved unless\n field prefill options are changed.\n </div>\n }\n\n @if (humanTaskFormFields().length > 0) {\n <div\n class=\"mt-3 overflow-hidden rounded-md border border-surface-200 bg-surface-0\"\n >\n <div\n class=\"flex flex-wrap items-center justify-between gap-2 border-b border-surface-200 bg-surface-50 px-3 py-2\"\n >\n <div class=\"text-[13px] font-semibold text-color\">\n FormBuilder fields\n </div>\n <div class=\"flex flex-wrap gap-2\">\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n severity=\"secondary\"\n label=\"Select all\"\n (onClick)=\"selectAllHumanTaskPrefillFields()\"\n />\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n severity=\"secondary\"\n label=\"Clear\"\n (onClick)=\"clearHumanTaskPrefillFields()\"\n />\n </div>\n </div>\n <div class=\"divide-y divide-surface-200\">\n @for (field of humanTaskFormFields(); track field.key) {\n <div\n class=\"grid gap-3 px-3 py-3 md:grid-cols-[minmax(0,1fr)_auto] md:items-center\"\n >\n <div class=\"min-w-0\">\n <div class=\"flex flex-wrap items-center gap-2\">\n <span\n class=\"truncate text-[13px] font-semibold text-(--p-text-color)\"\n >\n {{ field.label }}\n </span>\n <span\n class=\"rounded-md bg-(--p-surface-100) px-2 py-0.5 font-mono text-[11px] text-(--p-text-muted-color)\"\n >\n {{ field.key }}\n </span>\n @if (field.required) {\n <span\n class=\"rounded-md bg-[rgb(var(--fp-danger))]/10 px-2 py-0.5 text-[11px] font-semibold text-[rgb(var(--fp-danger))]\"\n >\n Required\n </span>\n }\n @if (field.read === true) {\n <span\n class=\"rounded-md bg-(--p-surface-100) px-2 py-0.5 text-[11px] text-(--p-text-muted-color)\"\n >\n Read\n </span>\n }\n @if (field.write === true) {\n <span\n class=\"rounded-md bg-(--p-surface-100) px-2 py-0.5 text-[11px] text-(--p-text-muted-color)\"\n >\n Write\n </span>\n }\n </div>\n @if (field.description) {\n <p\n class=\"m-0 mt-1 text-[12px] leading-5 text-(--p-text-muted-color)\"\n >\n {{ field.description }}\n </p>\n }\n </div>\n <mt-toggle-field\n size=\"small\"\n label=\"Prefill\"\n labelPosition=\"end\"\n [ngModel]=\"\n humanTaskPrefillFieldKeySet().has(field.key)\n \"\n (ngModelChange)=\"\n onHumanTaskPrefillFieldToggle(\n field.key,\n $event === true\n )\n \"\n />\n </div>\n }\n </div>\n <div\n class=\"border-t border-surface-200 bg-surface-50 px-3 py-2 text-[12px] leading-5 text-(--p-text-muted-color)\"\n >\n Selected fields are saved as\n <span class=\"font-mono\"\n >inputMappingJson.fieldKeys</span\n >\n so backend can prefill matching upstream values.\n </div>\n </div>\n } @else if (formSchema()) {\n <div\n class=\"mt-3 rounded-md border border-surface-200 bg-surface-50 px-3 py-2 text-[12px] leading-5 text-(--p-text-muted-color)\"\n >\n No FormBuilder fields were returned for this form schema.\n </div>\n }\n\n @if (formSchema()?.requiresSnapshotOnPublish) {\n <div\n class=\"mt-3 rounded-md border border-surface-200 bg-surface-50 px-3 py-2 text-[12px] leading-5 text-(--p-text-muted-color)\"\n >\n Backend requires this form version snapshot during\n publish.\n </div>\n }\n </div>\n <div class=\"border-t border-surface-200 px-4 py-3\">\n <div class=\"flex flex-wrap items-center gap-2\">\n <mt-button\n size=\"small\"\n severity=\"primary\"\n label=\"Save task binding\"\n (onClick)=\"saveFormBinding()\"\n />\n @if (formBinding()) {\n <span\n class=\"rounded-lg bg-(--p-surface-100) px-2 py-1 font-mono text-[11px] text-(--p-text-muted-color)\"\n >\n {{ formBinding()!.formVersionId }}\n </span>\n }\n </div>\n </div>\n </div>\n } @else {\n <div class=\"mt-3 grid gap-3 md:grid-cols-2\">\n <mt-select-field\n [ngModel]=\"config()['triggerKey'] ?? ''\"\n (ngModelChange)=\"onHumanTaskReuseTriggerChange($event)\"\n label=\"Trigger to reuse\"\n hint=\"Explicitly select the FormSubmitTrigger whose submitted form payload is reused.\"\n [options]=\"triggerOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n </div>\n @if (humanTaskReuseFormBinding()) {\n <div\n class=\"mt-3 rounded-md border border-surface-200 bg-surface-0 p-3\"\n >\n <div class=\"fp-ae-label mb-2\">Reused trigger form</div>\n @if (humanTaskFormSummary(); as summary) {\n <div\n class=\"flex flex-wrap items-center gap-2 text-[12px] text-(--p-text-muted-color)\"\n >\n @if (summary.formLabel) {\n <span class=\"rounded-md bg-(--p-surface-100) px-2 py-1\">\n {{ summary.formLabel }}\n </span>\n }\n @if (summary.formVersionId) {\n <span\n class=\"rounded-md bg-(--p-surface-100) px-2 py-1 font-mono text-[11px]\"\n >\n {{ summary.formVersionId }}\n </span>\n }\n <span class=\"rounded-md bg-(--p-surface-100) px-2 py-1\">\n {{ summary.fieldCount }} fields\n </span>\n <span class=\"rounded-md bg-(--p-surface-100) px-2 py-1\">\n {{ summary.requiredCount }} required\n </span>\n </div>\n }\n </div>\n }\n }\n\n @if (formSchema(); as schema) {\n <details\n class=\"mt-3 rounded-lg border border-(--p-content-border-color)\"\n >\n <summary\n class=\"cursor-pointer px-3 py-2 text-[12px] font-semibold\"\n >\n Form schema preview\n </summary>\n <pre class=\"fp-ae-code\">{{ schemaText(schema.schema) }}</pre>\n </details>\n }\n\n <div\n class=\"mt-3 rounded-md border border-surface-200 bg-surface-0 p-3\"\n >\n <div class=\"fp-ae-label mb-2\">HumanTask routes</div>\n <div class=\"flex flex-wrap gap-1.5\">\n @for (key of routeOutputKeys(); track key) {\n <span\n class=\"rounded-md bg-(--p-surface-100) px-2 py-1 font-mono text-[11px]\"\n >{{ key }}</span\n >\n }\n </div>\n </div>\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 (\n assignmentOptions()?.providerStatus &&\n assignmentOptions()?.providerStatus !== \"Available\"\n ) {\n <p class=\"fp-ae-copy\">\n Assignment provider status:\n {{ assignmentOptions()?.providerStatus }}. The backend provider\n 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 [required]=\"isConfigFieldRequired('title')\"\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 [required]=\"isConfigFieldRequired('assignment')\"\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 [required]=\"isConfigFieldRequired('priority')\"\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 [required]=\"isConfigFieldRequired('message')\"\n rows=\"4\"\n />\n @if (\n humanApprovalSupportsConfig(\"dueDate\") ||\n humanApprovalSupportsConfig(\"dueInSeconds\") ||\n humanApprovalSupportsConfig(\"timeoutSeconds\") ||\n humanApprovalSupportsConfig(\"expiresAt\") ||\n humanApprovalSupportsConfig(\"commentsRequired\") ||\n humanApprovalSupportsConfig(\"allowReturn\")\n ) {\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 [required]=\"isConfigFieldRequired('dueDate')\"\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)=\"\n onConfigFieldChange('dueInSeconds', $event)\n \"\n label=\"Due in seconds\"\n hint=\"Relative approval due duration when supported by the backend schema.\"\n [required]=\"isConfigFieldRequired('dueInSeconds')\"\n [min]=\"0\"\n />\n }\n @if (humanApprovalSupportsConfig(\"timeoutSeconds\")) {\n <mt-number-field\n [ngModel]=\"numberValue(config()['timeoutSeconds'])\"\n (ngModelChange)=\"\n onConfigFieldChange('timeoutSeconds', $event)\n \"\n label=\"Timeout seconds\"\n hint=\"Approval timeout emitted according to backend approval runtime support.\"\n [required]=\"isConfigFieldRequired('timeoutSeconds')\"\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 [required]=\"isConfigFieldRequired('expiresAt')\"\n [showTime]=\"true\"\n [showClear]=\"true\"\n [pInputs]=\"dateTimePickerInputs\"\n />\n }\n @if (humanApprovalSupportsConfig(\"commentsRequired\")) {\n <div\n class=\"flex min-h-[4.25rem] items-end rounded-md border border-surface-200 bg-surface-0 px-3 py-2\"\n >\n <mt-toggle-field\n size=\"small\"\n label=\"Comments required\"\n labelPosition=\"end\"\n [ngModel]=\"config()['commentsRequired'] === true\"\n [required]=\"isConfigFieldRequired('commentsRequired')\"\n (ngModelChange)=\"\n onConfigFieldChange('commentsRequired', $event === true)\n \"\n hint=\"Require approver comments when the backend supports this flag.\"\n />\n </div>\n }\n @if (humanApprovalSupportsConfig(\"allowReturn\")) {\n <div\n class=\"flex min-h-[4.25rem] items-end rounded-md border border-surface-200 bg-surface-0 px-3 py-2\"\n >\n <mt-toggle-field\n size=\"small\"\n label=\"Allow return for changes\"\n labelPosition=\"end\"\n [ngModel]=\"config()['allowReturn'] !== false\"\n [required]=\"isConfigFieldRequired('allowReturn')\"\n (ngModelChange)=\"\n onConfigFieldChange('allowReturn', $event === true)\n \"\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\n class=\"overflow-hidden rounded-md border border-surface-200 bg-surface-0\"\n >\n @if (selectedApprovalDecisionRows().length > 0) {\n <div\n 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 >\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 (\n decision of selectedApprovalDecisionRows();\n track decision.value\n ) {\n <div\n 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 >\n <div class=\"min-w-0\">\n <div\n class=\"text-[11px] font-semibold text-(--p-text-muted-color) xl:hidden\"\n >\n Decision label\n </div>\n <div\n class=\"truncate text-[13px] font-semibold text-(--p-text-color)\"\n >\n {{ decision.label }}\n </div>\n </div>\n <div class=\"min-w-0\">\n <div\n class=\"text-[11px] font-semibold text-(--p-text-muted-color) xl:hidden\"\n >\n Canonical value\n </div>\n <div\n class=\"truncate font-mono text-[12px] text-(--p-text-color)\"\n >\n {{ decision.value }}\n </div>\n </div>\n <div class=\"min-w-0\">\n <div\n class=\"text-[11px] font-semibold text-(--p-text-muted-color) xl:hidden\"\n >\n Route output key\n </div>\n <div\n class=\"truncate font-mono text-[12px] text-(--p-text-muted-color)\"\n >\n {{ decision.routeOutputKey }}\n </div>\n </div>\n <div>\n <div\n class=\"text-[11px] font-semibold text-(--p-text-muted-color) xl:hidden\"\n >\n Routes\n </div>\n <div class=\"text-[13px] text-(--p-text-color)\">\n {{ decision.routeCount }}\n </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\n class=\"px-3 py-4 text-[12px] text-(--p-text-muted-color)\"\n >\n Add at least one backend-supported approval decision.\n </div>\n }\n </div>\n @if (approvalDecisionIssues().length > 0) {\n <div\n 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 >\n @for (issue of approvalDecisionIssues(); track $index) {\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 [required]=\"isConfigFieldRequired('allowedDecisions')\"\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 (\n humanApprovalSupportsConfig(\"payload\") ||\n humanApprovalSupportsConfig(\"context\") ||\n humanApprovalSupportsConfig(\"metadata\")\n ) {\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 [required]=\"isConfigFieldRequired('payload')\"\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 [required]=\"isConfigFieldRequired('context')\"\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 [required]=\"isConfigFieldRequired('metadata')\"\n rows=\"4\"\n />\n }\n </div>\n }\n <div class=\"mt-3 flex gap-2\">\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n label=\"Validate assignment\"\n (onClick)=\"validateAssignment()\"\n />\n </div>\n @if (assignmentValidation(); as result) {\n <div\n class=\"mt-3 rounded-lg bg-(--p-surface-100) px-3 py-2 text-[12px]\"\n >\n {{\n result.isValid === false\n ? \"Assignment invalid\"\n : \"Assignment accepted by helper\"\n }}\n @for (issue of resultIssues(result); track $index) {\n <div class=\"mt-1 text-[11px] text-(--p-text-muted-color)\">\n {{ issue }}\n </div>\n }\n </div>\n }\n </section>\n }\n }\n @case (\"ConnectorAction\") {\n @if (sectionInMain(\"humanReviewRequest\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">\n {{ humanReviewProviderLabel() }} human review\n </div>\n <div class=\"grid gap-3 md:grid-cols-2\">\n <mt-text-field\n [ngModel]=\"config()['channelType'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('channelType', $event)\"\n label=\"Channel\"\n [required]=\"isConfigFieldRequired('channelType')\"\n />\n </div>\n\n <div class=\"mt-4 grid gap-3 md:grid-cols-3\">\n <mt-text-field\n [ngModel]=\"humanReviewDestination()['recipient'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:destination:recipient')\"\n (ngModelChange)=\"\n onNestedConfigFieldChange('destination', 'recipient', $event)\n \"\n label=\"Recipient\"\n [required]=\"isConnectorDestinationRequired()\"\n />\n <mt-text-field\n [ngModel]=\"humanReviewDestination()['channelId'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:destination:channelId')\"\n (ngModelChange)=\"\n onNestedConfigFieldChange('destination', 'channelId', $event)\n \"\n label=\"Channel ID\"\n [required]=\"isConnectorDestinationRequired()\"\n />\n <mt-text-field\n [ngModel]=\"humanReviewDestination()['threadKey'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:destination:threadKey')\"\n (ngModelChange)=\"\n onNestedConfigFieldChange('destination', 'threadKey', $event)\n \"\n label=\"Thread key\"\n [required]=\"isConnectorDestinationRequired()\"\n />\n </div>\n\n <div class=\"mt-4 grid gap-3\">\n <mt-text-field\n [ngModel]=\"humanReviewMessage()['subject'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:message:subject')\"\n (ngModelChange)=\"\n onNestedConfigFieldChange('message', 'subject', $event)\n \"\n label=\"Subject\"\n [required]=\"isConnectorMessageRequired()\"\n />\n <mt-textarea-field\n class=\"w-full\"\n [ngModel]=\"humanReviewMessage()['body'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:message:body')\"\n (ngModelChange)=\"\n onNestedConfigFieldChange('message', 'body', $event)\n \"\n label=\"Message\"\n [required]=\"isConnectorMessageRequired()\"\n rows=\"5\"\n />\n <mt-text-field\n [ngModel]=\"humanReviewMessage()['templateKey'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:message:templateKey')\"\n (ngModelChange)=\"\n onNestedConfigFieldChange('message', 'templateKey', $event)\n \"\n label=\"Template key\"\n [required]=\"isConnectorMessageRequired()\"\n />\n </div>\n\n <div class=\"mt-4\">\n <div class=\"mb-2 flex items-center justify-between gap-3\">\n <div class=\"fp-ae-label\">Response options</div>\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n label=\"Add option\"\n (onClick)=\"addHumanReviewResponseOption()\"\n />\n </div>\n <div class=\"grid gap-2\">\n @for (\n option of humanReviewResponseOptions();\n track option.key + \":\" + $index\n ) {\n <div\n class=\"grid gap-2 rounded-md border border-(--p-content-border-color) bg-(--p-surface-50) p-2 md:grid-cols-[1fr_1fr_1fr_auto]\"\n >\n <mt-text-field\n [ngModel]=\"option.key\"\n (ngModelChange)=\"\n updateHumanReviewResponseOption($index, 'key', $event)\n \"\n label=\"Key\"\n [required]=\"isConfigFieldRequired('responseOptions')\"\n />\n <mt-text-field\n [ngModel]=\"option.label\"\n (ngModelChange)=\"\n updateHumanReviewResponseOption($index, 'label', $event)\n \"\n label=\"Label\"\n [required]=\"isConfigFieldRequired('responseOptions')\"\n />\n <mt-text-field\n [ngModel]=\"option.routeOutputKey\"\n (ngModelChange)=\"\n updateHumanReviewResponseOption(\n $index,\n 'routeOutputKey',\n $event\n )\n \"\n label=\"Route output\"\n [required]=\"isConfigFieldRequired('responseOptions')\"\n />\n <div class=\"flex items-end justify-end\">\n <mt-button\n size=\"small\"\n variant=\"text\"\n severity=\"secondary\"\n icon=\"general.x-close\"\n [disabled]=\"option.routeCount > 0\"\n (onClick)=\"removeHumanReviewResponseOption($index)\"\n [attr.aria-label]=\"'Remove response option'\"\n />\n </div>\n </div>\n }\n </div>\n </div>\n\n <div class=\"mt-4 grid gap-3 md:grid-cols-2\">\n <mt-select-field\n [ngModel]=\"selectedAssignmentOptionKey()\"\n (ngModelChange)=\"onAssignmentOptionChange($event)\"\n label=\"Assignment\"\n [required]=\"isConfigFieldRequired('assignment')\"\n [options]=\"assignmentSelectOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-text-field\n [ngModel]=\"config()['credentialRef'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('credentialRef', $event)\"\n label=\"Credential\"\n [required]=\"isConfigFieldRequired('credentialRef')\"\n />\n <mt-number-field\n [ngModel]=\"numberValue(config()['timeoutSeconds'])\"\n (ngModelChange)=\"onConfigFieldChange('timeoutSeconds', $event)\"\n label=\"Timeout seconds\"\n [required]=\"isConfigFieldRequired('timeoutSeconds')\"\n [min]=\"1\"\n />\n <mt-select-field\n [ngModel]=\"config()['failureBehavior'] ?? 'RouteFailure'\"\n (ngModelChange)=\"onConfigFieldChange('failureBehavior', $event)\"\n label=\"Failure behavior\"\n [required]=\"isConfigFieldRequired('failureBehavior')\"\n [options]=\"humanReviewFailureBehaviorOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n </div>\n\n <div class=\"mt-3 flex gap-2\">\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n label=\"Validate assignment\"\n (onClick)=\"validateAssignment()\"\n />\n </div>\n @if (assignmentValidation(); as result) {\n <div\n class=\"mt-3 rounded-lg bg-(--p-surface-100) px-3 py-2 text-[12px]\"\n >\n {{\n result.isValid === false\n ? \"Assignment invalid\"\n : \"Assignment accepted by helper\"\n }}\n @for (issue of resultIssues(result); track $index) {\n <div class=\"mt-1 text-[11px] text-(--p-text-muted-color)\">\n {{ issue }}\n </div>\n }\n </div>\n }\n\n <div\n class=\"mt-4 rounded-md border border-(--p-content-border-color) bg-(--p-surface-50) p-3\"\n >\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\n class=\"rounded-md bg-(--p-surface-100) px-2 py-1 font-mono text-[11px]\"\n >{{ key }}</span\n >\n }\n </div>\n </div>\n </section>\n }\n }\n @case (\"ConvertToFile\") {\n @if (sectionInMain(\"convertToFile\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">\n {{ convertToFileActionLabel() }}\n </div>\n @if (convertToFileUnavailableReason(); as reason) {\n <div\n class=\"mb-3 rounded-md border border-(--p-content-border-color) bg-(--p-surface-50) px-3 py-2 text-[12px] leading-5 text-(--p-text-muted-color)\"\n >\n {{ reason }}\n </div>\n }\n\n <div class=\"grid gap-3 md:grid-cols-2\">\n <mt-text-field\n [ngModel]=\"config()['fileName'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:fileName')\"\n (ngModelChange)=\"onConfigFieldChange('fileName', $event)\"\n label=\"File name\"\n [required]=\"isConfigFieldRequired('fileName')\"\n />\n @if (convertToFileIsBase64()) {\n <mt-text-field\n [ngModel]=\"config()['contentType'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('contentType', $event)\"\n label=\"Content type\"\n [required]=\"isConfigFieldRequired('contentType')\"\n />\n } @else {\n <mt-text-field\n [ngModel]=\"convertToFileContentType()\"\n [readonly]=\"true\"\n label=\"Content type\"\n />\n }\n </div>\n\n @if (\n convertToFileIsCsv() ||\n convertToFileIsJson() ||\n convertToFileIsSpreadsheet()\n ) {\n <div class=\"mt-4 grid gap-3 md:grid-cols-2\">\n <mt-select-field\n [ngModel]=\"convertToFileSourceMode()\"\n (ngModelChange)=\"onConvertToFileSourceModeChange($event)\"\n label=\"Data source\"\n [required]=\"isConfigFieldRequired('sourceExpression')\"\n [options]=\"convertToFileSourceOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n @if (convertToFileSourceMode() === \"expression\") {\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"convertToFileSourceExpression()\"\n (focusin)=\"setExpressionTarget('config:sourceExpression')\"\n (ngModelChange)=\"\n onConvertToFileSourceExpressionChange($event)\n \"\n label=\"Source expression\"\n [required]=\"isConfigFieldRequired('sourceExpression')\"\n />\n }\n @if (convertToFileIsCsv() || convertToFileIsSpreadsheet()) {\n <div\n class=\"rounded-md border border-(--p-content-border-color) bg-(--p-surface-50) p-3 md:col-span-2\"\n >\n <div class=\"mb-2 flex items-center justify-between gap-3\">\n <div class=\"fp-ae-label\">Columns</div>\n @if (convertToFileColumns().length) {\n <mt-button\n size=\"small\"\n variant=\"text\"\n severity=\"secondary\"\n label=\"Clear\"\n (onClick)=\"clearConvertToFileColumns()\"\n />\n }\n </div>\n <div class=\"mb-3 flex min-h-8 flex-wrap gap-1.5\">\n @if (convertToFileColumns().length) {\n @for (column of convertToFileColumns(); track column) {\n <span\n class=\"inline-flex items-center gap-1 rounded-md bg-(--p-surface-100) px-2 py-1 font-mono text-[11px]\"\n >\n {{ column }}\n <button\n type=\"button\"\n class=\"inline-flex h-4 w-4 items-center justify-center rounded-sm text-[10px] hover:bg-(--p-surface-200) focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--p-primary-color)\"\n (click)=\"removeConvertToFileColumn(column)\"\n [attr.aria-label]=\"'Remove column ' + column\"\n >\n x\n </button>\n </span>\n }\n } @else {\n <span\n class=\"rounded-md bg-(--p-content-background) px-2 py-1 text-[12px] text-(--p-text-muted-color)\"\n >\n All fields\n </span>\n }\n </div>\n <div class=\"grid items-end gap-2 md:grid-cols-[1fr_auto]\">\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"convertToFileColumnDraft()\"\n (ngModelChange)=\"\n onConvertToFileColumnDraftChange($event)\n \"\n (keyup.enter)=\"addConvertToFileColumns()\"\n label=\"Add column\"\n [required]=\"isConfigFieldRequired('columns')\"\n />\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n icon=\"general.plus\"\n label=\"Add\"\n [disabled]=\"!convertToFileColumnDraft().trim()\"\n (onClick)=\"addConvertToFileColumns()\"\n />\n </div>\n </div>\n }\n </div>\n }\n\n @if (convertToFileIsCsv()) {\n <div class=\"mt-4 grid gap-3 md:grid-cols-3\">\n <mt-toggle-field\n size=\"small\"\n labelPosition=\"end\"\n [ngModel]=\"config()['includeHeaders'] !== false\"\n (ngModelChange)=\"\n onConfigFieldChange('includeHeaders', $event === true)\n \"\n label=\"Include headers\"\n />\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"config()['delimiter'] ?? ','\"\n (ngModelChange)=\"onConfigFieldChange('delimiter', $event)\"\n label=\"Delimiter\"\n [required]=\"isConfigFieldRequired('delimiter')\"\n />\n <mt-text-field\n [ngModel]=\"config()['encoding'] ?? 'utf-8'\"\n [readonly]=\"true\"\n label=\"Encoding\"\n />\n </div>\n }\n\n @if (convertToFileIsJson()) {\n <div class=\"mt-4 grid gap-3 md:grid-cols-2\">\n <mt-toggle-field\n size=\"small\"\n labelPosition=\"end\"\n [ngModel]=\"config()['pretty'] !== false\"\n (ngModelChange)=\"\n onConfigFieldChange('pretty', $event === true)\n \"\n label=\"Pretty JSON\"\n />\n </div>\n }\n\n @if (convertToFileIsHtml()) {\n <div class=\"mt-4 grid gap-3\">\n <mt-text-field\n [ngModel]=\"config()['title'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:title')\"\n (ngModelChange)=\"onConfigFieldChange('title', $event)\"\n label=\"Title\"\n [required]=\"isConfigFieldRequired('title')\"\n />\n <mt-textarea-field\n class=\"w-full\"\n [ngModel]=\"config()['body'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:body')\"\n (ngModelChange)=\"onConfigFieldChange('body', $event)\"\n label=\"Body\"\n [required]=\"isConfigFieldRequired('body')\"\n rows=\"7\"\n />\n <mt-toggle-field\n size=\"small\"\n labelPosition=\"end\"\n [ngModel]=\"config()['sanitize'] !== false\"\n (ngModelChange)=\"\n onConfigFieldChange('sanitize', $event === true)\n \"\n label=\"Sanitize HTML\"\n />\n </div>\n }\n\n @if (convertToFileIsIcs()) {\n <div class=\"mt-4 grid gap-3 md:grid-cols-2\">\n <mt-text-field\n [ngModel]=\"config()['summary'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:summary')\"\n (ngModelChange)=\"onConfigFieldChange('summary', $event)\"\n label=\"Event summary\"\n [required]=\"isConfigFieldRequired('summary')\"\n />\n <mt-text-field\n [ngModel]=\"config()['timezone'] ?? 'UTC'\"\n (ngModelChange)=\"onConfigFieldChange('timezone', $event)\"\n label=\"Timezone\"\n [required]=\"isConfigFieldRequired('timezone')\"\n />\n <mt-date-field\n [ngModel]=\"config()['start'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('start', $event)\"\n label=\"Start\"\n [required]=\"isConfigFieldRequired('start')\"\n [showTime]=\"true\"\n [showClear]=\"true\"\n [pInputs]=\"dateTimePickerInputs\"\n />\n <mt-date-field\n [ngModel]=\"config()['end'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('end', $event)\"\n label=\"End\"\n [required]=\"isConfigFieldRequired('end')\"\n [showTime]=\"true\"\n [showClear]=\"true\"\n [pInputs]=\"dateTimePickerInputs\"\n />\n <mt-text-field\n [ngModel]=\"config()['location'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:location')\"\n (ngModelChange)=\"onConfigFieldChange('location', $event)\"\n label=\"Location\"\n [required]=\"isConfigFieldRequired('location')\"\n />\n <mt-textarea-field\n class=\"w-full\"\n [ngModel]=\"config()['description'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:description')\"\n (ngModelChange)=\"onConfigFieldChange('description', $event)\"\n label=\"Description\"\n [required]=\"isConfigFieldRequired('description')\"\n rows=\"4\"\n />\n </div>\n }\n\n @if (convertToFileIsSpreadsheet()) {\n <div\n class=\"mt-4 rounded-md border border-(--p-content-border-color) bg-(--p-surface-50) p-3\"\n >\n <div class=\"mb-3 flex items-center justify-between gap-3\">\n <div class=\"fp-ae-label\">Sheets</div>\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n icon=\"general.plus\"\n label=\"Add sheet\"\n (onClick)=\"addConvertToFileSheet()\"\n />\n </div>\n <div class=\"grid gap-3\">\n @for (sheet of convertToFileSheetRows(); track sheet.index) {\n <div\n class=\"rounded-md border border-(--p-content-border-color) bg-(--p-content-background) p-3\"\n >\n <div\n class=\"grid gap-3 md:grid-cols-[minmax(140px,0.45fr)_minmax(0,1fr)_minmax(180px,0.6fr)_auto]\"\n >\n <mt-text-field\n [ngModel]=\"sheet.name\"\n (ngModelChange)=\"\n updateConvertToFileSheet(\n sheet.index,\n 'name',\n $event\n )\n \"\n label=\"Sheet name\"\n [required]=\"isConfigFieldRequired('sheets')\"\n />\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"sheet.sourceExpression\"\n (focusin)=\"\n setExpressionTarget(\n 'config:sheets:' + sheet.index + ':source'\n )\n \"\n (ngModelChange)=\"\n updateConvertToFileSheet(\n sheet.index,\n 'sourceExpression',\n $event\n )\n \"\n label=\"Source\"\n [required]=\"isConfigFieldRequired('sheets')\"\n />\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"sheet.columnsText\"\n (ngModelChange)=\"\n updateConvertToFileSheet(\n sheet.index,\n 'columns',\n $event\n )\n \"\n label=\"Columns\"\n [required]=\"isConfigFieldRequired('sheets')\"\n />\n <div class=\"flex items-end justify-end\">\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n severity=\"danger\"\n icon=\"general.trash-01\"\n tooltip=\"Remove sheet\"\n (onClick)=\"removeConvertToFileSheet(sheet.index)\"\n />\n </div>\n </div>\n </div>\n } @empty {\n <div\n class=\"rounded-md bg-(--p-content-background) px-3 py-2 text-[12px] text-(--p-text-muted-color)\"\n >\n Single sheet uses the selected data source.\n </div>\n }\n </div>\n </div>\n }\n\n @if (convertToFileIsText()) {\n <div class=\"mt-4 grid gap-3\">\n <mt-textarea-field\n class=\"w-full\"\n [ngModel]=\"config()['content'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:content')\"\n (ngModelChange)=\"onConfigFieldChange('content', $event)\"\n label=\"Content\"\n [required]=\"isConfigFieldRequired('content')\"\n rows=\"7\"\n />\n <mt-text-field\n [ngModel]=\"config()['encoding'] ?? 'utf-8'\"\n [readonly]=\"true\"\n label=\"Encoding\"\n />\n </div>\n }\n\n @if (convertToFileIsBase64()) {\n <div class=\"mt-4 grid gap-3 md:grid-cols-2\">\n <mt-textarea-field\n class=\"w-full font-mono md:col-span-2\"\n [ngModel]=\"config()['base64Expression'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:base64Expression')\"\n (ngModelChange)=\"\n onConfigFieldChange('base64Expression', $event)\n \"\n label=\"Base64 source\"\n [required]=\"isConfigFieldRequired('base64Expression')\"\n rows=\"5\"\n />\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"config()['extension'] ?? convertToFileExtension()\"\n (ngModelChange)=\"onConfigFieldChange('extension', $event)\"\n label=\"Extension\"\n [required]=\"isConfigFieldRequired('extension')\"\n />\n </div>\n }\n </section>\n }\n }\n @case (\"DataTransform\") {\n @if (sectionInMain(\"dataTransform\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">\n {{ dataTransformActionLabel() }}\n </div>\n @if (dataTransformUnavailableReason(); as reason) {\n <div\n class=\"mb-3 rounded-md border border-(--p-content-border-color) bg-(--p-surface-50) px-3 py-2 text-[12px] leading-5 text-(--p-text-muted-color)\"\n >\n {{ reason }}\n </div>\n }\n\n @if (\n !dataTransformIsMerge() && dataTransformUsesKnownActionEditor()\n ) {\n <div class=\"grid gap-3\">\n <div class=\"grid gap-3 md:grid-cols-[minmax(220px,0.45fr)_1fr]\">\n <mt-select-field\n [ngModel]=\"dataTransformSourcePreset()\"\n (ngModelChange)=\"onDataTransformSourcePresetChange($event)\"\n [label]=\"dataTransformSourceLabel()\"\n hint=\"Choose which list this action should transform.\"\n [required]=\"isConfigFieldRequired('sourceExpression')\"\n [options]=\"dataTransformSourceOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n @if (showDataTransformSourceExpression()) {\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"dataTransformSourceExpression()\"\n (focusin)=\"setExpressionTarget('config:sourceExpression')\"\n (ngModelChange)=\"\n onDataTransformSourceExpressionChange($event)\n \"\n [label]=\"dataTransformSourceExpressionLabel()\"\n hint=\"Must resolve to an array of items.\"\n [required]=\"isConfigFieldRequired('sourceExpression')\"\n />\n } @else {\n <div\n class=\"rounded-md border border-surface-200 bg-surface-50 px-3 py-2\"\n >\n <div class=\"fp-ae-label mb-1\">Selected input</div>\n <div\n class=\"truncate font-mono text-sm text-(--p-text-color)\"\n >\n {{ dataTransformSourceSummary() }}\n </div>\n </div>\n }\n </div>\n <div class=\"flex flex-wrap gap-2\">\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n icon=\"general.search-sm\"\n label=\"Preview source\"\n [disabled]=\"!dataTransformSourceExpression()\"\n (onClick)=\"previewDataTransformSourceExpression()\"\n />\n </div>\n </div>\n }\n\n @if (expressionPreview(); as preview) {\n <div\n class=\"mt-3 rounded-md border border-(--p-content-border-color) bg-(--p-surface-50) p-3\"\n >\n <div class=\"fp-ae-label mb-2\">Source preview</div>\n <pre class=\"fp-ae-code\">{{ schemaText(preview) }}</pre>\n </div>\n }\n\n @if (dataTransformIsFilter()) {\n <div class=\"mt-4 grid gap-3\">\n @if (dataTransformUsesStructuredConditionEditor()) {\n <div class=\"fp-ae-label\">Keep item when</div>\n <div class=\"grid gap-3 md:grid-cols-[1fr_180px_1fr]\">\n <mt-text-field\n [ngModel]=\"dataTransformConditionFieldText('left')\"\n (focusin)=\"setExpressionTarget('config:condition:left')\"\n (ngModelChange)=\"\n onDataTransformConditionFieldChange('left', $event)\n \"\n label=\"Left value\"\n hint=\"Field, value, or expression from each item.\"\n [required]=\"isConfigFieldRequired('condition.left')\"\n />\n <mt-select-field\n [ngModel]=\"\n dataTransformConditionFieldValue('operator') || 'equals'\n \"\n (ngModelChange)=\"\n onDataTransformConditionFieldChange('operator', $event)\n \"\n label=\"Operator\"\n [required]=\"isConfigFieldRequired('condition.operator')\"\n [options]=\"ifOperatorOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-text-field\n [ngModel]=\"dataTransformConditionFieldText('right')\"\n (focusin)=\"setExpressionTarget('config:condition:right')\"\n (ngModelChange)=\"\n onDataTransformConditionFieldChange('right', $event)\n \"\n label=\"Right value\"\n hint=\"Value or expression to compare against.\"\n [required]=\"isConfigFieldRequired('condition.right')\"\n />\n </div>\n } @else {\n <mt-textarea-field\n class=\"w-full font-mono\"\n [ngModel]=\"dataTransformConditionText()\"\n (focusin)=\"setExpressionTarget('config:condition')\"\n (ngModelChange)=\"onDataTransformConditionChange($event)\"\n label=\"Keep item when\"\n hint=\"Condition expression or JSON supplied by the backend catalog.\"\n [required]=\"isConfigFieldRequired('condition')\"\n rows=\"4\"\n />\n }\n </div>\n }\n\n @if (dataTransformIsSort()) {\n <div class=\"mt-4 grid gap-3 md:grid-cols-2\">\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"config()['sortBy'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:sortBy')\"\n (ngModelChange)=\"onConfigFieldChange('sortBy', $event)\"\n label=\"Sort by field\"\n hint=\"Field path inside each item, for example createdAt.\"\n [required]=\"isConfigFieldRequired('sortBy')\"\n />\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"config()['sortExpression'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:sortExpression')\"\n (ngModelChange)=\"\n onConfigFieldChange('sortExpression', $event)\n \"\n label=\"Custom sort expression\"\n hint=\"Optional expression when a simple field path is not enough.\"\n [required]=\"isConfigFieldRequired('sortExpression')\"\n />\n <mt-select-field\n [ngModel]=\"config()['direction'] ?? 'asc'\"\n (ngModelChange)=\"onConfigFieldChange('direction', $event)\"\n label=\"Direction\"\n [required]=\"isConfigFieldRequired('direction')\"\n [options]=\"dataTransformDirectionOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-select-field\n [ngModel]=\"config()['valueType'] ?? 'string'\"\n (ngModelChange)=\"onConfigFieldChange('valueType', $event)\"\n label=\"Value type\"\n [required]=\"isConfigFieldRequired('valueType')\"\n [options]=\"dataTransformValueTypeOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n </div>\n }\n\n @if (dataTransformIsLimit()) {\n <div class=\"mt-4 grid gap-3 md:grid-cols-2\">\n <mt-number-field\n [ngModel]=\"numberValue(config()['offset']) ?? 0\"\n (ngModelChange)=\"onConfigFieldChange('offset', $event)\"\n label=\"Skip first\"\n [required]=\"isConfigFieldRequired('offset')\"\n [min]=\"0\"\n />\n <mt-number-field\n [ngModel]=\"numberValue(config()['limit'])\"\n (ngModelChange)=\"onConfigFieldChange('limit', $event)\"\n label=\"Take at most\"\n [required]=\"isConfigFieldRequired('limit')\"\n [min]=\"1\"\n />\n </div>\n }\n\n @if (dataTransformIsMapFields()) {\n <div class=\"mt-4 grid gap-3\">\n <mt-select-field\n [ngModel]=\"config()['mode'] ?? 'merge'\"\n (ngModelChange)=\"onConfigFieldChange('mode', $event)\"\n label=\"Field mode\"\n [required]=\"isConfigFieldRequired('mode')\"\n [options]=\"dataTransformMapModeOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <ng-container\n *ngTemplateOutlet=\"\n mapEditor;\n context: {\n objectKey: 'fields',\n rows: fieldsRows(),\n keyLabel: 'Output field',\n valueLabel: 'Value or expression',\n addLabel: 'Add mapped field',\n emptyText:\n 'No fields mapped yet. Add an output field to create or update item data.',\n keyHint: 'Name of the field in the output item.',\n valueHint:\n 'Use a fixed value or an expression from each item.',\n required: isConfigFieldRequired('fields'),\n }\n \"\n />\n </div>\n }\n\n @if (dataTransformIsAggregate()) {\n <div class=\"mt-4 grid gap-3\">\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"config()['groupBy'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:groupBy')\"\n (ngModelChange)=\"onConfigFieldChange('groupBy', $event)\"\n label=\"Group items by\"\n hint=\"Optional field path or expression used to group results.\"\n [required]=\"isConfigFieldRequired('groupBy')\"\n />\n <div class=\"rounded-md border border-surface-200 p-3\">\n <div\n class=\"mb-3 flex flex-wrap items-center justify-between gap-2\"\n >\n <div class=\"fp-ae-label\">Summary fields</div>\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n icon=\"general.plus\"\n label=\"Add summary\"\n (onClick)=\"addDataTransformAggregation()\"\n />\n </div>\n <div class=\"space-y-3\">\n @for (\n item of dataTransformAggregationRows();\n track item.index\n ) {\n <div\n class=\"grid gap-2 rounded-md bg-surface-50 p-3 md:grid-cols-[minmax(160px,0.8fr)_minmax(180px,0.9fr)_1fr_minmax(150px,0.7fr)_auto]\"\n >\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"item.key\"\n (ngModelChange)=\"\n updateDataTransformAggregation(\n item.index,\n 'key',\n $event\n )\n \"\n label=\"Output field\"\n hint=\"Name saved in the summary output.\"\n [required]=\"isConfigFieldRequired('aggregations')\"\n />\n <mt-select-field\n [ngModel]=\"item.operation\"\n (ngModelChange)=\"\n updateDataTransformAggregation(\n item.index,\n 'operation',\n $event\n )\n \"\n label=\"Calculation\"\n [required]=\"isConfigFieldRequired('aggregations')\"\n [options]=\"dataTransformAggregationOperationOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"item.field\"\n [readonly]=\"\n !dataTransformAggregationNeedsField(item.operation)\n \"\n (focusin)=\"\n setExpressionTarget(\n 'config:aggregations:' + item.index + ':field'\n )\n \"\n (ngModelChange)=\"\n updateDataTransformAggregation(\n item.index,\n 'field',\n $event\n )\n \"\n label=\"Field or expression\"\n [hint]=\"\n dataTransformAggregationFieldHint(item.operation)\n \"\n [required]=\"\n isConfigFieldRequired('aggregations') &&\n dataTransformAggregationNeedsField(item.operation)\n \"\n />\n <mt-select-field\n [ngModel]=\"item.valueType\"\n (ngModelChange)=\"\n updateDataTransformAggregation(\n item.index,\n 'valueType',\n $event\n )\n \"\n label=\"Value type\"\n [required]=\"isConfigFieldRequired('aggregations')\"\n [options]=\"dataTransformValueTypeOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-button\n class=\"self-end\"\n size=\"small\"\n severity=\"secondary\"\n variant=\"outlined\"\n label=\"Remove\"\n (onClick)=\"removeDataTransformAggregation(item.index)\"\n />\n </div>\n } @empty {\n <div\n class=\"rounded-md bg-(--p-content-background) px-3 py-2 text-[12px] text-(--p-text-muted-color)\"\n >\n No summary fields yet. Add a summary such as count,\n total, average, minimum, or maximum.\n </div>\n }\n </div>\n </div>\n </div>\n }\n\n @if (dataTransformIsMerge()) {\n <div class=\"mt-4 grid gap-3 md:grid-cols-2\">\n <mt-textarea-field\n class=\"w-full font-mono md:col-span-2\"\n [ngModel]=\"config()['leftExpression'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:leftExpression')\"\n (ngModelChange)=\"\n onConfigFieldChange('leftExpression', $event)\n \"\n label=\"First list\"\n hint=\"Expression that resolves to the first list of items.\"\n [required]=\"isConfigFieldRequired('leftExpression')\"\n rows=\"3\"\n />\n <mt-textarea-field\n class=\"w-full font-mono md:col-span-2\"\n [ngModel]=\"config()['rightExpression'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:rightExpression')\"\n (ngModelChange)=\"\n onConfigFieldChange('rightExpression', $event)\n \"\n label=\"Second list\"\n hint=\"Expression that resolves to the second list of items.\"\n [required]=\"isConfigFieldRequired('rightExpression')\"\n rows=\"3\"\n />\n <mt-select-field\n [ngModel]=\"config()['strategy'] ?? 'append'\"\n (ngModelChange)=\"onConfigFieldChange('strategy', $event)\"\n label=\"Merge strategy\"\n [required]=\"isConfigFieldRequired('strategy')\"\n [options]=\"dataTransformMergeStrategyOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n @if (dataTransformUsesKeyedMerge()) {\n <div class=\"grid gap-3 md:grid-cols-2\">\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"config()['leftKey'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('leftKey', $event)\"\n label=\"First list key\"\n [required]=\"isConfigFieldRequired('leftKey')\"\n />\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"config()['rightKey'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('rightKey', $event)\"\n label=\"Second list key\"\n [required]=\"isConfigFieldRequired('rightKey')\"\n />\n </div>\n }\n </div>\n }\n\n @if (!dataTransformUsesKnownActionEditor()) {\n <div class=\"mt-4 grid gap-3\">\n @for (field of dataTransformGenericFields(); track field.key) {\n @if (field.enumValues.length) {\n <mt-select-field\n [ngModel]=\"config()[field.key] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange(field.key, $event)\"\n [label]=\"field.label\"\n [hint]=\"field.description ?? ''\"\n [required]=\"isConfigFieldRequired(field.key)\"\n [options]=\"enumOptions(field.enumValues)\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n } @else if (field.type === \"number\") {\n <mt-number-field\n [ngModel]=\"numberValue(config()[field.key])\"\n (ngModelChange)=\"onConfigFieldChange(field.key, $event)\"\n [label]=\"field.label\"\n [hint]=\"field.description ?? ''\"\n [required]=\"isConfigFieldRequired(field.key)\"\n />\n } @else {\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"fieldText(field.key)\"\n (focusin)=\"setExpressionTarget('config:' + field.key)\"\n (ngModelChange)=\"onConfigFieldChange(field.key, $event)\"\n [label]=\"field.label\"\n [hint]=\"field.description ?? ''\"\n [required]=\"isConfigFieldRequired(field.key)\"\n />\n }\n } @empty {\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"dataTransformActionKey()\"\n [readonly]=\"true\"\n label=\"Action\"\n />\n }\n </div>\n }\n\n <div class=\"mt-4 grid gap-3 md:grid-cols-3\">\n <mt-number-field\n [ngModel]=\"numberValue(config()['maxOutputItems'])\"\n (ngModelChange)=\"onConfigFieldChange('maxOutputItems', $event)\"\n label=\"Output limit\"\n [required]=\"isConfigFieldRequired('maxOutputItems')\"\n [min]=\"1\"\n />\n <div\n class=\"rounded-md border border-(--p-content-border-color) bg-(--p-surface-50) p-3 md:col-span-2\"\n >\n <div class=\"fp-ae-label mb-2\">Output</div>\n <div class=\"flex flex-wrap gap-1.5\">\n @for (field of dataTransformOutputFieldLabels; track field) {\n <span\n class=\"rounded-md bg-(--p-surface-100) px-2 py-1 font-mono text-[11px]\"\n >{{ field }}</span\n >\n }\n </div>\n </div>\n </div>\n </section>\n }\n }\n @case (\"LoopOverItems\") {\n @if (sectionInMain(\"loopOverItems\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Loop over items</div>\n\n <div class=\"grid gap-3\">\n <div class=\"grid gap-3 md:grid-cols-[minmax(220px,0.45fr)_1fr]\">\n <mt-select-field\n [ngModel]=\"loopSourceMode()\"\n (ngModelChange)=\"onLoopSourceModeChange($event)\"\n label=\"List source\"\n hint=\"Choose an output array or type an array expression.\"\n [required]=\"isConfigFieldRequired('itemsExpression')\"\n [options]=\"loopSourceModeOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n @if (showLoopItemsExpression()) {\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"loopItemsExpression()\"\n (focusin)=\"setExpressionTarget('config:itemsExpression')\"\n (ngModelChange)=\"onLoopItemsExpressionChange($event)\"\n label=\"Array expression\"\n hint=\"Must resolve to an array.\"\n [required]=\"isConfigFieldRequired('itemsExpression')\"\n />\n } @else {\n <mt-select-field\n [ngModel]=\"loopSelectedOutputArray()\"\n (ngModelChange)=\"onLoopOutputArrayChange($event)\"\n label=\"Output array\"\n hint=\"Arrays exposed by previous connected steps.\"\n [required]=\"isConfigFieldRequired('itemsExpression')\"\n [options]=\"loopOutputArrayOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n }\n </div>\n <div class=\"flex flex-wrap gap-2\">\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n icon=\"general.search-sm\"\n label=\"Preview array\"\n [disabled]=\"!loopItemsExpression()\"\n (onClick)=\"previewLoopItemsExpression()\"\n />\n </div>\n </div>\n\n @if (expressionPreview(); as preview) {\n <div\n class=\"mt-3 rounded-md border border-(--p-content-border-color) bg-(--p-surface-50) p-3\"\n >\n <div class=\"fp-ae-label mb-2\">Expression preview</div>\n <pre class=\"fp-ae-code\">{{ schemaText(preview) }}</pre>\n </div>\n }\n\n <div class=\"mt-4 border-t border-surface-100 pt-4\">\n <div class=\"fp-ae-label mb-3\">Run settings</div>\n <div class=\"grid gap-3 md:grid-cols-2 xl:grid-cols-4\">\n <mt-number-field\n [ngModel]=\"\n numberValue(\n config()['maxItems'] ?? config()['maxIterations']\n )\n \"\n (ngModelChange)=\"onConfigFieldChange('maxItems', $event)\"\n label=\"Item limit\"\n hint=\"Optional maximum number of items to process.\"\n [required]=\"isConfigFieldRequired('maxItems')\"\n [min]=\"1\"\n />\n <mt-select-field\n [ngModel]=\"loopFailureBehavior()\"\n (ngModelChange)=\"onLoopFailureBehaviorChange($event)\"\n label=\"When an item fails\"\n [required]=\"isConfigFieldRequired('failureBehavior')\"\n [options]=\"loopFailureBehaviorOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-select-field\n [ngModel]=\"config()['emptyBehavior'] ?? 'done'\"\n (ngModelChange)=\"onConfigFieldChange('emptyBehavior', $event)\"\n label=\"When list is empty\"\n [required]=\"isConfigFieldRequired('emptyBehavior')\"\n [options]=\"loopEmptyBehaviorOptions\"\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()['collectResults'] === true\"\n (ngModelChange)=\"\n onConfigFieldChange('collectResults', $event === true)\n \"\n label=\"Save item results\"\n hint=\"Stores a bounded summary for the done route.\"\n />\n </div>\n </div>\n\n <details class=\"mt-4 border-t border-surface-100 pt-3\">\n <summary\n class=\"cursor-pointer text-sm font-semibold text-(--p-text-color)\"\n >\n Advanced runtime details\n </summary>\n <div class=\"mt-3 grid gap-3 md:grid-cols-3\">\n <div>\n <div class=\"fp-ae-label mb-1\">Current item token</div>\n <div\n class=\"rounded-md border border-surface-200 bg-surface-50 px-3 py-2 font-mono text-sm text-(--p-text-color)\"\n >\n $item.json\n </div>\n </div>\n <div>\n <div class=\"fp-ae-label mb-1\">Loop context token</div>\n <div\n class=\"rounded-md border border-surface-200 bg-surface-50 px-3 py-2 font-mono text-sm text-(--p-text-color)\"\n >\n $loop.current\n </div>\n </div>\n <mt-number-field\n [ngModel]=\"numberValue(config()['concurrency']) ?? 1\"\n (ngModelChange)=\"onLoopConcurrencyChange($event)\"\n label=\"Concurrency\"\n hint=\"This deployment supports 1.\"\n [required]=\"isConfigFieldRequired('concurrency')\"\n [min]=\"1\"\n [max]=\"1\"\n />\n </div>\n </details>\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\n does 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 [required]=\"isConfigFieldRequired('targetModule')\"\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 [required]=\"isConfigFieldRequired('operation')\"\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 [required]=\"isConfigFieldRequired('idempotencyKey')\"\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\n >{{ field.viewType ?? \"Value\" }}\n @if (field.required) {\n *\n }\n </strong>\n </div>\n }\n </div>\n </div>\n }\n <div class=\"mt-3\">\n <ng-container\n *ngTemplateOutlet=\"\n mapEditor;\n context: {\n objectKey: 'mapping',\n rows: mappingRows(),\n keyLabel: 'Module field',\n valueLabel: 'Expression / value',\n addLabel: 'Add module field',\n required: isConfigFieldRequired('mapping'),\n }\n \"\n />\n </div>\n <div class=\"mt-3 flex gap-2\">\n <mt-button\n size=\"small\"\n severity=\"primary\"\n label=\"Validate mapping\"\n (onClick)=\"validateCommitMapping()\"\n />\n </div>\n @if (commitValidation(); as result) {\n <div\n class=\"mt-3 rounded-lg bg-(--p-surface-100) px-3 py-2 text-[12px]\"\n >\n {{\n result.isValid === false\n ? \"Mapping invalid\"\n : \"Mapping accepted by helper\"\n }}\n @for (issue of resultIssues(result); track $index) {\n <div class=\"mt-1 text-[11px] text-(--p-text-muted-color)\">\n {{ issue }}\n </div>\n }\n </div>\n }\n @if (sectionInMain(\"credentials\")) {\n <div\n class=\"mt-3 rounded-md border border-surface-200 bg-surface-0 p-3\"\n >\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 [required]=\"isConfigFieldRequired('statusCode')\"\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 [required]=\"isConfigFieldRequired('responseMode')\"\n [options]=\"responseModeOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n </div>\n <div class=\"mt-3\">\n <ng-container\n *ngTemplateOutlet=\"\n mapEditor;\n context: {\n objectKey: 'headers',\n rows: headerRows(),\n keyLabel: 'Header',\n valueLabel: 'Value',\n addLabel: 'Add header',\n }\n \"\n />\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 [required]=\"isConfigFieldRequired('body')\"\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 [required]=\"isConfigFieldRequired('targetAutomationId')\"\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 [required]=\"isConfigFieldRequired('revisionMode')\"\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)=\"\n onConfigFieldChange('specificRevisionId', $event)\n \"\n label=\"Specific revision id\"\n hint=\"Required only when revision mode is Specific revision.\"\n [required]=\"isConfigFieldRequired('specificRevisionId')\"\n />\n }\n <mt-toggle-field\n size=\"small\"\n labelPosition=\"end\"\n [ngModel]=\"config()['waitForCompletion'] !== false\"\n (ngModelChange)=\"\n onConfigFieldChange('waitForCompletion', $event === true)\n \"\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)=\"\n onConfigFieldChange('inputMappingJson', $event)\n \"\n label=\"Input mapping JSON\"\n hint=\"JSON object or expression map sent into the child automation input.\"\n [required]=\"isConfigFieldRequired('inputMappingJson')\"\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)=\"\n onConfigFieldChange('outputMappingJson', $event)\n \"\n label=\"Output mapping JSON\"\n hint=\"JSON object or expression map for child output/status returned to the parent.\"\n [required]=\"isConfigFieldRequired('outputMappingJson')\"\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 [required]=\"isConfigFieldRequired('targetAutomationId')\"\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 [required]=\"isConfigFieldRequired('revisionMode')\"\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)=\"\n onConfigFieldChange('specificRevisionId', $event)\n \"\n label=\"Specific revision id\"\n hint=\"Required only when revision mode is Specific revision.\"\n [required]=\"isConfigFieldRequired('specificRevisionId')\"\n />\n }\n <mt-toggle-field\n size=\"small\"\n labelPosition=\"end\"\n [ngModel]=\"config()['waitForCompletion'] !== false\"\n (ngModelChange)=\"\n onConfigFieldChange('waitForCompletion', $event === true)\n \"\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)=\"\n onConfigFieldChange('inputMappingJson', $event)\n \"\n label=\"Input mapping JSON\"\n hint=\"JSON object or expression map sent into the child automation input.\"\n [required]=\"isConfigFieldRequired('inputMappingJson')\"\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)=\"\n onConfigFieldChange('outputMappingJson', $event)\n \"\n label=\"Output mapping JSON\"\n hint=\"JSON object or expression map for child output/status returned to the parent.\"\n [required]=\"isConfigFieldRequired('outputMappingJson')\"\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\n class=\"flex items-center justify-between gap-3 border-b border-surface-200 bg-surface-50 px-4 py-3\"\n >\n <div class=\"fp-ae-section-title !m-0\">Parallel start</div>\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n icon=\"general.plus\"\n label=\"Add branch\"\n (onClick)=\"addParallelBranch()\"\n />\n </div>\n <div class=\"space-y-3 p-4\">\n @for (\n branch of parallelBranches();\n track branch.key;\n let i = $index\n ) {\n <div\n class=\"rounded-md border border-surface-200 bg-surface-0 p-3\"\n >\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 [required]=\"isConfigFieldRequired('branches')\"\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 [required]=\"isConfigFieldRequired('branches')\"\n />\n <mt-text-field\n [ngModel]=\"branch.description\"\n (ngModelChange)=\"\n updateParallelBranch(i, 'description', $event)\n \"\n label=\"Description\"\n hint=\"Optional branch note for reviewers.\"\n [required]=\"false\"\n />\n <div class=\"flex items-end gap-1\">\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n icon=\"arrow.arrow-up\"\n tooltip=\"Move up\"\n (onClick)=\"moveParallelBranch(i, -1)\"\n />\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n icon=\"arrow.arrow-down\"\n tooltip=\"Move down\"\n (onClick)=\"moveParallelBranch(i, 1)\"\n />\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n severity=\"danger\"\n icon=\"general.trash-01\"\n tooltip=\"Remove branch\"\n (onClick)=\"removeParallelBranch(i)\"\n />\n </div>\n </div>\n <div\n class=\"mt-3 flex flex-wrap items-center gap-1.5 text-[11px]\"\n >\n <span\n 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)\"\n >{{ branch.key }}</span\n >\n <span\n 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)\"\n >{{ branch.routeCount }} connected route{{\n branch.routeCount === 1 ? \"\" : \"s\"\n }}</span\n >\n </div>\n </div>\n }\n @if (parallelBranches().length === 0) {\n <p class=\"fp-ae-copy\">\n Add at least two stable branch keys. Backend validation blocks\n publish until branches are valid.\n </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 [required]=\"isConfigFieldRequired('joinPolicy')\"\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 [required]=\"isConfigFieldRequired('threshold')\"\n [min]=\"1\"\n />\n }\n <mt-select-field\n [ngModel]=\"config()['aggregationStrategy'] ?? 'MergeObjects'\"\n (ngModelChange)=\"\n onConfigFieldChange('aggregationStrategy', $event)\n \"\n label=\"Aggregation strategy\"\n hint=\"How backend joins branch outputs into the join node output.\"\n [required]=\"isConfigFieldRequired('aggregationStrategy')\"\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)=\"\n onConfigFieldChange('outputTargetPath', $event)\n \"\n label=\"Output target path\"\n hint=\"Optional context path where joined output should be written.\"\n [required]=\"isConfigFieldRequired('outputTargetPath')\"\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\n class=\"grid items-end gap-3 md:grid-cols-[minmax(0,1fr)_minmax(280px,0.7fr)]\"\n >\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 [required]=\"isConfigFieldRequired('mode')\"\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)=\"\n onConfigFieldChange('firstMatch', $event === true)\n \"\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]=\"\n showSwitchExpression()\n ? 'font-mono'\n : 'font-mono md:col-span-2'\n \"\n [ngModel]=\"config()['sourceValue'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:sourceValue')\"\n (ngModelChange)=\"\n onConfigFieldChange('sourceValue', $event)\n \"\n label=\"Source value\"\n hint=\"Value or expression used for value matching.\"\n [required]=\"isConfigFieldRequired('sourceValue')\"\n />\n }\n @if (showSwitchExpression()) {\n <mt-text-field\n [class]=\"\n showSwitchSourceValue()\n ? 'font-mono'\n : 'font-mono md:col-span-2'\n \"\n [ngModel]=\"config()['expression'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:expression')\"\n (ngModelChange)=\"\n onConfigFieldChange('expression', $event)\n \"\n label=\"Expression\"\n hint=\"Expression evaluated when mode is expression or rules.\"\n [required]=\"isConfigFieldRequired('expression')\"\n />\n }\n </div>\n }\n <div\n class=\"grid items-end gap-3 md:grid-cols-[minmax(0,1fr)_minmax(280px,0.7fr)]\"\n >\n @if (showSwitchDefaultOutputKey()) {\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"config()['defaultOutputKey'] ?? 'default'\"\n (ngModelChange)=\"\n onConfigFieldChange('defaultOutputKey', $event)\n \"\n label=\"Default output key\"\n hint=\"Stable default route key used when no case matches.\"\n [required]=\"isConfigFieldRequired('defaultOutputKey')\"\n />\n }\n <mt-toggle-field\n [class]=\"\n showSwitchDefaultOutputKey()\n ? 'self-end pb-1'\n : 'self-end pb-1 md:col-span-2'\n \"\n size=\"small\"\n labelPosition=\"end\"\n [ngModel]=\"config()['includeDefaultOutput'] !== false\"\n (ngModelChange)=\"\n onConfigFieldChange('includeDefaultOutput', $event === true)\n \"\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\n class=\"flex items-center justify-between gap-3 border-b border-surface-200 bg-surface-50 px-4 py-3\"\n >\n <div class=\"fp-ae-section-title !m-0\">Switch cases</div>\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n icon=\"general.plus\"\n label=\"Add case\"\n (onClick)=\"addSwitchCase()\"\n />\n </div>\n <div class=\"space-y-3 p-4\">\n @for (item of switchCases(); track item.key; let i = $index) {\n <div\n class=\"rounded-md border border-surface-200 bg-surface-0 p-3\"\n >\n <div\n class=\"flex flex-wrap items-start justify-between gap-3 border-b border-surface-100 pb-3\"\n >\n <div class=\"min-w-0\">\n <div class=\"flex flex-wrap items-center gap-1.5\">\n <span\n class=\"text-sm font-semibold text-(--p-text-color)\"\n >Case {{ i + 1 }}</span\n >\n <span\n class=\"inline-flex h-7 items-center rounded-md border border-surface-200 bg-surface-50 px-2 font-mono text-[11px] font-semibold text-(--p-text-color)\"\n >{{ item.routeKey }}</span\n >\n <span\n class=\"inline-flex h-7 items-center rounded-md border border-surface-200 bg-surface-0 px-2 text-[11px] font-medium text-(--p-text-muted-color)\"\n >{{ item.routeCount }} connected route{{\n item.routeCount === 1 ? \"\" : \"s\"\n }}</span\n >\n </div>\n <div\n class=\"mt-1 truncate text-xs font-medium text-(--p-text-muted-color)\"\n >\n {{ item.label || item.routeKey }}\n </div>\n </div>\n <div class=\"flex shrink-0 items-center gap-1\">\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n icon=\"arrow.arrow-up\"\n tooltip=\"Move up\"\n (onClick)=\"moveSwitchCase(i, -1)\"\n />\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n icon=\"arrow.arrow-down\"\n tooltip=\"Move down\"\n (onClick)=\"moveSwitchCase(i, 1)\"\n />\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n severity=\"danger\"\n icon=\"general.trash-01\"\n tooltip=\"Remove case\"\n (onClick)=\"removeSwitchCase(i)\"\n />\n </div>\n </div>\n <div class=\"mt-3 grid gap-3 md:grid-cols-2\">\n <mt-text-field\n [ngModel]=\"item.label\"\n (ngModelChange)=\"updateSwitchCase(i, 'label', $event)\"\n label=\"Case name\"\n hint=\"Shown in route labels.\"\n [required]=\"isConfigFieldRequired('cases')\"\n />\n @if (showSwitchCaseValue()) {\n <mt-text-field\n [ngModel]=\"item.value\"\n (focusin)=\"\n setExpressionTarget(\n 'config:cases:' + item.key + ':value'\n )\n \"\n (ngModelChange)=\"updateSwitchCase(i, 'value', $event)\"\n label=\"Match value\"\n hint=\"Compared with the source value.\"\n [required]=\"isConfigFieldRequired('cases')\"\n />\n }\n @if (showSwitchCaseCondition()) {\n <mt-text-field\n class=\"font-mono md:col-span-2\"\n [ngModel]=\"item.condition\"\n (focusin)=\"\n setExpressionTarget(\n 'config:cases:' + item.key + ':condition'\n )\n \"\n (ngModelChange)=\"\n updateSwitchCase(i, 'condition', $event)\n \"\n label=\"Rule condition\"\n hint=\"When true, this case is selected.\"\n [required]=\"isConfigFieldRequired('cases')\"\n />\n }\n </div>\n <details\n class=\"mt-3 rounded-md border border-surface-200 bg-surface-50 px-3 py-2 text-xs text-(--p-text-muted-color)\"\n >\n <summary class=\"cursor-pointer font-medium\">\n Advanced route settings\n </summary>\n <div class=\"mt-3 grid gap-3 md:grid-cols-2\">\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"item.key\"\n (ngModelChange)=\"updateSwitchCase(i, 'key', $event)\"\n label=\"Route key\"\n hint=\"Stable output key for connected routes.\"\n [required]=\"isConfigFieldRequired('cases')\"\n />\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"item.expression\"\n (focusin)=\"\n setExpressionTarget(\n 'config:cases:' + item.key + ':expression'\n )\n \"\n (ngModelChange)=\"\n updateSwitchCase(i, 'expression', $event)\n \"\n label=\"Advanced condition expression\"\n hint=\"Optional true or false expression for this case.\"\n [required]=\"false\"\n />\n </div>\n <div\n class=\"mt-3 inline-flex h-7 items-center rounded-md border border-surface-200 bg-surface-0 px-2 font-medium\"\n >\n Visual order {{ i + 1 }}\n </div>\n </details>\n </div>\n }\n @if (switchCases().length === 0) {\n <p class=\"fp-ae-copy\">\n Add a case for each route this switch can select.\n </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 (\n supportsConfigKey(\"status\") ||\n supportsConfigKey(\"message\") ||\n supportsConfigKey(\"output\")\n ) {\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 Stop nodes use their backend defaults and do not expose extra\n configuration.\n </p>\n }\n </section>\n }\n }\n }\n\n @if (sectionInAdvanced(\"policy\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">\n Retry, timeout, and failure handling\n </div>\n <div class=\"grid gap-3 md:grid-cols-3\">\n <mt-number-field\n [ngModel]=\"policyNumberValue('timeoutPolicyJson', 'timeoutSeconds')\"\n (ngModelChange)=\"\n updatePolicyNumber('timeoutPolicyJson', 'timeoutSeconds', $event)\n \"\n label=\"Step timeout (seconds)\"\n hint=\"How long this step may run before Automation Engine marks it timed out.\"\n [min]=\"1\"\n />\n <mt-number-field\n [ngModel]=\"policyNumberValue('retryPolicyJson', 'maxAttempts')\"\n (ngModelChange)=\"\n updatePolicyNumber('retryPolicyJson', 'maxAttempts', $event)\n \"\n label=\"Max attempts\"\n hint=\"Total tries including the first run. Use 1 for no retry.\"\n [min]=\"1\"\n />\n <mt-select-field\n [ngModel]=\"errorPolicyMode()\"\n (ngModelChange)=\"updateErrorPolicyMode($event)\"\n label=\"When this step fails\"\n hint=\"Choose whether failures pause for recovery, use the failure path, or fail the workflow.\"\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 count, or\n failure rule here.\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(\"mapping\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Custom data handoff</div>\n <div class=\"grid gap-3 md:grid-cols-2\">\n <div>\n <div class=\"fp-ae-label mb-2\">Inputs sent to this step</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)\">{{\n row.key\n }}</span>\n <mt-text-field\n [ngModel]=\"valueText(row.value)\"\n (focusin)=\"\n setExpressionTarget('config:inputMapping:' + row.key)\n \"\n (ngModelChange)=\"\n updateJsonField('inputMappingJson', row.key, $event)\n \"\n hint=\"Value this step receives from the workflow context.\"\n />\n </label>\n }\n </div>\n <div>\n <div class=\"fp-ae-label mb-2\">Outputs saved for next steps</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)\">{{\n row.key\n }}</span>\n <mt-text-field\n [ngModel]=\"valueText(row.value)\"\n (focusin)=\"\n setExpressionTarget('config:outputMapping:' + row.key)\n \"\n (ngModelChange)=\"\n updateJsonField('outputMappingJson', row.key, $event)\n \"\n hint=\"Value this step exposes to the rest of the workflow.\"\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 Default handoff is active. This step receives the current workflow\n item and exposes its normal output to the next connected step. Add a\n custom handoff only when the business data must be reshaped.\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.\n Triggers 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\">\n Config schema\n </summary>\n <pre class=\"fp-ae-code\">{{ schemaText(configSchema()) }}</pre>\n </details>\n @if (hasTriggerPayloadSchema() && triggerPayloadSchema(); as schema) {\n <details\n class=\"rounded-lg border border-(--p-content-border-color)\"\n >\n <summary\n class=\"cursor-pointer px-3 py-2 text-[12px] font-semibold\"\n >\n Payload schema\n </summary>\n <pre class=\"fp-ae-code\">{{ schemaText(schema) }}</pre>\n </details>\n }\n @if (authPolicySchema()) {\n <details\n class=\"rounded-lg border border-(--p-content-border-color)\"\n >\n <summary\n class=\"cursor-pointer px-3 py-2 text-[12px] font-semibold\"\n >\n Auth policy schema\n </summary>\n <pre class=\"fp-ae-code\">{{ schemaText(authPolicySchema()) }}</pre>\n </details>\n }\n @if (triggerPayloadSample(); as sample) {\n <details\n class=\"rounded-lg border border-(--p-content-border-color)\"\n >\n <summary\n class=\"cursor-pointer px-3 py-2 text-[12px] font-semibold\"\n >\n Payload or request sample\n </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\">\n Config schema\n </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\">\n Input schema\n </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\">\n Output schema\n </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\n class=\"rounded-md bg-(--p-surface-100) px-2 py-1 font-mono text-[11px]\"\n >{{ key }}</span\n >\n }\n @if (routeOutputKeys().length === 0) {\n <span class=\"text-[12px] text-(--p-text-muted-color)\"\n >No outgoing route keys</span\n >\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]=\"\n field.type === 'object' || field.type === 'array'\n \"\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\"\n [hint]=\"field.description ?? ''\"\n [required]=\"isConfigFieldRequired(field.key)\"\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)=\"\n onConfigFieldChange(field.key, $event === true)\n \"\n [label]=\"field.label\"\n [hint]=\"field.description ?? ''\"\n [required]=\"isConfigFieldRequired(field.key)\"\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\"\n [hint]=\"field.description ?? ''\"\n [required]=\"isConfigFieldRequired(field.key)\"\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\"\n [hint]=\"field.description ?? ''\"\n [required]=\"isConfigFieldRequired(field.key)\"\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\"\n [hint]=\"field.description ?? ''\"\n [required]=\"isConfigFieldRequired(field.key)\"\n rows=\"5\"\n />\n } @else {\n <mt-text-field\n [ngModel]=\"fieldText(field.key)\"\n (focusin)=\"\n field.expressionEnabled &&\n setExpressionTarget('config:' + field.key)\n \"\n (ngModelChange)=\"onConfigFieldChange(field.key, $event)\"\n [label]=\"field.label\"\n [hint]=\"field.description ?? ''\"\n [required]=\"isConfigFieldRequired(field.key)\"\n />\n }\n </div>\n }\n </div>\n </details>\n }\n </div>\n</div>\n\n<ng-template #credentialSelector>\n @if (activeCredentialProvider(); as provider) {\n <div\n class=\"mb-4 rounded-lg border border-(--p-content-border-color) bg-(--p-content-background) p-4 shadow-[0_1px_2px_rgba(15,23,42,0.05)]\"\n >\n <div class=\"mb-2 text-[12px] font-bold text-(--p-text-color)\">\n Credential\n </div>\n <div class=\"flex flex-wrap items-center gap-2\">\n @if (provider.connectAvailable) {\n <button\n type=\"button\"\n class=\"inline-flex min-h-10 max-w-full items-center gap-2 rounded-md border border-(--p-content-border-color) bg-(--p-content-background) px-3 py-2 text-[13px] font-bold text-(--p-text-color) shadow-sm transition-[border-color,box-shadow,background] hover:border-(--p-primary-color) hover:shadow-[0_2px_7px_rgba(15,23,42,0.10)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--p-primary-color)\"\n [disabled]=\"credentialOauthState() === 'loading'\"\n [attr.aria-busy]=\"\n credentialOauthState() === 'loading' ? 'true' : null\n \"\n (click)=\"connectCredentialProvider()\"\n >\n <fp-automation-node-icon\n class=\"text-[22px]\"\n [itemKey]=\"provider.providerKey\"\n [displayName]=\"provider.displayName\"\n [metadata]=\"credentialProviderIconMetadata(provider)\"\n />\n <span class=\"truncate\">{{ credentialConnectLabel() }}</span>\n </button>\n @if (provider.manualSetupAvailable) {\n <span class=\"text-[12px] text-(--p-text-muted-color)\">or</span>\n <button\n type=\"button\"\n class=\"rounded-md px-1 py-1 text-[12px] font-semibold text-(--p-text-color) underline decoration-(--p-text-muted-color) underline-offset-4 transition-colors hover:text-(--p-primary-color) focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--p-primary-color)\"\n (click)=\"showManualCredentialSetup()\"\n >\n setup manually\n </button>\n }\n } @else if (provider.manualSetupAvailable) {\n <button\n type=\"button\"\n class=\"inline-flex min-h-10 max-w-full items-center gap-2 rounded-md border border-(--p-content-border-color) bg-(--p-content-background) px-3 py-2 text-[13px] font-bold text-(--p-text-color) shadow-sm transition-[border-color,box-shadow,background] hover:border-(--p-primary-color) hover:shadow-[0_2px_7px_rgba(15,23,42,0.10)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--p-primary-color)\"\n (click)=\"showManualCredentialSetup()\"\n >\n <fp-automation-node-icon\n class=\"text-[22px]\"\n [itemKey]=\"provider.providerKey\"\n [displayName]=\"provider.displayName\"\n [metadata]=\"credentialProviderIconMetadata(provider)\"\n />\n <span class=\"truncate\"\n >Set up\n {{ provider.displayName || humanReviewProviderLabel() }}\n credential</span\n >\n </button>\n }\n </div>\n <div\n class=\"mt-2 flex flex-wrap items-center gap-1.5 text-[11px] text-(--p-text-muted-color)\"\n >\n @if (provider.authModes?.length) {\n @for (mode of provider.authModes; track mode) {\n <span\n class=\"rounded-full bg-(--p-surface-100) px-2 py-0.5 font-semibold\"\n >\n {{ mode }}\n </span>\n }\n }\n @if (provider.requiredScopes?.length) {\n <span class=\"truncate\">\n Scopes: {{ provider.requiredScopes.join(\", \") }}\n </span>\n }\n </div>\n @if (provider.setupInstructions) {\n <p class=\"m-0 mt-2 text-[11.5px] leading-5 text-(--p-text-muted-color)\">\n {{ provider.setupInstructions }}\n </p>\n }\n </div>\n }\n\n @if (manualCredentialOpen()) {\n <div\n class=\"mb-3 rounded-md border border-(--p-content-border-color) bg-(--p-content-background) p-3\"\n >\n <div class=\"grid gap-3 md:grid-cols-2\">\n <mt-text-field\n [ngModel]=\"manualCredentialDisplayName()\"\n (ngModelChange)=\"onManualCredentialDisplayNameChange($event)\"\n label=\"Credential name\"\n />\n <mt-select-field\n [ngModel]=\"manualCredentialType()\"\n (ngModelChange)=\"onManualCredentialTypeChange($event)\"\n label=\"Credential type\"\n [options]=\"credentialTypeOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n </div>\n <div class=\"mt-3 grid gap-3 md:grid-cols-2\">\n @for (field of manualCredentialFields(); track field.key) {\n @if (manualCredentialFieldIsToggle(field)) {\n <mt-toggle-field\n [ngModel]=\"manualCredentialFieldValue(field.key) === 'true'\"\n (ngModelChange)=\"onManualCredentialFieldChange(field.key, $event)\"\n [label]=\"\n manualCredentialFieldLabel(field) + (field.required ? ' *' : '')\n \"\n size=\"small\"\n />\n } @else {\n <mt-text-field\n [ngModel]=\"manualCredentialFieldValue(field.key)\"\n (ngModelChange)=\"onManualCredentialFieldChange(field.key, $event)\"\n [label]=\"\n manualCredentialFieldLabel(field) + (field.required ? ' *' : '')\n \"\n [placeholder]=\"field.placeholder ?? ''\"\n [type]=\"manualCredentialFieldInputType(field)\"\n />\n }\n }\n </div>\n @if (credentialManualError(); as error) {\n <div\n class=\"mt-3 rounded-md border border-(--p-content-border-color) bg-(--p-surface-50) px-3 py-2 text-[12px] leading-5 text-(--p-text-muted-color)\"\n >\n {{ error }}\n </div>\n }\n <div class=\"mt-3 flex justify-end gap-2\">\n <mt-button\n size=\"small\"\n variant=\"text\"\n label=\"Cancel\"\n (onClick)=\"cancelManualCredentialSetup()\"\n />\n <mt-button\n size=\"small\"\n severity=\"primary\"\n label=\"Save credential\"\n [loading]=\"credentialManualState() === 'loading'\"\n [disabled]=\"credentialManualState() === 'loading'\"\n (onClick)=\"createManualCredential()\"\n />\n </div>\n </div>\n }\n\n @if (credentialOptions().length) {\n <div class=\"space-y-2\">\n <mt-select-field\n [ngModel]=\"selectedCredentialRef()\"\n (ngModelChange)=\"selectCredential($event)\"\n label=\"Saved credential\"\n [options]=\"credentialOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n [showClear]=\"true\"\n />\n @for (credential of credentials(); track credential.credentialRef) {\n @if (credential.credentialRef === selectedCredentialRef()) {\n <div\n class=\"rounded-md border border-(--p-content-border-color) bg-(--p-surface-50) px-3 py-2\"\n >\n <div class=\"flex items-center justify-between gap-3\">\n <div class=\"min-w-0\">\n <div class=\"truncate text-[12px] font-semibold\">\n {{ credential.displayName ?? credential.credentialRef }}\n </div>\n <div\n class=\"truncate font-mono text-[11px] text-(--p-text-muted-color)\"\n >\n {{ credential.credentialRef }} /\n {{\n credential.status ??\n (credential.resolved ? \"Resolved\" : \"Unresolved\")\n }}\n </div>\n </div>\n <div class=\"flex flex-none items-center gap-2\">\n <mt-button\n size=\"small\"\n variant=\"text\"\n label=\"Test\"\n [loading]=\"\n credentialTestingRef() === credential.credentialRef\n \"\n [disabled]=\"\n credentialTestingRef() === credential.credentialRef\n \"\n (onClick)=\"testCredential(credential.credentialRef)\"\n />\n @if (activeCredentialProvider()?.connectAvailable) {\n <mt-button\n size=\"small\"\n variant=\"text\"\n label=\"Reconnect\"\n [disabled]=\"credentialOauthState() === 'loading'\"\n (onClick)=\"connectCredentialProvider()\"\n />\n }\n <mt-button\n size=\"small\"\n variant=\"text\"\n severity=\"danger\"\n label=\"Revoke\"\n [loading]=\"credentialRevokeRef() === credential.credentialRef\"\n [disabled]=\"\n credentialRevokeRef() === credential.credentialRef\n \"\n (onClick)=\"revokeCredential(credential.credentialRef)\"\n />\n </div>\n </div>\n @if (credential.maskedFields; as maskedFields) {\n <div\n class=\"mt-2 flex flex-wrap gap-1.5 text-[11px] text-(--p-text-muted-color)\"\n >\n @for (field of maskedFields | keyvalue; track field.key) {\n <span\n class=\"rounded-md bg-(--p-content-background) px-2 py-1 font-mono\"\n >\n {{ field.key }}: {{ field.value }}\n </span>\n }\n </div>\n }\n </div>\n }\n }\n </div>\n } @else if (sectionHasAdvancedEmptyState(\"credentials\")) {\n <p class=\"fp-ae-copy\">\n No saved credential exists for this provider. Connect the provider or set\n it up manually before publishing.\n </p>\n }\n @if (credentialTest(); as test) {\n <div class=\"mt-3 rounded-md bg-(--p-surface-100) px-3 py-2 text-[12px]\">\n {{ test.status ?? (test.succeeded ? \"Succeeded\" : \"Failed\") }}\n @if (test.message) {\n <span>- {{ test.message }}</span>\n }\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 let-emptyText=\"emptyText\"\n let-keyHint=\"keyHint\"\n let-valueHint=\"valueHint\"\n let-required=\"required\"\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)=\"\n updateObjectRow(objectKey, row.key, $event, row.value)\n \"\n [label]=\"keyLabel\"\n [hint]=\"\n keyHint || 'Configuration key saved to the backend JSON object.'\n \"\n [required]=\"required === true\"\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]=\"\n valueHint ||\n 'Configuration value. Use expressions when this field should resolve at runtime.'\n \"\n [required]=\"required === true\"\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 } @empty {\n @if (emptyText) {\n <div\n class=\"rounded-md bg-(--p-content-background) px-3 py-2 text-[12px] text-(--p-text-muted-color)\"\n >\n {{ emptyText }}\n </div>\n }\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" }]
|
|
20304
|
+
], 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]=\"true\"\n (dataDropEvent)=\"insertExpressionDrop($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\n class=\"text-[12px] font-semibold text-(--p-text-muted-color)\"\n >\n First node connected\n </div>\n <div\n class=\"truncate text-[14px] font-semibold text-(--p-text-color)\"\n >\n {{ startConnection().label }}\n </div>\n <div\n class=\"flex flex-wrap items-center gap-2 text-[12px] text-(--p-text-muted-color)\"\n >\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\n class=\"m-0 text-[12px] leading-5 text-(--p-text-muted-color)\"\n >\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\n class=\"flex flex-wrap items-center gap-2 text-[12px] text-(--p-text-muted-color)\"\n >\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\n class=\"rounded-lg border border-dashed border-(--p-content-border-color) bg-(--p-surface-50) p-3\"\n >\n <div class=\"text-[14px] font-semibold text-(--p-text-color)\">\n No first node connected\n </div>\n <p\n class=\"m-0 mt-1 text-[12px] leading-5 text-(--p-text-muted-color)\"\n >\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 (\n hasTriggerPayloadSchema() && triggerPayloadSchema();\n as schema\n ) {\n <div\n class=\"rounded-md border border-surface-200 bg-surface-0 p-3\"\n >\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\n class=\"mt-3 rounded-md border border-surface-200 bg-surface-0 p-3\"\n >\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\n 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\n class=\"self-end\"\n size=\"small\"\n variant=\"outlined\"\n label=\"Copy\"\n (onClick)=\"copyWebhookUrl()\"\n />\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>{{\n (setup.requiredHeaders ?? []).join(\", \") || \"-\"\n }}</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\n class=\"mt-3 rounded-lg border border-(--p-content-border-color)\"\n >\n <summary\n class=\"cursor-pointer px-3 py-2 text-[12px] font-semibold\"\n >\n Sample request\n </summary>\n <pre class=\"fp-ae-code\">{{\n schemaText(setup.sampleRequest)\n }}</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 [required]=\"isAuthFieldRequired('mode')\"\n [options]=\"authModeOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-text-field\n [ngModel]=\"authPolicy()['signatureHeaderName'] ?? ''\"\n (ngModelChange)=\"\n onAuthFieldChange('signatureHeaderName', $event)\n \"\n label=\"Signature header\"\n hint=\"Header name that carries the webhook signature when the policy requires signed requests.\"\n [required]=\"isAuthFieldRequired('signatureHeaderName')\"\n />\n <mt-text-field\n [ngModel]=\"authPolicy()['timestampHeaderName'] ?? ''\"\n (ngModelChange)=\"\n onAuthFieldChange('timestampHeaderName', $event)\n \"\n label=\"Timestamp header\"\n hint=\"Header name that carries the request timestamp for replay protection.\"\n [required]=\"isAuthFieldRequired('timestampHeaderName')\"\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 [required]=\"true\"\n [options]=\"formOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-select-field\n [ngModel]=\"\n selectedFormVersionId() || formBinding()?.formVersionId || ''\n \"\n (ngModelChange)=\"onFormVersionChange($event)\"\n label=\"Form version\"\n hint=\"Persist the exact backend formVersionId. Do not type or generate IDs manually.\"\n [required]=\"true\"\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\n size=\"small\"\n severity=\"primary\"\n label=\"Save binding\"\n (onClick)=\"saveFormBinding()\"\n />\n @if (formBinding()) {\n <span\n class=\"rounded-lg bg-(--p-surface-100) px-2 py-1 font-mono text-[11px] text-(--p-text-muted-color)\"\n >\n {{ formBinding()!.formVersionId }}\n </span>\n }\n </div>\n @if (formSchema(); as schema) {\n <details\n class=\"mt-3 rounded-lg border border-(--p-content-border-color)\"\n >\n <summary\n class=\"cursor-pointer px-3 py-2 text-[12px] font-semibold\"\n >\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 [required]=\"true\"\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 [required]=\"isConfigFieldRequired('timezone')\"\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 [required]=\"showScheduleCron()\"\n />\n }\n @if (showScheduleInterval()) {\n <mt-number-field\n [ngModel]=\"numberValue(config()['intervalSeconds'])\"\n (ngModelChange)=\"\n onConfigFieldChange('intervalSeconds', $event)\n \"\n label=\"Interval seconds\"\n hint=\"Repeat interval in seconds.\"\n [required]=\"showScheduleInterval()\"\n [min]=\"0\"\n />\n }\n @if (showScheduleOnce()) {\n <mt-date-field\n class=\"md:col-span-2\"\n [ngModel]=\"\n config()['runAt'] ??\n config()['runAtUtc'] ??\n config()['oneTimeAt'] ??\n config()['at'] ??\n ''\n \"\n (ngModelChange)=\"onConfigFieldChange('runAt', $event)\"\n label=\"Run at UTC\"\n hint=\"UTC date/time for the one-time schedule.\"\n [required]=\"showScheduleOnce()\"\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 [required]=\"isConfigFieldRequired('startDate')\"\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 [required]=\"isConfigFieldRequired('misfirePolicy')\"\n [options]=\"misfirePolicyOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n </div>\n <div class=\"mt-3 flex gap-2\">\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n label=\"Validate\"\n (onClick)=\"validateSchedule()\"\n />\n <mt-button\n size=\"small\"\n severity=\"primary\"\n label=\"Preview next fire\"\n (onClick)=\"previewSchedule()\"\n />\n </div>\n @if (schedulePreview(); as preview) {\n <div\n class=\"mt-3 rounded-lg bg-(--p-surface-100) px-3 py-2 text-[12px]\"\n >\n <strong class=\"text-emerald-600\">Valid</strong>\n <span class=\"ms-2\"\n >Next fire: {{ preview.nextFireAtUtc ?? \"-\" }}</span\n >\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\n *ngTemplateOutlet=\"\n mapEditor;\n context: {\n objectKey: 'fields',\n rows: fieldsRows(),\n keyLabel: 'Field',\n valueLabel: 'Value',\n addLabel: 'Add field',\n required: isConfigFieldRequired('fields'),\n }\n \"\n />\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]=\"ifConditionFieldText('left')\"\n (focusin)=\"setExpressionTarget('config:left')\"\n (ngModelChange)=\"onIfConditionFieldChange('left', $event)\"\n label=\"Left\"\n hint=\"Left-side value or expression to compare.\"\n [required]=\"isConfigFieldRequired('left')\"\n />\n <mt-select-field\n [ngModel]=\"ifConditionFieldValue('operator') || 'equals'\"\n (ngModelChange)=\"onIfConditionFieldChange('operator', $event)\"\n label=\"Operator\"\n hint=\"Comparison operator used by the backend condition evaluator.\"\n [required]=\"isConfigFieldRequired('operator')\"\n [options]=\"ifOperatorOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-text-field\n [ngModel]=\"ifConditionFieldText('right')\"\n (focusin)=\"setExpressionTarget('config:right')\"\n (ngModelChange)=\"onIfConditionFieldChange('right', $event)\"\n label=\"Right\"\n hint=\"Right-side value or expression to compare against.\"\n [required]=\"isConfigFieldRequired('right')\"\n />\n </div>\n <div\n class=\"mt-3 rounded-md border border-surface-200 bg-surface-0 p-3\"\n >\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\n class=\"rounded-md bg-(--p-surface-100) px-2 py-1 font-mono text-[11px]\"\n >{{ key }}</span\n >\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]=\"httpMethod()\"\n (ngModelChange)=\"onHttpMethodChange($event)\"\n label=\"Method\"\n hint=\"HTTP method used for the outbound request.\"\n [required]=\"isConfigFieldRequired('method')\"\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 [required]=\"isConfigFieldRequired('url')\"\n />\n </div>\n <div class=\"mt-3\">\n <ng-container\n *ngTemplateOutlet=\"\n mapEditor;\n context: {\n objectKey: 'headers',\n rows: headerRows(),\n keyLabel: 'Header',\n valueLabel: 'Value',\n addLabel: 'Add header',\n }\n \"\n />\n </div>\n <div class=\"mt-3\">\n <ng-container\n *ngTemplateOutlet=\"\n mapEditor;\n context: {\n objectKey: 'query',\n rows: queryRows(),\n keyLabel: 'Query param',\n valueLabel: 'Value',\n addLabel: 'Add query param',\n }\n \"\n />\n </div>\n @if (httpMethodAllowsPayload()) {\n @if (httpSupportsBodyMode()) {\n <mt-select-field\n class=\"mt-3\"\n [ngModel]=\"httpBodyMode()\"\n (ngModelChange)=\"onHttpBodyModeChange($event)\"\n label=\"Payload mode\"\n hint=\"How the request payload should be sent by the backend HTTP runtime.\"\n [required]=\"isConfigFieldRequired(httpBodyModeConfigKey())\"\n [options]=\"httpBodyModeOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n }\n @if (httpBodyEnabled()) {\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=\"Request payload\"\n hint=\"Payload sent by this HTTP method. Use JSON or expressions when the backend schema allows it.\"\n [required]=\"isConfigFieldRequired('body')\"\n rows=\"6\"\n />\n }\n } @else {\n <div\n class=\"mt-3 rounded-md border border-(--p-content-border-color) bg-(--p-surface-50) px-3 py-2 text-[12px] leading-5 text-(--p-text-muted-color)\"\n >\n {{ httpMethod() }} requests do not send a payload. Put request\n data in query parameters or headers.\n </div>\n }\n @if (\n supportsConfigKey(\"timeoutSeconds\") ||\n supportsConfigKey(\"responseHandling\")\n ) {\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)=\"\n onConfigFieldChange('timeoutSeconds', $event)\n \"\n label=\"Timeout seconds\"\n hint=\"Request timeout when exposed by the backend schema.\"\n [required]=\"isConfigFieldRequired('timeoutSeconds')\"\n [min]=\"0\"\n />\n }\n @if (supportsConfigKey(\"responseHandling\")) {\n <mt-text-field\n [ngModel]=\"config()['responseHandling'] ?? ''\"\n (ngModelChange)=\"\n onConfigFieldChange('responseHandling', $event)\n \"\n label=\"Response handling\"\n hint=\"Backend-supported response handling mode or expression.\"\n [required]=\"isConfigFieldRequired('responseHandling')\"\n />\n }\n </div>\n }\n @if (sectionInMain(\"credentials\")) {\n <div\n class=\"mt-3 rounded-md border border-surface-200 bg-surface-0 p-3\"\n >\n <div class=\"fp-ae-label mb-2\">Credential</div>\n <ng-container *ngTemplateOutlet=\"credentialSelector\" />\n </div>\n }\n <div\n class=\"mt-3 rounded-md border border-(--p-content-border-color) bg-(--p-surface-50) p-3\"\n >\n <div class=\"flex items-center justify-between gap-3\">\n <div class=\"min-w-0\">\n <div class=\"fp-ae-label\">Configuration check</div>\n <p class=\"fp-ae-copy m-0 mt-1\">\n Runs backend dry-run validation for method, URL,\n expressions, and request policy before execution.\n </p>\n </div>\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n severity=\"secondary\"\n label=\"Check request\"\n [disabled]=\"httpCheckState() === 'loading'\"\n (onClick)=\"checkHttpRequest()\"\n />\n </div>\n @if (httpLocalIssues().length > 0) {\n <div\n class=\"mt-2 rounded-md border border-[rgb(var(--fp-warning))]/25 bg-[rgb(var(--fp-warning))]/10 px-2.5 py-2 text-[12px] text-(--p-text-color)\"\n >\n {{ httpLocalIssues()[0] }}\n </div>\n } @else if (httpCheckError(); as error) {\n <div\n class=\"mt-2 rounded-md border border-[rgb(var(--fp-error))]/25 bg-[rgb(var(--fp-error))]/10 px-2.5 py-2 text-[12px] text-(--p-text-color)\"\n >\n {{ error }}\n </div>\n } @else if (httpCheckResult(); as result) {\n <div\n class=\"mt-2 rounded-md border border-[rgb(var(--fp-commit))]/25 bg-[rgb(var(--fp-commit))]/10 px-2.5 py-2 text-[12px] text-(--p-text-color)\"\n >\n Backend check\n {{\n result.success || result.canStart\n ? \"passed\"\n : \"completed with issues\"\n }}.\n </div>\n }\n </div>\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]=\"waitMode()\"\n (ngModelChange)=\"onWaitModeChange($event)\"\n label=\"Mode\"\n hint=\"Choose how execution should pause.\"\n [required]=\"isConfigFieldRequired('mode')\"\n [options]=\"waitModeOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n @if (showWaitDuration()) {\n <mt-number-field\n [ngModel]=\"numberValue(config()['durationSeconds'])\"\n (ngModelChange)=\"\n onConfigFieldChange('durationSeconds', $event)\n \"\n label=\"Wait duration\"\n hint=\"Seconds to pause before continuing.\"\n [required]=\"isConfigFieldRequired('durationSeconds')\"\n [min]=\"1\"\n />\n }\n @if (showWaitUntilDate()) {\n <mt-date-field\n [ngModel]=\"waitUntilDateValue()\"\n (ngModelChange)=\"onWaitUntilDateChange($event)\"\n label=\"Resume at\"\n hint=\"Date and time when execution should continue.\"\n [required]=\"isConfigFieldRequired('untilDate')\"\n [showTime]=\"true\"\n [showClear]=\"true\"\n [pInputs]=\"dateTimePickerInputs\"\n />\n }\n </div>\n @if (showWaitResumePayloadSchema()) {\n <mt-textarea-field\n class=\"mt-3 w-full font-mono\"\n [ngModel]=\"waitResumePayloadSchemaText()\"\n (focusin)=\"setExpressionTarget('config:resumePayloadSchema')\"\n (ngModelChange)=\"onWaitResumePayloadSchemaChange($event)\"\n label=\"Resume payload schema\"\n hint=\"Optional JSON schema for data sent back when this wait is resumed.\"\n [required]=\"isConfigFieldRequired('resumePayloadSchema')\"\n rows=\"5\"\n />\n }\n </section>\n }\n }\n @case (\"HumanTask\") {\n @if (sectionInMain(\"humanTask\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Human task</div>\n @if (\n assignmentOptions()?.providerStatus &&\n assignmentOptions()?.providerStatus !== \"Available\"\n ) {\n <p class=\"fp-ae-copy\">\n Assignment provider status:\n {{ assignmentOptions()?.providerStatus }}. The backend provider\n is the source of truth for available assignees.\n </p>\n }\n <div\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 Task details\n </h3>\n <div class=\"grid gap-3 p-4 xl:grid-cols-3\">\n <mt-text-field\n [ngModel]=\"config()['title'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:title')\"\n (ngModelChange)=\"onConfigFieldChange('title', $event)\"\n label=\"Task title\"\n hint=\"Title shown to the assigned user while the execution waits.\"\n [required]=\"isConfigFieldRequired('title')\"\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 submit this task.\"\n [required]=\"isConfigFieldRequired('assignment')\"\n [options]=\"assignmentSelectOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-text-field\n [ngModel]=\"config()['priority'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('priority', $event)\"\n label=\"Priority\"\n hint=\"Optional task priority passed to backend task orchestration.\"\n [required]=\"isConfigFieldRequired('priority')\"\n />\n <mt-textarea-field\n class=\"xl:col-span-3\"\n [ngModel]=\"config()['description'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:description')\"\n (ngModelChange)=\"onConfigFieldChange('description', $event)\"\n label=\"Task description\"\n hint=\"Instructions shown with the HumanTask form.\"\n [required]=\"isConfigFieldRequired('description')\"\n rows=\"4\"\n />\n </div>\n </div>\n\n <div\n class=\"mt-3 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 Task behavior\n </h3>\n <div class=\"grid gap-3 p-4 xl:grid-cols-4\">\n <mt-date-field\n [ngModel]=\"\n config()['dueDateUtc'] ?? config()['dueDate'] ?? ''\n \"\n (ngModelChange)=\"onConfigFieldChange('dueDateUtc', $event)\"\n label=\"Due date\"\n hint=\"Optional backend due timestamp for the waiting task.\"\n [required]=\"isConfigFieldRequired('dueDateUtc')\"\n [showTime]=\"true\"\n [showClear]=\"true\"\n [pInputs]=\"dateTimePickerInputs\"\n />\n <mt-number-field\n [ngModel]=\"numberValue(config()['dueInSeconds'])\"\n (ngModelChange)=\"onConfigFieldChange('dueInSeconds', $event)\"\n label=\"Due in seconds\"\n hint=\"Relative due duration when the backend should calculate the timestamp.\"\n [required]=\"isConfigFieldRequired('dueInSeconds')\"\n [min]=\"0\"\n />\n <mt-text-field\n [ngModel]=\"config()['contextOutputPath'] ?? 'task'\"\n (ngModelChange)=\"\n onConfigFieldChange('contextOutputPath', $event)\n \"\n label=\"Context output path\"\n hint=\"Where submitted task values are written in execution context.\"\n [required]=\"isConfigFieldRequired('contextOutputPath')\"\n />\n <mt-select-field\n [ngModel]=\"config()['cancelBehavior'] ?? 'RouteCancelled'\"\n (ngModelChange)=\"\n onConfigFieldChange('cancelBehavior', $event)\n \"\n label=\"Cancel behavior\"\n hint=\"Route cancelled when users cancel, or fail this node.\"\n [required]=\"isConfigFieldRequired('cancelBehavior')\"\n [options]=\"humanTaskCancelBehaviorOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <div\n class=\"flex min-h-[4.25rem] items-end rounded-md border border-surface-200 bg-surface-0 px-3 py-2\"\n >\n <mt-toggle-field\n size=\"small\"\n label=\"Allow draft save\"\n labelPosition=\"end\"\n [ngModel]=\"config()['saveDraft'] !== false\"\n [required]=\"isConfigFieldRequired('saveDraft')\"\n (ngModelChange)=\"\n onConfigFieldChange('saveDraft', $event === true)\n \"\n hint=\"Expose backend draft save for this task when supported.\"\n />\n </div>\n @if (supportsConfigKey(\"submitBehavior\")) {\n <mt-text-field\n [ngModel]=\"config()['submitBehavior'] ?? ''\"\n (ngModelChange)=\"\n onConfigFieldChange('submitBehavior', $event)\n \"\n label=\"Submit behavior\"\n hint=\"Backend submit behavior when exposed by the node schema.\"\n [required]=\"isConfigFieldRequired('submitBehavior')\"\n />\n }\n </div>\n </div>\n\n <div class=\"mt-3 flex gap-2\">\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n label=\"Validate assignment\"\n (onClick)=\"validateAssignment()\"\n />\n </div>\n @if (assignmentValidation(); as result) {\n <div\n class=\"mt-3 rounded-lg bg-(--p-surface-100) px-3 py-2 text-[12px]\"\n >\n {{\n result.isValid === false\n ? \"Assignment invalid\"\n : \"Assignment accepted by helper\"\n }}\n @for (issue of resultIssues(result); track $index) {\n <div class=\"mt-1 text-[11px] text-(--p-text-muted-color)\">\n {{ issue }}\n </div>\n }\n </div>\n }\n </section>\n }\n\n @if (sectionInMain(\"formBinding\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Task input form</div>\n <mt-radio-cards-field\n [ngModel]=\"humanTaskFormSource()\"\n (ngModelChange)=\"onHumanTaskFormSourceChange($event)\"\n label=\"Form source\"\n hint=\"HumanTask requires either its own node form binding or an explicit reused FormSubmitTrigger payload.\"\n [required]=\"isConfigFieldRequired('formSource')\"\n [options]=\"humanTaskFormSourceOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n size=\"small\"\n >\n <ng-template #option let-item>\n <div class=\"w-full min-w-0\">\n <div class=\"text-[13px] font-semibold leading-5\">\n {{ item.label }}\n </div>\n <p\n class=\"mt-1 text-[12px] leading-5 text-(--p-text-muted-color)\"\n >\n {{ item.description }}\n </p>\n </div>\n </ng-template>\n </mt-radio-cards-field>\n\n @if (humanTaskUsesExplicitFormBinding()) {\n <div\n class=\"mt-3 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 HumanTask input binding\n </h3>\n <div class=\"grid gap-3 p-4 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 for this HumanTask node.\"\n [required]=\"humanTaskUsesExplicitFormBinding()\"\n [options]=\"formOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-select-field\n [ngModel]=\"\n selectedFormVersionId() ||\n formBinding()?.formVersionId ||\n ''\n \"\n (ngModelChange)=\"onFormVersionChange($event)\"\n label=\"Form version\"\n hint=\"Persist the exact backend formVersionId for the node binding.\"\n [required]=\"humanTaskUsesExplicitFormBinding()\"\n [options]=\"formVersionOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n </div>\n <div class=\"border-t border-surface-200 px-4 py-3\">\n @if (humanTaskFormSummary(); as summary) {\n <div\n class=\"flex flex-wrap items-center gap-2 text-[12px] text-(--p-text-muted-color)\"\n >\n @if (summary.formLabel) {\n <span class=\"rounded-md bg-(--p-surface-100) px-2 py-1\">\n {{ summary.formLabel }}\n </span>\n }\n @if (summary.formVersionId) {\n <span\n class=\"rounded-md bg-(--p-surface-100) px-2 py-1 font-mono text-[11px]\"\n >\n {{ summary.formVersionId }}\n </span>\n }\n <span class=\"rounded-md bg-(--p-surface-100) px-2 py-1\">\n {{ summary.fieldCount }} fields\n </span>\n <span class=\"rounded-md bg-(--p-surface-100) px-2 py-1\">\n {{ summary.requiredCount }} required\n </span>\n <span class=\"rounded-md bg-(--p-surface-100) px-2 py-1\">\n {{ summary.writableCount }} writable\n </span>\n </div>\n }\n\n @if (humanTaskHasCustomInputMapping()) {\n <div\n class=\"mt-3 rounded-md 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 Existing custom input mapping will be preserved unless\n field prefill options are changed.\n </div>\n }\n\n @if (humanTaskFormFields().length > 0) {\n <div\n class=\"mt-3 overflow-hidden rounded-md border border-surface-200 bg-surface-0\"\n >\n <div\n class=\"flex flex-wrap items-center justify-between gap-2 border-b border-surface-200 bg-surface-50 px-3 py-2\"\n >\n <div class=\"text-[13px] font-semibold text-color\">\n FormBuilder fields\n </div>\n <div class=\"flex flex-wrap gap-2\">\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n severity=\"secondary\"\n label=\"Select all\"\n (onClick)=\"selectAllHumanTaskPrefillFields()\"\n />\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n severity=\"secondary\"\n label=\"Clear\"\n (onClick)=\"clearHumanTaskPrefillFields()\"\n />\n </div>\n </div>\n <div class=\"divide-y divide-surface-200\">\n @for (field of humanTaskFormFields(); track field.key) {\n <div\n class=\"grid gap-3 px-3 py-3 md:grid-cols-[minmax(0,1fr)_auto] md:items-center\"\n >\n <div class=\"min-w-0\">\n <div class=\"flex flex-wrap items-center gap-2\">\n <span\n class=\"truncate text-[13px] font-semibold text-(--p-text-color)\"\n >\n {{ field.label }}\n </span>\n <span\n class=\"rounded-md bg-(--p-surface-100) px-2 py-0.5 font-mono text-[11px] text-(--p-text-muted-color)\"\n >\n {{ field.key }}\n </span>\n @if (field.required) {\n <span\n class=\"rounded-md bg-[rgb(var(--fp-danger))]/10 px-2 py-0.5 text-[11px] font-semibold text-[rgb(var(--fp-danger))]\"\n >\n Required\n </span>\n }\n @if (field.read === true) {\n <span\n class=\"rounded-md bg-(--p-surface-100) px-2 py-0.5 text-[11px] text-(--p-text-muted-color)\"\n >\n Read\n </span>\n }\n @if (field.write === true) {\n <span\n class=\"rounded-md bg-(--p-surface-100) px-2 py-0.5 text-[11px] text-(--p-text-muted-color)\"\n >\n Write\n </span>\n }\n </div>\n @if (field.description) {\n <p\n class=\"m-0 mt-1 text-[12px] leading-5 text-(--p-text-muted-color)\"\n >\n {{ field.description }}\n </p>\n }\n </div>\n <mt-toggle-field\n size=\"small\"\n label=\"Prefill\"\n labelPosition=\"end\"\n [ngModel]=\"\n humanTaskPrefillFieldKeySet().has(field.key)\n \"\n (ngModelChange)=\"\n onHumanTaskPrefillFieldToggle(\n field.key,\n $event === true\n )\n \"\n />\n </div>\n }\n </div>\n <div\n class=\"border-t border-surface-200 bg-surface-50 px-3 py-2 text-[12px] leading-5 text-(--p-text-muted-color)\"\n >\n Selected fields are saved as\n <span class=\"font-mono\"\n >inputMappingJson.fieldKeys</span\n >\n so backend can prefill matching upstream values.\n </div>\n </div>\n } @else if (formSchema()) {\n <div\n class=\"mt-3 rounded-md border border-surface-200 bg-surface-50 px-3 py-2 text-[12px] leading-5 text-(--p-text-muted-color)\"\n >\n No FormBuilder fields were returned for this form schema.\n </div>\n }\n\n @if (formSchema()?.requiresSnapshotOnPublish) {\n <div\n class=\"mt-3 rounded-md border border-surface-200 bg-surface-50 px-3 py-2 text-[12px] leading-5 text-(--p-text-muted-color)\"\n >\n Backend requires this form version snapshot during\n publish.\n </div>\n }\n </div>\n <div class=\"border-t border-surface-200 px-4 py-3\">\n <div class=\"flex flex-wrap items-center gap-2\">\n <mt-button\n size=\"small\"\n severity=\"primary\"\n label=\"Save task binding\"\n (onClick)=\"saveFormBinding()\"\n />\n @if (formBinding()) {\n <span\n class=\"rounded-lg bg-(--p-surface-100) px-2 py-1 font-mono text-[11px] text-(--p-text-muted-color)\"\n >\n {{ formBinding()!.formVersionId }}\n </span>\n }\n </div>\n </div>\n </div>\n } @else {\n <div class=\"mt-3 grid gap-3 md:grid-cols-2\">\n <mt-select-field\n [ngModel]=\"config()['triggerKey'] ?? ''\"\n (ngModelChange)=\"onHumanTaskReuseTriggerChange($event)\"\n label=\"Trigger to reuse\"\n hint=\"Explicitly select the FormSubmitTrigger whose submitted form payload is reused.\"\n [options]=\"triggerOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n </div>\n @if (humanTaskReuseFormBinding()) {\n <div\n class=\"mt-3 rounded-md border border-surface-200 bg-surface-0 p-3\"\n >\n <div class=\"fp-ae-label mb-2\">Reused trigger form</div>\n @if (humanTaskFormSummary(); as summary) {\n <div\n class=\"flex flex-wrap items-center gap-2 text-[12px] text-(--p-text-muted-color)\"\n >\n @if (summary.formLabel) {\n <span class=\"rounded-md bg-(--p-surface-100) px-2 py-1\">\n {{ summary.formLabel }}\n </span>\n }\n @if (summary.formVersionId) {\n <span\n class=\"rounded-md bg-(--p-surface-100) px-2 py-1 font-mono text-[11px]\"\n >\n {{ summary.formVersionId }}\n </span>\n }\n <span class=\"rounded-md bg-(--p-surface-100) px-2 py-1\">\n {{ summary.fieldCount }} fields\n </span>\n <span class=\"rounded-md bg-(--p-surface-100) px-2 py-1\">\n {{ summary.requiredCount }} required\n </span>\n </div>\n }\n </div>\n }\n }\n\n @if (formSchema(); as schema) {\n <details\n class=\"mt-3 rounded-lg border border-(--p-content-border-color)\"\n >\n <summary\n class=\"cursor-pointer px-3 py-2 text-[12px] font-semibold\"\n >\n Form schema preview\n </summary>\n <pre class=\"fp-ae-code\">{{ schemaText(schema.schema) }}</pre>\n </details>\n }\n\n <div\n class=\"mt-3 rounded-md border border-surface-200 bg-surface-0 p-3\"\n >\n <div class=\"fp-ae-label mb-2\">HumanTask routes</div>\n <div class=\"flex flex-wrap gap-1.5\">\n @for (key of routeOutputKeys(); track key) {\n <span\n class=\"rounded-md bg-(--p-surface-100) px-2 py-1 font-mono text-[11px]\"\n >{{ key }}</span\n >\n }\n </div>\n </div>\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 (\n assignmentOptions()?.providerStatus &&\n assignmentOptions()?.providerStatus !== \"Available\"\n ) {\n <p class=\"fp-ae-copy\">\n Assignment provider status:\n {{ assignmentOptions()?.providerStatus }}. The backend provider\n 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 [required]=\"isConfigFieldRequired('title')\"\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 [required]=\"isConfigFieldRequired('assignment')\"\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 [required]=\"isConfigFieldRequired('priority')\"\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 [required]=\"isConfigFieldRequired('message')\"\n rows=\"4\"\n />\n @if (\n humanApprovalSupportsConfig(\"dueDate\") ||\n humanApprovalSupportsConfig(\"dueInSeconds\") ||\n humanApprovalSupportsConfig(\"timeoutSeconds\") ||\n humanApprovalSupportsConfig(\"expiresAt\") ||\n humanApprovalSupportsConfig(\"commentsRequired\") ||\n humanApprovalSupportsConfig(\"allowReturn\")\n ) {\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 [required]=\"isConfigFieldRequired('dueDate')\"\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)=\"\n onConfigFieldChange('dueInSeconds', $event)\n \"\n label=\"Due in seconds\"\n hint=\"Relative approval due duration when supported by the backend schema.\"\n [required]=\"isConfigFieldRequired('dueInSeconds')\"\n [min]=\"0\"\n />\n }\n @if (humanApprovalSupportsConfig(\"timeoutSeconds\")) {\n <mt-number-field\n [ngModel]=\"numberValue(config()['timeoutSeconds'])\"\n (ngModelChange)=\"\n onConfigFieldChange('timeoutSeconds', $event)\n \"\n label=\"Timeout seconds\"\n hint=\"Approval timeout emitted according to backend approval runtime support.\"\n [required]=\"isConfigFieldRequired('timeoutSeconds')\"\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 [required]=\"isConfigFieldRequired('expiresAt')\"\n [showTime]=\"true\"\n [showClear]=\"true\"\n [pInputs]=\"dateTimePickerInputs\"\n />\n }\n @if (humanApprovalSupportsConfig(\"commentsRequired\")) {\n <div\n class=\"flex min-h-[4.25rem] items-end rounded-md border border-surface-200 bg-surface-0 px-3 py-2\"\n >\n <mt-toggle-field\n size=\"small\"\n label=\"Comments required\"\n labelPosition=\"end\"\n [ngModel]=\"config()['commentsRequired'] === true\"\n [required]=\"isConfigFieldRequired('commentsRequired')\"\n (ngModelChange)=\"\n onConfigFieldChange('commentsRequired', $event === true)\n \"\n hint=\"Require approver comments when the backend supports this flag.\"\n />\n </div>\n }\n @if (humanApprovalSupportsConfig(\"allowReturn\")) {\n <div\n class=\"flex min-h-[4.25rem] items-end rounded-md border border-surface-200 bg-surface-0 px-3 py-2\"\n >\n <mt-toggle-field\n size=\"small\"\n label=\"Allow return for changes\"\n labelPosition=\"end\"\n [ngModel]=\"config()['allowReturn'] !== false\"\n [required]=\"isConfigFieldRequired('allowReturn')\"\n (ngModelChange)=\"\n onConfigFieldChange('allowReturn', $event === true)\n \"\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\n class=\"overflow-hidden rounded-md border border-surface-200 bg-surface-0\"\n >\n @if (selectedApprovalDecisionRows().length > 0) {\n <div\n 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 >\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 (\n decision of selectedApprovalDecisionRows();\n track decision.value\n ) {\n <div\n 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 >\n <div class=\"min-w-0\">\n <div\n class=\"text-[11px] font-semibold text-(--p-text-muted-color) xl:hidden\"\n >\n Decision label\n </div>\n <div\n class=\"truncate text-[13px] font-semibold text-(--p-text-color)\"\n >\n {{ decision.label }}\n </div>\n </div>\n <div class=\"min-w-0\">\n <div\n class=\"text-[11px] font-semibold text-(--p-text-muted-color) xl:hidden\"\n >\n Canonical value\n </div>\n <div\n class=\"truncate font-mono text-[12px] text-(--p-text-color)\"\n >\n {{ decision.value }}\n </div>\n </div>\n <div class=\"min-w-0\">\n <div\n class=\"text-[11px] font-semibold text-(--p-text-muted-color) xl:hidden\"\n >\n Route output key\n </div>\n <div\n class=\"truncate font-mono text-[12px] text-(--p-text-muted-color)\"\n >\n {{ decision.routeOutputKey }}\n </div>\n </div>\n <div>\n <div\n class=\"text-[11px] font-semibold text-(--p-text-muted-color) xl:hidden\"\n >\n Routes\n </div>\n <div class=\"text-[13px] text-(--p-text-color)\">\n {{ decision.routeCount }}\n </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\n class=\"px-3 py-4 text-[12px] text-(--p-text-muted-color)\"\n >\n Add at least one backend-supported approval decision.\n </div>\n }\n </div>\n @if (approvalDecisionIssues().length > 0) {\n <div\n 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 >\n @for (issue of approvalDecisionIssues(); track $index) {\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 [required]=\"isConfigFieldRequired('allowedDecisions')\"\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 (\n humanApprovalSupportsConfig(\"payload\") ||\n humanApprovalSupportsConfig(\"context\") ||\n humanApprovalSupportsConfig(\"metadata\")\n ) {\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 [required]=\"isConfigFieldRequired('payload')\"\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 [required]=\"isConfigFieldRequired('context')\"\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 [required]=\"isConfigFieldRequired('metadata')\"\n rows=\"4\"\n />\n }\n </div>\n }\n <div class=\"mt-3 flex gap-2\">\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n label=\"Validate assignment\"\n (onClick)=\"validateAssignment()\"\n />\n </div>\n @if (assignmentValidation(); as result) {\n <div\n class=\"mt-3 rounded-lg bg-(--p-surface-100) px-3 py-2 text-[12px]\"\n >\n {{\n result.isValid === false\n ? \"Assignment invalid\"\n : \"Assignment accepted by helper\"\n }}\n @for (issue of resultIssues(result); track $index) {\n <div class=\"mt-1 text-[11px] text-(--p-text-muted-color)\">\n {{ issue }}\n </div>\n }\n </div>\n }\n </section>\n }\n }\n @case (\"ConnectorAction\") {\n @if (sectionInMain(\"humanReviewRequest\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">\n {{ humanReviewProviderLabel() }} human review\n </div>\n <div class=\"grid gap-3 md:grid-cols-2\">\n <mt-text-field\n [ngModel]=\"config()['channelType'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('channelType', $event)\"\n label=\"Channel\"\n [required]=\"isConfigFieldRequired('channelType')\"\n />\n </div>\n\n <div class=\"mt-4 grid gap-3 md:grid-cols-3\">\n <mt-text-field\n [ngModel]=\"humanReviewDestination()['recipient'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:destination:recipient')\"\n (ngModelChange)=\"\n onNestedConfigFieldChange('destination', 'recipient', $event)\n \"\n label=\"Recipient\"\n [required]=\"isConnectorDestinationRequired()\"\n />\n <mt-text-field\n [ngModel]=\"humanReviewDestination()['channelId'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:destination:channelId')\"\n (ngModelChange)=\"\n onNestedConfigFieldChange('destination', 'channelId', $event)\n \"\n label=\"Channel ID\"\n [required]=\"isConnectorDestinationRequired()\"\n />\n <mt-text-field\n [ngModel]=\"humanReviewDestination()['threadKey'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:destination:threadKey')\"\n (ngModelChange)=\"\n onNestedConfigFieldChange('destination', 'threadKey', $event)\n \"\n label=\"Thread key\"\n [required]=\"isConnectorDestinationRequired()\"\n />\n </div>\n\n <div class=\"mt-4 grid gap-3\">\n <mt-text-field\n [ngModel]=\"humanReviewMessage()['subject'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:message:subject')\"\n (ngModelChange)=\"\n onNestedConfigFieldChange('message', 'subject', $event)\n \"\n label=\"Subject\"\n [required]=\"isConnectorMessageRequired()\"\n />\n <mt-textarea-field\n class=\"w-full\"\n [ngModel]=\"humanReviewMessage()['body'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:message:body')\"\n (ngModelChange)=\"\n onNestedConfigFieldChange('message', 'body', $event)\n \"\n label=\"Message\"\n [required]=\"isConnectorMessageRequired()\"\n rows=\"5\"\n />\n <mt-text-field\n [ngModel]=\"humanReviewMessage()['templateKey'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:message:templateKey')\"\n (ngModelChange)=\"\n onNestedConfigFieldChange('message', 'templateKey', $event)\n \"\n label=\"Template key\"\n [required]=\"isConnectorMessageRequired()\"\n />\n </div>\n\n <div class=\"mt-4\">\n <div class=\"mb-2 flex items-center justify-between gap-3\">\n <div class=\"fp-ae-label\">Response options</div>\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n label=\"Add option\"\n (onClick)=\"addHumanReviewResponseOption()\"\n />\n </div>\n <div class=\"grid gap-2\">\n @for (\n option of humanReviewResponseOptions();\n track option.key + \":\" + $index\n ) {\n <div\n class=\"grid gap-2 rounded-md border border-(--p-content-border-color) bg-(--p-surface-50) p-2 md:grid-cols-[1fr_1fr_1fr_auto]\"\n >\n <mt-text-field\n [ngModel]=\"option.key\"\n (ngModelChange)=\"\n updateHumanReviewResponseOption($index, 'key', $event)\n \"\n label=\"Key\"\n [required]=\"isConfigFieldRequired('responseOptions')\"\n />\n <mt-text-field\n [ngModel]=\"option.label\"\n (ngModelChange)=\"\n updateHumanReviewResponseOption($index, 'label', $event)\n \"\n label=\"Label\"\n [required]=\"isConfigFieldRequired('responseOptions')\"\n />\n <mt-text-field\n [ngModel]=\"option.routeOutputKey\"\n (ngModelChange)=\"\n updateHumanReviewResponseOption(\n $index,\n 'routeOutputKey',\n $event\n )\n \"\n label=\"Route output\"\n [required]=\"isConfigFieldRequired('responseOptions')\"\n />\n <div class=\"flex items-end justify-end\">\n <mt-button\n size=\"small\"\n variant=\"text\"\n severity=\"secondary\"\n icon=\"general.x-close\"\n [disabled]=\"option.routeCount > 0\"\n (onClick)=\"removeHumanReviewResponseOption($index)\"\n [attr.aria-label]=\"'Remove response option'\"\n />\n </div>\n </div>\n }\n </div>\n </div>\n\n <div class=\"mt-4 grid gap-3 md:grid-cols-2\">\n <mt-select-field\n [ngModel]=\"selectedAssignmentKey()\"\n (ngModelChange)=\"onAssignmentOptionChange($event)\"\n label=\"Assignment\"\n [required]=\"isConfigFieldRequired('assignment')\"\n [options]=\"assignmentSelectOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-text-field\n [ngModel]=\"config()['credentialRef'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('credentialRef', $event)\"\n label=\"Credential\"\n [required]=\"isConfigFieldRequired('credentialRef')\"\n />\n <mt-number-field\n [ngModel]=\"numberValue(config()['timeoutSeconds'])\"\n (ngModelChange)=\"onConfigFieldChange('timeoutSeconds', $event)\"\n label=\"Timeout seconds\"\n [required]=\"isConfigFieldRequired('timeoutSeconds')\"\n [min]=\"1\"\n />\n <mt-select-field\n [ngModel]=\"config()['failureBehavior'] ?? 'RouteFailure'\"\n (ngModelChange)=\"onConfigFieldChange('failureBehavior', $event)\"\n label=\"Failure behavior\"\n [required]=\"isConfigFieldRequired('failureBehavior')\"\n [options]=\"humanReviewFailureBehaviorOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n </div>\n\n <div class=\"mt-3 flex gap-2\">\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n label=\"Validate assignment\"\n (onClick)=\"validateAssignment()\"\n />\n </div>\n @if (assignmentValidation(); as result) {\n <div\n class=\"mt-3 rounded-lg bg-(--p-surface-100) px-3 py-2 text-[12px]\"\n >\n {{\n result.isValid === false\n ? \"Assignment invalid\"\n : \"Assignment accepted by helper\"\n }}\n @for (issue of resultIssues(result); track $index) {\n <div class=\"mt-1 text-[11px] text-(--p-text-muted-color)\">\n {{ issue }}\n </div>\n }\n </div>\n }\n\n <div\n class=\"mt-4 rounded-md border border-(--p-content-border-color) bg-(--p-surface-50) p-3\"\n >\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\n class=\"rounded-md bg-(--p-surface-100) px-2 py-1 font-mono text-[11px]\"\n >{{ key }}</span\n >\n }\n </div>\n </div>\n </section>\n }\n }\n @case (\"ConvertToFile\") {\n @if (sectionInMain(\"convertToFile\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">\n {{ convertToFileActionLabel() }}\n </div>\n @if (convertToFileUnavailableReason(); as reason) {\n <div\n class=\"mb-3 rounded-md border border-(--p-content-border-color) bg-(--p-surface-50) px-3 py-2 text-[12px] leading-5 text-(--p-text-muted-color)\"\n >\n {{ reason }}\n </div>\n }\n\n <div class=\"grid gap-3 md:grid-cols-2\">\n <mt-text-field\n [ngModel]=\"config()['fileName'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:fileName')\"\n (ngModelChange)=\"onConfigFieldChange('fileName', $event)\"\n label=\"File name\"\n [required]=\"isConfigFieldRequired('fileName')\"\n />\n @if (convertToFileIsBase64()) {\n <mt-text-field\n [ngModel]=\"config()['contentType'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('contentType', $event)\"\n label=\"Content type\"\n [required]=\"isConfigFieldRequired('contentType')\"\n />\n } @else {\n <mt-text-field\n [ngModel]=\"convertToFileContentType()\"\n [readonly]=\"true\"\n label=\"Content type\"\n />\n }\n </div>\n\n @if (\n convertToFileIsCsv() ||\n convertToFileIsJson() ||\n convertToFileIsSpreadsheet()\n ) {\n <div class=\"mt-4 grid gap-3 md:grid-cols-2\">\n <mt-select-field\n [ngModel]=\"convertToFileSourceMode()\"\n (ngModelChange)=\"onConvertToFileSourceModeChange($event)\"\n label=\"Data source\"\n [required]=\"isConfigFieldRequired('sourceExpression')\"\n [options]=\"convertToFileSourceOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n @if (convertToFileSourceMode() === \"expression\") {\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"convertToFileSourceExpression()\"\n (focusin)=\"setExpressionTarget('config:sourceExpression')\"\n (ngModelChange)=\"\n onConvertToFileSourceExpressionChange($event)\n \"\n label=\"Source expression\"\n [required]=\"isConfigFieldRequired('sourceExpression')\"\n />\n }\n @if (convertToFileIsCsv() || convertToFileIsSpreadsheet()) {\n <div\n class=\"rounded-md border border-(--p-content-border-color) bg-(--p-surface-50) p-3 md:col-span-2\"\n >\n <div class=\"mb-2 flex items-center justify-between gap-3\">\n <div class=\"fp-ae-label\">Columns</div>\n @if (convertToFileColumns().length) {\n <mt-button\n size=\"small\"\n variant=\"text\"\n severity=\"secondary\"\n label=\"Clear\"\n (onClick)=\"clearConvertToFileColumns()\"\n />\n }\n </div>\n <div class=\"mb-3 flex min-h-8 flex-wrap gap-1.5\">\n @if (convertToFileColumns().length) {\n @for (column of convertToFileColumns(); track column) {\n <span\n class=\"inline-flex items-center gap-1 rounded-md bg-(--p-surface-100) px-2 py-1 font-mono text-[11px]\"\n >\n {{ column }}\n <button\n type=\"button\"\n class=\"inline-flex h-4 w-4 items-center justify-center rounded-sm text-[10px] hover:bg-(--p-surface-200) focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--p-primary-color)\"\n (click)=\"removeConvertToFileColumn(column)\"\n [attr.aria-label]=\"'Remove column ' + column\"\n >\n x\n </button>\n </span>\n }\n } @else {\n <span\n class=\"rounded-md bg-(--p-content-background) px-2 py-1 text-[12px] text-(--p-text-muted-color)\"\n >\n All fields\n </span>\n }\n </div>\n <div class=\"grid items-end gap-2 md:grid-cols-[1fr_auto]\">\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"convertToFileColumnDraft()\"\n (ngModelChange)=\"\n onConvertToFileColumnDraftChange($event)\n \"\n (keyup.enter)=\"addConvertToFileColumns()\"\n label=\"Add column\"\n [required]=\"isConfigFieldRequired('columns')\"\n />\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n icon=\"general.plus\"\n label=\"Add\"\n [disabled]=\"!convertToFileColumnDraft().trim()\"\n (onClick)=\"addConvertToFileColumns()\"\n />\n </div>\n </div>\n }\n </div>\n }\n\n @if (convertToFileIsCsv()) {\n <div class=\"mt-4 grid gap-3 md:grid-cols-3\">\n <mt-toggle-field\n size=\"small\"\n labelPosition=\"end\"\n [ngModel]=\"config()['includeHeaders'] !== false\"\n (ngModelChange)=\"\n onConfigFieldChange('includeHeaders', $event === true)\n \"\n label=\"Include headers\"\n />\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"config()['delimiter'] ?? ','\"\n (ngModelChange)=\"onConfigFieldChange('delimiter', $event)\"\n label=\"Delimiter\"\n [required]=\"isConfigFieldRequired('delimiter')\"\n />\n <mt-text-field\n [ngModel]=\"config()['encoding'] ?? 'utf-8'\"\n [readonly]=\"true\"\n label=\"Encoding\"\n />\n </div>\n }\n\n @if (convertToFileIsJson()) {\n <div class=\"mt-4 grid gap-3 md:grid-cols-2\">\n <mt-toggle-field\n size=\"small\"\n labelPosition=\"end\"\n [ngModel]=\"config()['pretty'] !== false\"\n (ngModelChange)=\"\n onConfigFieldChange('pretty', $event === true)\n \"\n label=\"Pretty JSON\"\n />\n </div>\n }\n\n @if (convertToFileIsHtml()) {\n <div class=\"mt-4 grid gap-3\">\n <mt-text-field\n [ngModel]=\"config()['title'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:title')\"\n (ngModelChange)=\"onConfigFieldChange('title', $event)\"\n label=\"Title\"\n [required]=\"isConfigFieldRequired('title')\"\n />\n <mt-textarea-field\n class=\"w-full\"\n [ngModel]=\"config()['body'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:body')\"\n (ngModelChange)=\"onConfigFieldChange('body', $event)\"\n label=\"Body\"\n [required]=\"isConfigFieldRequired('body')\"\n rows=\"7\"\n />\n <mt-toggle-field\n size=\"small\"\n labelPosition=\"end\"\n [ngModel]=\"config()['sanitize'] !== false\"\n (ngModelChange)=\"\n onConfigFieldChange('sanitize', $event === true)\n \"\n label=\"Sanitize HTML\"\n />\n </div>\n }\n\n @if (convertToFileIsIcs()) {\n <div class=\"mt-4 grid gap-3 md:grid-cols-2\">\n <mt-text-field\n [ngModel]=\"config()['summary'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:summary')\"\n (ngModelChange)=\"onConfigFieldChange('summary', $event)\"\n label=\"Event summary\"\n [required]=\"isConfigFieldRequired('summary')\"\n />\n <mt-text-field\n [ngModel]=\"config()['timezone'] ?? 'UTC'\"\n (ngModelChange)=\"onConfigFieldChange('timezone', $event)\"\n label=\"Timezone\"\n [required]=\"isConfigFieldRequired('timezone')\"\n />\n <mt-date-field\n [ngModel]=\"config()['start'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('start', $event)\"\n label=\"Start\"\n [required]=\"isConfigFieldRequired('start')\"\n [showTime]=\"true\"\n [showClear]=\"true\"\n [pInputs]=\"dateTimePickerInputs\"\n />\n <mt-date-field\n [ngModel]=\"config()['end'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('end', $event)\"\n label=\"End\"\n [required]=\"isConfigFieldRequired('end')\"\n [showTime]=\"true\"\n [showClear]=\"true\"\n [pInputs]=\"dateTimePickerInputs\"\n />\n <mt-text-field\n [ngModel]=\"config()['location'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:location')\"\n (ngModelChange)=\"onConfigFieldChange('location', $event)\"\n label=\"Location\"\n [required]=\"isConfigFieldRequired('location')\"\n />\n <mt-textarea-field\n class=\"w-full\"\n [ngModel]=\"config()['description'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:description')\"\n (ngModelChange)=\"onConfigFieldChange('description', $event)\"\n label=\"Description\"\n [required]=\"isConfigFieldRequired('description')\"\n rows=\"4\"\n />\n </div>\n }\n\n @if (convertToFileIsSpreadsheet()) {\n <div\n class=\"mt-4 rounded-md border border-(--p-content-border-color) bg-(--p-surface-50) p-3\"\n >\n <div class=\"mb-3 flex items-center justify-between gap-3\">\n <div class=\"fp-ae-label\">Sheets</div>\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n icon=\"general.plus\"\n label=\"Add sheet\"\n (onClick)=\"addConvertToFileSheet()\"\n />\n </div>\n <div class=\"grid gap-3\">\n @for (sheet of convertToFileSheetRows(); track sheet.index) {\n <div\n class=\"rounded-md border border-(--p-content-border-color) bg-(--p-content-background) p-3\"\n >\n <div\n class=\"grid gap-3 md:grid-cols-[minmax(140px,0.45fr)_minmax(0,1fr)_minmax(180px,0.6fr)_auto]\"\n >\n <mt-text-field\n [ngModel]=\"sheet.name\"\n (ngModelChange)=\"\n updateConvertToFileSheet(\n sheet.index,\n 'name',\n $event\n )\n \"\n label=\"Sheet name\"\n [required]=\"isConfigFieldRequired('sheets')\"\n />\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"sheet.sourceExpression\"\n (focusin)=\"\n setExpressionTarget(\n 'config:sheets:' + sheet.index + ':source'\n )\n \"\n (ngModelChange)=\"\n updateConvertToFileSheet(\n sheet.index,\n 'sourceExpression',\n $event\n )\n \"\n label=\"Source\"\n [required]=\"isConfigFieldRequired('sheets')\"\n />\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"sheet.columnsText\"\n (ngModelChange)=\"\n updateConvertToFileSheet(\n sheet.index,\n 'columns',\n $event\n )\n \"\n label=\"Columns\"\n [required]=\"isConfigFieldRequired('sheets')\"\n />\n <div class=\"flex items-end justify-end\">\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n severity=\"danger\"\n icon=\"general.trash-01\"\n tooltip=\"Remove sheet\"\n (onClick)=\"removeConvertToFileSheet(sheet.index)\"\n />\n </div>\n </div>\n </div>\n } @empty {\n <div\n class=\"rounded-md bg-(--p-content-background) px-3 py-2 text-[12px] text-(--p-text-muted-color)\"\n >\n Single sheet uses the selected data source.\n </div>\n }\n </div>\n </div>\n }\n\n @if (convertToFileIsText()) {\n <div class=\"mt-4 grid gap-3\">\n <mt-textarea-field\n class=\"w-full\"\n [ngModel]=\"config()['content'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:content')\"\n (ngModelChange)=\"onConfigFieldChange('content', $event)\"\n label=\"Content\"\n [required]=\"isConfigFieldRequired('content')\"\n rows=\"7\"\n />\n <mt-text-field\n [ngModel]=\"config()['encoding'] ?? 'utf-8'\"\n [readonly]=\"true\"\n label=\"Encoding\"\n />\n </div>\n }\n\n @if (convertToFileIsBase64()) {\n <div class=\"mt-4 grid gap-3 md:grid-cols-2\">\n <mt-textarea-field\n class=\"w-full font-mono md:col-span-2\"\n [ngModel]=\"config()['base64Expression'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:base64Expression')\"\n (ngModelChange)=\"\n onConfigFieldChange('base64Expression', $event)\n \"\n label=\"Base64 source\"\n [required]=\"isConfigFieldRequired('base64Expression')\"\n rows=\"5\"\n />\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"config()['extension'] ?? convertToFileExtension()\"\n (ngModelChange)=\"onConfigFieldChange('extension', $event)\"\n label=\"Extension\"\n [required]=\"isConfigFieldRequired('extension')\"\n />\n </div>\n }\n </section>\n }\n }\n @case (\"DataTransform\") {\n @if (sectionInMain(\"dataTransform\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">\n {{ dataTransformActionLabel() }}\n </div>\n @if (dataTransformUnavailableReason(); as reason) {\n <div\n class=\"mb-3 rounded-md border border-(--p-content-border-color) bg-(--p-surface-50) px-3 py-2 text-[12px] leading-5 text-(--p-text-muted-color)\"\n >\n {{ reason }}\n </div>\n }\n\n @if (\n !dataTransformIsMerge() && dataTransformUsesKnownActionEditor()\n ) {\n <div class=\"grid gap-3\">\n <div class=\"grid gap-3 md:grid-cols-[minmax(220px,0.45fr)_1fr]\">\n <mt-select-field\n [ngModel]=\"dataTransformSourcePreset()\"\n (ngModelChange)=\"onDataTransformSourcePresetChange($event)\"\n [label]=\"dataTransformSourceLabel()\"\n hint=\"Choose which list this action should transform.\"\n [required]=\"isConfigFieldRequired('sourceExpression')\"\n [options]=\"dataTransformSourceOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n @if (showDataTransformSourceExpression()) {\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"dataTransformSourceExpression()\"\n (focusin)=\"setExpressionTarget('config:sourceExpression')\"\n (ngModelChange)=\"\n onDataTransformSourceExpressionChange($event)\n \"\n [label]=\"dataTransformSourceExpressionLabel()\"\n hint=\"Must resolve to an array of items.\"\n [required]=\"isConfigFieldRequired('sourceExpression')\"\n />\n } @else {\n <div\n class=\"rounded-md border border-surface-200 bg-surface-50 px-3 py-2\"\n >\n <div class=\"fp-ae-label mb-1\">Selected input</div>\n <div\n class=\"truncate font-mono text-sm text-(--p-text-color)\"\n >\n {{ dataTransformSourceSummary() }}\n </div>\n </div>\n }\n </div>\n <div class=\"flex flex-wrap gap-2\">\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n icon=\"general.search-sm\"\n label=\"Preview source\"\n [disabled]=\"!dataTransformSourceExpression()\"\n (onClick)=\"previewDataTransformSourceExpression()\"\n />\n </div>\n </div>\n }\n\n @if (expressionPreview(); as preview) {\n <div\n class=\"mt-3 rounded-md border border-(--p-content-border-color) bg-(--p-surface-50) p-3\"\n >\n <div class=\"fp-ae-label mb-2\">Source preview</div>\n <pre class=\"fp-ae-code\">{{ schemaText(preview) }}</pre>\n </div>\n }\n\n @if (dataTransformIsFilter()) {\n <div class=\"mt-4 grid gap-3\">\n @if (dataTransformUsesStructuredConditionEditor()) {\n <div class=\"fp-ae-label\">Keep item when</div>\n <div class=\"grid gap-3 md:grid-cols-[1fr_180px_1fr]\">\n <mt-text-field\n [ngModel]=\"dataTransformConditionFieldText('left')\"\n (focusin)=\"setExpressionTarget('config:condition:left')\"\n (ngModelChange)=\"\n onDataTransformConditionFieldChange('left', $event)\n \"\n label=\"Left value\"\n hint=\"Field, value, or expression from each item.\"\n [required]=\"isConfigFieldRequired('condition.left')\"\n />\n <mt-select-field\n [ngModel]=\"\n dataTransformConditionFieldValue('operator') || 'equals'\n \"\n (ngModelChange)=\"\n onDataTransformConditionFieldChange('operator', $event)\n \"\n label=\"Operator\"\n [required]=\"isConfigFieldRequired('condition.operator')\"\n [options]=\"ifOperatorOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-text-field\n [ngModel]=\"dataTransformConditionFieldText('right')\"\n (focusin)=\"setExpressionTarget('config:condition:right')\"\n (ngModelChange)=\"\n onDataTransformConditionFieldChange('right', $event)\n \"\n label=\"Right value\"\n hint=\"Value or expression to compare against.\"\n [required]=\"isConfigFieldRequired('condition.right')\"\n />\n </div>\n } @else {\n <mt-textarea-field\n class=\"w-full font-mono\"\n [ngModel]=\"dataTransformConditionText()\"\n (focusin)=\"setExpressionTarget('config:condition')\"\n (ngModelChange)=\"onDataTransformConditionChange($event)\"\n label=\"Keep item when\"\n hint=\"Condition expression or JSON supplied by the backend catalog.\"\n [required]=\"isConfigFieldRequired('condition')\"\n rows=\"4\"\n />\n }\n </div>\n }\n\n @if (dataTransformIsSort()) {\n <div class=\"mt-4 grid gap-3 md:grid-cols-2\">\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"config()['sortBy'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:sortBy')\"\n (ngModelChange)=\"onConfigFieldChange('sortBy', $event)\"\n label=\"Sort by field\"\n hint=\"Field path inside each item, for example createdAt.\"\n [required]=\"isConfigFieldRequired('sortBy')\"\n />\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"config()['sortExpression'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:sortExpression')\"\n (ngModelChange)=\"\n onConfigFieldChange('sortExpression', $event)\n \"\n label=\"Custom sort expression\"\n hint=\"Optional expression when a simple field path is not enough.\"\n [required]=\"isConfigFieldRequired('sortExpression')\"\n />\n <mt-select-field\n [ngModel]=\"config()['direction'] ?? 'asc'\"\n (ngModelChange)=\"onConfigFieldChange('direction', $event)\"\n label=\"Direction\"\n [required]=\"isConfigFieldRequired('direction')\"\n [options]=\"dataTransformDirectionOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-select-field\n [ngModel]=\"config()['valueType'] ?? 'string'\"\n (ngModelChange)=\"onConfigFieldChange('valueType', $event)\"\n label=\"Value type\"\n [required]=\"isConfigFieldRequired('valueType')\"\n [options]=\"dataTransformValueTypeOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n </div>\n }\n\n @if (dataTransformIsLimit()) {\n <div class=\"mt-4 grid gap-3 md:grid-cols-2\">\n <mt-number-field\n [ngModel]=\"numberValue(config()['offset']) ?? 0\"\n (ngModelChange)=\"onConfigFieldChange('offset', $event)\"\n label=\"Skip first\"\n [required]=\"isConfigFieldRequired('offset')\"\n [min]=\"0\"\n />\n <mt-number-field\n [ngModel]=\"numberValue(config()['limit'])\"\n (ngModelChange)=\"onConfigFieldChange('limit', $event)\"\n label=\"Take at most\"\n [required]=\"isConfigFieldRequired('limit')\"\n [min]=\"1\"\n />\n </div>\n }\n\n @if (dataTransformIsMapFields()) {\n <div class=\"mt-4 grid gap-3\">\n <mt-select-field\n [ngModel]=\"config()['mode'] ?? 'merge'\"\n (ngModelChange)=\"onConfigFieldChange('mode', $event)\"\n label=\"Field mode\"\n [required]=\"isConfigFieldRequired('mode')\"\n [options]=\"dataTransformMapModeOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <ng-container\n *ngTemplateOutlet=\"\n mapEditor;\n context: {\n objectKey: 'fields',\n rows: fieldsRows(),\n keyLabel: 'Output field',\n valueLabel: 'Value or expression',\n addLabel: 'Add mapped field',\n emptyText:\n 'No fields mapped yet. Add an output field to create or update item data.',\n keyHint: 'Name of the field in the output item.',\n valueHint:\n 'Use a fixed value or an expression from each item.',\n required: isConfigFieldRequired('fields'),\n }\n \"\n />\n </div>\n }\n\n @if (dataTransformIsAggregate()) {\n <div class=\"mt-4 grid gap-3\">\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"config()['groupBy'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:groupBy')\"\n (ngModelChange)=\"onConfigFieldChange('groupBy', $event)\"\n label=\"Group items by\"\n hint=\"Optional field path or expression used to group results.\"\n [required]=\"isConfigFieldRequired('groupBy')\"\n />\n <div class=\"rounded-md border border-surface-200 p-3\">\n <div\n class=\"mb-3 flex flex-wrap items-center justify-between gap-2\"\n >\n <div class=\"fp-ae-label\">Summary fields</div>\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n icon=\"general.plus\"\n label=\"Add summary\"\n (onClick)=\"addDataTransformAggregation()\"\n />\n </div>\n <div class=\"space-y-3\">\n @for (\n item of dataTransformAggregationRows();\n track item.index\n ) {\n <div\n class=\"grid gap-2 rounded-md bg-surface-50 p-3 md:grid-cols-[minmax(160px,0.8fr)_minmax(180px,0.9fr)_1fr_minmax(150px,0.7fr)_auto]\"\n >\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"item.key\"\n (ngModelChange)=\"\n updateDataTransformAggregation(\n item.index,\n 'key',\n $event\n )\n \"\n label=\"Output field\"\n hint=\"Name saved in the summary output.\"\n [required]=\"isConfigFieldRequired('aggregations')\"\n />\n <mt-select-field\n [ngModel]=\"item.operation\"\n (ngModelChange)=\"\n updateDataTransformAggregation(\n item.index,\n 'operation',\n $event\n )\n \"\n label=\"Calculation\"\n [required]=\"isConfigFieldRequired('aggregations')\"\n [options]=\"dataTransformAggregationOperationOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"item.field\"\n [readonly]=\"\n !dataTransformAggregationNeedsField(item.operation)\n \"\n (focusin)=\"\n setExpressionTarget(\n 'config:aggregations:' + item.index + ':field'\n )\n \"\n (ngModelChange)=\"\n updateDataTransformAggregation(\n item.index,\n 'field',\n $event\n )\n \"\n label=\"Field or expression\"\n [hint]=\"\n dataTransformAggregationFieldHint(item.operation)\n \"\n [required]=\"\n isConfigFieldRequired('aggregations') &&\n dataTransformAggregationNeedsField(item.operation)\n \"\n />\n <mt-select-field\n [ngModel]=\"item.valueType\"\n (ngModelChange)=\"\n updateDataTransformAggregation(\n item.index,\n 'valueType',\n $event\n )\n \"\n label=\"Value type\"\n [required]=\"isConfigFieldRequired('aggregations')\"\n [options]=\"dataTransformValueTypeOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-button\n class=\"self-end\"\n size=\"small\"\n severity=\"secondary\"\n variant=\"outlined\"\n label=\"Remove\"\n (onClick)=\"removeDataTransformAggregation(item.index)\"\n />\n </div>\n } @empty {\n <div\n class=\"rounded-md bg-(--p-content-background) px-3 py-2 text-[12px] text-(--p-text-muted-color)\"\n >\n No summary fields yet. Add a summary such as count,\n total, average, minimum, or maximum.\n </div>\n }\n </div>\n </div>\n </div>\n }\n\n @if (dataTransformIsMerge()) {\n <div class=\"mt-4 grid gap-3 md:grid-cols-2\">\n <mt-textarea-field\n class=\"w-full font-mono md:col-span-2\"\n [ngModel]=\"config()['leftExpression'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:leftExpression')\"\n (ngModelChange)=\"\n onConfigFieldChange('leftExpression', $event)\n \"\n label=\"First list\"\n hint=\"Expression that resolves to the first list of items.\"\n [required]=\"isConfigFieldRequired('leftExpression')\"\n rows=\"3\"\n />\n <mt-textarea-field\n class=\"w-full font-mono md:col-span-2\"\n [ngModel]=\"config()['rightExpression'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:rightExpression')\"\n (ngModelChange)=\"\n onConfigFieldChange('rightExpression', $event)\n \"\n label=\"Second list\"\n hint=\"Expression that resolves to the second list of items.\"\n [required]=\"isConfigFieldRequired('rightExpression')\"\n rows=\"3\"\n />\n <mt-select-field\n [ngModel]=\"config()['strategy'] ?? 'append'\"\n (ngModelChange)=\"onConfigFieldChange('strategy', $event)\"\n label=\"Merge strategy\"\n [required]=\"isConfigFieldRequired('strategy')\"\n [options]=\"dataTransformMergeStrategyOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n @if (dataTransformUsesKeyedMerge()) {\n <div class=\"grid gap-3 md:grid-cols-2\">\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"config()['leftKey'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('leftKey', $event)\"\n label=\"First list key\"\n [required]=\"isConfigFieldRequired('leftKey')\"\n />\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"config()['rightKey'] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange('rightKey', $event)\"\n label=\"Second list key\"\n [required]=\"isConfigFieldRequired('rightKey')\"\n />\n </div>\n }\n </div>\n }\n\n @if (!dataTransformUsesKnownActionEditor()) {\n <div class=\"mt-4 grid gap-3\">\n @for (field of dataTransformGenericFields(); track field.key) {\n @if (field.enumValues.length) {\n <mt-select-field\n [ngModel]=\"config()[field.key] ?? ''\"\n (ngModelChange)=\"onConfigFieldChange(field.key, $event)\"\n [label]=\"field.label\"\n [hint]=\"field.description ?? ''\"\n [required]=\"isConfigFieldRequired(field.key)\"\n [options]=\"enumOptions(field.enumValues)\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n } @else if (field.type === \"number\") {\n <mt-number-field\n [ngModel]=\"numberValue(config()[field.key])\"\n (ngModelChange)=\"onConfigFieldChange(field.key, $event)\"\n [label]=\"field.label\"\n [hint]=\"field.description ?? ''\"\n [required]=\"isConfigFieldRequired(field.key)\"\n />\n } @else {\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"fieldText(field.key)\"\n (focusin)=\"setExpressionTarget('config:' + field.key)\"\n (ngModelChange)=\"onConfigFieldChange(field.key, $event)\"\n [label]=\"field.label\"\n [hint]=\"field.description ?? ''\"\n [required]=\"isConfigFieldRequired(field.key)\"\n />\n }\n } @empty {\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"dataTransformActionKey()\"\n [readonly]=\"true\"\n label=\"Action\"\n />\n }\n </div>\n }\n\n <div class=\"mt-4 grid gap-3 md:grid-cols-3\">\n <mt-number-field\n [ngModel]=\"numberValue(config()['maxOutputItems'])\"\n (ngModelChange)=\"onConfigFieldChange('maxOutputItems', $event)\"\n label=\"Output limit\"\n [required]=\"isConfigFieldRequired('maxOutputItems')\"\n [min]=\"1\"\n />\n <div\n class=\"rounded-md border border-(--p-content-border-color) bg-(--p-surface-50) p-3 md:col-span-2\"\n >\n <div class=\"fp-ae-label mb-2\">Output</div>\n <div class=\"flex flex-wrap gap-1.5\">\n @for (field of dataTransformOutputFieldLabels; track field) {\n <span\n class=\"rounded-md bg-(--p-surface-100) px-2 py-1 font-mono text-[11px]\"\n >{{ field }}</span\n >\n }\n </div>\n </div>\n </div>\n </section>\n }\n }\n @case (\"LoopOverItems\") {\n @if (sectionInMain(\"loopOverItems\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Loop over items</div>\n\n <div class=\"grid gap-3\">\n <div class=\"grid gap-3 md:grid-cols-[minmax(220px,0.45fr)_1fr]\">\n <mt-select-field\n [ngModel]=\"loopSourceMode()\"\n (ngModelChange)=\"onLoopSourceModeChange($event)\"\n label=\"List source\"\n hint=\"Choose an output array or type an array expression.\"\n [required]=\"isConfigFieldRequired('itemsExpression')\"\n [options]=\"loopSourceModeOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n @if (showLoopItemsExpression()) {\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"loopItemsExpression()\"\n (focusin)=\"setExpressionTarget('config:itemsExpression')\"\n (ngModelChange)=\"onLoopItemsExpressionChange($event)\"\n label=\"Array expression\"\n hint=\"Must resolve to an array.\"\n [required]=\"isConfigFieldRequired('itemsExpression')\"\n />\n } @else {\n <mt-select-field\n [ngModel]=\"loopSelectedOutputArray()\"\n (ngModelChange)=\"onLoopOutputArrayChange($event)\"\n label=\"Output array\"\n hint=\"Arrays exposed by previous connected steps.\"\n [required]=\"isConfigFieldRequired('itemsExpression')\"\n [options]=\"loopOutputArrayOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n }\n </div>\n <div class=\"flex flex-wrap gap-2\">\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n icon=\"general.search-sm\"\n label=\"Preview array\"\n [disabled]=\"!loopItemsExpression()\"\n (onClick)=\"previewLoopItemsExpression()\"\n />\n </div>\n </div>\n\n @if (expressionPreview(); as preview) {\n <div\n class=\"mt-3 rounded-md border border-(--p-content-border-color) bg-(--p-surface-50) p-3\"\n >\n <div class=\"fp-ae-label mb-2\">Expression preview</div>\n <pre class=\"fp-ae-code\">{{ schemaText(preview) }}</pre>\n </div>\n }\n\n <div class=\"mt-4 border-t border-surface-100 pt-4\">\n <div class=\"fp-ae-label mb-3\">Run settings</div>\n <div class=\"grid gap-3 md:grid-cols-2 xl:grid-cols-4\">\n <mt-number-field\n [ngModel]=\"\n numberValue(\n config()['maxItems'] ?? config()['maxIterations']\n )\n \"\n (ngModelChange)=\"onConfigFieldChange('maxItems', $event)\"\n label=\"Item limit\"\n hint=\"Optional maximum number of items to process.\"\n [required]=\"isConfigFieldRequired('maxItems')\"\n [min]=\"1\"\n />\n <mt-select-field\n [ngModel]=\"loopFailureBehavior()\"\n (ngModelChange)=\"onLoopFailureBehaviorChange($event)\"\n label=\"When an item fails\"\n [required]=\"isConfigFieldRequired('failureBehavior')\"\n [options]=\"loopFailureBehaviorOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n <mt-select-field\n [ngModel]=\"config()['emptyBehavior'] ?? 'done'\"\n (ngModelChange)=\"onConfigFieldChange('emptyBehavior', $event)\"\n label=\"When list is empty\"\n [required]=\"isConfigFieldRequired('emptyBehavior')\"\n [options]=\"loopEmptyBehaviorOptions\"\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()['collectResults'] === true\"\n (ngModelChange)=\"\n onConfigFieldChange('collectResults', $event === true)\n \"\n label=\"Save item results\"\n hint=\"Stores a bounded summary for the done route.\"\n />\n </div>\n </div>\n\n <details class=\"mt-4 border-t border-surface-100 pt-3\">\n <summary\n class=\"cursor-pointer text-sm font-semibold text-(--p-text-color)\"\n >\n Advanced runtime details\n </summary>\n <div class=\"mt-3 grid gap-3 md:grid-cols-3\">\n <div>\n <div class=\"fp-ae-label mb-1\">Current item token</div>\n <div\n class=\"rounded-md border border-surface-200 bg-surface-50 px-3 py-2 font-mono text-sm text-(--p-text-color)\"\n >\n $item.json\n </div>\n </div>\n <div>\n <div class=\"fp-ae-label mb-1\">Loop context token</div>\n <div\n class=\"rounded-md border border-surface-200 bg-surface-50 px-3 py-2 font-mono text-sm text-(--p-text-color)\"\n >\n $loop.current\n </div>\n </div>\n <mt-number-field\n [ngModel]=\"numberValue(config()['concurrency']) ?? 1\"\n (ngModelChange)=\"onLoopConcurrencyChange($event)\"\n label=\"Concurrency\"\n hint=\"This deployment supports 1.\"\n [required]=\"isConfigFieldRequired('concurrency')\"\n [min]=\"1\"\n [max]=\"1\"\n />\n </div>\n </details>\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\n does 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 [required]=\"isConfigFieldRequired('targetModule')\"\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 [required]=\"isConfigFieldRequired('operation')\"\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 [required]=\"isConfigFieldRequired('idempotencyKey')\"\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\n >{{ field.viewType ?? \"Value\" }}\n @if (field.required) {\n *\n }\n </strong>\n </div>\n }\n </div>\n </div>\n }\n <div class=\"mt-3\">\n <ng-container\n *ngTemplateOutlet=\"\n mapEditor;\n context: {\n objectKey: 'mapping',\n rows: mappingRows(),\n keyLabel: 'Module field',\n valueLabel: 'Expression / value',\n addLabel: 'Add module field',\n required: isConfigFieldRequired('mapping'),\n }\n \"\n />\n </div>\n <div class=\"mt-3 flex gap-2\">\n <mt-button\n size=\"small\"\n severity=\"primary\"\n label=\"Validate mapping\"\n (onClick)=\"validateCommitMapping()\"\n />\n </div>\n @if (commitValidation(); as result) {\n <div\n class=\"mt-3 rounded-lg bg-(--p-surface-100) px-3 py-2 text-[12px]\"\n >\n {{\n result.isValid === false\n ? \"Mapping invalid\"\n : \"Mapping accepted by helper\"\n }}\n @for (issue of resultIssues(result); track $index) {\n <div class=\"mt-1 text-[11px] text-(--p-text-muted-color)\">\n {{ issue }}\n </div>\n }\n </div>\n }\n @if (sectionInMain(\"credentials\")) {\n <div\n class=\"mt-3 rounded-md border border-surface-200 bg-surface-0 p-3\"\n >\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 [required]=\"isConfigFieldRequired('statusCode')\"\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 [required]=\"isConfigFieldRequired('responseMode')\"\n [options]=\"responseModeOptions\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n </div>\n <div class=\"mt-3\">\n <ng-container\n *ngTemplateOutlet=\"\n mapEditor;\n context: {\n objectKey: 'headers',\n rows: headerRows(),\n keyLabel: 'Header',\n valueLabel: 'Value',\n addLabel: 'Add header',\n }\n \"\n />\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 [required]=\"isConfigFieldRequired('body')\"\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 [required]=\"isConfigFieldRequired('targetAutomationId')\"\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 [required]=\"isConfigFieldRequired('revisionMode')\"\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)=\"\n onConfigFieldChange('specificRevisionId', $event)\n \"\n label=\"Specific revision id\"\n hint=\"Required only when revision mode is Specific revision.\"\n [required]=\"isConfigFieldRequired('specificRevisionId')\"\n />\n }\n <mt-toggle-field\n size=\"small\"\n labelPosition=\"end\"\n [ngModel]=\"config()['waitForCompletion'] !== false\"\n (ngModelChange)=\"\n onConfigFieldChange('waitForCompletion', $event === true)\n \"\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)=\"\n onConfigFieldChange('inputMappingJson', $event)\n \"\n label=\"Input mapping JSON\"\n hint=\"JSON object or expression map sent into the child automation input.\"\n [required]=\"isConfigFieldRequired('inputMappingJson')\"\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)=\"\n onConfigFieldChange('outputMappingJson', $event)\n \"\n label=\"Output mapping JSON\"\n hint=\"JSON object or expression map for child output/status returned to the parent.\"\n [required]=\"isConfigFieldRequired('outputMappingJson')\"\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 [required]=\"isConfigFieldRequired('targetAutomationId')\"\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 [required]=\"isConfigFieldRequired('revisionMode')\"\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)=\"\n onConfigFieldChange('specificRevisionId', $event)\n \"\n label=\"Specific revision id\"\n hint=\"Required only when revision mode is Specific revision.\"\n [required]=\"isConfigFieldRequired('specificRevisionId')\"\n />\n }\n <mt-toggle-field\n size=\"small\"\n labelPosition=\"end\"\n [ngModel]=\"config()['waitForCompletion'] !== false\"\n (ngModelChange)=\"\n onConfigFieldChange('waitForCompletion', $event === true)\n \"\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)=\"\n onConfigFieldChange('inputMappingJson', $event)\n \"\n label=\"Input mapping JSON\"\n hint=\"JSON object or expression map sent into the child automation input.\"\n [required]=\"isConfigFieldRequired('inputMappingJson')\"\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)=\"\n onConfigFieldChange('outputMappingJson', $event)\n \"\n label=\"Output mapping JSON\"\n hint=\"JSON object or expression map for child output/status returned to the parent.\"\n [required]=\"isConfigFieldRequired('outputMappingJson')\"\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\n class=\"flex items-center justify-between gap-3 border-b border-surface-200 bg-surface-50 px-4 py-3\"\n >\n <div class=\"fp-ae-section-title !m-0\">Parallel start</div>\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n icon=\"general.plus\"\n label=\"Add branch\"\n (onClick)=\"addParallelBranch()\"\n />\n </div>\n <div class=\"space-y-3 p-4\">\n @for (\n branch of parallelBranches();\n track branch.key;\n let i = $index\n ) {\n <div\n class=\"rounded-md border border-surface-200 bg-surface-0 p-3\"\n >\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 [required]=\"isConfigFieldRequired('branches')\"\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 [required]=\"isConfigFieldRequired('branches')\"\n />\n <mt-text-field\n [ngModel]=\"branch.description\"\n (ngModelChange)=\"\n updateParallelBranch(i, 'description', $event)\n \"\n label=\"Description\"\n hint=\"Optional branch note for reviewers.\"\n [required]=\"false\"\n />\n <div class=\"flex items-end gap-1\">\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n icon=\"arrow.arrow-up\"\n tooltip=\"Move up\"\n (onClick)=\"moveParallelBranch(i, -1)\"\n />\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n icon=\"arrow.arrow-down\"\n tooltip=\"Move down\"\n (onClick)=\"moveParallelBranch(i, 1)\"\n />\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n severity=\"danger\"\n icon=\"general.trash-01\"\n tooltip=\"Remove branch\"\n (onClick)=\"removeParallelBranch(i)\"\n />\n </div>\n </div>\n <div\n class=\"mt-3 flex flex-wrap items-center gap-1.5 text-[11px]\"\n >\n <span\n 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)\"\n >{{ branch.key }}</span\n >\n <span\n 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)\"\n >{{ branch.routeCount }} connected route{{\n branch.routeCount === 1 ? \"\" : \"s\"\n }}</span\n >\n </div>\n </div>\n }\n @if (parallelBranches().length === 0) {\n <p class=\"fp-ae-copy\">\n Add at least two stable branch keys. Backend validation blocks\n publish until branches are valid.\n </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 [required]=\"isConfigFieldRequired('joinPolicy')\"\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 [required]=\"isConfigFieldRequired('threshold')\"\n [min]=\"1\"\n />\n }\n <mt-select-field\n [ngModel]=\"config()['aggregationStrategy'] ?? 'MergeObjects'\"\n (ngModelChange)=\"\n onConfigFieldChange('aggregationStrategy', $event)\n \"\n label=\"Aggregation strategy\"\n hint=\"How backend joins branch outputs into the join node output.\"\n [required]=\"isConfigFieldRequired('aggregationStrategy')\"\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)=\"\n onConfigFieldChange('outputTargetPath', $event)\n \"\n label=\"Output target path\"\n hint=\"Optional context path where joined output should be written.\"\n [required]=\"isConfigFieldRequired('outputTargetPath')\"\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\n class=\"grid items-end gap-3 md:grid-cols-[minmax(0,1fr)_minmax(280px,0.7fr)]\"\n >\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 [required]=\"isConfigFieldRequired('mode')\"\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)=\"\n onConfigFieldChange('firstMatch', $event === true)\n \"\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]=\"\n showSwitchExpression()\n ? 'font-mono'\n : 'font-mono md:col-span-2'\n \"\n [ngModel]=\"config()['sourceValue'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:sourceValue')\"\n (ngModelChange)=\"\n onConfigFieldChange('sourceValue', $event)\n \"\n label=\"Source value\"\n hint=\"Value or expression used for value matching.\"\n [required]=\"isConfigFieldRequired('sourceValue')\"\n />\n }\n @if (showSwitchExpression()) {\n <mt-text-field\n [class]=\"\n showSwitchSourceValue()\n ? 'font-mono'\n : 'font-mono md:col-span-2'\n \"\n [ngModel]=\"config()['expression'] ?? ''\"\n (focusin)=\"setExpressionTarget('config:expression')\"\n (ngModelChange)=\"\n onConfigFieldChange('expression', $event)\n \"\n label=\"Expression\"\n hint=\"Expression evaluated when mode is expression or rules.\"\n [required]=\"isConfigFieldRequired('expression')\"\n />\n }\n </div>\n }\n <div\n class=\"grid items-end gap-3 md:grid-cols-[minmax(0,1fr)_minmax(280px,0.7fr)]\"\n >\n @if (showSwitchDefaultOutputKey()) {\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"config()['defaultOutputKey'] ?? 'default'\"\n (ngModelChange)=\"\n onConfigFieldChange('defaultOutputKey', $event)\n \"\n label=\"Default output key\"\n hint=\"Stable default route key used when no case matches.\"\n [required]=\"isConfigFieldRequired('defaultOutputKey')\"\n />\n }\n <mt-toggle-field\n [class]=\"\n showSwitchDefaultOutputKey()\n ? 'self-end pb-1'\n : 'self-end pb-1 md:col-span-2'\n \"\n size=\"small\"\n labelPosition=\"end\"\n [ngModel]=\"config()['includeDefaultOutput'] !== false\"\n (ngModelChange)=\"\n onConfigFieldChange('includeDefaultOutput', $event === true)\n \"\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\n class=\"flex items-center justify-between gap-3 border-b border-surface-200 bg-surface-50 px-4 py-3\"\n >\n <div class=\"fp-ae-section-title !m-0\">Switch cases</div>\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n icon=\"general.plus\"\n label=\"Add case\"\n (onClick)=\"addSwitchCase()\"\n />\n </div>\n <div class=\"space-y-3 p-4\">\n @for (item of switchCases(); track item.key; let i = $index) {\n <div\n class=\"rounded-md border border-surface-200 bg-surface-0 p-3\"\n >\n <div\n class=\"flex flex-wrap items-start justify-between gap-3 border-b border-surface-100 pb-3\"\n >\n <div class=\"min-w-0\">\n <div class=\"flex flex-wrap items-center gap-1.5\">\n <span\n class=\"text-sm font-semibold text-(--p-text-color)\"\n >Case {{ i + 1 }}</span\n >\n <span\n class=\"inline-flex h-7 items-center rounded-md border border-surface-200 bg-surface-50 px-2 font-mono text-[11px] font-semibold text-(--p-text-color)\"\n >{{ item.routeKey }}</span\n >\n <span\n class=\"inline-flex h-7 items-center rounded-md border border-surface-200 bg-surface-0 px-2 text-[11px] font-medium text-(--p-text-muted-color)\"\n >{{ item.routeCount }} connected route{{\n item.routeCount === 1 ? \"\" : \"s\"\n }}</span\n >\n </div>\n <div\n class=\"mt-1 truncate text-xs font-medium text-(--p-text-muted-color)\"\n >\n {{ item.label || item.routeKey }}\n </div>\n </div>\n <div class=\"flex shrink-0 items-center gap-1\">\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n icon=\"arrow.arrow-up\"\n tooltip=\"Move up\"\n (onClick)=\"moveSwitchCase(i, -1)\"\n />\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n icon=\"arrow.arrow-down\"\n tooltip=\"Move down\"\n (onClick)=\"moveSwitchCase(i, 1)\"\n />\n <mt-button\n size=\"small\"\n variant=\"outlined\"\n severity=\"danger\"\n icon=\"general.trash-01\"\n tooltip=\"Remove case\"\n (onClick)=\"removeSwitchCase(i)\"\n />\n </div>\n </div>\n <div class=\"mt-3 grid gap-3 md:grid-cols-2\">\n <mt-text-field\n [ngModel]=\"item.label\"\n (ngModelChange)=\"updateSwitchCase(i, 'label', $event)\"\n label=\"Case name\"\n hint=\"Shown in route labels.\"\n [required]=\"isConfigFieldRequired('cases')\"\n />\n @if (showSwitchCaseValue()) {\n <mt-text-field\n [ngModel]=\"item.value\"\n (focusin)=\"\n setExpressionTarget(\n 'config:cases:' + item.key + ':value'\n )\n \"\n (ngModelChange)=\"updateSwitchCase(i, 'value', $event)\"\n label=\"Match value\"\n hint=\"Compared with the source value.\"\n [required]=\"isConfigFieldRequired('cases')\"\n />\n }\n @if (showSwitchCaseCondition()) {\n <mt-text-field\n class=\"font-mono md:col-span-2\"\n [ngModel]=\"item.condition\"\n (focusin)=\"\n setExpressionTarget(\n 'config:cases:' + item.key + ':condition'\n )\n \"\n (ngModelChange)=\"\n updateSwitchCase(i, 'condition', $event)\n \"\n label=\"Rule condition\"\n hint=\"When true, this case is selected.\"\n [required]=\"isConfigFieldRequired('cases')\"\n />\n }\n </div>\n <details\n class=\"mt-3 rounded-md border border-surface-200 bg-surface-50 px-3 py-2 text-xs text-(--p-text-muted-color)\"\n >\n <summary class=\"cursor-pointer font-medium\">\n Advanced route settings\n </summary>\n <div class=\"mt-3 grid gap-3 md:grid-cols-2\">\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"item.key\"\n (ngModelChange)=\"updateSwitchCase(i, 'key', $event)\"\n label=\"Route key\"\n hint=\"Stable output key for connected routes.\"\n [required]=\"isConfigFieldRequired('cases')\"\n />\n <mt-text-field\n class=\"font-mono\"\n [ngModel]=\"item.expression\"\n (focusin)=\"\n setExpressionTarget(\n 'config:cases:' + item.key + ':expression'\n )\n \"\n (ngModelChange)=\"\n updateSwitchCase(i, 'expression', $event)\n \"\n label=\"Advanced condition expression\"\n hint=\"Optional true or false expression for this case.\"\n [required]=\"false\"\n />\n </div>\n <div\n class=\"mt-3 inline-flex h-7 items-center rounded-md border border-surface-200 bg-surface-0 px-2 font-medium\"\n >\n Visual order {{ i + 1 }}\n </div>\n </details>\n </div>\n }\n @if (switchCases().length === 0) {\n <p class=\"fp-ae-copy\">\n Add a case for each route this switch can select.\n </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 (\n supportsConfigKey(\"status\") ||\n supportsConfigKey(\"message\") ||\n supportsConfigKey(\"output\")\n ) {\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 Stop nodes use their backend defaults and do not expose extra\n configuration.\n </p>\n }\n </section>\n }\n }\n }\n\n @if (sectionInAdvanced(\"policy\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">\n Retry, timeout, and failure handling\n </div>\n <div class=\"grid gap-3 md:grid-cols-3\">\n <mt-number-field\n [ngModel]=\"policyNumberValue('timeoutPolicyJson', 'timeoutSeconds')\"\n (ngModelChange)=\"\n updatePolicyNumber('timeoutPolicyJson', 'timeoutSeconds', $event)\n \"\n label=\"Step timeout (seconds)\"\n hint=\"How long this step may run before Automation Engine marks it timed out.\"\n [min]=\"1\"\n />\n <mt-number-field\n [ngModel]=\"policyNumberValue('retryPolicyJson', 'maxAttempts')\"\n (ngModelChange)=\"\n updatePolicyNumber('retryPolicyJson', 'maxAttempts', $event)\n \"\n label=\"Max attempts\"\n hint=\"Total tries including the first run. Use 1 for no retry.\"\n [min]=\"1\"\n />\n <mt-select-field\n [ngModel]=\"errorPolicyMode()\"\n (ngModelChange)=\"updateErrorPolicyMode($event)\"\n label=\"When this step fails\"\n hint=\"Choose whether failures pause for recovery, use the failure path, or fail the workflow.\"\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 count, or\n failure rule here.\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(\"mapping\")) {\n <section class=\"fp-ae-panel\">\n <div class=\"fp-ae-section-title\">Custom data handoff</div>\n <div class=\"grid gap-3 md:grid-cols-2\">\n <div>\n <div class=\"fp-ae-label mb-2\">Inputs sent to this step</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)\">{{\n row.key\n }}</span>\n <mt-text-field\n [ngModel]=\"valueText(row.value)\"\n (focusin)=\"\n setExpressionTarget('config:inputMapping:' + row.key)\n \"\n (ngModelChange)=\"\n updateJsonField('inputMappingJson', row.key, $event)\n \"\n hint=\"Value this step receives from the workflow context.\"\n />\n </label>\n }\n </div>\n <div>\n <div class=\"fp-ae-label mb-2\">Outputs saved for next steps</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)\">{{\n row.key\n }}</span>\n <mt-text-field\n [ngModel]=\"valueText(row.value)\"\n (focusin)=\"\n setExpressionTarget('config:outputMapping:' + row.key)\n \"\n (ngModelChange)=\"\n updateJsonField('outputMappingJson', row.key, $event)\n \"\n hint=\"Value this step exposes to the rest of the workflow.\"\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 Default handoff is active. This step receives the current workflow\n item and exposes its normal output to the next connected step. Add a\n custom handoff only when the business data must be reshaped.\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.\n Triggers 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\">\n Config schema\n </summary>\n <pre class=\"fp-ae-code\">{{ schemaText(configSchema()) }}</pre>\n </details>\n @if (hasTriggerPayloadSchema() && triggerPayloadSchema(); as schema) {\n <details\n class=\"rounded-lg border border-(--p-content-border-color)\"\n >\n <summary\n class=\"cursor-pointer px-3 py-2 text-[12px] font-semibold\"\n >\n Payload schema\n </summary>\n <pre class=\"fp-ae-code\">{{ schemaText(schema) }}</pre>\n </details>\n }\n @if (authPolicySchema()) {\n <details\n class=\"rounded-lg border border-(--p-content-border-color)\"\n >\n <summary\n class=\"cursor-pointer px-3 py-2 text-[12px] font-semibold\"\n >\n Auth policy schema\n </summary>\n <pre class=\"fp-ae-code\">{{ schemaText(authPolicySchema()) }}</pre>\n </details>\n }\n @if (triggerPayloadSample(); as sample) {\n <details\n class=\"rounded-lg border border-(--p-content-border-color)\"\n >\n <summary\n class=\"cursor-pointer px-3 py-2 text-[12px] font-semibold\"\n >\n Payload or request sample\n </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\">\n Config schema\n </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\">\n Input schema\n </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\">\n Output schema\n </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\n class=\"rounded-md bg-(--p-surface-100) px-2 py-1 font-mono text-[11px]\"\n >{{ key }}</span\n >\n }\n @if (routeOutputKeys().length === 0) {\n <span class=\"text-[12px] text-(--p-text-muted-color)\"\n >No outgoing route keys</span\n >\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]=\"\n field.type === 'object' || field.type === 'array'\n \"\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\"\n [hint]=\"field.description ?? ''\"\n [required]=\"isConfigFieldRequired(field.key)\"\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)=\"\n onConfigFieldChange(field.key, $event === true)\n \"\n [label]=\"field.label\"\n [hint]=\"field.description ?? ''\"\n [required]=\"isConfigFieldRequired(field.key)\"\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\"\n [hint]=\"field.description ?? ''\"\n [required]=\"isConfigFieldRequired(field.key)\"\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\"\n [hint]=\"field.description ?? ''\"\n [required]=\"isConfigFieldRequired(field.key)\"\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\"\n [hint]=\"field.description ?? ''\"\n [required]=\"isConfigFieldRequired(field.key)\"\n rows=\"5\"\n />\n } @else {\n <mt-text-field\n [ngModel]=\"fieldText(field.key)\"\n (focusin)=\"\n field.expressionEnabled &&\n setExpressionTarget('config:' + field.key)\n \"\n (ngModelChange)=\"onConfigFieldChange(field.key, $event)\"\n [label]=\"field.label\"\n [hint]=\"field.description ?? ''\"\n [required]=\"isConfigFieldRequired(field.key)\"\n />\n }\n </div>\n }\n </div>\n </details>\n }\n </div>\n</div>\n\n<ng-template #credentialSelector>\n @if (activeCredentialProvider(); as provider) {\n <div\n class=\"mb-4 rounded-lg border border-(--p-content-border-color) bg-(--p-content-background) p-4 shadow-[0_1px_2px_rgba(15,23,42,0.05)]\"\n >\n <div class=\"mb-2 text-[12px] font-bold text-(--p-text-color)\">\n Credential\n </div>\n <div class=\"flex flex-wrap items-center gap-2\">\n @if (provider.connectAvailable) {\n <button\n type=\"button\"\n class=\"inline-flex min-h-10 max-w-full items-center gap-2 rounded-md border border-(--p-content-border-color) bg-(--p-content-background) px-3 py-2 text-[13px] font-bold text-(--p-text-color) shadow-sm transition-[border-color,box-shadow,background] hover:border-(--p-primary-color) hover:shadow-[0_2px_7px_rgba(15,23,42,0.10)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--p-primary-color)\"\n [disabled]=\"credentialOauthState() === 'loading'\"\n [attr.aria-busy]=\"\n credentialOauthState() === 'loading' ? 'true' : null\n \"\n (click)=\"connectCredentialProvider()\"\n >\n <fp-automation-node-icon\n class=\"text-[22px]\"\n [itemKey]=\"provider.providerKey\"\n [displayName]=\"provider.displayName\"\n [metadata]=\"credentialProviderIconMetadata(provider)\"\n />\n <span class=\"truncate\">{{ credentialConnectLabel() }}</span>\n </button>\n @if (provider.manualSetupAvailable) {\n <span class=\"text-[12px] text-(--p-text-muted-color)\">or</span>\n <button\n type=\"button\"\n class=\"rounded-md px-1 py-1 text-[12px] font-semibold text-(--p-text-color) underline decoration-(--p-text-muted-color) underline-offset-4 transition-colors hover:text-(--p-primary-color) focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--p-primary-color)\"\n (click)=\"showManualCredentialSetup()\"\n >\n setup manually\n </button>\n }\n } @else if (provider.manualSetupAvailable) {\n <button\n type=\"button\"\n class=\"inline-flex min-h-10 max-w-full items-center gap-2 rounded-md border border-(--p-content-border-color) bg-(--p-content-background) px-3 py-2 text-[13px] font-bold text-(--p-text-color) shadow-sm transition-[border-color,box-shadow,background] hover:border-(--p-primary-color) hover:shadow-[0_2px_7px_rgba(15,23,42,0.10)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--p-primary-color)\"\n (click)=\"showManualCredentialSetup()\"\n >\n <fp-automation-node-icon\n class=\"text-[22px]\"\n [itemKey]=\"provider.providerKey\"\n [displayName]=\"provider.displayName\"\n [metadata]=\"credentialProviderIconMetadata(provider)\"\n />\n <span class=\"truncate\"\n >Set up\n {{ provider.displayName || humanReviewProviderLabel() }}\n credential</span\n >\n </button>\n }\n </div>\n <div\n class=\"mt-2 flex flex-wrap items-center gap-1.5 text-[11px] text-(--p-text-muted-color)\"\n >\n @if (provider.authModes?.length) {\n @for (mode of provider.authModes; track mode) {\n <span\n class=\"rounded-full bg-(--p-surface-100) px-2 py-0.5 font-semibold\"\n >\n {{ mode }}\n </span>\n }\n }\n @if (provider.requiredScopes?.length) {\n <span class=\"truncate\">\n Scopes: {{ provider.requiredScopes.join(\", \") }}\n </span>\n }\n </div>\n @if (provider.setupInstructions) {\n <p class=\"m-0 mt-2 text-[11.5px] leading-5 text-(--p-text-muted-color)\">\n {{ provider.setupInstructions }}\n </p>\n }\n </div>\n }\n\n @if (manualCredentialOpen()) {\n <div\n class=\"mb-3 rounded-md border border-(--p-content-border-color) bg-(--p-content-background) p-3\"\n >\n <div class=\"grid gap-3 md:grid-cols-2\">\n <mt-text-field\n [ngModel]=\"manualCredentialDisplayName()\"\n (ngModelChange)=\"onManualCredentialDisplayNameChange($event)\"\n label=\"Credential name\"\n />\n <mt-select-field\n [ngModel]=\"manualCredentialType()\"\n (ngModelChange)=\"onManualCredentialTypeChange($event)\"\n label=\"Credential type\"\n [options]=\"credentialTypeOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n />\n </div>\n <div class=\"mt-3 grid gap-3 md:grid-cols-2\">\n @for (field of manualCredentialFields(); track field.key) {\n @if (manualCredentialFieldIsToggle(field)) {\n <mt-toggle-field\n [ngModel]=\"manualCredentialFieldValue(field.key) === 'true'\"\n (ngModelChange)=\"onManualCredentialFieldChange(field.key, $event)\"\n [label]=\"\n manualCredentialFieldLabel(field) + (field.required ? ' *' : '')\n \"\n size=\"small\"\n />\n } @else {\n <mt-text-field\n [ngModel]=\"manualCredentialFieldValue(field.key)\"\n (ngModelChange)=\"onManualCredentialFieldChange(field.key, $event)\"\n [label]=\"\n manualCredentialFieldLabel(field) + (field.required ? ' *' : '')\n \"\n [placeholder]=\"field.placeholder ?? ''\"\n [type]=\"manualCredentialFieldInputType(field)\"\n />\n }\n }\n </div>\n @if (credentialManualError(); as error) {\n <div\n class=\"mt-3 rounded-md border border-(--p-content-border-color) bg-(--p-surface-50) px-3 py-2 text-[12px] leading-5 text-(--p-text-muted-color)\"\n >\n {{ error }}\n </div>\n }\n <div class=\"mt-3 flex justify-end gap-2\">\n <mt-button\n size=\"small\"\n variant=\"text\"\n label=\"Cancel\"\n (onClick)=\"cancelManualCredentialSetup()\"\n />\n <mt-button\n size=\"small\"\n severity=\"primary\"\n label=\"Save credential\"\n [loading]=\"credentialManualState() === 'loading'\"\n [disabled]=\"credentialManualState() === 'loading'\"\n (onClick)=\"createManualCredential()\"\n />\n </div>\n </div>\n }\n\n @if (credentialOptions().length) {\n <div class=\"space-y-2\">\n <mt-select-field\n [ngModel]=\"selectedCredentialRef()\"\n (ngModelChange)=\"selectCredential($event)\"\n label=\"Saved credential\"\n [options]=\"credentialOptions()\"\n optionValue=\"value\"\n optionLabel=\"label\"\n [showClear]=\"true\"\n />\n @for (credential of credentials(); track credential.credentialRef) {\n @if (credential.credentialRef === selectedCredentialRef()) {\n <div\n class=\"rounded-md border border-(--p-content-border-color) bg-(--p-surface-50) px-3 py-2\"\n >\n <div class=\"flex items-center justify-between gap-3\">\n <div class=\"min-w-0\">\n <div class=\"truncate text-[12px] font-semibold\">\n {{ credential.displayName ?? credential.credentialRef }}\n </div>\n <div\n class=\"truncate font-mono text-[11px] text-(--p-text-muted-color)\"\n >\n {{ credential.credentialRef }} /\n {{\n credential.status ??\n (credential.resolved ? \"Resolved\" : \"Unresolved\")\n }}\n </div>\n </div>\n <div class=\"flex flex-none items-center gap-2\">\n <mt-button\n size=\"small\"\n variant=\"text\"\n label=\"Test\"\n [loading]=\"\n credentialTestingRef() === credential.credentialRef\n \"\n [disabled]=\"\n credentialTestingRef() === credential.credentialRef\n \"\n (onClick)=\"testCredential(credential.credentialRef)\"\n />\n @if (activeCredentialProvider()?.connectAvailable) {\n <mt-button\n size=\"small\"\n variant=\"text\"\n label=\"Reconnect\"\n [disabled]=\"credentialOauthState() === 'loading'\"\n (onClick)=\"connectCredentialProvider()\"\n />\n }\n <mt-button\n size=\"small\"\n variant=\"text\"\n severity=\"danger\"\n label=\"Revoke\"\n [loading]=\"credentialRevokeRef() === credential.credentialRef\"\n [disabled]=\"\n credentialRevokeRef() === credential.credentialRef\n \"\n (onClick)=\"revokeCredential(credential.credentialRef)\"\n />\n </div>\n </div>\n @if (credential.maskedFields; as maskedFields) {\n <div\n class=\"mt-2 flex flex-wrap gap-1.5 text-[11px] text-(--p-text-muted-color)\"\n >\n @for (field of maskedFields | keyvalue; track field.key) {\n <span\n class=\"rounded-md bg-(--p-content-background) px-2 py-1 font-mono\"\n >\n {{ field.key }}: {{ field.value }}\n </span>\n }\n </div>\n }\n </div>\n }\n }\n </div>\n } @else if (sectionHasAdvancedEmptyState(\"credentials\")) {\n <p class=\"fp-ae-copy\">\n No saved credential exists for this provider. Connect the provider or set\n it up manually before publishing.\n </p>\n }\n @if (credentialTest(); as test) {\n <div class=\"mt-3 rounded-md bg-(--p-surface-100) px-3 py-2 text-[12px]\">\n {{ test.status ?? (test.succeeded ? \"Succeeded\" : \"Failed\") }}\n @if (test.message) {\n <span>- {{ test.message }}</span>\n }\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 let-emptyText=\"emptyText\"\n let-keyHint=\"keyHint\"\n let-valueHint=\"valueHint\"\n let-required=\"required\"\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)=\"\n updateObjectRow(objectKey, row.key, $event, row.value)\n \"\n [label]=\"keyLabel\"\n [hint]=\"\n keyHint || 'Configuration key saved to the backend JSON object.'\n \"\n [required]=\"required === true\"\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]=\"\n valueHint ||\n 'Configuration value. Use expressions when this field should resolve at runtime.'\n \"\n [required]=\"required === true\"\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 } @empty {\n @if (emptyText) {\n <div\n class=\"rounded-md bg-(--p-content-background) px-3 py-2 text-[12px] text-(--p-text-muted-color)\"\n >\n {{ emptyText }}\n </div>\n }\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" }]
|
|
20305
20305
|
}], 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 }] }] } });
|
|
20306
20306
|
function schemaFieldsFrom$1(schema, locale) {
|
|
20307
20307
|
const raw = asRecord$4(schema);
|