@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.
- package/fesm2022/epistola.app-valtimo-plugin.mjs +168 -30
- package/fesm2022/epistola.app-valtimo-plugin.mjs.map +1 -1
- package/lib/components/epistola-admin-page/epistola-admin-page.component.d.ts +10 -1
- package/lib/components/generate-document-configuration/generate-document-config.util.d.ts +14 -0
- package/lib/components/generate-document-configuration/generate-document-configuration.component.d.ts +1 -0
- package/lib/models/admin.d.ts +4 -0
- package/lib/utils/jsonata-path.d.ts +9 -0
- package/package.json +1 -1
- package/sbom.json +1 -1
|
@@ -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
|
-
//
|
|
919
|
-
//
|
|
920
|
-
//
|
|
921
|
-
insertText:
|
|
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
|
-
*
|
|
2148
|
-
*
|
|
2149
|
-
*
|
|
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
|
|
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
|
|
2291
|
+
node.steps?.length >= 2 &&
|
|
2258
2292
|
node.steps[0].type === 'variable' &&
|
|
2259
2293
|
node.steps[0].value === 'form' &&
|
|
2260
|
-
|
|
2261
|
-
return node.steps
|
|
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
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
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 ×\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 ×\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 ? '▶' : '▼' }}</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 ×\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 ×\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 ? '▶' : '▼' }}</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 ×\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 ×\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 ? '▶' : '▼' }}</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 ×\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 ×\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 ? '▶' : '▼' }}</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 ← {{ '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 ⤓\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 ← {{ '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 ⤓\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 ← {{ '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 ⤓\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 ← {{ '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 ⤓\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.',
|