@epistola.app/valtimo-plugin 0.13.0 → 0.14.0

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.
@@ -740,6 +740,41 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
740
740
  type: Output
741
741
  }] } });
742
742
 
743
+ /*
744
+ * Copyright 2025 Epistola.
745
+ *
746
+ * Licensed under EUPL, Version 1.2 (the "License");
747
+ * you may not use this file except in compliance with the License.
748
+ * You may obtain a copy of the License at
749
+ *
750
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
751
+ *
752
+ * Unless required by applicable law or agreed to in writing, software
753
+ * distributed under the License is distributed on an "AS IS" basis,
754
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
755
+ * See the License for the specific language governing permissions and
756
+ * limitations under the License.
757
+ *
758
+ * SPDX-License-Identifier: EUPL-1.2
759
+ */
760
+ const BARE_PATH_SEGMENT = /^[A-Za-z_][A-Za-z0-9_]*(\[\d*\])*$/;
761
+ /**
762
+ * Render a JSONata path tail for a Formio-style dotted key.
763
+ *
764
+ * Formio stores component keys through lodash get/set semantics, so dots in a
765
+ * component key are path separators. JSONata backticks should therefore quote
766
+ * only the segment that needs it, not the whole dotted key.
767
+ */
768
+ function renderJsonataPathTail(key) {
769
+ return key
770
+ .split('.')
771
+ .map((segment) => (BARE_PATH_SEGMENT.test(segment) ? segment : '`' + segment + '`'))
772
+ .join('.');
773
+ }
774
+ function renderJsonataPath(variableName, key) {
775
+ return `$${variableName}.${renderJsonataPathTail(key)}`;
776
+ }
777
+
743
778
  /*
744
779
  * Copyright 2025 Epistola.
745
780
  *
@@ -915,10 +950,10 @@ function registerJsonataLanguage(monaco) {
915
950
  suggestions.push({
916
951
  label: field,
917
952
  kind: CompletionItemKind.Field,
918
- // Path-style suggestions (doc/pv: `a.b`, `a[].b`) insert as-is; keys
919
- // with characters invalid in a bare JSONata name (e.g. "pv:motivation")
920
- // are backtick-quoted so they resolve as a single property.
921
- insertText: /[^A-Za-z0-9_.[\]]/.test(field) ? '`' + field + '`' : field,
953
+ // Formio and JSONata both treat dots as path separators. Quote only
954
+ // individual unsafe segments (e.g. `doc:adres`.straat), never the
955
+ // whole dotted key.
956
+ insertText: renderJsonataPathTail(field),
922
957
  detail: `$${fieldMatch[1]} field`,
923
958
  });
924
959
  }
@@ -2141,15 +2176,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
2141
2176
  * SPDX-License-Identifier: EUPL-1.2
2142
2177
  */
2143
2178
  const jsonata$2 = _jsonata.default || _jsonata;
2144
- const BARE_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
2145
2179
  /**
2146
2180
  * Render a `$form` reference for a form-field key. Keys that aren't bare
2147
- * identifiers (e.g. `pv:motivation`) are backtick-quoted so JSONata treats the
2148
- * whole key as a single property name matching the old flat `formData[key]`
2149
- * lookup rather than a nested path traversal.
2181
+ * path segments (e.g. `pv:motivation`) are backtick-quoted per segment. Dots
2182
+ * remain path separators because Formio stores dotted component keys through
2183
+ * lodash get/set semantics.
2150
2184
  */
2151
2185
  function formRef(key) {
2152
- return BARE_IDENTIFIER.test(key) ? `$form.${key}` : '$form.`' + key + '`';
2186
+ return renderJsonataPath('form', key);
2153
2187
  }
2154
2188
  function insert(tree, segments, leaf) {
2155
2189
  let node = tree;
@@ -2254,11 +2288,14 @@ function collectLeaves(entries, prefix, scope, rows) {
2254
2288
  /** Extract the form-field key from a `$form.<key>` path node, or null. */
2255
2289
  function formKeyOf(node) {
2256
2290
  if (node?.type === 'path' &&
2257
- node.steps?.length === 2 &&
2291
+ node.steps?.length >= 2 &&
2258
2292
  node.steps[0].type === 'variable' &&
2259
2293
  node.steps[0].value === 'form' &&
2260
- typeof node.steps[1]?.value === 'string') {
2261
- return node.steps[1].value;
2294
+ node.steps.slice(1).every((step) => typeof step?.value === 'string')) {
2295
+ return node.steps
2296
+ .slice(1)
2297
+ .map((step) => step.value)
2298
+ .join('.');
2262
2299
  }
2263
2300
  return null;
2264
2301
  }
@@ -2446,6 +2483,44 @@ async function computeInputOverrides(mapping, formData) {
2446
2483
  return result;
2447
2484
  }
2448
2485
 
2486
+ /*
2487
+ * Copyright 2025 Epistola.
2488
+ *
2489
+ * Licensed under EUPL, Version 1.2 (the "License");
2490
+ * you may not use this file except in compliance with the License.
2491
+ * You may obtain a copy of the License at
2492
+ *
2493
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
2494
+ *
2495
+ * Unless required by applicable law or agreed to in writing, software
2496
+ * distributed under the License is distributed on an "AS IS" basis,
2497
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2498
+ * See the License for the specific language governing permissions and
2499
+ * limitations under the License.
2500
+ *
2501
+ * SPDX-License-Identifier: EUPL-1.2
2502
+ */
2503
+ const PROCESS_VARIABLE_NAME_PATTERN = /^[A-Za-z0-9]+$/;
2504
+ function isProcessVariableNameValid(value) {
2505
+ return typeof value === 'string' && PROCESS_VARIABLE_NAME_PATTERN.test(value);
2506
+ }
2507
+ function isGenerateDocumentConfigValid(config, options) {
2508
+ const baseComplete = !!(options.selectedCatalogId &&
2509
+ config &&
2510
+ config.templateId &&
2511
+ config.outputFormat &&
2512
+ options.filename &&
2513
+ options.filename.trim() &&
2514
+ isProcessVariableNameValid(config.resultProcessVariable));
2515
+ let variantValid = true;
2516
+ if (options.variantSelectionMode === 'attributes' &&
2517
+ options.variantAttributeEntries &&
2518
+ options.variantAttributeEntries.length > 0) {
2519
+ variantValid = options.variantAttributeEntries.every((entry) => !!entry.key && !!entry.value);
2520
+ }
2521
+ return baseComplete && variantValid;
2522
+ }
2523
+
2449
2524
  /*
2450
2525
  * Copyright 2025 Epistola.
2451
2526
  *
@@ -2526,6 +2601,7 @@ class GenerateDocumentConfigurationComponent {
2526
2601
  editorContextVariables = { doc: [], pv: [], case: [] };
2527
2602
  prefillDataMapping = {};
2528
2603
  validationErrors$ = new BehaviorSubject([]);
2604
+ resultProcessVariableInvalid$ = new BehaviorSubject(false);
2529
2605
  destroy$ = new Subject();
2530
2606
  saveSubscription;
2531
2607
  formValue$ = new BehaviorSubject(null);
@@ -2847,19 +2923,14 @@ class GenerateDocumentConfigurationComponent {
2847
2923
  });
2848
2924
  }
2849
2925
  handleValid(formValue) {
2850
- const filenameProvided = this.filenameExpressionMode
2851
- ? !!this.filenameExpression?.trim()
2852
- : !!this.filenameValue?.trim();
2853
- const baseComplete = !!(this.selectedCatalogId$.getValue() &&
2854
- formValue?.templateId &&
2855
- formValue?.outputFormat &&
2856
- filenameProvided &&
2857
- formValue?.resultProcessVariable);
2858
- let variantValid = true;
2859
- if (this.variantSelectionMode === 'attributes' && this.variantAttributeEntries.length > 0) {
2860
- variantValid = this.variantAttributeEntries.every((e) => !!e.key && !!e.value);
2861
- }
2862
- const valid = baseComplete && variantValid;
2926
+ this.resultProcessVariableInvalid$.next(!!formValue?.resultProcessVariable &&
2927
+ !isProcessVariableNameValid(formValue.resultProcessVariable));
2928
+ const valid = isGenerateDocumentConfigValid(formValue, {
2929
+ selectedCatalogId: this.selectedCatalogId$.getValue(),
2930
+ filename: this.filenameExpressionMode ? this.filenameExpression : this.filenameValue,
2931
+ variantSelectionMode: this.variantSelectionMode,
2932
+ variantAttributeEntries: this.variantAttributeEntries,
2933
+ });
2863
2934
  this.valid$.next(valid);
2864
2935
  this.valid.emit(valid);
2865
2936
  }
@@ -2936,7 +3007,7 @@ class GenerateDocumentConfigurationComponent {
2936
3007
  });
2937
3008
  }
2938
3009
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: GenerateDocumentConfigurationComponent, deps: [{ token: EpistolaPluginService }, { token: i2$2.ProcessLinkStateService }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
2939
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: GenerateDocumentConfigurationComponent, isStandalone: true, selector: "epistola-generate-document-configuration", inputs: { save$: "save$", disabled$: "disabled$", pluginId: "pluginId", prefillConfiguration$: "prefillConfiguration$", selectedPluginConfigurationData$: "selectedPluginConfigurationData$", context$: "context$" }, outputs: { valid: "valid", configuration: "configuration" }, ngImport: i0, template: "<v-form\n data-testid=\"epistola-generate-form\"\n (valueChange)=\"formValueChange($event)\"\n *ngIf=\"{\n disabled: disabled$ | async,\n prefill: prefillConfiguration$ ? (prefillConfiguration$ | async) : null,\n catalogs: catalogs$ | async,\n templates: templates$ | async,\n variants: variants$ | async,\n environments: environments$ | async,\n templateFields: templateFields$ | async,\n selectedCatalogId: selectedCatalogId$ | async,\n selectedTemplateId: selectedTemplateId$ | async,\n validationErrors: validationErrors$ | async,\n } as obs\"\n>\n <div\n *ngIf=\"obs.validationErrors && obs.validationErrors.length > 0\"\n class=\"jsonata-validation-errors\"\n data-testid=\"epistola-generate-validation-errors\"\n >\n <strong>{{ 'jsonataValidationErrorsHeading' | pluginTranslate: pluginId | async }}</strong>\n <ul>\n <li *ngFor=\"let err of obs.validationErrors\">\n <code>{{ err.field }}</code\n >: {{ err.message }}\n </li>\n </ul>\n </div>\n <v-select\n name=\"catalogId\"\n data-testid=\"epistola-generate-catalog-id\"\n [title]=\"'catalogId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'catalogIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"obs.catalogs.data\"\n [defaultSelectionId]=\"obs.prefill?.catalogId\"\n [disabled]=\"obs.disabled || obs.catalogs.loading\"\n [required]=\"true\"\n [loading]=\"obs.catalogs.loading\"\n >\n </v-select>\n <div\n *ngIf=\"obs.catalogs.error\"\n class=\"loading-error\"\n data-testid=\"epistola-generate-catalog-error\"\n >\n {{ obs.catalogs.error }}\n </div>\n\n <v-select\n name=\"templateId\"\n data-testid=\"epistola-generate-template-id\"\n [title]=\"'templateId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'templateIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"obs.templates.data\"\n [defaultSelectionId]=\"obs.prefill?.templateId\"\n [clearSelectionSubject$]=\"clearTemplateId$\"\n [disabled]=\"obs.disabled || obs.templates.loading || !obs.selectedCatalogId\"\n [required]=\"true\"\n [loading]=\"obs.templates.loading\"\n >\n </v-select>\n <div\n *ngIf=\"obs.templates.error\"\n class=\"loading-error\"\n data-testid=\"epistola-generate-template-error\"\n >\n {{ obs.templates.error }}\n </div>\n\n <!-- Variant selection mode toggle -->\n <div\n class=\"variant-mode-toggle\"\n *ngIf=\"obs.selectedTemplateId\"\n data-testid=\"epistola-generate-variant-mode-toggle\"\n >\n <label class=\"variant-mode-label\">{{\n 'variantSelectionMode' | pluginTranslate: pluginId | async\n }}</label>\n <div class=\"variant-mode-buttons\">\n <button\n type=\"button\"\n class=\"variant-mode-btn\"\n data-testid=\"epistola-generate-variant-mode-explicit\"\n [class.active]=\"variantSelectionMode === 'explicit'\"\n (click)=\"onVariantSelectionModeChange('explicit')\"\n [disabled]=\"obs.disabled\"\n >\n {{ 'selectByVariant' | pluginTranslate: pluginId | async }}\n </button>\n <button\n type=\"button\"\n class=\"variant-mode-btn\"\n data-testid=\"epistola-generate-variant-mode-attributes\"\n [class.active]=\"variantSelectionMode === 'attributes'\"\n (click)=\"onVariantSelectionModeChange('attributes')\"\n [disabled]=\"obs.disabled\"\n >\n {{ 'selectByAttributes' | pluginTranslate: pluginId | async }}\n </button>\n </div>\n </div>\n\n <!-- Explicit variant selection (dropdown or expression) -->\n <!--\n The v-select / v-input here is intentionally NOT named into the v-form: v-form's\n @ContentChildren queries default to descendants:false and would skip anything nested\n in this `field-with-fx` wrapper. The value is tracked via (selectedChange)/(valueChange)\n on the component, see variantIdValue / filenameValue.\n -->\n <div *ngIf=\"variantSelectionMode === 'explicit'\" class=\"field-with-fx\">\n <v-select\n *ngIf=\"!variantIdExpressionMode\"\n data-testid=\"epistola-generate-variant-id\"\n [title]=\"'variantId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'variantIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"obs.variants.data\"\n [defaultSelectionId]=\"variantIdValue\"\n [clearSelectionSubject$]=\"clearVariantId$\"\n (selectedChange)=\"onVariantIdValueChange($event)\"\n [disabled]=\"obs.disabled || obs.variants.loading || !obs.selectedTemplateId\"\n [required]=\"false\"\n [loading]=\"obs.variants.loading\"\n >\n </v-select>\n <div *ngIf=\"variantIdExpressionMode\" class=\"fx-input-group\">\n <label class=\"fx-input-label\">{{ 'variantId' | pluginTranslate: pluginId | async }}</label>\n <input\n type=\"text\"\n class=\"fx-input\"\n data-testid=\"epistola-generate-variant-id-expression\"\n [ngModel]=\"variantIdExpression\"\n (ngModelChange)=\"variantIdExpression = $event; onVariantIdExpressionChange()\"\n [disabled]=\"obs.disabled\"\n placeholder=\"$pv.letterType\"\n />\n </div>\n <button\n type=\"button\"\n class=\"fx-toggle\"\n data-testid=\"epistola-generate-variant-id-fx-toggle\"\n (click)=\"toggleVariantIdExpressionMode()\"\n [disabled]=\"obs.disabled\"\n [title]=\"variantIdExpressionMode ? 'Switch to dropdown' : 'Switch to expression'\"\n >\n {{ variantIdExpressionMode ? '\u00B7' : 'fx' }}\n </button>\n </div>\n <div\n *ngIf=\"obs.variants.error\"\n class=\"loading-error\"\n data-testid=\"epistola-generate-variant-error\"\n >\n {{ obs.variants.error }}\n </div>\n\n <!-- Attribute-based variant selection -->\n <div\n *ngIf=\"variantSelectionMode === 'attributes' && obs.selectedTemplateId\"\n class=\"variant-attributes-section\"\n data-testid=\"epistola-generate-variant-attributes\"\n >\n <label class=\"variant-attributes-label\">{{\n 'variantAttributes' | pluginTranslate: pluginId | async\n }}</label>\n <div class=\"variant-attributes-list\">\n <div\n *ngFor=\"let entry of variantAttributeEntries; let i = index\"\n class=\"variant-attribute-row\"\n [attr.data-testid]=\"'epistola-generate-attribute-row-' + i\"\n >\n <select\n *ngIf=\"!entry._customKey\"\n class=\"variant-attribute-input\"\n [attr.data-testid]=\"'epistola-generate-attribute-key-' + i\"\n [ngModel]=\"entry.key\"\n (ngModelChange)=\"onKeySelected(entry, $event)\"\n [disabled]=\"obs.disabled\"\n >\n <option value=\"\" disabled>\n {{ 'attributeKey' | pluginTranslate: pluginId | async }}\n </option>\n <option *ngFor=\"let key of availableAttributeKeys\" [value]=\"key\">{{ key }}</option>\n <option value=\"__custom__\">\n {{ 'attributeKeyCustom' | pluginTranslate: pluginId | async }}\n </option>\n </select>\n <div *ngIf=\"entry._customKey\" class=\"custom-key-input\">\n <input\n type=\"text\"\n class=\"variant-attribute-input\"\n [attr.data-testid]=\"'epistola-generate-attribute-key-custom-' + i\"\n [placeholder]=\"'attributeKey' | pluginTranslate: pluginId | async\"\n [(ngModel)]=\"entry.key\"\n (ngModelChange)=\"onAttributeEntryChange()\"\n [disabled]=\"obs.disabled\"\n />\n <button\n type=\"button\"\n class=\"custom-key-cancel\"\n [attr.data-testid]=\"'epistola-generate-attribute-key-custom-cancel-' + i\"\n (click)=\"cancelCustomKey(entry)\"\n [disabled]=\"obs.disabled\"\n >\n &times;\n </button>\n </div>\n <div class=\"attribute-value-with-fx\">\n <input\n type=\"text\"\n class=\"variant-attribute-input\"\n [attr.data-testid]=\"'epistola-generate-attribute-value-' + i\"\n [class.fx-input]=\"entry._expressionMode\"\n [placeholder]=\"\n entry._expressionMode\n ? '$pv.language'\n : ('attributeValue' | pluginTranslate: pluginId | async)\n \"\n [(ngModel)]=\"entry.value\"\n (ngModelChange)=\"onAttributeEntryChange()\"\n [disabled]=\"obs.disabled\"\n />\n <button\n type=\"button\"\n class=\"fx-toggle fx-toggle--inline\"\n [attr.data-testid]=\"'epistola-generate-attribute-value-fx-toggle-' + i\"\n (click)=\"entry._expressionMode = !entry._expressionMode\"\n [disabled]=\"obs.disabled\"\n [title]=\"entry._expressionMode ? 'Switch to plain value' : 'Switch to expression'\"\n >\n {{ entry._expressionMode ? '\u00B7' : 'fx' }}\n </button>\n </div>\n <label class=\"variant-attribute-required-toggle\">\n <input\n type=\"checkbox\"\n [attr.data-testid]=\"'epistola-generate-attribute-required-' + i\"\n [(ngModel)]=\"entry.required\"\n (ngModelChange)=\"onAttributeEntryChange()\"\n [disabled]=\"obs.disabled\"\n />\n <span class=\"required-label\">{{\n (entry.required ? 'attributeRequired' : 'attributePreferred')\n | pluginTranslate: pluginId\n | async\n }}</span>\n </label>\n <button\n type=\"button\"\n class=\"variant-attribute-remove-btn\"\n [attr.data-testid]=\"'epistola-generate-attribute-remove-' + i\"\n (click)=\"removeAttributeEntry(i)\"\n [disabled]=\"obs.disabled\"\n title=\"{{ 'removeAttribute' | pluginTranslate: pluginId | async }}\"\n >\n &times;\n </button>\n </div>\n </div>\n <button\n type=\"button\"\n class=\"variant-attribute-add-btn\"\n data-testid=\"epistola-generate-attribute-add\"\n (click)=\"addAttributeEntry()\"\n [disabled]=\"obs.disabled\"\n >\n + {{ 'addAttribute' | pluginTranslate: pluginId | async }}\n </button>\n </div>\n\n <v-select\n name=\"environmentId\"\n data-testid=\"epistola-generate-environment-id\"\n [title]=\"'environmentId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'environmentIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"obs.environments.data\"\n [defaultSelectionId]=\"obs.prefill?.environmentId\"\n [disabled]=\"obs.disabled || obs.environments.loading\"\n [required]=\"false\"\n [loading]=\"obs.environments.loading\"\n >\n </v-select>\n <div\n *ngIf=\"obs.environments.error\"\n class=\"loading-error\"\n data-testid=\"epistola-generate-environment-error\"\n >\n {{ obs.environments.error }}\n </div>\n\n <v-select\n name=\"outputFormat\"\n data-testid=\"epistola-generate-output-format\"\n [title]=\"'outputFormat' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'outputFormatTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"outputFormatOptions\"\n [defaultSelectionId]=\"obs.prefill?.outputFormat || 'PDF'\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-select>\n\n <div class=\"field-with-fx\">\n <v-input\n *ngIf=\"!filenameExpressionMode\"\n data-testid=\"epistola-generate-filename\"\n [title]=\"'filename' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'filenameTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"filenameValue\"\n (valueChange)=\"onFilenameValueChange($event)\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n <div *ngIf=\"filenameExpressionMode\" class=\"fx-input-group\">\n <label class=\"fx-input-label\">{{ 'filename' | pluginTranslate: pluginId | async }}</label>\n <input\n type=\"text\"\n class=\"fx-input\"\n data-testid=\"epistola-generate-filename-expression\"\n [ngModel]=\"filenameExpression\"\n (ngModelChange)=\"filenameExpression = $event; onFilenameExpressionChange()\"\n [disabled]=\"obs.disabled\"\n placeholder='\"besluit-\" & $doc.name & \".pdf\"'\n />\n </div>\n <button\n type=\"button\"\n class=\"fx-toggle\"\n data-testid=\"epistola-generate-filename-fx-toggle\"\n (click)=\"toggleFilenameExpressionMode()\"\n [disabled]=\"obs.disabled\"\n [title]=\"filenameExpressionMode ? 'Switch to plain input' : 'Switch to expression'\"\n >\n {{ filenameExpressionMode ? '\u00B7' : 'fx' }}\n </button>\n </div>\n\n <v-input\n name=\"correlationId\"\n data-testid=\"epistola-generate-correlation-id\"\n [title]=\"'correlationId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'correlationIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.correlationId\"\n [disabled]=\"obs.disabled\"\n [required]=\"false\"\n >\n </v-input>\n\n <v-input\n name=\"resultProcessVariable\"\n data-testid=\"epistola-generate-result-process-variable\"\n [title]=\"'resultProcessVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'resultProcessVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.resultProcessVariable\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n</v-form>\n\n<div *ngIf=\"(templateFields$ | async)?.error as templateFieldsError\" class=\"loading-error\">\n {{ templateFieldsError }}\n</div>\n\n<div *ngIf=\"selectedTemplateId$ | async\" class=\"mapping-section\">\n <h5 class=\"mapping-section__title\">\n {{ 'dataMappingTitle' | pluginTranslate: pluginId | async }}\n </h5>\n <p class=\"mapping-section__description\">\n {{ 'dataMappingDescription' | pluginTranslate: pluginId | async }}\n </p>\n <div class=\"mapping-mode-toggle\">\n <button\n class=\"mapping-mode-toggle__btn\"\n data-testid=\"epistola-generate-mapping-mode-simple\"\n [class.mapping-mode-toggle__btn--active]=\"mappingMode === 'simple'\"\n (click)=\"mappingMode = 'simple'\"\n >\n {{ 'mappingModeSimple' | pluginTranslate: pluginId | async }}\n </button>\n <button\n class=\"mapping-mode-toggle__btn\"\n data-testid=\"epistola-generate-mapping-mode-advanced\"\n [class.mapping-mode-toggle__btn--active]=\"mappingMode === 'advanced'\"\n (click)=\"mappingMode = 'advanced'\"\n >\n {{ 'mappingModeAdvanced' | pluginTranslate: pluginId | async }}\n </button>\n </div>\n\n <!-- Editor area (full width) -->\n <epistola-mapping-builder\n *ngIf=\"mappingMode === 'simple'\"\n [expression]=\"dataMapping$ | async\"\n [templateFields]=\"(templateFields$ | async)?.data ?? []\"\n [suggestions]=\"variableSuggestions\"\n [disabled]=\"!!(disabled$ | async)\"\n (expressionChange)=\"onDataMappingChange($event)\"\n ></epistola-mapping-builder>\n\n <epistola-jsonata-editor\n *ngIf=\"mappingMode === 'advanced'\"\n [expression]=\"dataMapping$ | async\"\n [disabled]=\"!!(disabled$ | async)\"\n [contextVariables]=\"editorContextVariables\"\n [functions]=\"expressionFunctions\"\n (expressionChange)=\"onDataMappingChange($event)\"\n ></epistola-jsonata-editor>\n\n <!-- Bottom tabs: Schema + Preview (collapsible) -->\n <div class=\"mapping-tools\" [class.mapping-tools--collapsed]=\"toolsCollapsed\">\n <div\n class=\"mapping-tools__header\"\n data-testid=\"epistola-generate-mapping-tools-header\"\n (click)=\"toolsCollapsed = !toolsCollapsed\"\n >\n <span class=\"mapping-tools__chevron\">{{ toolsCollapsed ? '&#x25B6;' : '&#x25BC;' }}</span>\n <span>{{ 'mappingTools' | pluginTranslate: pluginId | async }}</span>\n </div>\n <div *ngIf=\"!toolsCollapsed\" class=\"mapping-tools__content\">\n <div class=\"mapping-tools__tabs\">\n <button\n class=\"mapping-tools__tab\"\n data-testid=\"epistola-generate-mapping-tab-schema\"\n [class.mapping-tools__tab--active]=\"activeToolTab === 'schema'\"\n (click)=\"activeToolTab = 'schema'\"\n >\n {{ 'expectedStructure' | pluginTranslate: pluginId | async }}\n </button>\n <button\n class=\"mapping-tools__tab\"\n data-testid=\"epistola-generate-mapping-tab-preview\"\n [class.mapping-tools__tab--active]=\"activeToolTab === 'preview'\"\n (click)=\"activeToolTab = 'preview'\"\n >\n {{ 'previewTitle' | pluginTranslate: pluginId | async }}\n </button>\n </div>\n\n <epistola-expected-structure\n *ngIf=\"activeToolTab === 'schema'\"\n [templateFields]=\"(templateFields$ | async)?.data ?? []\"\n ></epistola-expected-structure>\n\n <epistola-mapping-preview\n *ngIf=\"activeToolTab === 'preview'\"\n [expression]=\"dataMapping$ | async\"\n [templateFields]=\"(templateFields$ | async)?.data ?? []\"\n [caseDefinitionKey]=\"caseDefinitionKey\"\n ></epistola-mapping-preview>\n </div>\n </div>\n</div>\n", styles: [".loading-error{padding:.25rem .75rem;font-size:.8125rem;color:#dc3545}.jsonata-validation-errors{margin-bottom:1rem;padding:.75rem 1rem;border:1px solid #dc3545;border-radius:4px;background:#fdf3f4;color:#dc3545;font-size:.875rem}.jsonata-validation-errors ul{margin:.5rem 0 0;padding-left:1.25rem}.jsonata-validation-errors code{background:#dc35451a;padding:0 .25rem;border-radius:2px;font-family:monospace}.validation-summary{margin-top:.5rem;padding:.5rem .75rem;border-radius:4px;font-size:.875rem}.validation-summary .validation-complete{color:#198754}.validation-summary .validation-incomplete{color:#dc3545;font-weight:500}.variant-mode-toggle{margin-bottom:1rem;padding:0 .75rem}.variant-mode-label{display:block;font-size:.875rem;font-weight:500;margin-bottom:.375rem}.variant-mode-buttons{display:flex;gap:0;border:1px solid #d1d5db;border-radius:4px;overflow:hidden;width:fit-content}.variant-mode-btn{padding:.375rem .75rem;font-size:.8125rem;background:#fff;border:none;border-right:1px solid #d1d5db;cursor:pointer;color:#374151;transition:background-color .15s,color .15s}.variant-mode-btn:last-child{border-right:none}.variant-mode-btn:hover:not([disabled]){background:#f3f4f6}.variant-mode-btn.active{background:#2563eb;color:#fff}.variant-mode-btn[disabled]{opacity:.5;cursor:not-allowed}.variant-attributes-section{margin-bottom:1rem;padding:0 .75rem}.variant-attributes-label{display:block;font-size:.875rem;font-weight:500;margin-bottom:.375rem}.variant-attributes-list{display:flex;flex-direction:column;gap:.375rem}.variant-attribute-row{display:flex;gap:.375rem;align-items:center}.variant-attribute-input{flex:1;padding:.375rem .5rem;font-size:.8125rem;border:1px solid #d1d5db;border-radius:4px;outline:none}.variant-attribute-input:focus{border-color:#2563eb;box-shadow:0 0 0 1px #2563eb}.variant-attribute-input[disabled]{opacity:.5;background:#f9fafb}.custom-key-input{display:flex;flex:1;gap:.25rem}.custom-key-input .variant-attribute-input{flex:1}.custom-key-cancel{padding:.25rem .5rem;font-size:1rem;line-height:1;background:none;border:1px solid #d1d5db;border-radius:4px;cursor:pointer;color:#6b7280}.custom-key-cancel:hover:not([disabled]){color:#dc3545;border-color:#dc3545}.custom-key-cancel[disabled]{opacity:.5;cursor:not-allowed}.variant-attribute-required-toggle{display:flex;align-items:center;gap:.25rem;font-size:.75rem;color:#374151;white-space:nowrap;cursor:pointer}.variant-attribute-required-toggle input[type=checkbox]{margin:0;cursor:pointer}.variant-attribute-required-toggle .required-label{-webkit-user-select:none;user-select:none}.variant-attribute-remove-btn{padding:.25rem .5rem;font-size:1rem;line-height:1;background:none;border:1px solid #d1d5db;border-radius:4px;cursor:pointer;color:#6b7280}.variant-attribute-remove-btn:hover:not([disabled]){color:#dc3545;border-color:#dc3545}.variant-attribute-remove-btn[disabled]{opacity:.5;cursor:not-allowed}.variant-attribute-add-btn{margin-top:.375rem;padding:.25rem .5rem;font-size:.8125rem;background:none;border:1px dashed #d1d5db;border-radius:4px;cursor:pointer;color:#6b7280}.variant-attribute-add-btn:hover:not([disabled]){color:#2563eb;border-color:#2563eb}.variant-attribute-add-btn[disabled]{opacity:.5;cursor:not-allowed}.field-with-fx{display:flex;align-items:flex-start;gap:4px}.field-with-fx>*:first-child{flex:1;min-width:0}.fx-toggle{width:28px;height:28px;margin-top:22px;border:1px solid #e0e0e0;border-radius:4px;background:#fff;cursor:pointer;font-family:monospace;font-size:.8em;display:flex;align-items:center;justify-content:center;flex-shrink:0}.fx-toggle:hover{background:#f4f4f4}.fx-toggle--inline{margin-top:0}.fx-input-group{flex:1;min-width:0;margin-bottom:.75rem}.fx-input-label{display:block;font-size:.875rem;margin-bottom:.25rem;color:#525252}.fx-input{width:100%;border:1px solid #8d8d8d;border-radius:0;padding:.4rem .75rem;font-family:monospace;font-size:.875rem;background:#f4f4f4}.attribute-value-with-fx{display:flex;align-items:center;gap:4px;flex:1;min-width:0}.attribute-value-with-fx>input{flex:1;min-width:0}.mapping-section{margin-top:1rem}.mapping-section__title{font-size:1rem;font-weight:600;margin:0 0 4px}.mapping-section__description{font-size:.85em;color:#6f6f6f;margin:0 0 12px}.mapping-mode-toggle{display:flex;gap:0;margin-bottom:12px}.mapping-mode-toggle__btn{padding:6px 16px;border:1px solid #e0e0e0;background:#fff;font-size:.85em;cursor:pointer}.mapping-mode-toggle__btn:first-child{border-radius:4px 0 0 4px}.mapping-mode-toggle__btn:last-child{border-radius:0 4px 4px 0;border-left:none}.mapping-mode-toggle__btn--active{background:#0f62fe;color:#fff;border-color:#0f62fe}.mapping-tools{margin-top:12px;border:1px solid #e0e0e0;border-radius:4px;overflow:hidden}.mapping-tools__header{display:flex;align-items:center;gap:6px;padding:8px 12px;background:#f4f4f4;cursor:pointer;font-size:.85em;font-weight:500;-webkit-user-select:none;user-select:none}.mapping-tools__header:hover{background:#e8e8e8}.mapping-tools__chevron{font-size:.7em}.mapping-tools__content{border-top:1px solid #e0e0e0}.mapping-tools__tabs{display:flex;border-bottom:1px solid #e0e0e0}.mapping-tools__tab{padding:6px 16px;border:none;background:transparent;font-size:.8em;cursor:pointer;border-bottom:2px solid transparent}.mapping-tools__tab--active{border-bottom-color:#0f62fe;font-weight:500}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i3$1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i3$1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i3$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i3$1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i3$1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i3$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: PluginTranslatePipeModule }, { kind: "pipe", type: i2$1.PluginTranslatePipe, name: "pluginTranslate" }, { kind: "ngmodule", type: FormModule }, { kind: "component", type: i3.FormComponent, selector: "v-form", inputs: ["className"], outputs: ["valueChange"] }, { kind: "ngmodule", type: InputModule }, { kind: "component", type: i3.InputComponent, selector: "v-input", inputs: ["name", "type", "title", "titleTranslationKey", "defaultValue", "widthPx", "fullWidth", "margin", "smallMargin", "disabled", "step", "min", "maxLength", "tooltip", "required", "hideNumberSpinBox", "smallLabel", "rows", "clear$", "carbonTheme", "placeholder", "dataTestId", "trim", "presetsTitle", "presetOptions"], outputs: ["valueChange"] }, { kind: "ngmodule", type: SelectModule }, { kind: "component", type: i3.SelectComponent, selector: "v-select", inputs: ["items", "defaultSelection", "defaultSelectionId", "defaultSelectionIds", "disabled", "dropUp", "invalid", "multiple", "margin", "widthInPx", "notFoundText", "clearAllText", "clearText", "clearable", "name", "title", "titleTranslationKey", "clearSelectionSubject$", "tooltip", "required", "loading", "loadingText", "placeholder", "smallMargin", "carbonTheme", "appendInline", "warn", "warnText", "dataTestId"], outputs: ["selectedChange"] }, { kind: "component", type: ExpectedStructureComponent, selector: "epistola-expected-structure", inputs: ["templateFields"] }, { kind: "component", type: JsonataEditorComponent, selector: "epistola-jsonata-editor", inputs: ["expression", "disabled", "contextVariables", "functions", "variablesHint"], outputs: ["expressionChange", "validChange"] }, { kind: "component", type: MappingBuilderComponent, selector: "epistola-mapping-builder", inputs: ["expression", "templateFields", "suggestions", "disabled"], outputs: ["expressionChange"] }, { kind: "component", type: MappingPreviewComponent, selector: "epistola-mapping-preview", inputs: ["expression", "templateFields", "caseDefinitionKey"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3010
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: GenerateDocumentConfigurationComponent, isStandalone: true, selector: "epistola-generate-document-configuration", inputs: { save$: "save$", disabled$: "disabled$", pluginId: "pluginId", prefillConfiguration$: "prefillConfiguration$", selectedPluginConfigurationData$: "selectedPluginConfigurationData$", context$: "context$" }, outputs: { valid: "valid", configuration: "configuration" }, ngImport: i0, template: "<v-form\n data-testid=\"epistola-generate-form\"\n (valueChange)=\"formValueChange($event)\"\n *ngIf=\"{\n disabled: disabled$ | async,\n prefill: prefillConfiguration$ ? (prefillConfiguration$ | async) : null,\n catalogs: catalogs$ | async,\n templates: templates$ | async,\n variants: variants$ | async,\n environments: environments$ | async,\n templateFields: templateFields$ | async,\n selectedCatalogId: selectedCatalogId$ | async,\n selectedTemplateId: selectedTemplateId$ | async,\n validationErrors: validationErrors$ | async,\n resultProcessVariableInvalid: resultProcessVariableInvalid$ | async,\n } as obs\"\n>\n <div\n *ngIf=\"obs.validationErrors && obs.validationErrors.length > 0\"\n class=\"jsonata-validation-errors\"\n data-testid=\"epistola-generate-validation-errors\"\n >\n <strong>{{ 'jsonataValidationErrorsHeading' | pluginTranslate: pluginId | async }}</strong>\n <ul>\n <li *ngFor=\"let err of obs.validationErrors\">\n <code>{{ err.field }}</code\n >: {{ err.message }}\n </li>\n </ul>\n </div>\n <v-select\n name=\"catalogId\"\n data-testid=\"epistola-generate-catalog-id\"\n [title]=\"'catalogId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'catalogIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"obs.catalogs.data\"\n [defaultSelectionId]=\"obs.prefill?.catalogId\"\n [disabled]=\"obs.disabled || obs.catalogs.loading\"\n [required]=\"true\"\n [loading]=\"obs.catalogs.loading\"\n >\n </v-select>\n <div\n *ngIf=\"obs.catalogs.error\"\n class=\"loading-error\"\n data-testid=\"epistola-generate-catalog-error\"\n >\n {{ obs.catalogs.error }}\n </div>\n\n <v-select\n name=\"templateId\"\n data-testid=\"epistola-generate-template-id\"\n [title]=\"'templateId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'templateIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"obs.templates.data\"\n [defaultSelectionId]=\"obs.prefill?.templateId\"\n [clearSelectionSubject$]=\"clearTemplateId$\"\n [disabled]=\"obs.disabled || obs.templates.loading || !obs.selectedCatalogId\"\n [required]=\"true\"\n [loading]=\"obs.templates.loading\"\n >\n </v-select>\n <div\n *ngIf=\"obs.templates.error\"\n class=\"loading-error\"\n data-testid=\"epistola-generate-template-error\"\n >\n {{ obs.templates.error }}\n </div>\n\n <!-- Variant selection mode toggle -->\n <div\n class=\"variant-mode-toggle\"\n *ngIf=\"obs.selectedTemplateId\"\n data-testid=\"epistola-generate-variant-mode-toggle\"\n >\n <label class=\"variant-mode-label\">{{\n 'variantSelectionMode' | pluginTranslate: pluginId | async\n }}</label>\n <div class=\"variant-mode-buttons\">\n <button\n type=\"button\"\n class=\"variant-mode-btn\"\n data-testid=\"epistola-generate-variant-mode-explicit\"\n [class.active]=\"variantSelectionMode === 'explicit'\"\n (click)=\"onVariantSelectionModeChange('explicit')\"\n [disabled]=\"obs.disabled\"\n >\n {{ 'selectByVariant' | pluginTranslate: pluginId | async }}\n </button>\n <button\n type=\"button\"\n class=\"variant-mode-btn\"\n data-testid=\"epistola-generate-variant-mode-attributes\"\n [class.active]=\"variantSelectionMode === 'attributes'\"\n (click)=\"onVariantSelectionModeChange('attributes')\"\n [disabled]=\"obs.disabled\"\n >\n {{ 'selectByAttributes' | pluginTranslate: pluginId | async }}\n </button>\n </div>\n </div>\n\n <!-- Explicit variant selection (dropdown or expression) -->\n <!--\n The v-select / v-input here is intentionally NOT named into the v-form: v-form's\n @ContentChildren queries default to descendants:false and would skip anything nested\n in this `field-with-fx` wrapper. The value is tracked via (selectedChange)/(valueChange)\n on the component, see variantIdValue / filenameValue.\n -->\n <div *ngIf=\"variantSelectionMode === 'explicit'\" class=\"field-with-fx\">\n <v-select\n *ngIf=\"!variantIdExpressionMode\"\n data-testid=\"epistola-generate-variant-id\"\n [title]=\"'variantId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'variantIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"obs.variants.data\"\n [defaultSelectionId]=\"variantIdValue\"\n [clearSelectionSubject$]=\"clearVariantId$\"\n (selectedChange)=\"onVariantIdValueChange($event)\"\n [disabled]=\"obs.disabled || obs.variants.loading || !obs.selectedTemplateId\"\n [required]=\"false\"\n [loading]=\"obs.variants.loading\"\n >\n </v-select>\n <div *ngIf=\"variantIdExpressionMode\" class=\"fx-input-group\">\n <label class=\"fx-input-label\">{{ 'variantId' | pluginTranslate: pluginId | async }}</label>\n <input\n type=\"text\"\n class=\"fx-input\"\n data-testid=\"epistola-generate-variant-id-expression\"\n [ngModel]=\"variantIdExpression\"\n (ngModelChange)=\"variantIdExpression = $event; onVariantIdExpressionChange()\"\n [disabled]=\"obs.disabled\"\n placeholder=\"$pv.letterType\"\n />\n </div>\n <button\n type=\"button\"\n class=\"fx-toggle\"\n data-testid=\"epistola-generate-variant-id-fx-toggle\"\n (click)=\"toggleVariantIdExpressionMode()\"\n [disabled]=\"obs.disabled\"\n [title]=\"variantIdExpressionMode ? 'Switch to dropdown' : 'Switch to expression'\"\n >\n {{ variantIdExpressionMode ? '\u00B7' : 'fx' }}\n </button>\n </div>\n <div\n *ngIf=\"obs.variants.error\"\n class=\"loading-error\"\n data-testid=\"epistola-generate-variant-error\"\n >\n {{ obs.variants.error }}\n </div>\n\n <!-- Attribute-based variant selection -->\n <div\n *ngIf=\"variantSelectionMode === 'attributes' && obs.selectedTemplateId\"\n class=\"variant-attributes-section\"\n data-testid=\"epistola-generate-variant-attributes\"\n >\n <label class=\"variant-attributes-label\">{{\n 'variantAttributes' | pluginTranslate: pluginId | async\n }}</label>\n <div class=\"variant-attributes-list\">\n <div\n *ngFor=\"let entry of variantAttributeEntries; let i = index\"\n class=\"variant-attribute-row\"\n [attr.data-testid]=\"'epistola-generate-attribute-row-' + i\"\n >\n <select\n *ngIf=\"!entry._customKey\"\n class=\"variant-attribute-input\"\n [attr.data-testid]=\"'epistola-generate-attribute-key-' + i\"\n [ngModel]=\"entry.key\"\n (ngModelChange)=\"onKeySelected(entry, $event)\"\n [disabled]=\"obs.disabled\"\n >\n <option value=\"\" disabled>\n {{ 'attributeKey' | pluginTranslate: pluginId | async }}\n </option>\n <option *ngFor=\"let key of availableAttributeKeys\" [value]=\"key\">{{ key }}</option>\n <option value=\"__custom__\">\n {{ 'attributeKeyCustom' | pluginTranslate: pluginId | async }}\n </option>\n </select>\n <div *ngIf=\"entry._customKey\" class=\"custom-key-input\">\n <input\n type=\"text\"\n class=\"variant-attribute-input\"\n [attr.data-testid]=\"'epistola-generate-attribute-key-custom-' + i\"\n [placeholder]=\"'attributeKey' | pluginTranslate: pluginId | async\"\n [(ngModel)]=\"entry.key\"\n (ngModelChange)=\"onAttributeEntryChange()\"\n [disabled]=\"obs.disabled\"\n />\n <button\n type=\"button\"\n class=\"custom-key-cancel\"\n [attr.data-testid]=\"'epistola-generate-attribute-key-custom-cancel-' + i\"\n (click)=\"cancelCustomKey(entry)\"\n [disabled]=\"obs.disabled\"\n >\n &times;\n </button>\n </div>\n <div class=\"attribute-value-with-fx\">\n <input\n type=\"text\"\n class=\"variant-attribute-input\"\n [attr.data-testid]=\"'epistola-generate-attribute-value-' + i\"\n [class.fx-input]=\"entry._expressionMode\"\n [placeholder]=\"\n entry._expressionMode\n ? '$pv.language'\n : ('attributeValue' | pluginTranslate: pluginId | async)\n \"\n [(ngModel)]=\"entry.value\"\n (ngModelChange)=\"onAttributeEntryChange()\"\n [disabled]=\"obs.disabled\"\n />\n <button\n type=\"button\"\n class=\"fx-toggle fx-toggle--inline\"\n [attr.data-testid]=\"'epistola-generate-attribute-value-fx-toggle-' + i\"\n (click)=\"entry._expressionMode = !entry._expressionMode\"\n [disabled]=\"obs.disabled\"\n [title]=\"entry._expressionMode ? 'Switch to plain value' : 'Switch to expression'\"\n >\n {{ entry._expressionMode ? '\u00B7' : 'fx' }}\n </button>\n </div>\n <label class=\"variant-attribute-required-toggle\">\n <input\n type=\"checkbox\"\n [attr.data-testid]=\"'epistola-generate-attribute-required-' + i\"\n [(ngModel)]=\"entry.required\"\n (ngModelChange)=\"onAttributeEntryChange()\"\n [disabled]=\"obs.disabled\"\n />\n <span class=\"required-label\">{{\n (entry.required ? 'attributeRequired' : 'attributePreferred')\n | pluginTranslate: pluginId\n | async\n }}</span>\n </label>\n <button\n type=\"button\"\n class=\"variant-attribute-remove-btn\"\n [attr.data-testid]=\"'epistola-generate-attribute-remove-' + i\"\n (click)=\"removeAttributeEntry(i)\"\n [disabled]=\"obs.disabled\"\n title=\"{{ 'removeAttribute' | pluginTranslate: pluginId | async }}\"\n >\n &times;\n </button>\n </div>\n </div>\n <button\n type=\"button\"\n class=\"variant-attribute-add-btn\"\n data-testid=\"epistola-generate-attribute-add\"\n (click)=\"addAttributeEntry()\"\n [disabled]=\"obs.disabled\"\n >\n + {{ 'addAttribute' | pluginTranslate: pluginId | async }}\n </button>\n </div>\n\n <v-select\n name=\"environmentId\"\n data-testid=\"epistola-generate-environment-id\"\n [title]=\"'environmentId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'environmentIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"obs.environments.data\"\n [defaultSelectionId]=\"obs.prefill?.environmentId\"\n [disabled]=\"obs.disabled || obs.environments.loading\"\n [required]=\"false\"\n [loading]=\"obs.environments.loading\"\n >\n </v-select>\n <div\n *ngIf=\"obs.environments.error\"\n class=\"loading-error\"\n data-testid=\"epistola-generate-environment-error\"\n >\n {{ obs.environments.error }}\n </div>\n\n <v-select\n name=\"outputFormat\"\n data-testid=\"epistola-generate-output-format\"\n [title]=\"'outputFormat' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'outputFormatTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"outputFormatOptions\"\n [defaultSelectionId]=\"obs.prefill?.outputFormat || 'PDF'\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-select>\n\n <div class=\"field-with-fx\">\n <v-input\n *ngIf=\"!filenameExpressionMode\"\n data-testid=\"epistola-generate-filename\"\n [title]=\"'filename' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'filenameTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"filenameValue\"\n (valueChange)=\"onFilenameValueChange($event)\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n <div *ngIf=\"filenameExpressionMode\" class=\"fx-input-group\">\n <label class=\"fx-input-label\">{{ 'filename' | pluginTranslate: pluginId | async }}</label>\n <input\n type=\"text\"\n class=\"fx-input\"\n data-testid=\"epistola-generate-filename-expression\"\n [ngModel]=\"filenameExpression\"\n (ngModelChange)=\"filenameExpression = $event; onFilenameExpressionChange()\"\n [disabled]=\"obs.disabled\"\n placeholder='\"besluit-\" & $doc.name & \".pdf\"'\n />\n </div>\n <button\n type=\"button\"\n class=\"fx-toggle\"\n data-testid=\"epistola-generate-filename-fx-toggle\"\n (click)=\"toggleFilenameExpressionMode()\"\n [disabled]=\"obs.disabled\"\n [title]=\"filenameExpressionMode ? 'Switch to plain input' : 'Switch to expression'\"\n >\n {{ filenameExpressionMode ? '\u00B7' : 'fx' }}\n </button>\n </div>\n\n <v-input\n name=\"correlationId\"\n data-testid=\"epistola-generate-correlation-id\"\n [title]=\"'correlationId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'correlationIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.correlationId\"\n [disabled]=\"obs.disabled\"\n [required]=\"false\"\n >\n </v-input>\n\n <v-input\n name=\"resultProcessVariable\"\n data-testid=\"epistola-generate-result-process-variable\"\n [title]=\"'resultProcessVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'resultProcessVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.resultProcessVariable\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n <div\n *ngIf=\"obs.resultProcessVariableInvalid\"\n class=\"field-validation-error\"\n data-testid=\"epistola-generate-result-process-variable-error\"\n >\n {{ 'resultProcessVariableInvalid' | pluginTranslate: pluginId | async }}\n </div>\n</v-form>\n\n<div *ngIf=\"(templateFields$ | async)?.error as templateFieldsError\" class=\"loading-error\">\n {{ templateFieldsError }}\n</div>\n\n<div *ngIf=\"selectedTemplateId$ | async\" class=\"mapping-section\">\n <h5 class=\"mapping-section__title\">\n {{ 'dataMappingTitle' | pluginTranslate: pluginId | async }}\n </h5>\n <p class=\"mapping-section__description\">\n {{ 'dataMappingDescription' | pluginTranslate: pluginId | async }}\n </p>\n <div class=\"mapping-mode-toggle\">\n <button\n class=\"mapping-mode-toggle__btn\"\n data-testid=\"epistola-generate-mapping-mode-simple\"\n [class.mapping-mode-toggle__btn--active]=\"mappingMode === 'simple'\"\n (click)=\"mappingMode = 'simple'\"\n >\n {{ 'mappingModeSimple' | pluginTranslate: pluginId | async }}\n </button>\n <button\n class=\"mapping-mode-toggle__btn\"\n data-testid=\"epistola-generate-mapping-mode-advanced\"\n [class.mapping-mode-toggle__btn--active]=\"mappingMode === 'advanced'\"\n (click)=\"mappingMode = 'advanced'\"\n >\n {{ 'mappingModeAdvanced' | pluginTranslate: pluginId | async }}\n </button>\n </div>\n\n <!-- Editor area (full width) -->\n <epistola-mapping-builder\n *ngIf=\"mappingMode === 'simple'\"\n [expression]=\"dataMapping$ | async\"\n [templateFields]=\"(templateFields$ | async)?.data ?? []\"\n [suggestions]=\"variableSuggestions\"\n [disabled]=\"!!(disabled$ | async)\"\n (expressionChange)=\"onDataMappingChange($event)\"\n ></epistola-mapping-builder>\n\n <epistola-jsonata-editor\n *ngIf=\"mappingMode === 'advanced'\"\n [expression]=\"dataMapping$ | async\"\n [disabled]=\"!!(disabled$ | async)\"\n [contextVariables]=\"editorContextVariables\"\n [functions]=\"expressionFunctions\"\n (expressionChange)=\"onDataMappingChange($event)\"\n ></epistola-jsonata-editor>\n\n <!-- Bottom tabs: Schema + Preview (collapsible) -->\n <div class=\"mapping-tools\" [class.mapping-tools--collapsed]=\"toolsCollapsed\">\n <div\n class=\"mapping-tools__header\"\n data-testid=\"epistola-generate-mapping-tools-header\"\n (click)=\"toolsCollapsed = !toolsCollapsed\"\n >\n <span class=\"mapping-tools__chevron\">{{ toolsCollapsed ? '&#x25B6;' : '&#x25BC;' }}</span>\n <span>{{ 'mappingTools' | pluginTranslate: pluginId | async }}</span>\n </div>\n <div *ngIf=\"!toolsCollapsed\" class=\"mapping-tools__content\">\n <div class=\"mapping-tools__tabs\">\n <button\n class=\"mapping-tools__tab\"\n data-testid=\"epistola-generate-mapping-tab-schema\"\n [class.mapping-tools__tab--active]=\"activeToolTab === 'schema'\"\n (click)=\"activeToolTab = 'schema'\"\n >\n {{ 'expectedStructure' | pluginTranslate: pluginId | async }}\n </button>\n <button\n class=\"mapping-tools__tab\"\n data-testid=\"epistola-generate-mapping-tab-preview\"\n [class.mapping-tools__tab--active]=\"activeToolTab === 'preview'\"\n (click)=\"activeToolTab = 'preview'\"\n >\n {{ 'previewTitle' | pluginTranslate: pluginId | async }}\n </button>\n </div>\n\n <epistola-expected-structure\n *ngIf=\"activeToolTab === 'schema'\"\n [templateFields]=\"(templateFields$ | async)?.data ?? []\"\n ></epistola-expected-structure>\n\n <epistola-mapping-preview\n *ngIf=\"activeToolTab === 'preview'\"\n [expression]=\"dataMapping$ | async\"\n [templateFields]=\"(templateFields$ | async)?.data ?? []\"\n [caseDefinitionKey]=\"caseDefinitionKey\"\n ></epistola-mapping-preview>\n </div>\n </div>\n</div>\n", styles: [".loading-error{padding:.25rem .75rem;font-size:.8125rem;color:#dc3545}.field-validation-error{margin:-.5rem 0 .75rem;padding:0 .75rem;font-size:.8125rem;color:#dc3545}.jsonata-validation-errors{margin-bottom:1rem;padding:.75rem 1rem;border:1px solid #dc3545;border-radius:4px;background:#fdf3f4;color:#dc3545;font-size:.875rem}.jsonata-validation-errors ul{margin:.5rem 0 0;padding-left:1.25rem}.jsonata-validation-errors code{background:#dc35451a;padding:0 .25rem;border-radius:2px;font-family:monospace}.validation-summary{margin-top:.5rem;padding:.5rem .75rem;border-radius:4px;font-size:.875rem}.validation-summary .validation-complete{color:#198754}.validation-summary .validation-incomplete{color:#dc3545;font-weight:500}.variant-mode-toggle{margin-bottom:1rem;padding:0 .75rem}.variant-mode-label{display:block;font-size:.875rem;font-weight:500;margin-bottom:.375rem}.variant-mode-buttons{display:flex;gap:0;border:1px solid #d1d5db;border-radius:4px;overflow:hidden;width:fit-content}.variant-mode-btn{padding:.375rem .75rem;font-size:.8125rem;background:#fff;border:none;border-right:1px solid #d1d5db;cursor:pointer;color:#374151;transition:background-color .15s,color .15s}.variant-mode-btn:last-child{border-right:none}.variant-mode-btn:hover:not([disabled]){background:#f3f4f6}.variant-mode-btn.active{background:#2563eb;color:#fff}.variant-mode-btn[disabled]{opacity:.5;cursor:not-allowed}.variant-attributes-section{margin-bottom:1rem;padding:0 .75rem}.variant-attributes-label{display:block;font-size:.875rem;font-weight:500;margin-bottom:.375rem}.variant-attributes-list{display:flex;flex-direction:column;gap:.375rem}.variant-attribute-row{display:flex;gap:.375rem;align-items:center}.variant-attribute-input{flex:1;padding:.375rem .5rem;font-size:.8125rem;border:1px solid #d1d5db;border-radius:4px;outline:none}.variant-attribute-input:focus{border-color:#2563eb;box-shadow:0 0 0 1px #2563eb}.variant-attribute-input[disabled]{opacity:.5;background:#f9fafb}.custom-key-input{display:flex;flex:1;gap:.25rem}.custom-key-input .variant-attribute-input{flex:1}.custom-key-cancel{padding:.25rem .5rem;font-size:1rem;line-height:1;background:none;border:1px solid #d1d5db;border-radius:4px;cursor:pointer;color:#6b7280}.custom-key-cancel:hover:not([disabled]){color:#dc3545;border-color:#dc3545}.custom-key-cancel[disabled]{opacity:.5;cursor:not-allowed}.variant-attribute-required-toggle{display:flex;align-items:center;gap:.25rem;font-size:.75rem;color:#374151;white-space:nowrap;cursor:pointer}.variant-attribute-required-toggle input[type=checkbox]{margin:0;cursor:pointer}.variant-attribute-required-toggle .required-label{-webkit-user-select:none;user-select:none}.variant-attribute-remove-btn{padding:.25rem .5rem;font-size:1rem;line-height:1;background:none;border:1px solid #d1d5db;border-radius:4px;cursor:pointer;color:#6b7280}.variant-attribute-remove-btn:hover:not([disabled]){color:#dc3545;border-color:#dc3545}.variant-attribute-remove-btn[disabled]{opacity:.5;cursor:not-allowed}.variant-attribute-add-btn{margin-top:.375rem;padding:.25rem .5rem;font-size:.8125rem;background:none;border:1px dashed #d1d5db;border-radius:4px;cursor:pointer;color:#6b7280}.variant-attribute-add-btn:hover:not([disabled]){color:#2563eb;border-color:#2563eb}.variant-attribute-add-btn[disabled]{opacity:.5;cursor:not-allowed}.field-with-fx{display:flex;align-items:flex-start;gap:4px}.field-with-fx>*:first-child{flex:1;min-width:0}.fx-toggle{width:28px;height:28px;margin-top:22px;border:1px solid #e0e0e0;border-radius:4px;background:#fff;cursor:pointer;font-family:monospace;font-size:.8em;display:flex;align-items:center;justify-content:center;flex-shrink:0}.fx-toggle:hover{background:#f4f4f4}.fx-toggle--inline{margin-top:0}.fx-input-group{flex:1;min-width:0;margin-bottom:.75rem}.fx-input-label{display:block;font-size:.875rem;margin-bottom:.25rem;color:#525252}.fx-input{width:100%;border:1px solid #8d8d8d;border-radius:0;padding:.4rem .75rem;font-family:monospace;font-size:.875rem;background:#f4f4f4}.attribute-value-with-fx{display:flex;align-items:center;gap:4px;flex:1;min-width:0}.attribute-value-with-fx>input{flex:1;min-width:0}.mapping-section{margin-top:1rem}.mapping-section__title{font-size:1rem;font-weight:600;margin:0 0 4px}.mapping-section__description{font-size:.85em;color:#6f6f6f;margin:0 0 12px}.mapping-mode-toggle{display:flex;gap:0;margin-bottom:12px}.mapping-mode-toggle__btn{padding:6px 16px;border:1px solid #e0e0e0;background:#fff;font-size:.85em;cursor:pointer}.mapping-mode-toggle__btn:first-child{border-radius:4px 0 0 4px}.mapping-mode-toggle__btn:last-child{border-radius:0 4px 4px 0;border-left:none}.mapping-mode-toggle__btn--active{background:#0f62fe;color:#fff;border-color:#0f62fe}.mapping-tools{margin-top:12px;border:1px solid #e0e0e0;border-radius:4px;overflow:hidden}.mapping-tools__header{display:flex;align-items:center;gap:6px;padding:8px 12px;background:#f4f4f4;cursor:pointer;font-size:.85em;font-weight:500;-webkit-user-select:none;user-select:none}.mapping-tools__header:hover{background:#e8e8e8}.mapping-tools__chevron{font-size:.7em}.mapping-tools__content{border-top:1px solid #e0e0e0}.mapping-tools__tabs{display:flex;border-bottom:1px solid #e0e0e0}.mapping-tools__tab{padding:6px 16px;border:none;background:transparent;font-size:.8em;cursor:pointer;border-bottom:2px solid transparent}.mapping-tools__tab--active{border-bottom-color:#0f62fe;font-weight:500}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i3$1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i3$1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i3$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i3$1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i3$1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i3$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: PluginTranslatePipeModule }, { kind: "pipe", type: i2$1.PluginTranslatePipe, name: "pluginTranslate" }, { kind: "ngmodule", type: FormModule }, { kind: "component", type: i3.FormComponent, selector: "v-form", inputs: ["className"], outputs: ["valueChange"] }, { kind: "ngmodule", type: InputModule }, { kind: "component", type: i3.InputComponent, selector: "v-input", inputs: ["name", "type", "title", "titleTranslationKey", "defaultValue", "widthPx", "fullWidth", "margin", "smallMargin", "disabled", "step", "min", "maxLength", "tooltip", "required", "hideNumberSpinBox", "smallLabel", "rows", "clear$", "carbonTheme", "placeholder", "dataTestId", "trim", "presetsTitle", "presetOptions"], outputs: ["valueChange"] }, { kind: "ngmodule", type: SelectModule }, { kind: "component", type: i3.SelectComponent, selector: "v-select", inputs: ["items", "defaultSelection", "defaultSelectionId", "defaultSelectionIds", "disabled", "dropUp", "invalid", "multiple", "margin", "widthInPx", "notFoundText", "clearAllText", "clearText", "clearable", "name", "title", "titleTranslationKey", "clearSelectionSubject$", "tooltip", "required", "loading", "loadingText", "placeholder", "smallMargin", "carbonTheme", "appendInline", "warn", "warnText", "dataTestId"], outputs: ["selectedChange"] }, { kind: "component", type: ExpectedStructureComponent, selector: "epistola-expected-structure", inputs: ["templateFields"] }, { kind: "component", type: JsonataEditorComponent, selector: "epistola-jsonata-editor", inputs: ["expression", "disabled", "contextVariables", "functions", "variablesHint"], outputs: ["expressionChange", "validChange"] }, { kind: "component", type: MappingBuilderComponent, selector: "epistola-mapping-builder", inputs: ["expression", "templateFields", "suggestions", "disabled"], outputs: ["expressionChange"] }, { kind: "component", type: MappingPreviewComponent, selector: "epistola-mapping-preview", inputs: ["expression", "templateFields", "caseDefinitionKey"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2940
3011
  }
2941
3012
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: GenerateDocumentConfigurationComponent, decorators: [{
2942
3013
  type: Component,
@@ -2951,7 +3022,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImpo
2951
3022
  JsonataEditorComponent,
2952
3023
  MappingBuilderComponent,
2953
3024
  MappingPreviewComponent,
2954
- ], template: "<v-form\n data-testid=\"epistola-generate-form\"\n (valueChange)=\"formValueChange($event)\"\n *ngIf=\"{\n disabled: disabled$ | async,\n prefill: prefillConfiguration$ ? (prefillConfiguration$ | async) : null,\n catalogs: catalogs$ | async,\n templates: templates$ | async,\n variants: variants$ | async,\n environments: environments$ | async,\n templateFields: templateFields$ | async,\n selectedCatalogId: selectedCatalogId$ | async,\n selectedTemplateId: selectedTemplateId$ | async,\n validationErrors: validationErrors$ | async,\n } as obs\"\n>\n <div\n *ngIf=\"obs.validationErrors && obs.validationErrors.length > 0\"\n class=\"jsonata-validation-errors\"\n data-testid=\"epistola-generate-validation-errors\"\n >\n <strong>{{ 'jsonataValidationErrorsHeading' | pluginTranslate: pluginId | async }}</strong>\n <ul>\n <li *ngFor=\"let err of obs.validationErrors\">\n <code>{{ err.field }}</code\n >: {{ err.message }}\n </li>\n </ul>\n </div>\n <v-select\n name=\"catalogId\"\n data-testid=\"epistola-generate-catalog-id\"\n [title]=\"'catalogId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'catalogIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"obs.catalogs.data\"\n [defaultSelectionId]=\"obs.prefill?.catalogId\"\n [disabled]=\"obs.disabled || obs.catalogs.loading\"\n [required]=\"true\"\n [loading]=\"obs.catalogs.loading\"\n >\n </v-select>\n <div\n *ngIf=\"obs.catalogs.error\"\n class=\"loading-error\"\n data-testid=\"epistola-generate-catalog-error\"\n >\n {{ obs.catalogs.error }}\n </div>\n\n <v-select\n name=\"templateId\"\n data-testid=\"epistola-generate-template-id\"\n [title]=\"'templateId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'templateIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"obs.templates.data\"\n [defaultSelectionId]=\"obs.prefill?.templateId\"\n [clearSelectionSubject$]=\"clearTemplateId$\"\n [disabled]=\"obs.disabled || obs.templates.loading || !obs.selectedCatalogId\"\n [required]=\"true\"\n [loading]=\"obs.templates.loading\"\n >\n </v-select>\n <div\n *ngIf=\"obs.templates.error\"\n class=\"loading-error\"\n data-testid=\"epistola-generate-template-error\"\n >\n {{ obs.templates.error }}\n </div>\n\n <!-- Variant selection mode toggle -->\n <div\n class=\"variant-mode-toggle\"\n *ngIf=\"obs.selectedTemplateId\"\n data-testid=\"epistola-generate-variant-mode-toggle\"\n >\n <label class=\"variant-mode-label\">{{\n 'variantSelectionMode' | pluginTranslate: pluginId | async\n }}</label>\n <div class=\"variant-mode-buttons\">\n <button\n type=\"button\"\n class=\"variant-mode-btn\"\n data-testid=\"epistola-generate-variant-mode-explicit\"\n [class.active]=\"variantSelectionMode === 'explicit'\"\n (click)=\"onVariantSelectionModeChange('explicit')\"\n [disabled]=\"obs.disabled\"\n >\n {{ 'selectByVariant' | pluginTranslate: pluginId | async }}\n </button>\n <button\n type=\"button\"\n class=\"variant-mode-btn\"\n data-testid=\"epistola-generate-variant-mode-attributes\"\n [class.active]=\"variantSelectionMode === 'attributes'\"\n (click)=\"onVariantSelectionModeChange('attributes')\"\n [disabled]=\"obs.disabled\"\n >\n {{ 'selectByAttributes' | pluginTranslate: pluginId | async }}\n </button>\n </div>\n </div>\n\n <!-- Explicit variant selection (dropdown or expression) -->\n <!--\n The v-select / v-input here is intentionally NOT named into the v-form: v-form's\n @ContentChildren queries default to descendants:false and would skip anything nested\n in this `field-with-fx` wrapper. The value is tracked via (selectedChange)/(valueChange)\n on the component, see variantIdValue / filenameValue.\n -->\n <div *ngIf=\"variantSelectionMode === 'explicit'\" class=\"field-with-fx\">\n <v-select\n *ngIf=\"!variantIdExpressionMode\"\n data-testid=\"epistola-generate-variant-id\"\n [title]=\"'variantId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'variantIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"obs.variants.data\"\n [defaultSelectionId]=\"variantIdValue\"\n [clearSelectionSubject$]=\"clearVariantId$\"\n (selectedChange)=\"onVariantIdValueChange($event)\"\n [disabled]=\"obs.disabled || obs.variants.loading || !obs.selectedTemplateId\"\n [required]=\"false\"\n [loading]=\"obs.variants.loading\"\n >\n </v-select>\n <div *ngIf=\"variantIdExpressionMode\" class=\"fx-input-group\">\n <label class=\"fx-input-label\">{{ 'variantId' | pluginTranslate: pluginId | async }}</label>\n <input\n type=\"text\"\n class=\"fx-input\"\n data-testid=\"epistola-generate-variant-id-expression\"\n [ngModel]=\"variantIdExpression\"\n (ngModelChange)=\"variantIdExpression = $event; onVariantIdExpressionChange()\"\n [disabled]=\"obs.disabled\"\n placeholder=\"$pv.letterType\"\n />\n </div>\n <button\n type=\"button\"\n class=\"fx-toggle\"\n data-testid=\"epistola-generate-variant-id-fx-toggle\"\n (click)=\"toggleVariantIdExpressionMode()\"\n [disabled]=\"obs.disabled\"\n [title]=\"variantIdExpressionMode ? 'Switch to dropdown' : 'Switch to expression'\"\n >\n {{ variantIdExpressionMode ? '\u00B7' : 'fx' }}\n </button>\n </div>\n <div\n *ngIf=\"obs.variants.error\"\n class=\"loading-error\"\n data-testid=\"epistola-generate-variant-error\"\n >\n {{ obs.variants.error }}\n </div>\n\n <!-- Attribute-based variant selection -->\n <div\n *ngIf=\"variantSelectionMode === 'attributes' && obs.selectedTemplateId\"\n class=\"variant-attributes-section\"\n data-testid=\"epistola-generate-variant-attributes\"\n >\n <label class=\"variant-attributes-label\">{{\n 'variantAttributes' | pluginTranslate: pluginId | async\n }}</label>\n <div class=\"variant-attributes-list\">\n <div\n *ngFor=\"let entry of variantAttributeEntries; let i = index\"\n class=\"variant-attribute-row\"\n [attr.data-testid]=\"'epistola-generate-attribute-row-' + i\"\n >\n <select\n *ngIf=\"!entry._customKey\"\n class=\"variant-attribute-input\"\n [attr.data-testid]=\"'epistola-generate-attribute-key-' + i\"\n [ngModel]=\"entry.key\"\n (ngModelChange)=\"onKeySelected(entry, $event)\"\n [disabled]=\"obs.disabled\"\n >\n <option value=\"\" disabled>\n {{ 'attributeKey' | pluginTranslate: pluginId | async }}\n </option>\n <option *ngFor=\"let key of availableAttributeKeys\" [value]=\"key\">{{ key }}</option>\n <option value=\"__custom__\">\n {{ 'attributeKeyCustom' | pluginTranslate: pluginId | async }}\n </option>\n </select>\n <div *ngIf=\"entry._customKey\" class=\"custom-key-input\">\n <input\n type=\"text\"\n class=\"variant-attribute-input\"\n [attr.data-testid]=\"'epistola-generate-attribute-key-custom-' + i\"\n [placeholder]=\"'attributeKey' | pluginTranslate: pluginId | async\"\n [(ngModel)]=\"entry.key\"\n (ngModelChange)=\"onAttributeEntryChange()\"\n [disabled]=\"obs.disabled\"\n />\n <button\n type=\"button\"\n class=\"custom-key-cancel\"\n [attr.data-testid]=\"'epistola-generate-attribute-key-custom-cancel-' + i\"\n (click)=\"cancelCustomKey(entry)\"\n [disabled]=\"obs.disabled\"\n >\n &times;\n </button>\n </div>\n <div class=\"attribute-value-with-fx\">\n <input\n type=\"text\"\n class=\"variant-attribute-input\"\n [attr.data-testid]=\"'epistola-generate-attribute-value-' + i\"\n [class.fx-input]=\"entry._expressionMode\"\n [placeholder]=\"\n entry._expressionMode\n ? '$pv.language'\n : ('attributeValue' | pluginTranslate: pluginId | async)\n \"\n [(ngModel)]=\"entry.value\"\n (ngModelChange)=\"onAttributeEntryChange()\"\n [disabled]=\"obs.disabled\"\n />\n <button\n type=\"button\"\n class=\"fx-toggle fx-toggle--inline\"\n [attr.data-testid]=\"'epistola-generate-attribute-value-fx-toggle-' + i\"\n (click)=\"entry._expressionMode = !entry._expressionMode\"\n [disabled]=\"obs.disabled\"\n [title]=\"entry._expressionMode ? 'Switch to plain value' : 'Switch to expression'\"\n >\n {{ entry._expressionMode ? '\u00B7' : 'fx' }}\n </button>\n </div>\n <label class=\"variant-attribute-required-toggle\">\n <input\n type=\"checkbox\"\n [attr.data-testid]=\"'epistola-generate-attribute-required-' + i\"\n [(ngModel)]=\"entry.required\"\n (ngModelChange)=\"onAttributeEntryChange()\"\n [disabled]=\"obs.disabled\"\n />\n <span class=\"required-label\">{{\n (entry.required ? 'attributeRequired' : 'attributePreferred')\n | pluginTranslate: pluginId\n | async\n }}</span>\n </label>\n <button\n type=\"button\"\n class=\"variant-attribute-remove-btn\"\n [attr.data-testid]=\"'epistola-generate-attribute-remove-' + i\"\n (click)=\"removeAttributeEntry(i)\"\n [disabled]=\"obs.disabled\"\n title=\"{{ 'removeAttribute' | pluginTranslate: pluginId | async }}\"\n >\n &times;\n </button>\n </div>\n </div>\n <button\n type=\"button\"\n class=\"variant-attribute-add-btn\"\n data-testid=\"epistola-generate-attribute-add\"\n (click)=\"addAttributeEntry()\"\n [disabled]=\"obs.disabled\"\n >\n + {{ 'addAttribute' | pluginTranslate: pluginId | async }}\n </button>\n </div>\n\n <v-select\n name=\"environmentId\"\n data-testid=\"epistola-generate-environment-id\"\n [title]=\"'environmentId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'environmentIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"obs.environments.data\"\n [defaultSelectionId]=\"obs.prefill?.environmentId\"\n [disabled]=\"obs.disabled || obs.environments.loading\"\n [required]=\"false\"\n [loading]=\"obs.environments.loading\"\n >\n </v-select>\n <div\n *ngIf=\"obs.environments.error\"\n class=\"loading-error\"\n data-testid=\"epistola-generate-environment-error\"\n >\n {{ obs.environments.error }}\n </div>\n\n <v-select\n name=\"outputFormat\"\n data-testid=\"epistola-generate-output-format\"\n [title]=\"'outputFormat' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'outputFormatTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"outputFormatOptions\"\n [defaultSelectionId]=\"obs.prefill?.outputFormat || 'PDF'\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-select>\n\n <div class=\"field-with-fx\">\n <v-input\n *ngIf=\"!filenameExpressionMode\"\n data-testid=\"epistola-generate-filename\"\n [title]=\"'filename' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'filenameTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"filenameValue\"\n (valueChange)=\"onFilenameValueChange($event)\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n <div *ngIf=\"filenameExpressionMode\" class=\"fx-input-group\">\n <label class=\"fx-input-label\">{{ 'filename' | pluginTranslate: pluginId | async }}</label>\n <input\n type=\"text\"\n class=\"fx-input\"\n data-testid=\"epistola-generate-filename-expression\"\n [ngModel]=\"filenameExpression\"\n (ngModelChange)=\"filenameExpression = $event; onFilenameExpressionChange()\"\n [disabled]=\"obs.disabled\"\n placeholder='\"besluit-\" & $doc.name & \".pdf\"'\n />\n </div>\n <button\n type=\"button\"\n class=\"fx-toggle\"\n data-testid=\"epistola-generate-filename-fx-toggle\"\n (click)=\"toggleFilenameExpressionMode()\"\n [disabled]=\"obs.disabled\"\n [title]=\"filenameExpressionMode ? 'Switch to plain input' : 'Switch to expression'\"\n >\n {{ filenameExpressionMode ? '\u00B7' : 'fx' }}\n </button>\n </div>\n\n <v-input\n name=\"correlationId\"\n data-testid=\"epistola-generate-correlation-id\"\n [title]=\"'correlationId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'correlationIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.correlationId\"\n [disabled]=\"obs.disabled\"\n [required]=\"false\"\n >\n </v-input>\n\n <v-input\n name=\"resultProcessVariable\"\n data-testid=\"epistola-generate-result-process-variable\"\n [title]=\"'resultProcessVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'resultProcessVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.resultProcessVariable\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n</v-form>\n\n<div *ngIf=\"(templateFields$ | async)?.error as templateFieldsError\" class=\"loading-error\">\n {{ templateFieldsError }}\n</div>\n\n<div *ngIf=\"selectedTemplateId$ | async\" class=\"mapping-section\">\n <h5 class=\"mapping-section__title\">\n {{ 'dataMappingTitle' | pluginTranslate: pluginId | async }}\n </h5>\n <p class=\"mapping-section__description\">\n {{ 'dataMappingDescription' | pluginTranslate: pluginId | async }}\n </p>\n <div class=\"mapping-mode-toggle\">\n <button\n class=\"mapping-mode-toggle__btn\"\n data-testid=\"epistola-generate-mapping-mode-simple\"\n [class.mapping-mode-toggle__btn--active]=\"mappingMode === 'simple'\"\n (click)=\"mappingMode = 'simple'\"\n >\n {{ 'mappingModeSimple' | pluginTranslate: pluginId | async }}\n </button>\n <button\n class=\"mapping-mode-toggle__btn\"\n data-testid=\"epistola-generate-mapping-mode-advanced\"\n [class.mapping-mode-toggle__btn--active]=\"mappingMode === 'advanced'\"\n (click)=\"mappingMode = 'advanced'\"\n >\n {{ 'mappingModeAdvanced' | pluginTranslate: pluginId | async }}\n </button>\n </div>\n\n <!-- Editor area (full width) -->\n <epistola-mapping-builder\n *ngIf=\"mappingMode === 'simple'\"\n [expression]=\"dataMapping$ | async\"\n [templateFields]=\"(templateFields$ | async)?.data ?? []\"\n [suggestions]=\"variableSuggestions\"\n [disabled]=\"!!(disabled$ | async)\"\n (expressionChange)=\"onDataMappingChange($event)\"\n ></epistola-mapping-builder>\n\n <epistola-jsonata-editor\n *ngIf=\"mappingMode === 'advanced'\"\n [expression]=\"dataMapping$ | async\"\n [disabled]=\"!!(disabled$ | async)\"\n [contextVariables]=\"editorContextVariables\"\n [functions]=\"expressionFunctions\"\n (expressionChange)=\"onDataMappingChange($event)\"\n ></epistola-jsonata-editor>\n\n <!-- Bottom tabs: Schema + Preview (collapsible) -->\n <div class=\"mapping-tools\" [class.mapping-tools--collapsed]=\"toolsCollapsed\">\n <div\n class=\"mapping-tools__header\"\n data-testid=\"epistola-generate-mapping-tools-header\"\n (click)=\"toolsCollapsed = !toolsCollapsed\"\n >\n <span class=\"mapping-tools__chevron\">{{ toolsCollapsed ? '&#x25B6;' : '&#x25BC;' }}</span>\n <span>{{ 'mappingTools' | pluginTranslate: pluginId | async }}</span>\n </div>\n <div *ngIf=\"!toolsCollapsed\" class=\"mapping-tools__content\">\n <div class=\"mapping-tools__tabs\">\n <button\n class=\"mapping-tools__tab\"\n data-testid=\"epistola-generate-mapping-tab-schema\"\n [class.mapping-tools__tab--active]=\"activeToolTab === 'schema'\"\n (click)=\"activeToolTab = 'schema'\"\n >\n {{ 'expectedStructure' | pluginTranslate: pluginId | async }}\n </button>\n <button\n class=\"mapping-tools__tab\"\n data-testid=\"epistola-generate-mapping-tab-preview\"\n [class.mapping-tools__tab--active]=\"activeToolTab === 'preview'\"\n (click)=\"activeToolTab = 'preview'\"\n >\n {{ 'previewTitle' | pluginTranslate: pluginId | async }}\n </button>\n </div>\n\n <epistola-expected-structure\n *ngIf=\"activeToolTab === 'schema'\"\n [templateFields]=\"(templateFields$ | async)?.data ?? []\"\n ></epistola-expected-structure>\n\n <epistola-mapping-preview\n *ngIf=\"activeToolTab === 'preview'\"\n [expression]=\"dataMapping$ | async\"\n [templateFields]=\"(templateFields$ | async)?.data ?? []\"\n [caseDefinitionKey]=\"caseDefinitionKey\"\n ></epistola-mapping-preview>\n </div>\n </div>\n</div>\n", styles: [".loading-error{padding:.25rem .75rem;font-size:.8125rem;color:#dc3545}.jsonata-validation-errors{margin-bottom:1rem;padding:.75rem 1rem;border:1px solid #dc3545;border-radius:4px;background:#fdf3f4;color:#dc3545;font-size:.875rem}.jsonata-validation-errors ul{margin:.5rem 0 0;padding-left:1.25rem}.jsonata-validation-errors code{background:#dc35451a;padding:0 .25rem;border-radius:2px;font-family:monospace}.validation-summary{margin-top:.5rem;padding:.5rem .75rem;border-radius:4px;font-size:.875rem}.validation-summary .validation-complete{color:#198754}.validation-summary .validation-incomplete{color:#dc3545;font-weight:500}.variant-mode-toggle{margin-bottom:1rem;padding:0 .75rem}.variant-mode-label{display:block;font-size:.875rem;font-weight:500;margin-bottom:.375rem}.variant-mode-buttons{display:flex;gap:0;border:1px solid #d1d5db;border-radius:4px;overflow:hidden;width:fit-content}.variant-mode-btn{padding:.375rem .75rem;font-size:.8125rem;background:#fff;border:none;border-right:1px solid #d1d5db;cursor:pointer;color:#374151;transition:background-color .15s,color .15s}.variant-mode-btn:last-child{border-right:none}.variant-mode-btn:hover:not([disabled]){background:#f3f4f6}.variant-mode-btn.active{background:#2563eb;color:#fff}.variant-mode-btn[disabled]{opacity:.5;cursor:not-allowed}.variant-attributes-section{margin-bottom:1rem;padding:0 .75rem}.variant-attributes-label{display:block;font-size:.875rem;font-weight:500;margin-bottom:.375rem}.variant-attributes-list{display:flex;flex-direction:column;gap:.375rem}.variant-attribute-row{display:flex;gap:.375rem;align-items:center}.variant-attribute-input{flex:1;padding:.375rem .5rem;font-size:.8125rem;border:1px solid #d1d5db;border-radius:4px;outline:none}.variant-attribute-input:focus{border-color:#2563eb;box-shadow:0 0 0 1px #2563eb}.variant-attribute-input[disabled]{opacity:.5;background:#f9fafb}.custom-key-input{display:flex;flex:1;gap:.25rem}.custom-key-input .variant-attribute-input{flex:1}.custom-key-cancel{padding:.25rem .5rem;font-size:1rem;line-height:1;background:none;border:1px solid #d1d5db;border-radius:4px;cursor:pointer;color:#6b7280}.custom-key-cancel:hover:not([disabled]){color:#dc3545;border-color:#dc3545}.custom-key-cancel[disabled]{opacity:.5;cursor:not-allowed}.variant-attribute-required-toggle{display:flex;align-items:center;gap:.25rem;font-size:.75rem;color:#374151;white-space:nowrap;cursor:pointer}.variant-attribute-required-toggle input[type=checkbox]{margin:0;cursor:pointer}.variant-attribute-required-toggle .required-label{-webkit-user-select:none;user-select:none}.variant-attribute-remove-btn{padding:.25rem .5rem;font-size:1rem;line-height:1;background:none;border:1px solid #d1d5db;border-radius:4px;cursor:pointer;color:#6b7280}.variant-attribute-remove-btn:hover:not([disabled]){color:#dc3545;border-color:#dc3545}.variant-attribute-remove-btn[disabled]{opacity:.5;cursor:not-allowed}.variant-attribute-add-btn{margin-top:.375rem;padding:.25rem .5rem;font-size:.8125rem;background:none;border:1px dashed #d1d5db;border-radius:4px;cursor:pointer;color:#6b7280}.variant-attribute-add-btn:hover:not([disabled]){color:#2563eb;border-color:#2563eb}.variant-attribute-add-btn[disabled]{opacity:.5;cursor:not-allowed}.field-with-fx{display:flex;align-items:flex-start;gap:4px}.field-with-fx>*:first-child{flex:1;min-width:0}.fx-toggle{width:28px;height:28px;margin-top:22px;border:1px solid #e0e0e0;border-radius:4px;background:#fff;cursor:pointer;font-family:monospace;font-size:.8em;display:flex;align-items:center;justify-content:center;flex-shrink:0}.fx-toggle:hover{background:#f4f4f4}.fx-toggle--inline{margin-top:0}.fx-input-group{flex:1;min-width:0;margin-bottom:.75rem}.fx-input-label{display:block;font-size:.875rem;margin-bottom:.25rem;color:#525252}.fx-input{width:100%;border:1px solid #8d8d8d;border-radius:0;padding:.4rem .75rem;font-family:monospace;font-size:.875rem;background:#f4f4f4}.attribute-value-with-fx{display:flex;align-items:center;gap:4px;flex:1;min-width:0}.attribute-value-with-fx>input{flex:1;min-width:0}.mapping-section{margin-top:1rem}.mapping-section__title{font-size:1rem;font-weight:600;margin:0 0 4px}.mapping-section__description{font-size:.85em;color:#6f6f6f;margin:0 0 12px}.mapping-mode-toggle{display:flex;gap:0;margin-bottom:12px}.mapping-mode-toggle__btn{padding:6px 16px;border:1px solid #e0e0e0;background:#fff;font-size:.85em;cursor:pointer}.mapping-mode-toggle__btn:first-child{border-radius:4px 0 0 4px}.mapping-mode-toggle__btn:last-child{border-radius:0 4px 4px 0;border-left:none}.mapping-mode-toggle__btn--active{background:#0f62fe;color:#fff;border-color:#0f62fe}.mapping-tools{margin-top:12px;border:1px solid #e0e0e0;border-radius:4px;overflow:hidden}.mapping-tools__header{display:flex;align-items:center;gap:6px;padding:8px 12px;background:#f4f4f4;cursor:pointer;font-size:.85em;font-weight:500;-webkit-user-select:none;user-select:none}.mapping-tools__header:hover{background:#e8e8e8}.mapping-tools__chevron{font-size:.7em}.mapping-tools__content{border-top:1px solid #e0e0e0}.mapping-tools__tabs{display:flex;border-bottom:1px solid #e0e0e0}.mapping-tools__tab{padding:6px 16px;border:none;background:transparent;font-size:.8em;cursor:pointer;border-bottom:2px solid transparent}.mapping-tools__tab--active{border-bottom-color:#0f62fe;font-weight:500}\n"] }]
3025
+ ], template: "<v-form\n data-testid=\"epistola-generate-form\"\n (valueChange)=\"formValueChange($event)\"\n *ngIf=\"{\n disabled: disabled$ | async,\n prefill: prefillConfiguration$ ? (prefillConfiguration$ | async) : null,\n catalogs: catalogs$ | async,\n templates: templates$ | async,\n variants: variants$ | async,\n environments: environments$ | async,\n templateFields: templateFields$ | async,\n selectedCatalogId: selectedCatalogId$ | async,\n selectedTemplateId: selectedTemplateId$ | async,\n validationErrors: validationErrors$ | async,\n resultProcessVariableInvalid: resultProcessVariableInvalid$ | async,\n } as obs\"\n>\n <div\n *ngIf=\"obs.validationErrors && obs.validationErrors.length > 0\"\n class=\"jsonata-validation-errors\"\n data-testid=\"epistola-generate-validation-errors\"\n >\n <strong>{{ 'jsonataValidationErrorsHeading' | pluginTranslate: pluginId | async }}</strong>\n <ul>\n <li *ngFor=\"let err of obs.validationErrors\">\n <code>{{ err.field }}</code\n >: {{ err.message }}\n </li>\n </ul>\n </div>\n <v-select\n name=\"catalogId\"\n data-testid=\"epistola-generate-catalog-id\"\n [title]=\"'catalogId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'catalogIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"obs.catalogs.data\"\n [defaultSelectionId]=\"obs.prefill?.catalogId\"\n [disabled]=\"obs.disabled || obs.catalogs.loading\"\n [required]=\"true\"\n [loading]=\"obs.catalogs.loading\"\n >\n </v-select>\n <div\n *ngIf=\"obs.catalogs.error\"\n class=\"loading-error\"\n data-testid=\"epistola-generate-catalog-error\"\n >\n {{ obs.catalogs.error }}\n </div>\n\n <v-select\n name=\"templateId\"\n data-testid=\"epistola-generate-template-id\"\n [title]=\"'templateId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'templateIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"obs.templates.data\"\n [defaultSelectionId]=\"obs.prefill?.templateId\"\n [clearSelectionSubject$]=\"clearTemplateId$\"\n [disabled]=\"obs.disabled || obs.templates.loading || !obs.selectedCatalogId\"\n [required]=\"true\"\n [loading]=\"obs.templates.loading\"\n >\n </v-select>\n <div\n *ngIf=\"obs.templates.error\"\n class=\"loading-error\"\n data-testid=\"epistola-generate-template-error\"\n >\n {{ obs.templates.error }}\n </div>\n\n <!-- Variant selection mode toggle -->\n <div\n class=\"variant-mode-toggle\"\n *ngIf=\"obs.selectedTemplateId\"\n data-testid=\"epistola-generate-variant-mode-toggle\"\n >\n <label class=\"variant-mode-label\">{{\n 'variantSelectionMode' | pluginTranslate: pluginId | async\n }}</label>\n <div class=\"variant-mode-buttons\">\n <button\n type=\"button\"\n class=\"variant-mode-btn\"\n data-testid=\"epistola-generate-variant-mode-explicit\"\n [class.active]=\"variantSelectionMode === 'explicit'\"\n (click)=\"onVariantSelectionModeChange('explicit')\"\n [disabled]=\"obs.disabled\"\n >\n {{ 'selectByVariant' | pluginTranslate: pluginId | async }}\n </button>\n <button\n type=\"button\"\n class=\"variant-mode-btn\"\n data-testid=\"epistola-generate-variant-mode-attributes\"\n [class.active]=\"variantSelectionMode === 'attributes'\"\n (click)=\"onVariantSelectionModeChange('attributes')\"\n [disabled]=\"obs.disabled\"\n >\n {{ 'selectByAttributes' | pluginTranslate: pluginId | async }}\n </button>\n </div>\n </div>\n\n <!-- Explicit variant selection (dropdown or expression) -->\n <!--\n The v-select / v-input here is intentionally NOT named into the v-form: v-form's\n @ContentChildren queries default to descendants:false and would skip anything nested\n in this `field-with-fx` wrapper. The value is tracked via (selectedChange)/(valueChange)\n on the component, see variantIdValue / filenameValue.\n -->\n <div *ngIf=\"variantSelectionMode === 'explicit'\" class=\"field-with-fx\">\n <v-select\n *ngIf=\"!variantIdExpressionMode\"\n data-testid=\"epistola-generate-variant-id\"\n [title]=\"'variantId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'variantIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"obs.variants.data\"\n [defaultSelectionId]=\"variantIdValue\"\n [clearSelectionSubject$]=\"clearVariantId$\"\n (selectedChange)=\"onVariantIdValueChange($event)\"\n [disabled]=\"obs.disabled || obs.variants.loading || !obs.selectedTemplateId\"\n [required]=\"false\"\n [loading]=\"obs.variants.loading\"\n >\n </v-select>\n <div *ngIf=\"variantIdExpressionMode\" class=\"fx-input-group\">\n <label class=\"fx-input-label\">{{ 'variantId' | pluginTranslate: pluginId | async }}</label>\n <input\n type=\"text\"\n class=\"fx-input\"\n data-testid=\"epistola-generate-variant-id-expression\"\n [ngModel]=\"variantIdExpression\"\n (ngModelChange)=\"variantIdExpression = $event; onVariantIdExpressionChange()\"\n [disabled]=\"obs.disabled\"\n placeholder=\"$pv.letterType\"\n />\n </div>\n <button\n type=\"button\"\n class=\"fx-toggle\"\n data-testid=\"epistola-generate-variant-id-fx-toggle\"\n (click)=\"toggleVariantIdExpressionMode()\"\n [disabled]=\"obs.disabled\"\n [title]=\"variantIdExpressionMode ? 'Switch to dropdown' : 'Switch to expression'\"\n >\n {{ variantIdExpressionMode ? '\u00B7' : 'fx' }}\n </button>\n </div>\n <div\n *ngIf=\"obs.variants.error\"\n class=\"loading-error\"\n data-testid=\"epistola-generate-variant-error\"\n >\n {{ obs.variants.error }}\n </div>\n\n <!-- Attribute-based variant selection -->\n <div\n *ngIf=\"variantSelectionMode === 'attributes' && obs.selectedTemplateId\"\n class=\"variant-attributes-section\"\n data-testid=\"epistola-generate-variant-attributes\"\n >\n <label class=\"variant-attributes-label\">{{\n 'variantAttributes' | pluginTranslate: pluginId | async\n }}</label>\n <div class=\"variant-attributes-list\">\n <div\n *ngFor=\"let entry of variantAttributeEntries; let i = index\"\n class=\"variant-attribute-row\"\n [attr.data-testid]=\"'epistola-generate-attribute-row-' + i\"\n >\n <select\n *ngIf=\"!entry._customKey\"\n class=\"variant-attribute-input\"\n [attr.data-testid]=\"'epistola-generate-attribute-key-' + i\"\n [ngModel]=\"entry.key\"\n (ngModelChange)=\"onKeySelected(entry, $event)\"\n [disabled]=\"obs.disabled\"\n >\n <option value=\"\" disabled>\n {{ 'attributeKey' | pluginTranslate: pluginId | async }}\n </option>\n <option *ngFor=\"let key of availableAttributeKeys\" [value]=\"key\">{{ key }}</option>\n <option value=\"__custom__\">\n {{ 'attributeKeyCustom' | pluginTranslate: pluginId | async }}\n </option>\n </select>\n <div *ngIf=\"entry._customKey\" class=\"custom-key-input\">\n <input\n type=\"text\"\n class=\"variant-attribute-input\"\n [attr.data-testid]=\"'epistola-generate-attribute-key-custom-' + i\"\n [placeholder]=\"'attributeKey' | pluginTranslate: pluginId | async\"\n [(ngModel)]=\"entry.key\"\n (ngModelChange)=\"onAttributeEntryChange()\"\n [disabled]=\"obs.disabled\"\n />\n <button\n type=\"button\"\n class=\"custom-key-cancel\"\n [attr.data-testid]=\"'epistola-generate-attribute-key-custom-cancel-' + i\"\n (click)=\"cancelCustomKey(entry)\"\n [disabled]=\"obs.disabled\"\n >\n &times;\n </button>\n </div>\n <div class=\"attribute-value-with-fx\">\n <input\n type=\"text\"\n class=\"variant-attribute-input\"\n [attr.data-testid]=\"'epistola-generate-attribute-value-' + i\"\n [class.fx-input]=\"entry._expressionMode\"\n [placeholder]=\"\n entry._expressionMode\n ? '$pv.language'\n : ('attributeValue' | pluginTranslate: pluginId | async)\n \"\n [(ngModel)]=\"entry.value\"\n (ngModelChange)=\"onAttributeEntryChange()\"\n [disabled]=\"obs.disabled\"\n />\n <button\n type=\"button\"\n class=\"fx-toggle fx-toggle--inline\"\n [attr.data-testid]=\"'epistola-generate-attribute-value-fx-toggle-' + i\"\n (click)=\"entry._expressionMode = !entry._expressionMode\"\n [disabled]=\"obs.disabled\"\n [title]=\"entry._expressionMode ? 'Switch to plain value' : 'Switch to expression'\"\n >\n {{ entry._expressionMode ? '\u00B7' : 'fx' }}\n </button>\n </div>\n <label class=\"variant-attribute-required-toggle\">\n <input\n type=\"checkbox\"\n [attr.data-testid]=\"'epistola-generate-attribute-required-' + i\"\n [(ngModel)]=\"entry.required\"\n (ngModelChange)=\"onAttributeEntryChange()\"\n [disabled]=\"obs.disabled\"\n />\n <span class=\"required-label\">{{\n (entry.required ? 'attributeRequired' : 'attributePreferred')\n | pluginTranslate: pluginId\n | async\n }}</span>\n </label>\n <button\n type=\"button\"\n class=\"variant-attribute-remove-btn\"\n [attr.data-testid]=\"'epistola-generate-attribute-remove-' + i\"\n (click)=\"removeAttributeEntry(i)\"\n [disabled]=\"obs.disabled\"\n title=\"{{ 'removeAttribute' | pluginTranslate: pluginId | async }}\"\n >\n &times;\n </button>\n </div>\n </div>\n <button\n type=\"button\"\n class=\"variant-attribute-add-btn\"\n data-testid=\"epistola-generate-attribute-add\"\n (click)=\"addAttributeEntry()\"\n [disabled]=\"obs.disabled\"\n >\n + {{ 'addAttribute' | pluginTranslate: pluginId | async }}\n </button>\n </div>\n\n <v-select\n name=\"environmentId\"\n data-testid=\"epistola-generate-environment-id\"\n [title]=\"'environmentId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'environmentIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"obs.environments.data\"\n [defaultSelectionId]=\"obs.prefill?.environmentId\"\n [disabled]=\"obs.disabled || obs.environments.loading\"\n [required]=\"false\"\n [loading]=\"obs.environments.loading\"\n >\n </v-select>\n <div\n *ngIf=\"obs.environments.error\"\n class=\"loading-error\"\n data-testid=\"epistola-generate-environment-error\"\n >\n {{ obs.environments.error }}\n </div>\n\n <v-select\n name=\"outputFormat\"\n data-testid=\"epistola-generate-output-format\"\n [title]=\"'outputFormat' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'outputFormatTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [items]=\"outputFormatOptions\"\n [defaultSelectionId]=\"obs.prefill?.outputFormat || 'PDF'\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-select>\n\n <div class=\"field-with-fx\">\n <v-input\n *ngIf=\"!filenameExpressionMode\"\n data-testid=\"epistola-generate-filename\"\n [title]=\"'filename' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'filenameTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"filenameValue\"\n (valueChange)=\"onFilenameValueChange($event)\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n <div *ngIf=\"filenameExpressionMode\" class=\"fx-input-group\">\n <label class=\"fx-input-label\">{{ 'filename' | pluginTranslate: pluginId | async }}</label>\n <input\n type=\"text\"\n class=\"fx-input\"\n data-testid=\"epistola-generate-filename-expression\"\n [ngModel]=\"filenameExpression\"\n (ngModelChange)=\"filenameExpression = $event; onFilenameExpressionChange()\"\n [disabled]=\"obs.disabled\"\n placeholder='\"besluit-\" & $doc.name & \".pdf\"'\n />\n </div>\n <button\n type=\"button\"\n class=\"fx-toggle\"\n data-testid=\"epistola-generate-filename-fx-toggle\"\n (click)=\"toggleFilenameExpressionMode()\"\n [disabled]=\"obs.disabled\"\n [title]=\"filenameExpressionMode ? 'Switch to plain input' : 'Switch to expression'\"\n >\n {{ filenameExpressionMode ? '\u00B7' : 'fx' }}\n </button>\n </div>\n\n <v-input\n name=\"correlationId\"\n data-testid=\"epistola-generate-correlation-id\"\n [title]=\"'correlationId' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'correlationIdTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.correlationId\"\n [disabled]=\"obs.disabled\"\n [required]=\"false\"\n >\n </v-input>\n\n <v-input\n name=\"resultProcessVariable\"\n data-testid=\"epistola-generate-result-process-variable\"\n [title]=\"'resultProcessVariable' | pluginTranslate: pluginId | async\"\n [tooltip]=\"'resultProcessVariableTooltip' | pluginTranslate: pluginId | async\"\n [margin]=\"true\"\n [defaultValue]=\"obs.prefill?.resultProcessVariable\"\n [disabled]=\"obs.disabled\"\n [required]=\"true\"\n >\n </v-input>\n <div\n *ngIf=\"obs.resultProcessVariableInvalid\"\n class=\"field-validation-error\"\n data-testid=\"epistola-generate-result-process-variable-error\"\n >\n {{ 'resultProcessVariableInvalid' | pluginTranslate: pluginId | async }}\n </div>\n</v-form>\n\n<div *ngIf=\"(templateFields$ | async)?.error as templateFieldsError\" class=\"loading-error\">\n {{ templateFieldsError }}\n</div>\n\n<div *ngIf=\"selectedTemplateId$ | async\" class=\"mapping-section\">\n <h5 class=\"mapping-section__title\">\n {{ 'dataMappingTitle' | pluginTranslate: pluginId | async }}\n </h5>\n <p class=\"mapping-section__description\">\n {{ 'dataMappingDescription' | pluginTranslate: pluginId | async }}\n </p>\n <div class=\"mapping-mode-toggle\">\n <button\n class=\"mapping-mode-toggle__btn\"\n data-testid=\"epistola-generate-mapping-mode-simple\"\n [class.mapping-mode-toggle__btn--active]=\"mappingMode === 'simple'\"\n (click)=\"mappingMode = 'simple'\"\n >\n {{ 'mappingModeSimple' | pluginTranslate: pluginId | async }}\n </button>\n <button\n class=\"mapping-mode-toggle__btn\"\n data-testid=\"epistola-generate-mapping-mode-advanced\"\n [class.mapping-mode-toggle__btn--active]=\"mappingMode === 'advanced'\"\n (click)=\"mappingMode = 'advanced'\"\n >\n {{ 'mappingModeAdvanced' | pluginTranslate: pluginId | async }}\n </button>\n </div>\n\n <!-- Editor area (full width) -->\n <epistola-mapping-builder\n *ngIf=\"mappingMode === 'simple'\"\n [expression]=\"dataMapping$ | async\"\n [templateFields]=\"(templateFields$ | async)?.data ?? []\"\n [suggestions]=\"variableSuggestions\"\n [disabled]=\"!!(disabled$ | async)\"\n (expressionChange)=\"onDataMappingChange($event)\"\n ></epistola-mapping-builder>\n\n <epistola-jsonata-editor\n *ngIf=\"mappingMode === 'advanced'\"\n [expression]=\"dataMapping$ | async\"\n [disabled]=\"!!(disabled$ | async)\"\n [contextVariables]=\"editorContextVariables\"\n [functions]=\"expressionFunctions\"\n (expressionChange)=\"onDataMappingChange($event)\"\n ></epistola-jsonata-editor>\n\n <!-- Bottom tabs: Schema + Preview (collapsible) -->\n <div class=\"mapping-tools\" [class.mapping-tools--collapsed]=\"toolsCollapsed\">\n <div\n class=\"mapping-tools__header\"\n data-testid=\"epistola-generate-mapping-tools-header\"\n (click)=\"toolsCollapsed = !toolsCollapsed\"\n >\n <span class=\"mapping-tools__chevron\">{{ toolsCollapsed ? '&#x25B6;' : '&#x25BC;' }}</span>\n <span>{{ 'mappingTools' | pluginTranslate: pluginId | async }}</span>\n </div>\n <div *ngIf=\"!toolsCollapsed\" class=\"mapping-tools__content\">\n <div class=\"mapping-tools__tabs\">\n <button\n class=\"mapping-tools__tab\"\n data-testid=\"epistola-generate-mapping-tab-schema\"\n [class.mapping-tools__tab--active]=\"activeToolTab === 'schema'\"\n (click)=\"activeToolTab = 'schema'\"\n >\n {{ 'expectedStructure' | pluginTranslate: pluginId | async }}\n </button>\n <button\n class=\"mapping-tools__tab\"\n data-testid=\"epistola-generate-mapping-tab-preview\"\n [class.mapping-tools__tab--active]=\"activeToolTab === 'preview'\"\n (click)=\"activeToolTab = 'preview'\"\n >\n {{ 'previewTitle' | pluginTranslate: pluginId | async }}\n </button>\n </div>\n\n <epistola-expected-structure\n *ngIf=\"activeToolTab === 'schema'\"\n [templateFields]=\"(templateFields$ | async)?.data ?? []\"\n ></epistola-expected-structure>\n\n <epistola-mapping-preview\n *ngIf=\"activeToolTab === 'preview'\"\n [expression]=\"dataMapping$ | async\"\n [templateFields]=\"(templateFields$ | async)?.data ?? []\"\n [caseDefinitionKey]=\"caseDefinitionKey\"\n ></epistola-mapping-preview>\n </div>\n </div>\n</div>\n", styles: [".loading-error{padding:.25rem .75rem;font-size:.8125rem;color:#dc3545}.field-validation-error{margin:-.5rem 0 .75rem;padding:0 .75rem;font-size:.8125rem;color:#dc3545}.jsonata-validation-errors{margin-bottom:1rem;padding:.75rem 1rem;border:1px solid #dc3545;border-radius:4px;background:#fdf3f4;color:#dc3545;font-size:.875rem}.jsonata-validation-errors ul{margin:.5rem 0 0;padding-left:1.25rem}.jsonata-validation-errors code{background:#dc35451a;padding:0 .25rem;border-radius:2px;font-family:monospace}.validation-summary{margin-top:.5rem;padding:.5rem .75rem;border-radius:4px;font-size:.875rem}.validation-summary .validation-complete{color:#198754}.validation-summary .validation-incomplete{color:#dc3545;font-weight:500}.variant-mode-toggle{margin-bottom:1rem;padding:0 .75rem}.variant-mode-label{display:block;font-size:.875rem;font-weight:500;margin-bottom:.375rem}.variant-mode-buttons{display:flex;gap:0;border:1px solid #d1d5db;border-radius:4px;overflow:hidden;width:fit-content}.variant-mode-btn{padding:.375rem .75rem;font-size:.8125rem;background:#fff;border:none;border-right:1px solid #d1d5db;cursor:pointer;color:#374151;transition:background-color .15s,color .15s}.variant-mode-btn:last-child{border-right:none}.variant-mode-btn:hover:not([disabled]){background:#f3f4f6}.variant-mode-btn.active{background:#2563eb;color:#fff}.variant-mode-btn[disabled]{opacity:.5;cursor:not-allowed}.variant-attributes-section{margin-bottom:1rem;padding:0 .75rem}.variant-attributes-label{display:block;font-size:.875rem;font-weight:500;margin-bottom:.375rem}.variant-attributes-list{display:flex;flex-direction:column;gap:.375rem}.variant-attribute-row{display:flex;gap:.375rem;align-items:center}.variant-attribute-input{flex:1;padding:.375rem .5rem;font-size:.8125rem;border:1px solid #d1d5db;border-radius:4px;outline:none}.variant-attribute-input:focus{border-color:#2563eb;box-shadow:0 0 0 1px #2563eb}.variant-attribute-input[disabled]{opacity:.5;background:#f9fafb}.custom-key-input{display:flex;flex:1;gap:.25rem}.custom-key-input .variant-attribute-input{flex:1}.custom-key-cancel{padding:.25rem .5rem;font-size:1rem;line-height:1;background:none;border:1px solid #d1d5db;border-radius:4px;cursor:pointer;color:#6b7280}.custom-key-cancel:hover:not([disabled]){color:#dc3545;border-color:#dc3545}.custom-key-cancel[disabled]{opacity:.5;cursor:not-allowed}.variant-attribute-required-toggle{display:flex;align-items:center;gap:.25rem;font-size:.75rem;color:#374151;white-space:nowrap;cursor:pointer}.variant-attribute-required-toggle input[type=checkbox]{margin:0;cursor:pointer}.variant-attribute-required-toggle .required-label{-webkit-user-select:none;user-select:none}.variant-attribute-remove-btn{padding:.25rem .5rem;font-size:1rem;line-height:1;background:none;border:1px solid #d1d5db;border-radius:4px;cursor:pointer;color:#6b7280}.variant-attribute-remove-btn:hover:not([disabled]){color:#dc3545;border-color:#dc3545}.variant-attribute-remove-btn[disabled]{opacity:.5;cursor:not-allowed}.variant-attribute-add-btn{margin-top:.375rem;padding:.25rem .5rem;font-size:.8125rem;background:none;border:1px dashed #d1d5db;border-radius:4px;cursor:pointer;color:#6b7280}.variant-attribute-add-btn:hover:not([disabled]){color:#2563eb;border-color:#2563eb}.variant-attribute-add-btn[disabled]{opacity:.5;cursor:not-allowed}.field-with-fx{display:flex;align-items:flex-start;gap:4px}.field-with-fx>*:first-child{flex:1;min-width:0}.fx-toggle{width:28px;height:28px;margin-top:22px;border:1px solid #e0e0e0;border-radius:4px;background:#fff;cursor:pointer;font-family:monospace;font-size:.8em;display:flex;align-items:center;justify-content:center;flex-shrink:0}.fx-toggle:hover{background:#f4f4f4}.fx-toggle--inline{margin-top:0}.fx-input-group{flex:1;min-width:0;margin-bottom:.75rem}.fx-input-label{display:block;font-size:.875rem;margin-bottom:.25rem;color:#525252}.fx-input{width:100%;border:1px solid #8d8d8d;border-radius:0;padding:.4rem .75rem;font-family:monospace;font-size:.875rem;background:#f4f4f4}.attribute-value-with-fx{display:flex;align-items:center;gap:4px;flex:1;min-width:0}.attribute-value-with-fx>input{flex:1;min-width:0}.mapping-section{margin-top:1rem}.mapping-section__title{font-size:1rem;font-weight:600;margin:0 0 4px}.mapping-section__description{font-size:.85em;color:#6f6f6f;margin:0 0 12px}.mapping-mode-toggle{display:flex;gap:0;margin-bottom:12px}.mapping-mode-toggle__btn{padding:6px 16px;border:1px solid #e0e0e0;background:#fff;font-size:.85em;cursor:pointer}.mapping-mode-toggle__btn:first-child{border-radius:4px 0 0 4px}.mapping-mode-toggle__btn:last-child{border-radius:0 4px 4px 0;border-left:none}.mapping-mode-toggle__btn--active{background:#0f62fe;color:#fff;border-color:#0f62fe}.mapping-tools{margin-top:12px;border:1px solid #e0e0e0;border-radius:4px;overflow:hidden}.mapping-tools__header{display:flex;align-items:center;gap:6px;padding:8px 12px;background:#f4f4f4;cursor:pointer;font-size:.85em;font-weight:500;-webkit-user-select:none;user-select:none}.mapping-tools__header:hover{background:#e8e8e8}.mapping-tools__chevron{font-size:.7em}.mapping-tools__content{border-top:1px solid #e0e0e0}.mapping-tools__tabs{display:flex;border-bottom:1px solid #e0e0e0}.mapping-tools__tab{padding:6px 16px;border:none;background:transparent;font-size:.8em;cursor:pointer;border-bottom:2px solid transparent}.mapping-tools__tab--active{border-bottom-color:#0f62fe;font-weight:500}\n"] }]
2955
3026
  }], ctorParameters: () => [{ type: EpistolaPluginService }, { type: i2$2.ProcessLinkStateService }, { type: i0.ChangeDetectorRef }], propDecorators: { save$: [{
2956
3027
  type: Input
2957
3028
  }], disabled$: [{
@@ -4357,6 +4428,7 @@ class EpistolaAdminPageComponent {
4357
4428
  overviewTab = 'configurations';
4358
4429
  loading = false;
4359
4430
  pluginVersion = null;
4431
+ pluginContractVersion = null;
4360
4432
  changelog = null;
4361
4433
  changelogLoading = false;
4362
4434
  validationReport = null;
@@ -4418,6 +4490,45 @@ class EpistolaAdminPageComponent {
4418
4490
  this.updateUrl(card.configurationId, this.activeTab);
4419
4491
  this.loadCatalogs(card.configurationId);
4420
4492
  }
4493
+ hasContractCompatibilityWarning(card) {
4494
+ return (card.contractCompatibilitySeverity === 'WARNING' ||
4495
+ card.contractCompatibilitySeverity === 'ERROR' ||
4496
+ card.contractCompatibilitySeverity === 'UNKNOWN');
4497
+ }
4498
+ contractBadgeClass(card) {
4499
+ if (card.contractCompatibilitySeverity === 'ERROR') {
4500
+ return 'contract-compatibility--error';
4501
+ }
4502
+ if (card.contractCompatibilitySeverity === 'UNKNOWN') {
4503
+ return 'contract-compatibility--unknown';
4504
+ }
4505
+ return 'contract-compatibility--warning';
4506
+ }
4507
+ contractLabelKey(card) {
4508
+ if (card.contractCompatibilitySeverity === 'ERROR') {
4509
+ return 'epistolaAdminContractError';
4510
+ }
4511
+ if (card.contractCompatibilitySeverity === 'UNKNOWN') {
4512
+ return 'epistolaAdminContractUnknown';
4513
+ }
4514
+ return 'epistolaAdminContractWarning';
4515
+ }
4516
+ contractBodyKey(card) {
4517
+ if (card.contractCompatibilitySeverity === 'ERROR') {
4518
+ return 'epistolaAdminContractErrorBody';
4519
+ }
4520
+ if (card.contractCompatibilitySeverity === 'UNKNOWN') {
4521
+ return 'epistolaAdminContractUnknownBody';
4522
+ }
4523
+ return 'epistolaAdminContractWarningBody';
4524
+ }
4525
+ versionWithContract(version, contractVersion) {
4526
+ const displayVersion = version || '—';
4527
+ if (!contractVersion) {
4528
+ return displayVersion;
4529
+ }
4530
+ return `${displayVersion} (contract ${contractVersion})`;
4531
+ }
4421
4532
  backToOverview() {
4422
4533
  this.selectedCard = null;
4423
4534
  this.activeTab = 'actions';
@@ -4657,7 +4768,7 @@ class EpistolaAdminPageComponent {
4657
4768
  return this.redeployingSlugs.has(catalog.slug);
4658
4769
  }
4659
4770
  updateUrl(configurationId, tab) {
4660
- this.router.navigate([], {
4771
+ void this.router.navigate([], {
4661
4772
  relativeTo: this.route,
4662
4773
  queryParams: {
4663
4774
  configurationId: configurationId ?? null,
@@ -4734,12 +4845,17 @@ class EpistolaAdminPageComponent {
4734
4845
  latencyMs: status.latencyMs,
4735
4846
  errorMessage: status.errorMessage,
4736
4847
  serverVersion: status.serverVersion,
4848
+ contractVersion: status.contractVersion,
4849
+ serverContractVersion: status.serverContractVersion,
4850
+ contractCompatibilitySeverity: status.contractCompatibilitySeverity,
4737
4851
  usageCount: entries.length,
4738
4852
  problemCount,
4739
4853
  usageEntries: entries,
4740
4854
  pendingJobs: jobs,
4741
4855
  };
4742
4856
  });
4857
+ this.pluginContractVersion =
4858
+ this.cards.find((card) => card.contractVersion)?.contractVersion ?? null;
4743
4859
  // Restore deep link selection
4744
4860
  if (this.deepLinkConfigId) {
4745
4861
  const match = this.cards.find((c) => c.configurationId === this.deepLinkConfigId);
@@ -4759,11 +4875,11 @@ class EpistolaAdminPageComponent {
4759
4875
  });
4760
4876
  }
4761
4877
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaAdminPageComponent, deps: [{ token: EpistolaAdminService }, { token: i2$4.ActivatedRoute }, { token: i2$4.Router }], target: i0.ɵɵFactoryTarget.Component });
4762
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: EpistolaAdminPageComponent, isStandalone: true, selector: "epistola-admin-page", ngImport: i0, template: "<div class=\"epistola-admin\" data-testid=\"epistola-admin\">\n <!-- Overview: tabs (no configuration selected) -->\n <ng-container *ngIf=\"!selectedCard\">\n <div class=\"d-flex justify-content-between align-items-center mb-3\">\n <div class=\"d-flex align-items-center\">\n <h5 class=\"mb-0\">{{ 'epistolaAdminOverview' | pluginTranslate: 'epistola' | async }}</h5>\n <span *ngIf=\"pluginVersion\" class=\"version-badge ms-2\">v{{ pluginVersion }}</span>\n </div>\n <button\n class=\"btn btn-outline-primary btn-sm\"\n data-testid=\"epistola-admin-refresh\"\n (click)=\"refresh()\"\n [disabled]=\"loading\"\n >\n {{ 'epistolaAdminRefresh' | pluginTranslate: 'epistola' | async }}\n </button>\n </div>\n\n <ng-template #configurationsHeading>\n {{ 'epistolaAdminConfigurations' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" type=\"gray\" class=\"ms-1\">{{ cards.length }}</cds-tag>\n </ng-template>\n\n <ng-template #validationsHeading>\n {{ 'epistolaAdminValidations' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" [type]=\"validationViolations.length > 0 ? 'red' : 'gray'\" class=\"ms-1\">\n {{ validationViolations.length }}\n </cds-tag>\n </ng-template>\n\n <ng-template #changelogHeading>\n {{ 'epistolaAdminChangelog' | pluginTranslate: 'epistola' | async }}\n </ng-template>\n\n <!-- TEMPORARY (removed in 1.0.0): forms missing the task-id carrier -->\n <ng-template #formsHeading>\n {{ 'epistolaAdminForms' | pluginTranslate: 'epistola' | async }}\n <cds-tag\n *ngIf=\"formsScanLoaded\"\n size=\"sm\"\n [type]=\"formsAttentionCount > 0 ? 'red' : 'gray'\"\n class=\"ms-1\"\n >{{ formsAttentionCount }}</cds-tag\n >\n </ng-template>\n\n <cds-tabs [cacheActive]=\"true\" type=\"contained\" data-testid=\"epistola-admin-overview-tabs\">\n <cds-tab\n [heading]=\"configurationsHeading\"\n data-testid=\"epistola-admin-tab-configurations\"\n [active]=\"overviewTab === 'configurations'\"\n (selected)=\"setOverviewTab('configurations')\"\n >\n <div\n *ngIf=\"loading\"\n class=\"text-muted mt-3\"\n data-testid=\"epistola-admin-configurations-loading\"\n >\n {{ 'epistolaAdminLoading' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div\n *ngIf=\"!loading && cards.length === 0\"\n class=\"text-muted mt-3\"\n data-testid=\"epistola-admin-configurations-empty\"\n >\n {{ 'epistolaAdminNoConfigurations' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div\n *ngIf=\"!loading && cards.length > 0\"\n class=\"card-grid\"\n data-testid=\"epistola-admin-card-grid\"\n >\n <div\n *ngFor=\"let card of cards\"\n class=\"config-card\"\n [attr.data-testid]=\"'epistola-admin-config-card-' + card.tenantId\"\n [class.config-card--ok]=\"card.reachable && card.problemCount === 0\"\n [class.config-card--warning]=\"card.reachable && card.problemCount > 0\"\n [class.config-card--error]=\"!card.reachable\"\n (click)=\"selectConfiguration(card)\"\n >\n <div class=\"config-card__header\">\n <span\n class=\"status-dot\"\n [class.status-dot--ok]=\"card.reachable\"\n [class.status-dot--error]=\"!card.reachable\"\n >\n </span>\n <h5 class=\"config-card__title\">{{ card.configurationTitle }}</h5>\n </div>\n\n <div class=\"config-card__body\">\n <div class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminTenantId' | pluginTranslate: 'epistola' | async\n }}</span>\n <code class=\"config-card__value\">{{ card.tenantId }}</code>\n </div>\n <div class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminStatus' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" [type]=\"card.reachable ? 'green' : 'red'\">\n {{\n card.reachable\n ? ('epistolaAdminConnected' | pluginTranslate: 'epistola' | async)\n : ('epistolaAdminUnreachable' | pluginTranslate: 'epistola' | async)\n }}\n </cds-tag>\n </div>\n <div *ngIf=\"card.serverVersion\" class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminServerVersion' | pluginTranslate: 'epistola' | async\n }}</span>\n <span class=\"config-card__value\">{{ card.serverVersion }}</span>\n </div>\n <div class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminPluginActions' | pluginTranslate: 'epistola' | async\n }}</span>\n <span class=\"config-card__value\">{{ card.usageCount }}</span>\n </div>\n <div *ngIf=\"card.pendingJobs.length > 0\" class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminPendingJobs' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" type=\"blue\">{{ card.pendingJobs.length }}</cds-tag>\n </div>\n <div *ngIf=\"card.problemCount > 0\" class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminProblems' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" type=\"red\">{{ card.problemCount }}</cds-tag>\n </div>\n </div>\n\n <div class=\"config-card__footer\">\n <span class=\"config-card__latency\">{{ card.latencyMs }} ms</span>\n </div>\n </div>\n </div>\n </cds-tab>\n\n <cds-tab\n [heading]=\"validationsHeading\"\n data-testid=\"epistola-admin-tab-validations\"\n [active]=\"overviewTab === 'validations'\"\n (selected)=\"setOverviewTab('validations')\"\n >\n <div class=\"text-muted small mt-3\">\n <div>\n {{ 'epistolaAdminValidationLastChecked' | pluginTranslate: 'epistola' | async }}:\n <ng-container *ngIf=\"validationReport?.lastCheckedAt; else notYetRun\">\n {{ validationReport?.lastCheckedAt | date: 'medium' }}\n </ng-container>\n <ng-template #notYetRun>\n {{ 'epistolaAdminValidationNotYetRun' | pluginTranslate: 'epistola' | async }}\n </ng-template>\n \u00B7 {{ 'epistolaAdminValidationAutoRefresh' | pluginTranslate: 'epistola' | async }}\n {{ refreshIntervalMinutes }} min.\n </div>\n <div>\n {{ 'epistolaAdminValidationLatestVersionNote' | pluginTranslate: 'epistola' | async }}\n </div>\n </div>\n\n <div *ngIf=\"validationViolations.length === 0\" class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminNoValidations' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"validationViolations.length > 0\" class=\"mt-3\">\n <p class=\"text-muted mb-3\">\n {{ 'epistolaAdminValidationWarningBody' | pluginTranslate: 'epistola' | async }}\n </p>\n <table class=\"table table-striped\" data-testid=\"epistola-admin-validations-table\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminProcess' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminActivity' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminValidationCode' | pluginTranslate: 'epistola' | async }}</th>\n <th>\n {{ 'epistolaAdminValidationMessage' | pluginTranslate: 'epistola' | async }}\n </th>\n </tr>\n </thead>\n <tbody>\n <tr\n *ngFor=\"let v of validationViolations; let i = index\"\n [attr.data-testid]=\"'epistola-admin-validation-row-' + i\"\n >\n <td>\n {{ v.processDefinitionName || v.processDefinitionKey }}\n <div *ngIf=\"v.processDefinitionName\" class=\"text-muted small\">\n <code>{{ v.processDefinitionKey }}</code>\n </div>\n </td>\n <td>\n <code>{{ v.activityId }}</code>\n </td>\n <td>\n <cds-tag size=\"sm\" type=\"red\">{{ v.code }}</cds-tag>\n </td>\n <td>{{ v.message }}</td>\n </tr>\n </tbody>\n </table>\n </div>\n </cds-tab>\n\n <cds-tab\n [heading]=\"changelogHeading\"\n data-testid=\"epistola-admin-tab-changelog\"\n [active]=\"overviewTab === 'changelog'\"\n (selected)=\"setOverviewTab('changelog')\"\n >\n <div class=\"d-flex align-items-center mt-3 mb-3\">\n <span class=\"text-muted me-2\">{{\n 'epistolaAdminRunningVersion' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" type=\"blue\">v{{ pluginVersion || '\u2014' }}</cds-tag>\n </div>\n\n <div *ngIf=\"changelogLoading\" class=\"text-muted mb-3\">\n {{ 'epistolaAdminLoading' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div\n *ngIf=\"!changelogLoading && changelog && changelog.length === 0\"\n class=\"text-muted mb-3\"\n >\n {{ 'epistolaAdminNoChangelog' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div\n *ngIf=\"!changelogLoading && changelog && changelog.length > 0\"\n class=\"epistola-changelog\"\n >\n <section *ngFor=\"let release of changelog\" class=\"changelog-release\">\n <div class=\"changelog-release__header\">\n <cds-tag size=\"sm\" type=\"purple\">{{ release.version }}</cds-tag>\n <span *ngIf=\"release.date\" class=\"text-muted ms-2\">{{ release.date }}</span>\n </div>\n <div *ngFor=\"let section of release.sections\" class=\"changelog-section\">\n <h6 class=\"changelog-section__title\">{{ section.title }}</h6>\n <ul class=\"changelog-section__items\">\n <li *ngFor=\"let item of section.items\">{{ item }}</li>\n </ul>\n </div>\n </section>\n </div>\n </cds-tab>\n\n <!-- TEMPORARY (removed in 1.0.0): forms missing the task-id carrier -->\n <cds-tab\n [heading]=\"formsHeading\"\n data-testid=\"epistola-admin-tab-forms\"\n [active]=\"overviewTab === 'forms'\"\n (selected)=\"setOverviewTab('forms')\"\n >\n <p class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminFormsIntro' | pluginTranslate: 'epistola' | async }}\n </p>\n\n <div *ngIf=\"formIssuesLoading\" class=\"text-muted mb-3\">\n {{ 'epistolaAdminLoading' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div\n *ngIf=\"!formIssuesLoading && formIssues && formIssues.length === 0\"\n class=\"text-muted mb-3\"\n >\n {{ 'epistolaAdminNoFormIssues' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"!formIssuesLoading && formIssues && formIssues.length > 0\">\n <div class=\"mb-2\">\n <button\n class=\"btn btn-sm btn-primary\"\n data-testid=\"epistola-admin-repair-all\"\n (click)=\"repairAllForms()\"\n [disabled]=\"repairingAll\"\n >\n {{\n (repairingAll ? 'epistolaAdminRepairing' : 'epistolaAdminRepairAll')\n | pluginTranslate: 'epistola'\n | async\n }}\n </button>\n <span\n *ngIf=\"formFeedback && formFeedback.formId === 'all'\"\n class=\"small ms-2\"\n [class.text-success]=\"formFeedback.type === 'success'\"\n [class.text-danger]=\"formFeedback.type === 'error'\"\n >{{ formFeedback.message }}</span\n >\n </div>\n\n <table class=\"table table-striped\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminFormName' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminFormMissing' | pluginTranslate: 'epistola' | async }}</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n <tr\n *ngFor=\"let issue of formIssues\"\n [attr.data-testid]=\"'epistola-admin-form-issue-' + issue.formId\"\n >\n <td>\n <code>{{ issue.name }}</code>\n <cds-tag\n *ngIf=\"issue.readOnly\"\n size=\"sm\"\n type=\"warm-gray\"\n class=\"ms-1\"\n [title]=\"'epistolaAdminFormReadOnlyHint' | pluginTranslate: 'epistola' | async\"\n >{{\n 'epistolaAdminFormReadOnly' | pluginTranslate: 'epistola' | async\n }}</cds-tag\n >\n </td>\n <td>{{ issue.missingComponents }}</td>\n <td class=\"text-end\">\n <button\n class=\"btn btn-sm btn-outline-primary\"\n [attr.data-testid]=\"'epistola-admin-repair-form-' + issue.formId\"\n (click)=\"repairForm(issue)\"\n [disabled]=\"isRepairingForm(issue)\"\n [title]=\"'epistolaAdminRepairTooltip' | pluginTranslate: 'epistola' | async\"\n >\n {{\n (isRepairingForm(issue) ? 'epistolaAdminRepairing' : 'epistolaAdminRepair')\n | pluginTranslate: 'epistola'\n | async\n }}\n </button>\n <div\n *ngIf=\"formFeedback && formFeedback.formId === issue.formId\"\n class=\"small mt-1\"\n [class.text-success]=\"formFeedback.type === 'success'\"\n [class.text-danger]=\"formFeedback.type === 'error'\"\n >\n {{ formFeedback.message }}\n </div>\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n\n <!-- TEMPORARY: forms still using the legacy override-mapping object format -->\n <hr class=\"my-4\" />\n <h5 class=\"mb-1\">\n {{ 'epistolaAdminLegacyOverrideTitle' | pluginTranslate: 'epistola' | async }}\n </h5>\n <p class=\"text-muted mb-3\">\n {{ 'epistolaAdminLegacyOverrideIntro' | pluginTranslate: 'epistola' | async }}\n </p>\n\n <div *ngIf=\"legacyOverrideLoading\" class=\"text-muted mb-3\">\n {{ 'epistolaAdminLoading' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div\n *ngIf=\"!legacyOverrideLoading && legacyOverrideForms && legacyOverrideForms.length === 0\"\n class=\"text-muted mb-3\"\n >\n {{ 'epistolaAdminNoLegacyOverride' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <table\n *ngIf=\"!legacyOverrideLoading && legacyOverrideForms && legacyOverrideForms.length > 0\"\n class=\"table table-striped\"\n >\n <thead>\n <tr>\n <th>{{ 'epistolaAdminFormName' | pluginTranslate: 'epistola' | async }}</th>\n <th>\n {{ 'epistolaAdminLegacyOverrideComponents' | pluginTranslate: 'epistola' | async }}\n </th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let form of legacyOverrideForms\">\n <td>\n <code>{{ form.name }}</code>\n <cds-tag\n *ngIf=\"form.readOnly\"\n size=\"sm\"\n type=\"warm-gray\"\n class=\"ms-1\"\n [title]=\"'epistolaAdminFormReadOnlyHint' | pluginTranslate: 'epistola' | async\"\n >{{ 'epistolaAdminFormReadOnly' | pluginTranslate: 'epistola' | async }}</cds-tag\n >\n </td>\n <td>{{ form.legacyComponents }}</td>\n </tr>\n </tbody>\n </table>\n </cds-tab>\n </cds-tabs>\n </ng-container>\n\n <!-- Detail view: selected configuration -->\n <ng-container *ngIf=\"selectedCard\">\n <div class=\"detail-header mb-3\">\n <button\n class=\"btn btn-link btn-sm p-0\"\n data-testid=\"epistola-admin-back\"\n (click)=\"backToOverview()\"\n >\n &larr; {{ 'epistolaAdminBackToOverview' | pluginTranslate: 'epistola' | async }}\n </button>\n </div>\n\n <div class=\"detail-summary mb-4\">\n <h4>\n <span\n class=\"status-dot me-2\"\n [class.status-dot--ok]=\"selectedCard.reachable\"\n [class.status-dot--error]=\"!selectedCard.reachable\"\n >\n </span>\n {{ selectedCard.configurationTitle }}\n </h4>\n\n <table class=\"table table-sm detail-info-table\">\n <tbody>\n <tr>\n <th>{{ 'epistolaAdminTenantId' | pluginTranslate: 'epistola' | async }}</th>\n <td>\n <code>{{ selectedCard.tenantId }}</code>\n </td>\n </tr>\n <tr>\n <th>{{ 'epistolaAdminStatus' | pluginTranslate: 'epistola' | async }}</th>\n <td>\n <cds-tag size=\"sm\" [type]=\"selectedCard.reachable ? 'green' : 'red'\">\n {{\n selectedCard.reachable\n ? ('epistolaAdminConnected' | pluginTranslate: 'epistola' | async)\n : ('epistolaAdminUnreachable' | pluginTranslate: 'epistola' | async)\n }}\n </cds-tag>\n <span class=\"text-muted ms-2\">{{ selectedCard.latencyMs }} ms</span>\n </td>\n </tr>\n <tr *ngIf=\"selectedCard.serverVersion\">\n <th>{{ 'epistolaAdminServerVersion' | pluginTranslate: 'epistola' | async }}</th>\n <td>{{ selectedCard.serverVersion }}</td>\n </tr>\n <tr *ngIf=\"selectedCard.errorMessage\">\n <th>{{ 'epistolaAdminError' | pluginTranslate: 'epistola' | async }}</th>\n <td class=\"text-danger\">{{ selectedCard.errorMessage }}</td>\n </tr>\n </tbody>\n </table>\n </div>\n\n <!-- Tabs -->\n <ng-template #actionsHeading>\n {{ 'epistolaAdminPluginActions' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" type=\"gray\" class=\"ms-1\">{{ selectedCard.usageEntries.length }}</cds-tag>\n </ng-template>\n\n <ng-template #pendingHeading>\n {{ 'epistolaAdminPendingJobs' | pluginTranslate: 'epistola' | async }}\n <cds-tag\n size=\"sm\"\n [type]=\"selectedCard.pendingJobs.length > 0 ? 'blue' : 'gray'\"\n class=\"ms-1\"\n >\n {{ selectedCard.pendingJobs.length }}\n </cds-tag>\n </ng-template>\n\n <ng-template #catalogsHeading>\n {{ 'epistolaAdminCatalogs' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" type=\"gray\" class=\"ms-1\">{{ catalogs.length }}</cds-tag>\n </ng-template>\n\n <cds-tabs [cacheActive]=\"true\" type=\"contained\" data-testid=\"epistola-admin-detail-tabs\">\n <cds-tab\n [heading]=\"actionsHeading\"\n data-testid=\"epistola-admin-detail-tab-actions\"\n [active]=\"activeTab === 'actions'\"\n (selected)=\"setActiveTab('actions')\"\n >\n <div *ngIf=\"selectedCard.usageEntries.length === 0\" class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminNoUsageForConfig' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <table *ngIf=\"selectedCard.usageEntries.length > 0\" class=\"table table-striped mt-3\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminCase' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminProcess' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminActivity' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminAction' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminProblems' | pluginTranslate: 'epistola' | async }}</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n <tr\n *ngFor=\"let entry of selectedCard.usageEntries; let i = index\"\n [attr.data-testid]=\"'epistola-admin-usage-row-' + i\"\n [class.table-warning]=\"entry.problems.length > 0\"\n >\n <td>{{ entry.caseDefinitionKey || '-' }}</td>\n <td>\n <a\n *ngIf=\"entry.caseDefinitionKey && entry.caseDefinitionVersionTag\"\n [routerLink]=\"[\n '/case-management',\n 'case',\n entry.caseDefinitionKey,\n 'version',\n entry.caseDefinitionVersionTag,\n 'processes',\n entry.processDefinitionKey,\n ]\"\n class=\"usage-link\"\n >\n {{ entry.processDefinitionName }}\n </a>\n <span *ngIf=\"!entry.caseDefinitionKey || !entry.caseDefinitionVersionTag\">\n {{ entry.processDefinitionName }}\n </span>\n </td>\n <td>{{ entry.activityName }}</td>\n <td>\n <code>{{ entry.actionKey }}</code>\n </td>\n <td>\n <cds-tag *ngIf=\"entry.problems.length === 0\" size=\"sm\" type=\"green\">OK</cds-tag>\n <cds-tag\n *ngFor=\"let problem of entry.problems\"\n size=\"sm\"\n type=\"red\"\n class=\"d-block mb-1\"\n >\n {{ problem }}\n </cds-tag>\n </td>\n <td>\n <button\n class=\"btn btn-sm btn-outline-secondary\"\n [attr.data-testid]=\"'epistola-admin-export-' + i\"\n (click)=\"exportProcessLink(entry)\"\n [title]=\"'epistolaAdminExport' | pluginTranslate: 'epistola' | async\"\n >\n &#x2913;\n </button>\n </td>\n </tr>\n </tbody>\n </table>\n </cds-tab>\n\n <cds-tab\n [heading]=\"pendingHeading\"\n data-testid=\"epistola-admin-detail-tab-pending\"\n [active]=\"activeTab === 'pending'\"\n (selected)=\"setActiveTab('pending')\"\n >\n <div *ngIf=\"selectedCard.pendingJobs.length === 0\" class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminNoPendingJobs' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <table *ngIf=\"selectedCard.pendingJobs.length > 0\" class=\"table table-striped mt-3\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminProcess' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminActivity' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminStatus' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminRequestId' | pluginTranslate: 'epistola' | async }}</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n <tr\n *ngFor=\"let job of selectedCard.pendingJobs\"\n [attr.data-testid]=\"'epistola-admin-pending-row-' + job.executionId\"\n >\n <td>{{ job.processDefinitionName }}</td>\n <td>{{ job.activityName }}</td>\n <td>\n <cds-tag\n *ngIf=\"job.status === 'UNWIRED'; else waitingTag\"\n size=\"sm\"\n type=\"red\"\n [title]=\"'epistolaAdminUnwiredTooltip' | pluginTranslate: 'epistola' | async\"\n >\n {{ 'epistolaAdminStatusUnwired' | pluginTranslate: 'epistola' | async }}\n </cds-tag>\n <ng-template #waitingTag>\n <cds-tag size=\"sm\" type=\"blue\">\n {{ 'epistolaAdminStatusWaiting' | pluginTranslate: 'epistola' | async }}\n </cds-tag>\n </ng-template>\n </td>\n <td>\n <code *ngIf=\"job.status !== 'UNWIRED'\">{{ job.requestId }}</code>\n <span *ngIf=\"job.status === 'UNWIRED'\" class=\"text-muted\">\u2014</span>\n </td>\n <td class=\"text-end\">\n <button\n *ngIf=\"job.status !== 'UNWIRED'\"\n class=\"btn btn-sm btn-outline-primary\"\n [attr.data-testid]=\"'epistola-admin-reconcile-' + job.executionId\"\n (click)=\"reconcilePending(job)\"\n [disabled]=\"isReconciling(job)\"\n [title]=\"'epistolaAdminReconcileTooltip' | pluginTranslate: 'epistola' | async\"\n >\n <span *ngIf=\"!isReconciling(job)\">\n {{ 'epistolaAdminReconcile' | pluginTranslate: 'epistola' | async }}\n </span>\n <span *ngIf=\"isReconciling(job)\">\n {{ 'epistolaAdminReconciling' | pluginTranslate: 'epistola' | async }}\n </span>\n </button>\n <span\n *ngIf=\"job.status === 'UNWIRED'\"\n class=\"text-muted small\"\n [title]=\"'epistolaAdminUnwiredTooltip' | pluginTranslate: 'epistola' | async\"\n >\n {{ 'epistolaAdminUnwiredHint' | pluginTranslate: 'epistola' | async }}\n </span>\n <div\n *ngIf=\"reconcileFeedback && reconcileFeedback.executionId === job.executionId\"\n class=\"reconcile-feedback small mt-1\"\n [class.text-success]=\"reconcileFeedback.type === 'success'\"\n [class.text-warning]=\"reconcileFeedback.type === 'pending'\"\n [class.text-danger]=\"reconcileFeedback.type === 'error'\"\n >\n {{ reconcileFeedback.message }}\n </div>\n </td>\n </tr>\n </tbody>\n </table>\n </cds-tab>\n\n <cds-tab\n [heading]=\"catalogsHeading\"\n data-testid=\"epistola-admin-detail-tab-catalogs\"\n [active]=\"activeTab === 'catalogs'\"\n (selected)=\"setActiveTab('catalogs')\"\n >\n <p class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminCatalogsIntro' | pluginTranslate: 'epistola' | async }}\n </p>\n\n <div *ngIf=\"catalogsLoading\" class=\"text-muted mb-3\">\n {{ 'epistolaAdminLoading' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"!catalogsLoading && catalogs.length === 0\" class=\"text-muted mb-3\">\n {{ 'epistolaAdminNoCatalogs' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <table *ngIf=\"!catalogsLoading && catalogs.length > 0\" class=\"table table-striped\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminCatalogSlug' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminCatalogVersion' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminCatalogStatus' | pluginTranslate: 'epistola' | async }}</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n <tr\n *ngFor=\"let catalog of catalogs\"\n [attr.data-testid]=\"'epistola-admin-catalog-row-' + catalog.slug\"\n >\n <td>\n <code>{{ catalog.slug }}</code>\n </td>\n <td>{{ catalog.version }}</td>\n <td>\n <cds-tag *ngIf=\"catalog.status === 'IN_EPISTOLA'\" size=\"sm\" type=\"green\">\n {{ 'epistolaAdminCatalogInEpistola' | pluginTranslate: 'epistola' | async }}\n </cds-tag>\n <cds-tag *ngIf=\"catalog.status === 'NOT_IN_EPISTOLA'\" size=\"sm\" type=\"red\">\n {{ 'epistolaAdminCatalogNotInEpistola' | pluginTranslate: 'epistola' | async }}\n </cds-tag>\n <cds-tag *ngIf=\"catalog.status === 'UNKNOWN'\" size=\"sm\" type=\"gray\">\n {{ 'epistolaAdminCatalogStatusUnknown' | pluginTranslate: 'epistola' | async }}\n </cds-tag>\n </td>\n <td class=\"text-end\">\n <button\n class=\"btn btn-sm btn-outline-primary\"\n [attr.data-testid]=\"'epistola-admin-redeploy-' + catalog.slug\"\n (click)=\"redeployCatalog(catalog)\"\n [disabled]=\"isRedeploying(catalog)\"\n [title]=\"'epistolaAdminRedeployTooltip' | pluginTranslate: 'epistola' | async\"\n >\n <span *ngIf=\"!isRedeploying(catalog)\">\n {{ 'epistolaAdminRedeploy' | pluginTranslate: 'epistola' | async }}\n </span>\n <span *ngIf=\"isRedeploying(catalog)\">\n {{ 'epistolaAdminRedeploying' | pluginTranslate: 'epistola' | async }}\n </span>\n </button>\n <div\n *ngIf=\"catalogFeedback && catalogFeedback.slug === catalog.slug\"\n class=\"reconcile-feedback small mt-1\"\n [class.text-success]=\"catalogFeedback.type === 'success'\"\n [class.text-danger]=\"catalogFeedback.type === 'error'\"\n >\n {{ catalogFeedback.message }}\n </div>\n </td>\n </tr>\n </tbody>\n </table>\n </cds-tab>\n </cds-tabs>\n </ng-container>\n</div>\n", styles: [".epistola-admin{padding:1.5rem}.epistola-admin .version-badge{font-size:.75rem;font-weight:500;padding:.2em .6em;border-radius:4px;background-color:#e8e8e8;color:#525252}.epistola-admin .badge{font-size:.85em;padding:.35em .65em}.epistola-admin code{font-size:.9em;color:#525252}.epistola-admin table th{font-weight:600;white-space:nowrap}.epistola-admin .status-dot{display:inline-block;width:10px;height:10px;border-radius:50%;margin-right:.5rem;flex-shrink:0;background-color:#adb5bd}.epistola-admin .status-dot--ok{background-color:#198754}.epistola-admin .status-dot--error{background-color:#dc3545}.epistola-admin .card-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:1rem}.epistola-admin .config-card{border:1px solid #dee2e6;border-radius:8px;padding:1.25rem;cursor:pointer;transition:box-shadow .15s ease,border-color .15s ease;background:#fff}.epistola-admin .config-card:hover{box-shadow:0 2px 8px #0000001a}.epistola-admin .config-card--ok{border-left:4px solid #198754}.epistola-admin .config-card--warning{border-left:4px solid #ffc107}.epistola-admin .config-card--error{border-left:4px solid #dc3545}.epistola-admin .config-card__header{display:flex;align-items:center;margin-bottom:1rem}.epistola-admin .config-card__title{margin:0;font-size:1.05rem;font-weight:600;color:#161616}.epistola-admin .config-card__body{display:flex;flex-direction:column;gap:.5rem}.epistola-admin .config-card__field{display:flex;justify-content:space-between;align-items:center}.epistola-admin .config-card__label{font-size:.875rem;color:#6c757d}.epistola-admin .config-card__value{font-size:.875rem;color:#161616}.epistola-admin .config-card__footer{margin-top:1rem;padding-top:.75rem;border-top:1px solid #f0f0f0;text-align:right}.epistola-admin .config-card__latency{font-size:.8rem;color:#adb5bd}.epistola-admin .usage-link{color:#0f62fe;text-decoration:none}.epistola-admin .usage-link:hover{text-decoration:underline}.epistola-admin .detail-info-table{max-width:500px}.epistola-admin .detail-info-table th{width:140px}.epistola-admin .detail-summary{padding:1rem 0;border-bottom:1px solid #dee2e6}.epistola-admin .epistola-changelog{max-height:60vh;overflow:auto;padding:.5rem 1rem;border:1px solid #dee2e6;border-radius:4px}.epistola-admin .epistola-changelog .changelog-release{padding:.75rem 0}.epistola-admin .epistola-changelog .changelog-release+.changelog-release{border-top:1px solid #eee}.epistola-admin .epistola-changelog .changelog-release__header{display:flex;align-items:center;margin-bottom:.5rem}.epistola-admin .epistola-changelog .changelog-section{margin:.25rem 0 .75rem}.epistola-admin .epistola-changelog .changelog-section__title{font-size:.8125rem;font-weight:600;text-transform:uppercase;letter-spacing:.02em;color:#6f6f6f;margin-bottom:.25rem}.epistola-admin .epistola-changelog .changelog-section__items{margin:0;padding-left:1.25rem;font-size:.8125rem;line-height:1.5}.epistola-admin .epistola-changelog .changelog-section__items li{margin-bottom:.25rem;word-break:break-word}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }, { kind: "pipe", type: i1$1.DatePipe, name: "date" }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i2$4.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "ngmodule", type: PluginTranslatePipeModule }, { kind: "pipe", type: i2$1.PluginTranslatePipe, name: "pluginTranslate" }, { kind: "ngmodule", type: TabsModule }, { kind: "component", type: i5.Tabs, selector: "cds-tabs, ibm-tabs", inputs: ["position", "cacheActive", "followFocus", "isNavigation", "ariaLabel", "ariaLabelledby", "type", "theme", "skeleton"] }, { kind: "component", type: i5.Tab, selector: "cds-tab, ibm-tab", inputs: ["heading", "title", "context", "active", "disabled", "tabIndex", "id", "cacheActive", "tabContent", "templateContext"], outputs: ["selected"] }, { kind: "ngmodule", type: TagModule }, { kind: "component", type: i6.Tag, selector: "cds-tag, ibm-tag", inputs: ["type", "size", "class", "skeleton"] }] });
4878
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: EpistolaAdminPageComponent, isStandalone: true, selector: "epistola-admin-page", ngImport: i0, template: "<div class=\"epistola-admin\" data-testid=\"epistola-admin\">\n <!-- Overview: tabs (no configuration selected) -->\n <ng-container *ngIf=\"!selectedCard\">\n <div class=\"d-flex justify-content-between align-items-center mb-3\">\n <div class=\"d-flex align-items-center\">\n <h5 class=\"mb-0\">{{ 'epistolaAdminOverview' | pluginTranslate: 'epistola' | async }}</h5>\n <span *ngIf=\"pluginVersion || pluginContractVersion\" class=\"version-badge ms-2\">\n v{{ versionWithContract(pluginVersion, pluginContractVersion) }}\n </span>\n </div>\n <button\n class=\"btn btn-outline-primary btn-sm\"\n data-testid=\"epistola-admin-refresh\"\n (click)=\"refresh()\"\n [disabled]=\"loading\"\n >\n {{ 'epistolaAdminRefresh' | pluginTranslate: 'epistola' | async }}\n </button>\n </div>\n\n <ng-template #configurationsHeading>\n {{ 'epistolaAdminConfigurations' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" type=\"gray\" class=\"ms-1\">{{ cards.length }}</cds-tag>\n </ng-template>\n\n <ng-template #validationsHeading>\n {{ 'epistolaAdminValidations' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" [type]=\"validationViolations.length > 0 ? 'red' : 'gray'\" class=\"ms-1\">\n {{ validationViolations.length }}\n </cds-tag>\n </ng-template>\n\n <ng-template #changelogHeading>\n {{ 'epistolaAdminChangelog' | pluginTranslate: 'epistola' | async }}\n </ng-template>\n\n <!-- TEMPORARY (removed in 1.0.0): forms missing the task-id carrier -->\n <ng-template #formsHeading>\n {{ 'epistolaAdminForms' | pluginTranslate: 'epistola' | async }}\n <cds-tag\n *ngIf=\"formsScanLoaded\"\n size=\"sm\"\n [type]=\"formsAttentionCount > 0 ? 'red' : 'gray'\"\n class=\"ms-1\"\n >{{ formsAttentionCount }}</cds-tag\n >\n </ng-template>\n\n <cds-tabs [cacheActive]=\"true\" type=\"contained\" data-testid=\"epistola-admin-overview-tabs\">\n <cds-tab\n [heading]=\"configurationsHeading\"\n data-testid=\"epistola-admin-tab-configurations\"\n [active]=\"overviewTab === 'configurations'\"\n (selected)=\"setOverviewTab('configurations')\"\n >\n <div\n *ngIf=\"loading\"\n class=\"text-muted mt-3\"\n data-testid=\"epistola-admin-configurations-loading\"\n >\n {{ 'epistolaAdminLoading' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div\n *ngIf=\"!loading && cards.length === 0\"\n class=\"text-muted mt-3\"\n data-testid=\"epistola-admin-configurations-empty\"\n >\n {{ 'epistolaAdminNoConfigurations' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div\n *ngIf=\"!loading && cards.length > 0\"\n class=\"card-grid\"\n data-testid=\"epistola-admin-card-grid\"\n >\n <div\n *ngFor=\"let card of cards\"\n class=\"config-card\"\n [attr.data-testid]=\"'epistola-admin-config-card-' + card.tenantId\"\n [class.config-card--ok]=\"\n card.reachable && card.problemCount === 0 && !hasContractCompatibilityWarning(card)\n \"\n [class.config-card--warning]=\"\n card.reachable &&\n (card.problemCount > 0 ||\n card.contractCompatibilitySeverity === 'WARNING' ||\n card.contractCompatibilitySeverity === 'UNKNOWN')\n \"\n [class.config-card--error]=\"\n !card.reachable || card.contractCompatibilitySeverity === 'ERROR'\n \"\n (click)=\"selectConfiguration(card)\"\n >\n <div class=\"config-card__header\">\n <span\n class=\"status-dot\"\n [class.status-dot--ok]=\"card.reachable\"\n [class.status-dot--error]=\"!card.reachable\"\n >\n </span>\n <h5 class=\"config-card__title\">{{ card.configurationTitle }}</h5>\n </div>\n\n <div class=\"config-card__body\">\n <div class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminTenantId' | pluginTranslate: 'epistola' | async\n }}</span>\n <code class=\"config-card__value\">{{ card.tenantId }}</code>\n </div>\n <div class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminStatus' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" [type]=\"card.reachable ? 'green' : 'red'\">\n {{\n card.reachable\n ? ('epistolaAdminConnected' | pluginTranslate: 'epistola' | async)\n : ('epistolaAdminUnreachable' | pluginTranslate: 'epistola' | async)\n }}\n </cds-tag>\n </div>\n <div\n *ngIf=\"card.serverVersion || card.serverContractVersion\"\n class=\"config-card__field\"\n >\n <span class=\"config-card__label\">{{\n 'epistolaAdminServerVersion' | pluginTranslate: 'epistola' | async\n }}</span>\n <span class=\"config-card__value\">\n {{ versionWithContract(card.serverVersion, card.serverContractVersion) }}\n </span>\n </div>\n <div *ngIf=\"hasContractCompatibilityWarning(card)\" class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminContractCompatibility' | pluginTranslate: 'epistola' | async\n }}</span>\n <span\n class=\"contract-compatibility\"\n [ngClass]=\"contractBadgeClass(card)\"\n [title]=\"(contractBodyKey(card) | pluginTranslate: 'epistola' | async) || ''\"\n [attr.data-testid]=\"'epistola-admin-contract-alert-' + card.tenantId\"\n >\n <span class=\"contract-compatibility__icon\" aria-hidden=\"true\">!</span>\n {{ contractLabelKey(card) | pluginTranslate: 'epistola' | async }}\n </span>\n </div>\n <div class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminPluginActions' | pluginTranslate: 'epistola' | async\n }}</span>\n <span class=\"config-card__value\">{{ card.usageCount }}</span>\n </div>\n <div *ngIf=\"card.pendingJobs.length > 0\" class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminPendingJobs' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" type=\"blue\">{{ card.pendingJobs.length }}</cds-tag>\n </div>\n <div *ngIf=\"card.problemCount > 0\" class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminProblems' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" type=\"red\">{{ card.problemCount }}</cds-tag>\n </div>\n </div>\n\n <div class=\"config-card__footer\">\n <span class=\"config-card__latency\">{{ card.latencyMs }} ms</span>\n </div>\n </div>\n </div>\n </cds-tab>\n\n <cds-tab\n [heading]=\"validationsHeading\"\n data-testid=\"epistola-admin-tab-validations\"\n [active]=\"overviewTab === 'validations'\"\n (selected)=\"setOverviewTab('validations')\"\n >\n <div class=\"text-muted small mt-3\">\n <div>\n {{ 'epistolaAdminValidationLastChecked' | pluginTranslate: 'epistola' | async }}:\n <ng-container *ngIf=\"validationReport?.lastCheckedAt; else notYetRun\">\n {{ validationReport?.lastCheckedAt | date: 'medium' }}\n </ng-container>\n <ng-template #notYetRun>\n {{ 'epistolaAdminValidationNotYetRun' | pluginTranslate: 'epistola' | async }}\n </ng-template>\n \u00B7 {{ 'epistolaAdminValidationAutoRefresh' | pluginTranslate: 'epistola' | async }}\n {{ refreshIntervalMinutes }} min.\n </div>\n <div>\n {{ 'epistolaAdminValidationLatestVersionNote' | pluginTranslate: 'epistola' | async }}\n </div>\n </div>\n\n <div *ngIf=\"validationViolations.length === 0\" class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminNoValidations' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"validationViolations.length > 0\" class=\"mt-3\">\n <p class=\"text-muted mb-3\">\n {{ 'epistolaAdminValidationWarningBody' | pluginTranslate: 'epistola' | async }}\n </p>\n <table class=\"table table-striped\" data-testid=\"epistola-admin-validations-table\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminProcess' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminActivity' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminValidationCode' | pluginTranslate: 'epistola' | async }}</th>\n <th>\n {{ 'epistolaAdminValidationMessage' | pluginTranslate: 'epistola' | async }}\n </th>\n </tr>\n </thead>\n <tbody>\n <tr\n *ngFor=\"let v of validationViolations; let i = index\"\n [attr.data-testid]=\"'epistola-admin-validation-row-' + i\"\n >\n <td>\n {{ v.processDefinitionName || v.processDefinitionKey }}\n <div *ngIf=\"v.processDefinitionName\" class=\"text-muted small\">\n <code>{{ v.processDefinitionKey }}</code>\n </div>\n </td>\n <td>\n <code>{{ v.activityId }}</code>\n </td>\n <td>\n <cds-tag size=\"sm\" type=\"red\">{{ v.code }}</cds-tag>\n </td>\n <td>{{ v.message }}</td>\n </tr>\n </tbody>\n </table>\n </div>\n </cds-tab>\n\n <cds-tab\n [heading]=\"changelogHeading\"\n data-testid=\"epistola-admin-tab-changelog\"\n [active]=\"overviewTab === 'changelog'\"\n (selected)=\"setOverviewTab('changelog')\"\n >\n <div class=\"d-flex align-items-center mt-3 mb-3\">\n <span class=\"text-muted me-2\">{{\n 'epistolaAdminRunningVersion' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" type=\"blue\">\n v{{ versionWithContract(pluginVersion, pluginContractVersion) }}\n </cds-tag>\n </div>\n\n <div *ngIf=\"changelogLoading\" class=\"text-muted mb-3\">\n {{ 'epistolaAdminLoading' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div\n *ngIf=\"!changelogLoading && changelog && changelog.length === 0\"\n class=\"text-muted mb-3\"\n >\n {{ 'epistolaAdminNoChangelog' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div\n *ngIf=\"!changelogLoading && changelog && changelog.length > 0\"\n class=\"epistola-changelog\"\n >\n <section *ngFor=\"let release of changelog\" class=\"changelog-release\">\n <div class=\"changelog-release__header\">\n <cds-tag size=\"sm\" type=\"purple\">{{ release.version }}</cds-tag>\n <span *ngIf=\"release.date\" class=\"text-muted ms-2\">{{ release.date }}</span>\n </div>\n <div *ngFor=\"let section of release.sections\" class=\"changelog-section\">\n <h6 class=\"changelog-section__title\">{{ section.title }}</h6>\n <ul class=\"changelog-section__items\">\n <li *ngFor=\"let item of section.items\">{{ item }}</li>\n </ul>\n </div>\n </section>\n </div>\n </cds-tab>\n\n <!-- TEMPORARY (removed in 1.0.0): forms missing the task-id carrier -->\n <cds-tab\n [heading]=\"formsHeading\"\n data-testid=\"epistola-admin-tab-forms\"\n [active]=\"overviewTab === 'forms'\"\n (selected)=\"setOverviewTab('forms')\"\n >\n <p class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminFormsIntro' | pluginTranslate: 'epistola' | async }}\n </p>\n\n <div *ngIf=\"formIssuesLoading\" class=\"text-muted mb-3\">\n {{ 'epistolaAdminLoading' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div\n *ngIf=\"!formIssuesLoading && formIssues && formIssues.length === 0\"\n class=\"text-muted mb-3\"\n >\n {{ 'epistolaAdminNoFormIssues' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"!formIssuesLoading && formIssues && formIssues.length > 0\">\n <div class=\"mb-2\">\n <button\n class=\"btn btn-sm btn-primary\"\n data-testid=\"epistola-admin-repair-all\"\n (click)=\"repairAllForms()\"\n [disabled]=\"repairingAll\"\n >\n {{\n (repairingAll ? 'epistolaAdminRepairing' : 'epistolaAdminRepairAll')\n | pluginTranslate: 'epistola'\n | async\n }}\n </button>\n <span\n *ngIf=\"formFeedback && formFeedback.formId === 'all'\"\n class=\"small ms-2\"\n [class.text-success]=\"formFeedback.type === 'success'\"\n [class.text-danger]=\"formFeedback.type === 'error'\"\n >{{ formFeedback.message }}</span\n >\n </div>\n\n <table class=\"table table-striped\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminFormName' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminFormMissing' | pluginTranslate: 'epistola' | async }}</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n <tr\n *ngFor=\"let issue of formIssues\"\n [attr.data-testid]=\"'epistola-admin-form-issue-' + issue.formId\"\n >\n <td>\n <code>{{ issue.name }}</code>\n <cds-tag\n *ngIf=\"issue.readOnly\"\n size=\"sm\"\n type=\"warm-gray\"\n class=\"ms-1\"\n [title]=\"'epistolaAdminFormReadOnlyHint' | pluginTranslate: 'epistola' | async\"\n >{{\n 'epistolaAdminFormReadOnly' | pluginTranslate: 'epistola' | async\n }}</cds-tag\n >\n </td>\n <td>{{ issue.missingComponents }}</td>\n <td class=\"text-end\">\n <button\n class=\"btn btn-sm btn-outline-primary\"\n [attr.data-testid]=\"'epistola-admin-repair-form-' + issue.formId\"\n (click)=\"repairForm(issue)\"\n [disabled]=\"isRepairingForm(issue)\"\n [title]=\"'epistolaAdminRepairTooltip' | pluginTranslate: 'epistola' | async\"\n >\n {{\n (isRepairingForm(issue) ? 'epistolaAdminRepairing' : 'epistolaAdminRepair')\n | pluginTranslate: 'epistola'\n | async\n }}\n </button>\n <div\n *ngIf=\"formFeedback && formFeedback.formId === issue.formId\"\n class=\"small mt-1\"\n [class.text-success]=\"formFeedback.type === 'success'\"\n [class.text-danger]=\"formFeedback.type === 'error'\"\n >\n {{ formFeedback.message }}\n </div>\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n\n <!-- TEMPORARY: forms still using the legacy override-mapping object format -->\n <hr class=\"my-4\" />\n <h5 class=\"mb-1\">\n {{ 'epistolaAdminLegacyOverrideTitle' | pluginTranslate: 'epistola' | async }}\n </h5>\n <p class=\"text-muted mb-3\">\n {{ 'epistolaAdminLegacyOverrideIntro' | pluginTranslate: 'epistola' | async }}\n </p>\n\n <div *ngIf=\"legacyOverrideLoading\" class=\"text-muted mb-3\">\n {{ 'epistolaAdminLoading' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div\n *ngIf=\"!legacyOverrideLoading && legacyOverrideForms && legacyOverrideForms.length === 0\"\n class=\"text-muted mb-3\"\n >\n {{ 'epistolaAdminNoLegacyOverride' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <table\n *ngIf=\"!legacyOverrideLoading && legacyOverrideForms && legacyOverrideForms.length > 0\"\n class=\"table table-striped\"\n >\n <thead>\n <tr>\n <th>{{ 'epistolaAdminFormName' | pluginTranslate: 'epistola' | async }}</th>\n <th>\n {{ 'epistolaAdminLegacyOverrideComponents' | pluginTranslate: 'epistola' | async }}\n </th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let form of legacyOverrideForms\">\n <td>\n <code>{{ form.name }}</code>\n <cds-tag\n *ngIf=\"form.readOnly\"\n size=\"sm\"\n type=\"warm-gray\"\n class=\"ms-1\"\n [title]=\"'epistolaAdminFormReadOnlyHint' | pluginTranslate: 'epistola' | async\"\n >{{ 'epistolaAdminFormReadOnly' | pluginTranslate: 'epistola' | async }}</cds-tag\n >\n </td>\n <td>{{ form.legacyComponents }}</td>\n </tr>\n </tbody>\n </table>\n </cds-tab>\n </cds-tabs>\n </ng-container>\n\n <!-- Detail view: selected configuration -->\n <ng-container *ngIf=\"selectedCard\">\n <div class=\"detail-header mb-3\">\n <button\n class=\"btn btn-link btn-sm p-0\"\n data-testid=\"epistola-admin-back\"\n (click)=\"backToOverview()\"\n >\n &larr; {{ 'epistolaAdminBackToOverview' | pluginTranslate: 'epistola' | async }}\n </button>\n </div>\n\n <div class=\"detail-summary mb-4\">\n <h4>\n <span\n class=\"status-dot me-2\"\n [class.status-dot--ok]=\"selectedCard.reachable\"\n [class.status-dot--error]=\"!selectedCard.reachable\"\n >\n </span>\n {{ selectedCard.configurationTitle }}\n </h4>\n\n <table class=\"table table-sm detail-info-table\">\n <tbody>\n <tr>\n <th>{{ 'epistolaAdminTenantId' | pluginTranslate: 'epistola' | async }}</th>\n <td>\n <code>{{ selectedCard.tenantId }}</code>\n </td>\n </tr>\n <tr>\n <th>{{ 'epistolaAdminStatus' | pluginTranslate: 'epistola' | async }}</th>\n <td>\n <cds-tag size=\"sm\" [type]=\"selectedCard.reachable ? 'green' : 'red'\">\n {{\n selectedCard.reachable\n ? ('epistolaAdminConnected' | pluginTranslate: 'epistola' | async)\n : ('epistolaAdminUnreachable' | pluginTranslate: 'epistola' | async)\n }}\n </cds-tag>\n <span class=\"text-muted ms-2\">{{ selectedCard.latencyMs }} ms</span>\n </td>\n </tr>\n <tr *ngIf=\"selectedCard.serverVersion || selectedCard.serverContractVersion\">\n <th>{{ 'epistolaAdminServerVersion' | pluginTranslate: 'epistola' | async }}</th>\n <td>\n {{\n versionWithContract(selectedCard.serverVersion, selectedCard.serverContractVersion)\n }}\n </td>\n </tr>\n <tr *ngIf=\"hasContractCompatibilityWarning(selectedCard)\">\n <th>\n {{ 'epistolaAdminContractCompatibility' | pluginTranslate: 'epistola' | async }}\n </th>\n <td>\n <span\n class=\"contract-compatibility\"\n [ngClass]=\"contractBadgeClass(selectedCard)\"\n [title]=\"\n (contractBodyKey(selectedCard) | pluginTranslate: 'epistola' | async) || ''\n \"\n data-testid=\"epistola-admin-selected-contract-alert\"\n >\n <span class=\"contract-compatibility__icon\" aria-hidden=\"true\">!</span>\n {{ contractLabelKey(selectedCard) | pluginTranslate: 'epistola' | async }}\n </span>\n </td>\n </tr>\n <tr *ngIf=\"selectedCard.errorMessage\">\n <th>{{ 'epistolaAdminError' | pluginTranslate: 'epistola' | async }}</th>\n <td class=\"text-danger\">{{ selectedCard.errorMessage }}</td>\n </tr>\n </tbody>\n </table>\n </div>\n\n <!-- Tabs -->\n <ng-template #actionsHeading>\n {{ 'epistolaAdminPluginActions' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" type=\"gray\" class=\"ms-1\">{{ selectedCard.usageEntries.length }}</cds-tag>\n </ng-template>\n\n <ng-template #pendingHeading>\n {{ 'epistolaAdminPendingJobs' | pluginTranslate: 'epistola' | async }}\n <cds-tag\n size=\"sm\"\n [type]=\"selectedCard.pendingJobs.length > 0 ? 'blue' : 'gray'\"\n class=\"ms-1\"\n >\n {{ selectedCard.pendingJobs.length }}\n </cds-tag>\n </ng-template>\n\n <ng-template #catalogsHeading>\n {{ 'epistolaAdminCatalogs' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" type=\"gray\" class=\"ms-1\">{{ catalogs.length }}</cds-tag>\n </ng-template>\n\n <cds-tabs [cacheActive]=\"true\" type=\"contained\" data-testid=\"epistola-admin-detail-tabs\">\n <cds-tab\n [heading]=\"actionsHeading\"\n data-testid=\"epistola-admin-detail-tab-actions\"\n [active]=\"activeTab === 'actions'\"\n (selected)=\"setActiveTab('actions')\"\n >\n <div *ngIf=\"selectedCard.usageEntries.length === 0\" class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminNoUsageForConfig' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <table *ngIf=\"selectedCard.usageEntries.length > 0\" class=\"table table-striped mt-3\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminCase' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminProcess' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminActivity' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminAction' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminProblems' | pluginTranslate: 'epistola' | async }}</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n <tr\n *ngFor=\"let entry of selectedCard.usageEntries; let i = index\"\n [attr.data-testid]=\"'epistola-admin-usage-row-' + i\"\n [class.table-warning]=\"entry.problems.length > 0\"\n >\n <td>{{ entry.caseDefinitionKey || '-' }}</td>\n <td>\n <a\n *ngIf=\"entry.caseDefinitionKey && entry.caseDefinitionVersionTag\"\n [routerLink]=\"[\n '/case-management',\n 'case',\n entry.caseDefinitionKey,\n 'version',\n entry.caseDefinitionVersionTag,\n 'processes',\n entry.processDefinitionKey,\n ]\"\n class=\"usage-link\"\n >\n {{ entry.processDefinitionName }}\n </a>\n <span *ngIf=\"!entry.caseDefinitionKey || !entry.caseDefinitionVersionTag\">\n {{ entry.processDefinitionName }}\n </span>\n </td>\n <td>{{ entry.activityName }}</td>\n <td>\n <code>{{ entry.actionKey }}</code>\n </td>\n <td>\n <cds-tag *ngIf=\"entry.problems.length === 0\" size=\"sm\" type=\"green\">OK</cds-tag>\n <cds-tag\n *ngFor=\"let problem of entry.problems\"\n size=\"sm\"\n type=\"red\"\n class=\"d-block mb-1\"\n >\n {{ problem }}\n </cds-tag>\n </td>\n <td>\n <button\n class=\"btn btn-sm btn-outline-secondary\"\n [attr.data-testid]=\"'epistola-admin-export-' + i\"\n (click)=\"exportProcessLink(entry)\"\n [title]=\"'epistolaAdminExport' | pluginTranslate: 'epistola' | async\"\n >\n &#x2913;\n </button>\n </td>\n </tr>\n </tbody>\n </table>\n </cds-tab>\n\n <cds-tab\n [heading]=\"pendingHeading\"\n data-testid=\"epistola-admin-detail-tab-pending\"\n [active]=\"activeTab === 'pending'\"\n (selected)=\"setActiveTab('pending')\"\n >\n <div *ngIf=\"selectedCard.pendingJobs.length === 0\" class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminNoPendingJobs' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <table *ngIf=\"selectedCard.pendingJobs.length > 0\" class=\"table table-striped mt-3\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminProcess' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminActivity' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminStatus' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminRequestId' | pluginTranslate: 'epistola' | async }}</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n <tr\n *ngFor=\"let job of selectedCard.pendingJobs\"\n [attr.data-testid]=\"'epistola-admin-pending-row-' + job.executionId\"\n >\n <td>{{ job.processDefinitionName }}</td>\n <td>{{ job.activityName }}</td>\n <td>\n <cds-tag\n *ngIf=\"job.status === 'UNWIRED'; else waitingTag\"\n size=\"sm\"\n type=\"red\"\n [title]=\"'epistolaAdminUnwiredTooltip' | pluginTranslate: 'epistola' | async\"\n >\n {{ 'epistolaAdminStatusUnwired' | pluginTranslate: 'epistola' | async }}\n </cds-tag>\n <ng-template #waitingTag>\n <cds-tag size=\"sm\" type=\"blue\">\n {{ 'epistolaAdminStatusWaiting' | pluginTranslate: 'epistola' | async }}\n </cds-tag>\n </ng-template>\n </td>\n <td>\n <code *ngIf=\"job.status !== 'UNWIRED'\">{{ job.requestId }}</code>\n <span *ngIf=\"job.status === 'UNWIRED'\" class=\"text-muted\">\u2014</span>\n </td>\n <td class=\"text-end\">\n <button\n *ngIf=\"job.status !== 'UNWIRED'\"\n class=\"btn btn-sm btn-outline-primary\"\n [attr.data-testid]=\"'epistola-admin-reconcile-' + job.executionId\"\n (click)=\"reconcilePending(job)\"\n [disabled]=\"isReconciling(job)\"\n [title]=\"'epistolaAdminReconcileTooltip' | pluginTranslate: 'epistola' | async\"\n >\n <span *ngIf=\"!isReconciling(job)\">\n {{ 'epistolaAdminReconcile' | pluginTranslate: 'epistola' | async }}\n </span>\n <span *ngIf=\"isReconciling(job)\">\n {{ 'epistolaAdminReconciling' | pluginTranslate: 'epistola' | async }}\n </span>\n </button>\n <span\n *ngIf=\"job.status === 'UNWIRED'\"\n class=\"text-muted small\"\n [title]=\"'epistolaAdminUnwiredTooltip' | pluginTranslate: 'epistola' | async\"\n >\n {{ 'epistolaAdminUnwiredHint' | pluginTranslate: 'epistola' | async }}\n </span>\n <div\n *ngIf=\"reconcileFeedback && reconcileFeedback.executionId === job.executionId\"\n class=\"reconcile-feedback small mt-1\"\n [class.text-success]=\"reconcileFeedback.type === 'success'\"\n [class.text-warning]=\"reconcileFeedback.type === 'pending'\"\n [class.text-danger]=\"reconcileFeedback.type === 'error'\"\n >\n {{ reconcileFeedback.message }}\n </div>\n </td>\n </tr>\n </tbody>\n </table>\n </cds-tab>\n\n <cds-tab\n [heading]=\"catalogsHeading\"\n data-testid=\"epistola-admin-detail-tab-catalogs\"\n [active]=\"activeTab === 'catalogs'\"\n (selected)=\"setActiveTab('catalogs')\"\n >\n <p class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminCatalogsIntro' | pluginTranslate: 'epistola' | async }}\n </p>\n\n <div *ngIf=\"catalogsLoading\" class=\"text-muted mb-3\">\n {{ 'epistolaAdminLoading' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"!catalogsLoading && catalogs.length === 0\" class=\"text-muted mb-3\">\n {{ 'epistolaAdminNoCatalogs' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <table *ngIf=\"!catalogsLoading && catalogs.length > 0\" class=\"table table-striped\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminCatalogSlug' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminCatalogVersion' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminCatalogStatus' | pluginTranslate: 'epistola' | async }}</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n <tr\n *ngFor=\"let catalog of catalogs\"\n [attr.data-testid]=\"'epistola-admin-catalog-row-' + catalog.slug\"\n >\n <td>\n <code>{{ catalog.slug }}</code>\n </td>\n <td>{{ catalog.version }}</td>\n <td>\n <cds-tag *ngIf=\"catalog.status === 'IN_EPISTOLA'\" size=\"sm\" type=\"green\">\n {{ 'epistolaAdminCatalogInEpistola' | pluginTranslate: 'epistola' | async }}\n </cds-tag>\n <cds-tag *ngIf=\"catalog.status === 'NOT_IN_EPISTOLA'\" size=\"sm\" type=\"red\">\n {{ 'epistolaAdminCatalogNotInEpistola' | pluginTranslate: 'epistola' | async }}\n </cds-tag>\n <cds-tag *ngIf=\"catalog.status === 'UNKNOWN'\" size=\"sm\" type=\"gray\">\n {{ 'epistolaAdminCatalogStatusUnknown' | pluginTranslate: 'epistola' | async }}\n </cds-tag>\n </td>\n <td class=\"text-end\">\n <button\n class=\"btn btn-sm btn-outline-primary\"\n [attr.data-testid]=\"'epistola-admin-redeploy-' + catalog.slug\"\n (click)=\"redeployCatalog(catalog)\"\n [disabled]=\"isRedeploying(catalog)\"\n [title]=\"'epistolaAdminRedeployTooltip' | pluginTranslate: 'epistola' | async\"\n >\n <span *ngIf=\"!isRedeploying(catalog)\">\n {{ 'epistolaAdminRedeploy' | pluginTranslate: 'epistola' | async }}\n </span>\n <span *ngIf=\"isRedeploying(catalog)\">\n {{ 'epistolaAdminRedeploying' | pluginTranslate: 'epistola' | async }}\n </span>\n </button>\n <div\n *ngIf=\"catalogFeedback && catalogFeedback.slug === catalog.slug\"\n class=\"reconcile-feedback small mt-1\"\n [class.text-success]=\"catalogFeedback.type === 'success'\"\n [class.text-danger]=\"catalogFeedback.type === 'error'\"\n >\n {{ catalogFeedback.message }}\n </div>\n </td>\n </tr>\n </tbody>\n </table>\n </cds-tab>\n </cds-tabs>\n </ng-container>\n</div>\n", styles: [".epistola-admin{padding:1.5rem}.epistola-admin .version-badge{font-size:.75rem;font-weight:500;padding:.2em .6em;border-radius:4px;background-color:#e8e8e8;color:#525252}.epistola-admin .badge{font-size:.85em;padding:.35em .65em}.epistola-admin code{font-size:.9em;color:#525252}.epistola-admin table th{font-weight:600;white-space:nowrap}.epistola-admin .contract-compatibility{display:inline-flex;align-items:center;gap:.35rem;min-height:1.5rem;padding:0 .5rem 0 .35rem;border-radius:.75rem;font-size:.75rem;font-weight:600;line-height:1;white-space:nowrap}.epistola-admin .contract-compatibility--warning{background:#f1c21b;color:#161616}.epistola-admin .contract-compatibility--unknown{background:#ff832b;color:#161616}.epistola-admin .contract-compatibility--error{background:#da1e28;color:#fff}.epistola-admin .contract-compatibility__icon{display:inline-flex;align-items:center;justify-content:center;width:1rem;height:1rem;border:1px solid currentColor;border-radius:50%;font-size:.7rem;font-weight:700}.epistola-admin .status-dot{display:inline-block;width:10px;height:10px;border-radius:50%;margin-right:.5rem;flex-shrink:0;background-color:#adb5bd}.epistola-admin .status-dot--ok{background-color:#198754}.epistola-admin .status-dot--error{background-color:#dc3545}.epistola-admin .card-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:1rem}.epistola-admin .config-card{border:1px solid #dee2e6;border-radius:8px;padding:1.25rem;cursor:pointer;transition:box-shadow .15s ease,border-color .15s ease;background:#fff}.epistola-admin .config-card:hover{box-shadow:0 2px 8px #0000001a}.epistola-admin .config-card--ok{border-left:4px solid #198754}.epistola-admin .config-card--warning{border-left:4px solid #ffc107}.epistola-admin .config-card--error{border-left:4px solid #dc3545}.epistola-admin .config-card__header{display:flex;align-items:center;margin-bottom:1rem}.epistola-admin .config-card__title{margin:0;font-size:1.05rem;font-weight:600;color:#161616}.epistola-admin .config-card__body{display:flex;flex-direction:column;gap:.5rem}.epistola-admin .config-card__field{display:flex;justify-content:space-between;align-items:center}.epistola-admin .config-card__label{font-size:.875rem;color:#6c757d}.epistola-admin .config-card__value{font-size:.875rem;color:#161616}.epistola-admin .config-card__footer{margin-top:1rem;padding-top:.75rem;border-top:1px solid #f0f0f0;text-align:right}.epistola-admin .config-card__latency{font-size:.8rem;color:#adb5bd}.epistola-admin .usage-link{color:#0f62fe;text-decoration:none}.epistola-admin .usage-link:hover{text-decoration:underline}.epistola-admin .detail-info-table{max-width:500px}.epistola-admin .detail-info-table th{width:140px}.epistola-admin .detail-summary{padding:1rem 0;border-bottom:1px solid #dee2e6}.epistola-admin .epistola-changelog{max-height:60vh;overflow:auto;padding:.5rem 1rem;border:1px solid #dee2e6;border-radius:4px}.epistola-admin .epistola-changelog .changelog-release{padding:.75rem 0}.epistola-admin .epistola-changelog .changelog-release+.changelog-release{border-top:1px solid #eee}.epistola-admin .epistola-changelog .changelog-release__header{display:flex;align-items:center;margin-bottom:.5rem}.epistola-admin .epistola-changelog .changelog-section{margin:.25rem 0 .75rem}.epistola-admin .epistola-changelog .changelog-section__title{font-size:.8125rem;font-weight:600;text-transform:uppercase;letter-spacing:.02em;color:#6f6f6f;margin-bottom:.25rem}.epistola-admin .epistola-changelog .changelog-section__items{margin:0;padding-left:1.25rem;font-size:.8125rem;line-height:1.5}.epistola-admin .epistola-changelog .changelog-section__items li{margin-bottom:.25rem;word-break:break-word}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }, { kind: "pipe", type: i1$1.DatePipe, name: "date" }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i2$4.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "ngmodule", type: PluginTranslatePipeModule }, { kind: "pipe", type: i2$1.PluginTranslatePipe, name: "pluginTranslate" }, { kind: "ngmodule", type: TabsModule }, { kind: "component", type: i5.Tabs, selector: "cds-tabs, ibm-tabs", inputs: ["position", "cacheActive", "followFocus", "isNavigation", "ariaLabel", "ariaLabelledby", "type", "theme", "skeleton"] }, { kind: "component", type: i5.Tab, selector: "cds-tab, ibm-tab", inputs: ["heading", "title", "context", "active", "disabled", "tabIndex", "id", "cacheActive", "tabContent", "templateContext"], outputs: ["selected"] }, { kind: "ngmodule", type: TagModule }, { kind: "component", type: i6.Tag, selector: "cds-tag, ibm-tag", inputs: ["type", "size", "class", "skeleton"] }] });
4763
4879
  }
4764
4880
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: EpistolaAdminPageComponent, decorators: [{
4765
4881
  type: Component,
4766
- args: [{ selector: 'epistola-admin-page', standalone: true, imports: [CommonModule, RouterModule, PluginTranslatePipeModule, TabsModule, TagModule], template: "<div class=\"epistola-admin\" data-testid=\"epistola-admin\">\n <!-- Overview: tabs (no configuration selected) -->\n <ng-container *ngIf=\"!selectedCard\">\n <div class=\"d-flex justify-content-between align-items-center mb-3\">\n <div class=\"d-flex align-items-center\">\n <h5 class=\"mb-0\">{{ 'epistolaAdminOverview' | pluginTranslate: 'epistola' | async }}</h5>\n <span *ngIf=\"pluginVersion\" class=\"version-badge ms-2\">v{{ pluginVersion }}</span>\n </div>\n <button\n class=\"btn btn-outline-primary btn-sm\"\n data-testid=\"epistola-admin-refresh\"\n (click)=\"refresh()\"\n [disabled]=\"loading\"\n >\n {{ 'epistolaAdminRefresh' | pluginTranslate: 'epistola' | async }}\n </button>\n </div>\n\n <ng-template #configurationsHeading>\n {{ 'epistolaAdminConfigurations' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" type=\"gray\" class=\"ms-1\">{{ cards.length }}</cds-tag>\n </ng-template>\n\n <ng-template #validationsHeading>\n {{ 'epistolaAdminValidations' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" [type]=\"validationViolations.length > 0 ? 'red' : 'gray'\" class=\"ms-1\">\n {{ validationViolations.length }}\n </cds-tag>\n </ng-template>\n\n <ng-template #changelogHeading>\n {{ 'epistolaAdminChangelog' | pluginTranslate: 'epistola' | async }}\n </ng-template>\n\n <!-- TEMPORARY (removed in 1.0.0): forms missing the task-id carrier -->\n <ng-template #formsHeading>\n {{ 'epistolaAdminForms' | pluginTranslate: 'epistola' | async }}\n <cds-tag\n *ngIf=\"formsScanLoaded\"\n size=\"sm\"\n [type]=\"formsAttentionCount > 0 ? 'red' : 'gray'\"\n class=\"ms-1\"\n >{{ formsAttentionCount }}</cds-tag\n >\n </ng-template>\n\n <cds-tabs [cacheActive]=\"true\" type=\"contained\" data-testid=\"epistola-admin-overview-tabs\">\n <cds-tab\n [heading]=\"configurationsHeading\"\n data-testid=\"epistola-admin-tab-configurations\"\n [active]=\"overviewTab === 'configurations'\"\n (selected)=\"setOverviewTab('configurations')\"\n >\n <div\n *ngIf=\"loading\"\n class=\"text-muted mt-3\"\n data-testid=\"epistola-admin-configurations-loading\"\n >\n {{ 'epistolaAdminLoading' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div\n *ngIf=\"!loading && cards.length === 0\"\n class=\"text-muted mt-3\"\n data-testid=\"epistola-admin-configurations-empty\"\n >\n {{ 'epistolaAdminNoConfigurations' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div\n *ngIf=\"!loading && cards.length > 0\"\n class=\"card-grid\"\n data-testid=\"epistola-admin-card-grid\"\n >\n <div\n *ngFor=\"let card of cards\"\n class=\"config-card\"\n [attr.data-testid]=\"'epistola-admin-config-card-' + card.tenantId\"\n [class.config-card--ok]=\"card.reachable && card.problemCount === 0\"\n [class.config-card--warning]=\"card.reachable && card.problemCount > 0\"\n [class.config-card--error]=\"!card.reachable\"\n (click)=\"selectConfiguration(card)\"\n >\n <div class=\"config-card__header\">\n <span\n class=\"status-dot\"\n [class.status-dot--ok]=\"card.reachable\"\n [class.status-dot--error]=\"!card.reachable\"\n >\n </span>\n <h5 class=\"config-card__title\">{{ card.configurationTitle }}</h5>\n </div>\n\n <div class=\"config-card__body\">\n <div class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminTenantId' | pluginTranslate: 'epistola' | async\n }}</span>\n <code class=\"config-card__value\">{{ card.tenantId }}</code>\n </div>\n <div class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminStatus' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" [type]=\"card.reachable ? 'green' : 'red'\">\n {{\n card.reachable\n ? ('epistolaAdminConnected' | pluginTranslate: 'epistola' | async)\n : ('epistolaAdminUnreachable' | pluginTranslate: 'epistola' | async)\n }}\n </cds-tag>\n </div>\n <div *ngIf=\"card.serverVersion\" class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminServerVersion' | pluginTranslate: 'epistola' | async\n }}</span>\n <span class=\"config-card__value\">{{ card.serverVersion }}</span>\n </div>\n <div class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminPluginActions' | pluginTranslate: 'epistola' | async\n }}</span>\n <span class=\"config-card__value\">{{ card.usageCount }}</span>\n </div>\n <div *ngIf=\"card.pendingJobs.length > 0\" class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminPendingJobs' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" type=\"blue\">{{ card.pendingJobs.length }}</cds-tag>\n </div>\n <div *ngIf=\"card.problemCount > 0\" class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminProblems' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" type=\"red\">{{ card.problemCount }}</cds-tag>\n </div>\n </div>\n\n <div class=\"config-card__footer\">\n <span class=\"config-card__latency\">{{ card.latencyMs }} ms</span>\n </div>\n </div>\n </div>\n </cds-tab>\n\n <cds-tab\n [heading]=\"validationsHeading\"\n data-testid=\"epistola-admin-tab-validations\"\n [active]=\"overviewTab === 'validations'\"\n (selected)=\"setOverviewTab('validations')\"\n >\n <div class=\"text-muted small mt-3\">\n <div>\n {{ 'epistolaAdminValidationLastChecked' | pluginTranslate: 'epistola' | async }}:\n <ng-container *ngIf=\"validationReport?.lastCheckedAt; else notYetRun\">\n {{ validationReport?.lastCheckedAt | date: 'medium' }}\n </ng-container>\n <ng-template #notYetRun>\n {{ 'epistolaAdminValidationNotYetRun' | pluginTranslate: 'epistola' | async }}\n </ng-template>\n \u00B7 {{ 'epistolaAdminValidationAutoRefresh' | pluginTranslate: 'epistola' | async }}\n {{ refreshIntervalMinutes }} min.\n </div>\n <div>\n {{ 'epistolaAdminValidationLatestVersionNote' | pluginTranslate: 'epistola' | async }}\n </div>\n </div>\n\n <div *ngIf=\"validationViolations.length === 0\" class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminNoValidations' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"validationViolations.length > 0\" class=\"mt-3\">\n <p class=\"text-muted mb-3\">\n {{ 'epistolaAdminValidationWarningBody' | pluginTranslate: 'epistola' | async }}\n </p>\n <table class=\"table table-striped\" data-testid=\"epistola-admin-validations-table\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminProcess' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminActivity' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminValidationCode' | pluginTranslate: 'epistola' | async }}</th>\n <th>\n {{ 'epistolaAdminValidationMessage' | pluginTranslate: 'epistola' | async }}\n </th>\n </tr>\n </thead>\n <tbody>\n <tr\n *ngFor=\"let v of validationViolations; let i = index\"\n [attr.data-testid]=\"'epistola-admin-validation-row-' + i\"\n >\n <td>\n {{ v.processDefinitionName || v.processDefinitionKey }}\n <div *ngIf=\"v.processDefinitionName\" class=\"text-muted small\">\n <code>{{ v.processDefinitionKey }}</code>\n </div>\n </td>\n <td>\n <code>{{ v.activityId }}</code>\n </td>\n <td>\n <cds-tag size=\"sm\" type=\"red\">{{ v.code }}</cds-tag>\n </td>\n <td>{{ v.message }}</td>\n </tr>\n </tbody>\n </table>\n </div>\n </cds-tab>\n\n <cds-tab\n [heading]=\"changelogHeading\"\n data-testid=\"epistola-admin-tab-changelog\"\n [active]=\"overviewTab === 'changelog'\"\n (selected)=\"setOverviewTab('changelog')\"\n >\n <div class=\"d-flex align-items-center mt-3 mb-3\">\n <span class=\"text-muted me-2\">{{\n 'epistolaAdminRunningVersion' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" type=\"blue\">v{{ pluginVersion || '\u2014' }}</cds-tag>\n </div>\n\n <div *ngIf=\"changelogLoading\" class=\"text-muted mb-3\">\n {{ 'epistolaAdminLoading' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div\n *ngIf=\"!changelogLoading && changelog && changelog.length === 0\"\n class=\"text-muted mb-3\"\n >\n {{ 'epistolaAdminNoChangelog' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div\n *ngIf=\"!changelogLoading && changelog && changelog.length > 0\"\n class=\"epistola-changelog\"\n >\n <section *ngFor=\"let release of changelog\" class=\"changelog-release\">\n <div class=\"changelog-release__header\">\n <cds-tag size=\"sm\" type=\"purple\">{{ release.version }}</cds-tag>\n <span *ngIf=\"release.date\" class=\"text-muted ms-2\">{{ release.date }}</span>\n </div>\n <div *ngFor=\"let section of release.sections\" class=\"changelog-section\">\n <h6 class=\"changelog-section__title\">{{ section.title }}</h6>\n <ul class=\"changelog-section__items\">\n <li *ngFor=\"let item of section.items\">{{ item }}</li>\n </ul>\n </div>\n </section>\n </div>\n </cds-tab>\n\n <!-- TEMPORARY (removed in 1.0.0): forms missing the task-id carrier -->\n <cds-tab\n [heading]=\"formsHeading\"\n data-testid=\"epistola-admin-tab-forms\"\n [active]=\"overviewTab === 'forms'\"\n (selected)=\"setOverviewTab('forms')\"\n >\n <p class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminFormsIntro' | pluginTranslate: 'epistola' | async }}\n </p>\n\n <div *ngIf=\"formIssuesLoading\" class=\"text-muted mb-3\">\n {{ 'epistolaAdminLoading' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div\n *ngIf=\"!formIssuesLoading && formIssues && formIssues.length === 0\"\n class=\"text-muted mb-3\"\n >\n {{ 'epistolaAdminNoFormIssues' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"!formIssuesLoading && formIssues && formIssues.length > 0\">\n <div class=\"mb-2\">\n <button\n class=\"btn btn-sm btn-primary\"\n data-testid=\"epistola-admin-repair-all\"\n (click)=\"repairAllForms()\"\n [disabled]=\"repairingAll\"\n >\n {{\n (repairingAll ? 'epistolaAdminRepairing' : 'epistolaAdminRepairAll')\n | pluginTranslate: 'epistola'\n | async\n }}\n </button>\n <span\n *ngIf=\"formFeedback && formFeedback.formId === 'all'\"\n class=\"small ms-2\"\n [class.text-success]=\"formFeedback.type === 'success'\"\n [class.text-danger]=\"formFeedback.type === 'error'\"\n >{{ formFeedback.message }}</span\n >\n </div>\n\n <table class=\"table table-striped\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminFormName' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminFormMissing' | pluginTranslate: 'epistola' | async }}</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n <tr\n *ngFor=\"let issue of formIssues\"\n [attr.data-testid]=\"'epistola-admin-form-issue-' + issue.formId\"\n >\n <td>\n <code>{{ issue.name }}</code>\n <cds-tag\n *ngIf=\"issue.readOnly\"\n size=\"sm\"\n type=\"warm-gray\"\n class=\"ms-1\"\n [title]=\"'epistolaAdminFormReadOnlyHint' | pluginTranslate: 'epistola' | async\"\n >{{\n 'epistolaAdminFormReadOnly' | pluginTranslate: 'epistola' | async\n }}</cds-tag\n >\n </td>\n <td>{{ issue.missingComponents }}</td>\n <td class=\"text-end\">\n <button\n class=\"btn btn-sm btn-outline-primary\"\n [attr.data-testid]=\"'epistola-admin-repair-form-' + issue.formId\"\n (click)=\"repairForm(issue)\"\n [disabled]=\"isRepairingForm(issue)\"\n [title]=\"'epistolaAdminRepairTooltip' | pluginTranslate: 'epistola' | async\"\n >\n {{\n (isRepairingForm(issue) ? 'epistolaAdminRepairing' : 'epistolaAdminRepair')\n | pluginTranslate: 'epistola'\n | async\n }}\n </button>\n <div\n *ngIf=\"formFeedback && formFeedback.formId === issue.formId\"\n class=\"small mt-1\"\n [class.text-success]=\"formFeedback.type === 'success'\"\n [class.text-danger]=\"formFeedback.type === 'error'\"\n >\n {{ formFeedback.message }}\n </div>\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n\n <!-- TEMPORARY: forms still using the legacy override-mapping object format -->\n <hr class=\"my-4\" />\n <h5 class=\"mb-1\">\n {{ 'epistolaAdminLegacyOverrideTitle' | pluginTranslate: 'epistola' | async }}\n </h5>\n <p class=\"text-muted mb-3\">\n {{ 'epistolaAdminLegacyOverrideIntro' | pluginTranslate: 'epistola' | async }}\n </p>\n\n <div *ngIf=\"legacyOverrideLoading\" class=\"text-muted mb-3\">\n {{ 'epistolaAdminLoading' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div\n *ngIf=\"!legacyOverrideLoading && legacyOverrideForms && legacyOverrideForms.length === 0\"\n class=\"text-muted mb-3\"\n >\n {{ 'epistolaAdminNoLegacyOverride' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <table\n *ngIf=\"!legacyOverrideLoading && legacyOverrideForms && legacyOverrideForms.length > 0\"\n class=\"table table-striped\"\n >\n <thead>\n <tr>\n <th>{{ 'epistolaAdminFormName' | pluginTranslate: 'epistola' | async }}</th>\n <th>\n {{ 'epistolaAdminLegacyOverrideComponents' | pluginTranslate: 'epistola' | async }}\n </th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let form of legacyOverrideForms\">\n <td>\n <code>{{ form.name }}</code>\n <cds-tag\n *ngIf=\"form.readOnly\"\n size=\"sm\"\n type=\"warm-gray\"\n class=\"ms-1\"\n [title]=\"'epistolaAdminFormReadOnlyHint' | pluginTranslate: 'epistola' | async\"\n >{{ 'epistolaAdminFormReadOnly' | pluginTranslate: 'epistola' | async }}</cds-tag\n >\n </td>\n <td>{{ form.legacyComponents }}</td>\n </tr>\n </tbody>\n </table>\n </cds-tab>\n </cds-tabs>\n </ng-container>\n\n <!-- Detail view: selected configuration -->\n <ng-container *ngIf=\"selectedCard\">\n <div class=\"detail-header mb-3\">\n <button\n class=\"btn btn-link btn-sm p-0\"\n data-testid=\"epistola-admin-back\"\n (click)=\"backToOverview()\"\n >\n &larr; {{ 'epistolaAdminBackToOverview' | pluginTranslate: 'epistola' | async }}\n </button>\n </div>\n\n <div class=\"detail-summary mb-4\">\n <h4>\n <span\n class=\"status-dot me-2\"\n [class.status-dot--ok]=\"selectedCard.reachable\"\n [class.status-dot--error]=\"!selectedCard.reachable\"\n >\n </span>\n {{ selectedCard.configurationTitle }}\n </h4>\n\n <table class=\"table table-sm detail-info-table\">\n <tbody>\n <tr>\n <th>{{ 'epistolaAdminTenantId' | pluginTranslate: 'epistola' | async }}</th>\n <td>\n <code>{{ selectedCard.tenantId }}</code>\n </td>\n </tr>\n <tr>\n <th>{{ 'epistolaAdminStatus' | pluginTranslate: 'epistola' | async }}</th>\n <td>\n <cds-tag size=\"sm\" [type]=\"selectedCard.reachable ? 'green' : 'red'\">\n {{\n selectedCard.reachable\n ? ('epistolaAdminConnected' | pluginTranslate: 'epistola' | async)\n : ('epistolaAdminUnreachable' | pluginTranslate: 'epistola' | async)\n }}\n </cds-tag>\n <span class=\"text-muted ms-2\">{{ selectedCard.latencyMs }} ms</span>\n </td>\n </tr>\n <tr *ngIf=\"selectedCard.serverVersion\">\n <th>{{ 'epistolaAdminServerVersion' | pluginTranslate: 'epistola' | async }}</th>\n <td>{{ selectedCard.serverVersion }}</td>\n </tr>\n <tr *ngIf=\"selectedCard.errorMessage\">\n <th>{{ 'epistolaAdminError' | pluginTranslate: 'epistola' | async }}</th>\n <td class=\"text-danger\">{{ selectedCard.errorMessage }}</td>\n </tr>\n </tbody>\n </table>\n </div>\n\n <!-- Tabs -->\n <ng-template #actionsHeading>\n {{ 'epistolaAdminPluginActions' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" type=\"gray\" class=\"ms-1\">{{ selectedCard.usageEntries.length }}</cds-tag>\n </ng-template>\n\n <ng-template #pendingHeading>\n {{ 'epistolaAdminPendingJobs' | pluginTranslate: 'epistola' | async }}\n <cds-tag\n size=\"sm\"\n [type]=\"selectedCard.pendingJobs.length > 0 ? 'blue' : 'gray'\"\n class=\"ms-1\"\n >\n {{ selectedCard.pendingJobs.length }}\n </cds-tag>\n </ng-template>\n\n <ng-template #catalogsHeading>\n {{ 'epistolaAdminCatalogs' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" type=\"gray\" class=\"ms-1\">{{ catalogs.length }}</cds-tag>\n </ng-template>\n\n <cds-tabs [cacheActive]=\"true\" type=\"contained\" data-testid=\"epistola-admin-detail-tabs\">\n <cds-tab\n [heading]=\"actionsHeading\"\n data-testid=\"epistola-admin-detail-tab-actions\"\n [active]=\"activeTab === 'actions'\"\n (selected)=\"setActiveTab('actions')\"\n >\n <div *ngIf=\"selectedCard.usageEntries.length === 0\" class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminNoUsageForConfig' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <table *ngIf=\"selectedCard.usageEntries.length > 0\" class=\"table table-striped mt-3\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminCase' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminProcess' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminActivity' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminAction' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminProblems' | pluginTranslate: 'epistola' | async }}</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n <tr\n *ngFor=\"let entry of selectedCard.usageEntries; let i = index\"\n [attr.data-testid]=\"'epistola-admin-usage-row-' + i\"\n [class.table-warning]=\"entry.problems.length > 0\"\n >\n <td>{{ entry.caseDefinitionKey || '-' }}</td>\n <td>\n <a\n *ngIf=\"entry.caseDefinitionKey && entry.caseDefinitionVersionTag\"\n [routerLink]=\"[\n '/case-management',\n 'case',\n entry.caseDefinitionKey,\n 'version',\n entry.caseDefinitionVersionTag,\n 'processes',\n entry.processDefinitionKey,\n ]\"\n class=\"usage-link\"\n >\n {{ entry.processDefinitionName }}\n </a>\n <span *ngIf=\"!entry.caseDefinitionKey || !entry.caseDefinitionVersionTag\">\n {{ entry.processDefinitionName }}\n </span>\n </td>\n <td>{{ entry.activityName }}</td>\n <td>\n <code>{{ entry.actionKey }}</code>\n </td>\n <td>\n <cds-tag *ngIf=\"entry.problems.length === 0\" size=\"sm\" type=\"green\">OK</cds-tag>\n <cds-tag\n *ngFor=\"let problem of entry.problems\"\n size=\"sm\"\n type=\"red\"\n class=\"d-block mb-1\"\n >\n {{ problem }}\n </cds-tag>\n </td>\n <td>\n <button\n class=\"btn btn-sm btn-outline-secondary\"\n [attr.data-testid]=\"'epistola-admin-export-' + i\"\n (click)=\"exportProcessLink(entry)\"\n [title]=\"'epistolaAdminExport' | pluginTranslate: 'epistola' | async\"\n >\n &#x2913;\n </button>\n </td>\n </tr>\n </tbody>\n </table>\n </cds-tab>\n\n <cds-tab\n [heading]=\"pendingHeading\"\n data-testid=\"epistola-admin-detail-tab-pending\"\n [active]=\"activeTab === 'pending'\"\n (selected)=\"setActiveTab('pending')\"\n >\n <div *ngIf=\"selectedCard.pendingJobs.length === 0\" class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminNoPendingJobs' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <table *ngIf=\"selectedCard.pendingJobs.length > 0\" class=\"table table-striped mt-3\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminProcess' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminActivity' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminStatus' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminRequestId' | pluginTranslate: 'epistola' | async }}</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n <tr\n *ngFor=\"let job of selectedCard.pendingJobs\"\n [attr.data-testid]=\"'epistola-admin-pending-row-' + job.executionId\"\n >\n <td>{{ job.processDefinitionName }}</td>\n <td>{{ job.activityName }}</td>\n <td>\n <cds-tag\n *ngIf=\"job.status === 'UNWIRED'; else waitingTag\"\n size=\"sm\"\n type=\"red\"\n [title]=\"'epistolaAdminUnwiredTooltip' | pluginTranslate: 'epistola' | async\"\n >\n {{ 'epistolaAdminStatusUnwired' | pluginTranslate: 'epistola' | async }}\n </cds-tag>\n <ng-template #waitingTag>\n <cds-tag size=\"sm\" type=\"blue\">\n {{ 'epistolaAdminStatusWaiting' | pluginTranslate: 'epistola' | async }}\n </cds-tag>\n </ng-template>\n </td>\n <td>\n <code *ngIf=\"job.status !== 'UNWIRED'\">{{ job.requestId }}</code>\n <span *ngIf=\"job.status === 'UNWIRED'\" class=\"text-muted\">\u2014</span>\n </td>\n <td class=\"text-end\">\n <button\n *ngIf=\"job.status !== 'UNWIRED'\"\n class=\"btn btn-sm btn-outline-primary\"\n [attr.data-testid]=\"'epistola-admin-reconcile-' + job.executionId\"\n (click)=\"reconcilePending(job)\"\n [disabled]=\"isReconciling(job)\"\n [title]=\"'epistolaAdminReconcileTooltip' | pluginTranslate: 'epistola' | async\"\n >\n <span *ngIf=\"!isReconciling(job)\">\n {{ 'epistolaAdminReconcile' | pluginTranslate: 'epistola' | async }}\n </span>\n <span *ngIf=\"isReconciling(job)\">\n {{ 'epistolaAdminReconciling' | pluginTranslate: 'epistola' | async }}\n </span>\n </button>\n <span\n *ngIf=\"job.status === 'UNWIRED'\"\n class=\"text-muted small\"\n [title]=\"'epistolaAdminUnwiredTooltip' | pluginTranslate: 'epistola' | async\"\n >\n {{ 'epistolaAdminUnwiredHint' | pluginTranslate: 'epistola' | async }}\n </span>\n <div\n *ngIf=\"reconcileFeedback && reconcileFeedback.executionId === job.executionId\"\n class=\"reconcile-feedback small mt-1\"\n [class.text-success]=\"reconcileFeedback.type === 'success'\"\n [class.text-warning]=\"reconcileFeedback.type === 'pending'\"\n [class.text-danger]=\"reconcileFeedback.type === 'error'\"\n >\n {{ reconcileFeedback.message }}\n </div>\n </td>\n </tr>\n </tbody>\n </table>\n </cds-tab>\n\n <cds-tab\n [heading]=\"catalogsHeading\"\n data-testid=\"epistola-admin-detail-tab-catalogs\"\n [active]=\"activeTab === 'catalogs'\"\n (selected)=\"setActiveTab('catalogs')\"\n >\n <p class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminCatalogsIntro' | pluginTranslate: 'epistola' | async }}\n </p>\n\n <div *ngIf=\"catalogsLoading\" class=\"text-muted mb-3\">\n {{ 'epistolaAdminLoading' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"!catalogsLoading && catalogs.length === 0\" class=\"text-muted mb-3\">\n {{ 'epistolaAdminNoCatalogs' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <table *ngIf=\"!catalogsLoading && catalogs.length > 0\" class=\"table table-striped\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminCatalogSlug' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminCatalogVersion' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminCatalogStatus' | pluginTranslate: 'epistola' | async }}</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n <tr\n *ngFor=\"let catalog of catalogs\"\n [attr.data-testid]=\"'epistola-admin-catalog-row-' + catalog.slug\"\n >\n <td>\n <code>{{ catalog.slug }}</code>\n </td>\n <td>{{ catalog.version }}</td>\n <td>\n <cds-tag *ngIf=\"catalog.status === 'IN_EPISTOLA'\" size=\"sm\" type=\"green\">\n {{ 'epistolaAdminCatalogInEpistola' | pluginTranslate: 'epistola' | async }}\n </cds-tag>\n <cds-tag *ngIf=\"catalog.status === 'NOT_IN_EPISTOLA'\" size=\"sm\" type=\"red\">\n {{ 'epistolaAdminCatalogNotInEpistola' | pluginTranslate: 'epistola' | async }}\n </cds-tag>\n <cds-tag *ngIf=\"catalog.status === 'UNKNOWN'\" size=\"sm\" type=\"gray\">\n {{ 'epistolaAdminCatalogStatusUnknown' | pluginTranslate: 'epistola' | async }}\n </cds-tag>\n </td>\n <td class=\"text-end\">\n <button\n class=\"btn btn-sm btn-outline-primary\"\n [attr.data-testid]=\"'epistola-admin-redeploy-' + catalog.slug\"\n (click)=\"redeployCatalog(catalog)\"\n [disabled]=\"isRedeploying(catalog)\"\n [title]=\"'epistolaAdminRedeployTooltip' | pluginTranslate: 'epistola' | async\"\n >\n <span *ngIf=\"!isRedeploying(catalog)\">\n {{ 'epistolaAdminRedeploy' | pluginTranslate: 'epistola' | async }}\n </span>\n <span *ngIf=\"isRedeploying(catalog)\">\n {{ 'epistolaAdminRedeploying' | pluginTranslate: 'epistola' | async }}\n </span>\n </button>\n <div\n *ngIf=\"catalogFeedback && catalogFeedback.slug === catalog.slug\"\n class=\"reconcile-feedback small mt-1\"\n [class.text-success]=\"catalogFeedback.type === 'success'\"\n [class.text-danger]=\"catalogFeedback.type === 'error'\"\n >\n {{ catalogFeedback.message }}\n </div>\n </td>\n </tr>\n </tbody>\n </table>\n </cds-tab>\n </cds-tabs>\n </ng-container>\n</div>\n", styles: [".epistola-admin{padding:1.5rem}.epistola-admin .version-badge{font-size:.75rem;font-weight:500;padding:.2em .6em;border-radius:4px;background-color:#e8e8e8;color:#525252}.epistola-admin .badge{font-size:.85em;padding:.35em .65em}.epistola-admin code{font-size:.9em;color:#525252}.epistola-admin table th{font-weight:600;white-space:nowrap}.epistola-admin .status-dot{display:inline-block;width:10px;height:10px;border-radius:50%;margin-right:.5rem;flex-shrink:0;background-color:#adb5bd}.epistola-admin .status-dot--ok{background-color:#198754}.epistola-admin .status-dot--error{background-color:#dc3545}.epistola-admin .card-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:1rem}.epistola-admin .config-card{border:1px solid #dee2e6;border-radius:8px;padding:1.25rem;cursor:pointer;transition:box-shadow .15s ease,border-color .15s ease;background:#fff}.epistola-admin .config-card:hover{box-shadow:0 2px 8px #0000001a}.epistola-admin .config-card--ok{border-left:4px solid #198754}.epistola-admin .config-card--warning{border-left:4px solid #ffc107}.epistola-admin .config-card--error{border-left:4px solid #dc3545}.epistola-admin .config-card__header{display:flex;align-items:center;margin-bottom:1rem}.epistola-admin .config-card__title{margin:0;font-size:1.05rem;font-weight:600;color:#161616}.epistola-admin .config-card__body{display:flex;flex-direction:column;gap:.5rem}.epistola-admin .config-card__field{display:flex;justify-content:space-between;align-items:center}.epistola-admin .config-card__label{font-size:.875rem;color:#6c757d}.epistola-admin .config-card__value{font-size:.875rem;color:#161616}.epistola-admin .config-card__footer{margin-top:1rem;padding-top:.75rem;border-top:1px solid #f0f0f0;text-align:right}.epistola-admin .config-card__latency{font-size:.8rem;color:#adb5bd}.epistola-admin .usage-link{color:#0f62fe;text-decoration:none}.epistola-admin .usage-link:hover{text-decoration:underline}.epistola-admin .detail-info-table{max-width:500px}.epistola-admin .detail-info-table th{width:140px}.epistola-admin .detail-summary{padding:1rem 0;border-bottom:1px solid #dee2e6}.epistola-admin .epistola-changelog{max-height:60vh;overflow:auto;padding:.5rem 1rem;border:1px solid #dee2e6;border-radius:4px}.epistola-admin .epistola-changelog .changelog-release{padding:.75rem 0}.epistola-admin .epistola-changelog .changelog-release+.changelog-release{border-top:1px solid #eee}.epistola-admin .epistola-changelog .changelog-release__header{display:flex;align-items:center;margin-bottom:.5rem}.epistola-admin .epistola-changelog .changelog-section{margin:.25rem 0 .75rem}.epistola-admin .epistola-changelog .changelog-section__title{font-size:.8125rem;font-weight:600;text-transform:uppercase;letter-spacing:.02em;color:#6f6f6f;margin-bottom:.25rem}.epistola-admin .epistola-changelog .changelog-section__items{margin:0;padding-left:1.25rem;font-size:.8125rem;line-height:1.5}.epistola-admin .epistola-changelog .changelog-section__items li{margin-bottom:.25rem;word-break:break-word}\n"] }]
4882
+ args: [{ selector: 'epistola-admin-page', standalone: true, imports: [CommonModule, RouterModule, PluginTranslatePipeModule, TabsModule, TagModule], template: "<div class=\"epistola-admin\" data-testid=\"epistola-admin\">\n <!-- Overview: tabs (no configuration selected) -->\n <ng-container *ngIf=\"!selectedCard\">\n <div class=\"d-flex justify-content-between align-items-center mb-3\">\n <div class=\"d-flex align-items-center\">\n <h5 class=\"mb-0\">{{ 'epistolaAdminOverview' | pluginTranslate: 'epistola' | async }}</h5>\n <span *ngIf=\"pluginVersion || pluginContractVersion\" class=\"version-badge ms-2\">\n v{{ versionWithContract(pluginVersion, pluginContractVersion) }}\n </span>\n </div>\n <button\n class=\"btn btn-outline-primary btn-sm\"\n data-testid=\"epistola-admin-refresh\"\n (click)=\"refresh()\"\n [disabled]=\"loading\"\n >\n {{ 'epistolaAdminRefresh' | pluginTranslate: 'epistola' | async }}\n </button>\n </div>\n\n <ng-template #configurationsHeading>\n {{ 'epistolaAdminConfigurations' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" type=\"gray\" class=\"ms-1\">{{ cards.length }}</cds-tag>\n </ng-template>\n\n <ng-template #validationsHeading>\n {{ 'epistolaAdminValidations' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" [type]=\"validationViolations.length > 0 ? 'red' : 'gray'\" class=\"ms-1\">\n {{ validationViolations.length }}\n </cds-tag>\n </ng-template>\n\n <ng-template #changelogHeading>\n {{ 'epistolaAdminChangelog' | pluginTranslate: 'epistola' | async }}\n </ng-template>\n\n <!-- TEMPORARY (removed in 1.0.0): forms missing the task-id carrier -->\n <ng-template #formsHeading>\n {{ 'epistolaAdminForms' | pluginTranslate: 'epistola' | async }}\n <cds-tag\n *ngIf=\"formsScanLoaded\"\n size=\"sm\"\n [type]=\"formsAttentionCount > 0 ? 'red' : 'gray'\"\n class=\"ms-1\"\n >{{ formsAttentionCount }}</cds-tag\n >\n </ng-template>\n\n <cds-tabs [cacheActive]=\"true\" type=\"contained\" data-testid=\"epistola-admin-overview-tabs\">\n <cds-tab\n [heading]=\"configurationsHeading\"\n data-testid=\"epistola-admin-tab-configurations\"\n [active]=\"overviewTab === 'configurations'\"\n (selected)=\"setOverviewTab('configurations')\"\n >\n <div\n *ngIf=\"loading\"\n class=\"text-muted mt-3\"\n data-testid=\"epistola-admin-configurations-loading\"\n >\n {{ 'epistolaAdminLoading' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div\n *ngIf=\"!loading && cards.length === 0\"\n class=\"text-muted mt-3\"\n data-testid=\"epistola-admin-configurations-empty\"\n >\n {{ 'epistolaAdminNoConfigurations' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div\n *ngIf=\"!loading && cards.length > 0\"\n class=\"card-grid\"\n data-testid=\"epistola-admin-card-grid\"\n >\n <div\n *ngFor=\"let card of cards\"\n class=\"config-card\"\n [attr.data-testid]=\"'epistola-admin-config-card-' + card.tenantId\"\n [class.config-card--ok]=\"\n card.reachable && card.problemCount === 0 && !hasContractCompatibilityWarning(card)\n \"\n [class.config-card--warning]=\"\n card.reachable &&\n (card.problemCount > 0 ||\n card.contractCompatibilitySeverity === 'WARNING' ||\n card.contractCompatibilitySeverity === 'UNKNOWN')\n \"\n [class.config-card--error]=\"\n !card.reachable || card.contractCompatibilitySeverity === 'ERROR'\n \"\n (click)=\"selectConfiguration(card)\"\n >\n <div class=\"config-card__header\">\n <span\n class=\"status-dot\"\n [class.status-dot--ok]=\"card.reachable\"\n [class.status-dot--error]=\"!card.reachable\"\n >\n </span>\n <h5 class=\"config-card__title\">{{ card.configurationTitle }}</h5>\n </div>\n\n <div class=\"config-card__body\">\n <div class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminTenantId' | pluginTranslate: 'epistola' | async\n }}</span>\n <code class=\"config-card__value\">{{ card.tenantId }}</code>\n </div>\n <div class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminStatus' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" [type]=\"card.reachable ? 'green' : 'red'\">\n {{\n card.reachable\n ? ('epistolaAdminConnected' | pluginTranslate: 'epistola' | async)\n : ('epistolaAdminUnreachable' | pluginTranslate: 'epistola' | async)\n }}\n </cds-tag>\n </div>\n <div\n *ngIf=\"card.serverVersion || card.serverContractVersion\"\n class=\"config-card__field\"\n >\n <span class=\"config-card__label\">{{\n 'epistolaAdminServerVersion' | pluginTranslate: 'epistola' | async\n }}</span>\n <span class=\"config-card__value\">\n {{ versionWithContract(card.serverVersion, card.serverContractVersion) }}\n </span>\n </div>\n <div *ngIf=\"hasContractCompatibilityWarning(card)\" class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminContractCompatibility' | pluginTranslate: 'epistola' | async\n }}</span>\n <span\n class=\"contract-compatibility\"\n [ngClass]=\"contractBadgeClass(card)\"\n [title]=\"(contractBodyKey(card) | pluginTranslate: 'epistola' | async) || ''\"\n [attr.data-testid]=\"'epistola-admin-contract-alert-' + card.tenantId\"\n >\n <span class=\"contract-compatibility__icon\" aria-hidden=\"true\">!</span>\n {{ contractLabelKey(card) | pluginTranslate: 'epistola' | async }}\n </span>\n </div>\n <div class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminPluginActions' | pluginTranslate: 'epistola' | async\n }}</span>\n <span class=\"config-card__value\">{{ card.usageCount }}</span>\n </div>\n <div *ngIf=\"card.pendingJobs.length > 0\" class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminPendingJobs' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" type=\"blue\">{{ card.pendingJobs.length }}</cds-tag>\n </div>\n <div *ngIf=\"card.problemCount > 0\" class=\"config-card__field\">\n <span class=\"config-card__label\">{{\n 'epistolaAdminProblems' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" type=\"red\">{{ card.problemCount }}</cds-tag>\n </div>\n </div>\n\n <div class=\"config-card__footer\">\n <span class=\"config-card__latency\">{{ card.latencyMs }} ms</span>\n </div>\n </div>\n </div>\n </cds-tab>\n\n <cds-tab\n [heading]=\"validationsHeading\"\n data-testid=\"epistola-admin-tab-validations\"\n [active]=\"overviewTab === 'validations'\"\n (selected)=\"setOverviewTab('validations')\"\n >\n <div class=\"text-muted small mt-3\">\n <div>\n {{ 'epistolaAdminValidationLastChecked' | pluginTranslate: 'epistola' | async }}:\n <ng-container *ngIf=\"validationReport?.lastCheckedAt; else notYetRun\">\n {{ validationReport?.lastCheckedAt | date: 'medium' }}\n </ng-container>\n <ng-template #notYetRun>\n {{ 'epistolaAdminValidationNotYetRun' | pluginTranslate: 'epistola' | async }}\n </ng-template>\n \u00B7 {{ 'epistolaAdminValidationAutoRefresh' | pluginTranslate: 'epistola' | async }}\n {{ refreshIntervalMinutes }} min.\n </div>\n <div>\n {{ 'epistolaAdminValidationLatestVersionNote' | pluginTranslate: 'epistola' | async }}\n </div>\n </div>\n\n <div *ngIf=\"validationViolations.length === 0\" class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminNoValidations' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"validationViolations.length > 0\" class=\"mt-3\">\n <p class=\"text-muted mb-3\">\n {{ 'epistolaAdminValidationWarningBody' | pluginTranslate: 'epistola' | async }}\n </p>\n <table class=\"table table-striped\" data-testid=\"epistola-admin-validations-table\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminProcess' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminActivity' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminValidationCode' | pluginTranslate: 'epistola' | async }}</th>\n <th>\n {{ 'epistolaAdminValidationMessage' | pluginTranslate: 'epistola' | async }}\n </th>\n </tr>\n </thead>\n <tbody>\n <tr\n *ngFor=\"let v of validationViolations; let i = index\"\n [attr.data-testid]=\"'epistola-admin-validation-row-' + i\"\n >\n <td>\n {{ v.processDefinitionName || v.processDefinitionKey }}\n <div *ngIf=\"v.processDefinitionName\" class=\"text-muted small\">\n <code>{{ v.processDefinitionKey }}</code>\n </div>\n </td>\n <td>\n <code>{{ v.activityId }}</code>\n </td>\n <td>\n <cds-tag size=\"sm\" type=\"red\">{{ v.code }}</cds-tag>\n </td>\n <td>{{ v.message }}</td>\n </tr>\n </tbody>\n </table>\n </div>\n </cds-tab>\n\n <cds-tab\n [heading]=\"changelogHeading\"\n data-testid=\"epistola-admin-tab-changelog\"\n [active]=\"overviewTab === 'changelog'\"\n (selected)=\"setOverviewTab('changelog')\"\n >\n <div class=\"d-flex align-items-center mt-3 mb-3\">\n <span class=\"text-muted me-2\">{{\n 'epistolaAdminRunningVersion' | pluginTranslate: 'epistola' | async\n }}</span>\n <cds-tag size=\"sm\" type=\"blue\">\n v{{ versionWithContract(pluginVersion, pluginContractVersion) }}\n </cds-tag>\n </div>\n\n <div *ngIf=\"changelogLoading\" class=\"text-muted mb-3\">\n {{ 'epistolaAdminLoading' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div\n *ngIf=\"!changelogLoading && changelog && changelog.length === 0\"\n class=\"text-muted mb-3\"\n >\n {{ 'epistolaAdminNoChangelog' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div\n *ngIf=\"!changelogLoading && changelog && changelog.length > 0\"\n class=\"epistola-changelog\"\n >\n <section *ngFor=\"let release of changelog\" class=\"changelog-release\">\n <div class=\"changelog-release__header\">\n <cds-tag size=\"sm\" type=\"purple\">{{ release.version }}</cds-tag>\n <span *ngIf=\"release.date\" class=\"text-muted ms-2\">{{ release.date }}</span>\n </div>\n <div *ngFor=\"let section of release.sections\" class=\"changelog-section\">\n <h6 class=\"changelog-section__title\">{{ section.title }}</h6>\n <ul class=\"changelog-section__items\">\n <li *ngFor=\"let item of section.items\">{{ item }}</li>\n </ul>\n </div>\n </section>\n </div>\n </cds-tab>\n\n <!-- TEMPORARY (removed in 1.0.0): forms missing the task-id carrier -->\n <cds-tab\n [heading]=\"formsHeading\"\n data-testid=\"epistola-admin-tab-forms\"\n [active]=\"overviewTab === 'forms'\"\n (selected)=\"setOverviewTab('forms')\"\n >\n <p class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminFormsIntro' | pluginTranslate: 'epistola' | async }}\n </p>\n\n <div *ngIf=\"formIssuesLoading\" class=\"text-muted mb-3\">\n {{ 'epistolaAdminLoading' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div\n *ngIf=\"!formIssuesLoading && formIssues && formIssues.length === 0\"\n class=\"text-muted mb-3\"\n >\n {{ 'epistolaAdminNoFormIssues' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"!formIssuesLoading && formIssues && formIssues.length > 0\">\n <div class=\"mb-2\">\n <button\n class=\"btn btn-sm btn-primary\"\n data-testid=\"epistola-admin-repair-all\"\n (click)=\"repairAllForms()\"\n [disabled]=\"repairingAll\"\n >\n {{\n (repairingAll ? 'epistolaAdminRepairing' : 'epistolaAdminRepairAll')\n | pluginTranslate: 'epistola'\n | async\n }}\n </button>\n <span\n *ngIf=\"formFeedback && formFeedback.formId === 'all'\"\n class=\"small ms-2\"\n [class.text-success]=\"formFeedback.type === 'success'\"\n [class.text-danger]=\"formFeedback.type === 'error'\"\n >{{ formFeedback.message }}</span\n >\n </div>\n\n <table class=\"table table-striped\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminFormName' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminFormMissing' | pluginTranslate: 'epistola' | async }}</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n <tr\n *ngFor=\"let issue of formIssues\"\n [attr.data-testid]=\"'epistola-admin-form-issue-' + issue.formId\"\n >\n <td>\n <code>{{ issue.name }}</code>\n <cds-tag\n *ngIf=\"issue.readOnly\"\n size=\"sm\"\n type=\"warm-gray\"\n class=\"ms-1\"\n [title]=\"'epistolaAdminFormReadOnlyHint' | pluginTranslate: 'epistola' | async\"\n >{{\n 'epistolaAdminFormReadOnly' | pluginTranslate: 'epistola' | async\n }}</cds-tag\n >\n </td>\n <td>{{ issue.missingComponents }}</td>\n <td class=\"text-end\">\n <button\n class=\"btn btn-sm btn-outline-primary\"\n [attr.data-testid]=\"'epistola-admin-repair-form-' + issue.formId\"\n (click)=\"repairForm(issue)\"\n [disabled]=\"isRepairingForm(issue)\"\n [title]=\"'epistolaAdminRepairTooltip' | pluginTranslate: 'epistola' | async\"\n >\n {{\n (isRepairingForm(issue) ? 'epistolaAdminRepairing' : 'epistolaAdminRepair')\n | pluginTranslate: 'epistola'\n | async\n }}\n </button>\n <div\n *ngIf=\"formFeedback && formFeedback.formId === issue.formId\"\n class=\"small mt-1\"\n [class.text-success]=\"formFeedback.type === 'success'\"\n [class.text-danger]=\"formFeedback.type === 'error'\"\n >\n {{ formFeedback.message }}\n </div>\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n\n <!-- TEMPORARY: forms still using the legacy override-mapping object format -->\n <hr class=\"my-4\" />\n <h5 class=\"mb-1\">\n {{ 'epistolaAdminLegacyOverrideTitle' | pluginTranslate: 'epistola' | async }}\n </h5>\n <p class=\"text-muted mb-3\">\n {{ 'epistolaAdminLegacyOverrideIntro' | pluginTranslate: 'epistola' | async }}\n </p>\n\n <div *ngIf=\"legacyOverrideLoading\" class=\"text-muted mb-3\">\n {{ 'epistolaAdminLoading' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div\n *ngIf=\"!legacyOverrideLoading && legacyOverrideForms && legacyOverrideForms.length === 0\"\n class=\"text-muted mb-3\"\n >\n {{ 'epistolaAdminNoLegacyOverride' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <table\n *ngIf=\"!legacyOverrideLoading && legacyOverrideForms && legacyOverrideForms.length > 0\"\n class=\"table table-striped\"\n >\n <thead>\n <tr>\n <th>{{ 'epistolaAdminFormName' | pluginTranslate: 'epistola' | async }}</th>\n <th>\n {{ 'epistolaAdminLegacyOverrideComponents' | pluginTranslate: 'epistola' | async }}\n </th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let form of legacyOverrideForms\">\n <td>\n <code>{{ form.name }}</code>\n <cds-tag\n *ngIf=\"form.readOnly\"\n size=\"sm\"\n type=\"warm-gray\"\n class=\"ms-1\"\n [title]=\"'epistolaAdminFormReadOnlyHint' | pluginTranslate: 'epistola' | async\"\n >{{ 'epistolaAdminFormReadOnly' | pluginTranslate: 'epistola' | async }}</cds-tag\n >\n </td>\n <td>{{ form.legacyComponents }}</td>\n </tr>\n </tbody>\n </table>\n </cds-tab>\n </cds-tabs>\n </ng-container>\n\n <!-- Detail view: selected configuration -->\n <ng-container *ngIf=\"selectedCard\">\n <div class=\"detail-header mb-3\">\n <button\n class=\"btn btn-link btn-sm p-0\"\n data-testid=\"epistola-admin-back\"\n (click)=\"backToOverview()\"\n >\n &larr; {{ 'epistolaAdminBackToOverview' | pluginTranslate: 'epistola' | async }}\n </button>\n </div>\n\n <div class=\"detail-summary mb-4\">\n <h4>\n <span\n class=\"status-dot me-2\"\n [class.status-dot--ok]=\"selectedCard.reachable\"\n [class.status-dot--error]=\"!selectedCard.reachable\"\n >\n </span>\n {{ selectedCard.configurationTitle }}\n </h4>\n\n <table class=\"table table-sm detail-info-table\">\n <tbody>\n <tr>\n <th>{{ 'epistolaAdminTenantId' | pluginTranslate: 'epistola' | async }}</th>\n <td>\n <code>{{ selectedCard.tenantId }}</code>\n </td>\n </tr>\n <tr>\n <th>{{ 'epistolaAdminStatus' | pluginTranslate: 'epistola' | async }}</th>\n <td>\n <cds-tag size=\"sm\" [type]=\"selectedCard.reachable ? 'green' : 'red'\">\n {{\n selectedCard.reachable\n ? ('epistolaAdminConnected' | pluginTranslate: 'epistola' | async)\n : ('epistolaAdminUnreachable' | pluginTranslate: 'epistola' | async)\n }}\n </cds-tag>\n <span class=\"text-muted ms-2\">{{ selectedCard.latencyMs }} ms</span>\n </td>\n </tr>\n <tr *ngIf=\"selectedCard.serverVersion || selectedCard.serverContractVersion\">\n <th>{{ 'epistolaAdminServerVersion' | pluginTranslate: 'epistola' | async }}</th>\n <td>\n {{\n versionWithContract(selectedCard.serverVersion, selectedCard.serverContractVersion)\n }}\n </td>\n </tr>\n <tr *ngIf=\"hasContractCompatibilityWarning(selectedCard)\">\n <th>\n {{ 'epistolaAdminContractCompatibility' | pluginTranslate: 'epistola' | async }}\n </th>\n <td>\n <span\n class=\"contract-compatibility\"\n [ngClass]=\"contractBadgeClass(selectedCard)\"\n [title]=\"\n (contractBodyKey(selectedCard) | pluginTranslate: 'epistola' | async) || ''\n \"\n data-testid=\"epistola-admin-selected-contract-alert\"\n >\n <span class=\"contract-compatibility__icon\" aria-hidden=\"true\">!</span>\n {{ contractLabelKey(selectedCard) | pluginTranslate: 'epistola' | async }}\n </span>\n </td>\n </tr>\n <tr *ngIf=\"selectedCard.errorMessage\">\n <th>{{ 'epistolaAdminError' | pluginTranslate: 'epistola' | async }}</th>\n <td class=\"text-danger\">{{ selectedCard.errorMessage }}</td>\n </tr>\n </tbody>\n </table>\n </div>\n\n <!-- Tabs -->\n <ng-template #actionsHeading>\n {{ 'epistolaAdminPluginActions' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" type=\"gray\" class=\"ms-1\">{{ selectedCard.usageEntries.length }}</cds-tag>\n </ng-template>\n\n <ng-template #pendingHeading>\n {{ 'epistolaAdminPendingJobs' | pluginTranslate: 'epistola' | async }}\n <cds-tag\n size=\"sm\"\n [type]=\"selectedCard.pendingJobs.length > 0 ? 'blue' : 'gray'\"\n class=\"ms-1\"\n >\n {{ selectedCard.pendingJobs.length }}\n </cds-tag>\n </ng-template>\n\n <ng-template #catalogsHeading>\n {{ 'epistolaAdminCatalogs' | pluginTranslate: 'epistola' | async }}\n <cds-tag size=\"sm\" type=\"gray\" class=\"ms-1\">{{ catalogs.length }}</cds-tag>\n </ng-template>\n\n <cds-tabs [cacheActive]=\"true\" type=\"contained\" data-testid=\"epistola-admin-detail-tabs\">\n <cds-tab\n [heading]=\"actionsHeading\"\n data-testid=\"epistola-admin-detail-tab-actions\"\n [active]=\"activeTab === 'actions'\"\n (selected)=\"setActiveTab('actions')\"\n >\n <div *ngIf=\"selectedCard.usageEntries.length === 0\" class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminNoUsageForConfig' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <table *ngIf=\"selectedCard.usageEntries.length > 0\" class=\"table table-striped mt-3\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminCase' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminProcess' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminActivity' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminAction' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminProblems' | pluginTranslate: 'epistola' | async }}</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n <tr\n *ngFor=\"let entry of selectedCard.usageEntries; let i = index\"\n [attr.data-testid]=\"'epistola-admin-usage-row-' + i\"\n [class.table-warning]=\"entry.problems.length > 0\"\n >\n <td>{{ entry.caseDefinitionKey || '-' }}</td>\n <td>\n <a\n *ngIf=\"entry.caseDefinitionKey && entry.caseDefinitionVersionTag\"\n [routerLink]=\"[\n '/case-management',\n 'case',\n entry.caseDefinitionKey,\n 'version',\n entry.caseDefinitionVersionTag,\n 'processes',\n entry.processDefinitionKey,\n ]\"\n class=\"usage-link\"\n >\n {{ entry.processDefinitionName }}\n </a>\n <span *ngIf=\"!entry.caseDefinitionKey || !entry.caseDefinitionVersionTag\">\n {{ entry.processDefinitionName }}\n </span>\n </td>\n <td>{{ entry.activityName }}</td>\n <td>\n <code>{{ entry.actionKey }}</code>\n </td>\n <td>\n <cds-tag *ngIf=\"entry.problems.length === 0\" size=\"sm\" type=\"green\">OK</cds-tag>\n <cds-tag\n *ngFor=\"let problem of entry.problems\"\n size=\"sm\"\n type=\"red\"\n class=\"d-block mb-1\"\n >\n {{ problem }}\n </cds-tag>\n </td>\n <td>\n <button\n class=\"btn btn-sm btn-outline-secondary\"\n [attr.data-testid]=\"'epistola-admin-export-' + i\"\n (click)=\"exportProcessLink(entry)\"\n [title]=\"'epistolaAdminExport' | pluginTranslate: 'epistola' | async\"\n >\n &#x2913;\n </button>\n </td>\n </tr>\n </tbody>\n </table>\n </cds-tab>\n\n <cds-tab\n [heading]=\"pendingHeading\"\n data-testid=\"epistola-admin-detail-tab-pending\"\n [active]=\"activeTab === 'pending'\"\n (selected)=\"setActiveTab('pending')\"\n >\n <div *ngIf=\"selectedCard.pendingJobs.length === 0\" class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminNoPendingJobs' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <table *ngIf=\"selectedCard.pendingJobs.length > 0\" class=\"table table-striped mt-3\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminProcess' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminActivity' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminStatus' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminRequestId' | pluginTranslate: 'epistola' | async }}</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n <tr\n *ngFor=\"let job of selectedCard.pendingJobs\"\n [attr.data-testid]=\"'epistola-admin-pending-row-' + job.executionId\"\n >\n <td>{{ job.processDefinitionName }}</td>\n <td>{{ job.activityName }}</td>\n <td>\n <cds-tag\n *ngIf=\"job.status === 'UNWIRED'; else waitingTag\"\n size=\"sm\"\n type=\"red\"\n [title]=\"'epistolaAdminUnwiredTooltip' | pluginTranslate: 'epistola' | async\"\n >\n {{ 'epistolaAdminStatusUnwired' | pluginTranslate: 'epistola' | async }}\n </cds-tag>\n <ng-template #waitingTag>\n <cds-tag size=\"sm\" type=\"blue\">\n {{ 'epistolaAdminStatusWaiting' | pluginTranslate: 'epistola' | async }}\n </cds-tag>\n </ng-template>\n </td>\n <td>\n <code *ngIf=\"job.status !== 'UNWIRED'\">{{ job.requestId }}</code>\n <span *ngIf=\"job.status === 'UNWIRED'\" class=\"text-muted\">\u2014</span>\n </td>\n <td class=\"text-end\">\n <button\n *ngIf=\"job.status !== 'UNWIRED'\"\n class=\"btn btn-sm btn-outline-primary\"\n [attr.data-testid]=\"'epistola-admin-reconcile-' + job.executionId\"\n (click)=\"reconcilePending(job)\"\n [disabled]=\"isReconciling(job)\"\n [title]=\"'epistolaAdminReconcileTooltip' | pluginTranslate: 'epistola' | async\"\n >\n <span *ngIf=\"!isReconciling(job)\">\n {{ 'epistolaAdminReconcile' | pluginTranslate: 'epistola' | async }}\n </span>\n <span *ngIf=\"isReconciling(job)\">\n {{ 'epistolaAdminReconciling' | pluginTranslate: 'epistola' | async }}\n </span>\n </button>\n <span\n *ngIf=\"job.status === 'UNWIRED'\"\n class=\"text-muted small\"\n [title]=\"'epistolaAdminUnwiredTooltip' | pluginTranslate: 'epistola' | async\"\n >\n {{ 'epistolaAdminUnwiredHint' | pluginTranslate: 'epistola' | async }}\n </span>\n <div\n *ngIf=\"reconcileFeedback && reconcileFeedback.executionId === job.executionId\"\n class=\"reconcile-feedback small mt-1\"\n [class.text-success]=\"reconcileFeedback.type === 'success'\"\n [class.text-warning]=\"reconcileFeedback.type === 'pending'\"\n [class.text-danger]=\"reconcileFeedback.type === 'error'\"\n >\n {{ reconcileFeedback.message }}\n </div>\n </td>\n </tr>\n </tbody>\n </table>\n </cds-tab>\n\n <cds-tab\n [heading]=\"catalogsHeading\"\n data-testid=\"epistola-admin-detail-tab-catalogs\"\n [active]=\"activeTab === 'catalogs'\"\n (selected)=\"setActiveTab('catalogs')\"\n >\n <p class=\"text-muted mt-3 mb-3\">\n {{ 'epistolaAdminCatalogsIntro' | pluginTranslate: 'epistola' | async }}\n </p>\n\n <div *ngIf=\"catalogsLoading\" class=\"text-muted mb-3\">\n {{ 'epistolaAdminLoading' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <div *ngIf=\"!catalogsLoading && catalogs.length === 0\" class=\"text-muted mb-3\">\n {{ 'epistolaAdminNoCatalogs' | pluginTranslate: 'epistola' | async }}\n </div>\n\n <table *ngIf=\"!catalogsLoading && catalogs.length > 0\" class=\"table table-striped\">\n <thead>\n <tr>\n <th>{{ 'epistolaAdminCatalogSlug' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminCatalogVersion' | pluginTranslate: 'epistola' | async }}</th>\n <th>{{ 'epistolaAdminCatalogStatus' | pluginTranslate: 'epistola' | async }}</th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n <tr\n *ngFor=\"let catalog of catalogs\"\n [attr.data-testid]=\"'epistola-admin-catalog-row-' + catalog.slug\"\n >\n <td>\n <code>{{ catalog.slug }}</code>\n </td>\n <td>{{ catalog.version }}</td>\n <td>\n <cds-tag *ngIf=\"catalog.status === 'IN_EPISTOLA'\" size=\"sm\" type=\"green\">\n {{ 'epistolaAdminCatalogInEpistola' | pluginTranslate: 'epistola' | async }}\n </cds-tag>\n <cds-tag *ngIf=\"catalog.status === 'NOT_IN_EPISTOLA'\" size=\"sm\" type=\"red\">\n {{ 'epistolaAdminCatalogNotInEpistola' | pluginTranslate: 'epistola' | async }}\n </cds-tag>\n <cds-tag *ngIf=\"catalog.status === 'UNKNOWN'\" size=\"sm\" type=\"gray\">\n {{ 'epistolaAdminCatalogStatusUnknown' | pluginTranslate: 'epistola' | async }}\n </cds-tag>\n </td>\n <td class=\"text-end\">\n <button\n class=\"btn btn-sm btn-outline-primary\"\n [attr.data-testid]=\"'epistola-admin-redeploy-' + catalog.slug\"\n (click)=\"redeployCatalog(catalog)\"\n [disabled]=\"isRedeploying(catalog)\"\n [title]=\"'epistolaAdminRedeployTooltip' | pluginTranslate: 'epistola' | async\"\n >\n <span *ngIf=\"!isRedeploying(catalog)\">\n {{ 'epistolaAdminRedeploy' | pluginTranslate: 'epistola' | async }}\n </span>\n <span *ngIf=\"isRedeploying(catalog)\">\n {{ 'epistolaAdminRedeploying' | pluginTranslate: 'epistola' | async }}\n </span>\n </button>\n <div\n *ngIf=\"catalogFeedback && catalogFeedback.slug === catalog.slug\"\n class=\"reconcile-feedback small mt-1\"\n [class.text-success]=\"catalogFeedback.type === 'success'\"\n [class.text-danger]=\"catalogFeedback.type === 'error'\"\n >\n {{ catalogFeedback.message }}\n </div>\n </td>\n </tr>\n </tbody>\n </table>\n </cds-tab>\n </cds-tabs>\n </ng-container>\n</div>\n", styles: [".epistola-admin{padding:1.5rem}.epistola-admin .version-badge{font-size:.75rem;font-weight:500;padding:.2em .6em;border-radius:4px;background-color:#e8e8e8;color:#525252}.epistola-admin .badge{font-size:.85em;padding:.35em .65em}.epistola-admin code{font-size:.9em;color:#525252}.epistola-admin table th{font-weight:600;white-space:nowrap}.epistola-admin .contract-compatibility{display:inline-flex;align-items:center;gap:.35rem;min-height:1.5rem;padding:0 .5rem 0 .35rem;border-radius:.75rem;font-size:.75rem;font-weight:600;line-height:1;white-space:nowrap}.epistola-admin .contract-compatibility--warning{background:#f1c21b;color:#161616}.epistola-admin .contract-compatibility--unknown{background:#ff832b;color:#161616}.epistola-admin .contract-compatibility--error{background:#da1e28;color:#fff}.epistola-admin .contract-compatibility__icon{display:inline-flex;align-items:center;justify-content:center;width:1rem;height:1rem;border:1px solid currentColor;border-radius:50%;font-size:.7rem;font-weight:700}.epistola-admin .status-dot{display:inline-block;width:10px;height:10px;border-radius:50%;margin-right:.5rem;flex-shrink:0;background-color:#adb5bd}.epistola-admin .status-dot--ok{background-color:#198754}.epistola-admin .status-dot--error{background-color:#dc3545}.epistola-admin .card-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:1rem}.epistola-admin .config-card{border:1px solid #dee2e6;border-radius:8px;padding:1.25rem;cursor:pointer;transition:box-shadow .15s ease,border-color .15s ease;background:#fff}.epistola-admin .config-card:hover{box-shadow:0 2px 8px #0000001a}.epistola-admin .config-card--ok{border-left:4px solid #198754}.epistola-admin .config-card--warning{border-left:4px solid #ffc107}.epistola-admin .config-card--error{border-left:4px solid #dc3545}.epistola-admin .config-card__header{display:flex;align-items:center;margin-bottom:1rem}.epistola-admin .config-card__title{margin:0;font-size:1.05rem;font-weight:600;color:#161616}.epistola-admin .config-card__body{display:flex;flex-direction:column;gap:.5rem}.epistola-admin .config-card__field{display:flex;justify-content:space-between;align-items:center}.epistola-admin .config-card__label{font-size:.875rem;color:#6c757d}.epistola-admin .config-card__value{font-size:.875rem;color:#161616}.epistola-admin .config-card__footer{margin-top:1rem;padding-top:.75rem;border-top:1px solid #f0f0f0;text-align:right}.epistola-admin .config-card__latency{font-size:.8rem;color:#adb5bd}.epistola-admin .usage-link{color:#0f62fe;text-decoration:none}.epistola-admin .usage-link:hover{text-decoration:underline}.epistola-admin .detail-info-table{max-width:500px}.epistola-admin .detail-info-table th{width:140px}.epistola-admin .detail-summary{padding:1rem 0;border-bottom:1px solid #dee2e6}.epistola-admin .epistola-changelog{max-height:60vh;overflow:auto;padding:.5rem 1rem;border:1px solid #dee2e6;border-radius:4px}.epistola-admin .epistola-changelog .changelog-release{padding:.75rem 0}.epistola-admin .epistola-changelog .changelog-release+.changelog-release{border-top:1px solid #eee}.epistola-admin .epistola-changelog .changelog-release__header{display:flex;align-items:center;margin-bottom:.5rem}.epistola-admin .epistola-changelog .changelog-section{margin:.25rem 0 .75rem}.epistola-admin .epistola-changelog .changelog-section__title{font-size:.8125rem;font-weight:600;text-transform:uppercase;letter-spacing:.02em;color:#6f6f6f;margin-bottom:.25rem}.epistola-admin .epistola-changelog .changelog-section__items{margin:0;padding-left:1.25rem;font-size:.8125rem;line-height:1.5}.epistola-admin .epistola-changelog .changelog-section__items li{margin-bottom:.25rem;word-break:break-word}\n"] }]
4767
4883
  }], ctorParameters: () => [{ type: EpistolaAdminService }, { type: i2$4.ActivatedRoute }, { type: i2$4.Router }] });
4768
4884
 
4769
4885
  /*
@@ -6650,6 +6766,7 @@ const epistolaPluginSpecification = {
6650
6766
  filenameTooltip: 'De bestandsnaam voor het gegenereerde document',
6651
6767
  resultProcessVariable: 'Resultaat Procesvariabele',
6652
6768
  resultProcessVariableTooltip: 'De naam van de procesvariabele waarin het request ID wordt opgeslagen',
6769
+ resultProcessVariableInvalid: 'Gebruik alleen letters en cijfers (A-Z, a-z, 0-9).',
6653
6770
  pdf: 'PDF',
6654
6771
  html: 'HTML',
6655
6772
  // Data mapping builder translations
@@ -6733,6 +6850,16 @@ const epistolaPluginSpecification = {
6733
6850
  epistolaAdminActivity: 'Activiteit',
6734
6851
  epistolaAdminAction: 'Actie',
6735
6852
  epistolaAdminServerVersion: 'Server versie',
6853
+ epistolaAdminContractVersion: 'Contract versie',
6854
+ epistolaAdminContractPluginVersion: 'Plugin contract',
6855
+ epistolaAdminContractServerVersion: 'Server contract',
6856
+ epistolaAdminContractCompatibility: 'Contract compatibiliteit',
6857
+ epistolaAdminContractWarning: 'Waarschuwing',
6858
+ epistolaAdminContractUnknown: 'Versie niet gemeld',
6859
+ epistolaAdminContractError: 'Niet compatibel',
6860
+ epistolaAdminContractWarningBody: 'De Epistola server gebruikt een oudere minor contractversie dan deze plugin. Meestal is de suite backwards compatible, maar controleer de release notes voordat je doorgaat.',
6861
+ epistolaAdminContractUnknownBody: 'De Epistola server meldt geen contractversie. Compatibiliteit kan niet worden gecontroleerd; dit wordt ondersteund vanaf Epistola Suite 1.0.0-RC4. Upgrade of herdeploy de server naar 1.0.0-RC4 of later.',
6862
+ epistolaAdminContractErrorBody: 'De Epistola server en deze plugin gebruiken verschillende major contractversies. Dit kan documentgeneratie en catalogusuitrol breken; gebruik compatibele versies.',
6736
6863
  epistolaAdminExport: 'Exporteren',
6737
6864
  epistolaAdminPendingJobs: 'Wachtende taken',
6738
6865
  epistolaAdminNoPendingJobs: 'Geen wachtende taken voor deze verbinding.',
@@ -6835,6 +6962,7 @@ const epistolaPluginSpecification = {
6835
6962
  filenameTooltip: 'The filename for the generated document',
6836
6963
  resultProcessVariable: 'Result Process Variable',
6837
6964
  resultProcessVariableTooltip: 'The name of the process variable to store the request ID in',
6965
+ resultProcessVariableInvalid: 'Use only letters and numbers (A-Z, a-z, 0-9).',
6838
6966
  pdf: 'PDF',
6839
6967
  html: 'HTML',
6840
6968
  // Data mapping builder translations
@@ -6918,6 +7046,16 @@ const epistolaPluginSpecification = {
6918
7046
  epistolaAdminActivity: 'Activity',
6919
7047
  epistolaAdminAction: 'Action',
6920
7048
  epistolaAdminServerVersion: 'Server version',
7049
+ epistolaAdminContractVersion: 'Contract version',
7050
+ epistolaAdminContractPluginVersion: 'Plugin contract',
7051
+ epistolaAdminContractServerVersion: 'Server contract',
7052
+ epistolaAdminContractCompatibility: 'Contract compatibility',
7053
+ epistolaAdminContractWarning: 'Warning',
7054
+ epistolaAdminContractUnknown: 'Version not reported',
7055
+ epistolaAdminContractError: 'Incompatible',
7056
+ epistolaAdminContractWarningBody: 'The Epistola server is on an older minor contract version than this plugin. The suite is usually backwards compatible, but check the release notes before continuing.',
7057
+ epistolaAdminContractUnknownBody: 'The Epistola server does not report a contract version. Compatibility cannot be checked; this is supported from Epistola Suite 1.0.0-RC4. Upgrade or redeploy the server to 1.0.0-RC4 or later.',
7058
+ epistolaAdminContractErrorBody: 'The Epistola server and this plugin use different major contract versions. This can break document generation and catalog deployment; use compatible versions.',
6921
7059
  epistolaAdminExport: 'Export',
6922
7060
  epistolaAdminPendingJobs: 'Pending jobs',
6923
7061
  epistolaAdminNoPendingJobs: 'No pending jobs for this connection.',