@praxisui/crud 10.0.0-rc.19 → 10.0.0-rc.21
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/README.md +12 -1
- package/ai/component-registry.json +558 -44
- package/fesm2022/praxisui-crud.mjs +2765 -124
- package/package.json +7 -7
- package/types/praxisui-crud.d.ts +124 -12
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { inject, DestroyRef, signal, HostListener, ViewChild, Inject, ViewEncapsulation, Component, EventEmitter, ChangeDetectorRef, Output, Input, ChangeDetectionStrategy, Injectable, InjectionToken, input, computed, effect, output, Injector, booleanAttribute, Optional, ENVIRONMENT_INITIALIZER } from '@angular/core';
|
|
2
|
+
import { inject, DestroyRef, signal, HostListener, ViewChild, Inject, ViewEncapsulation, Component, EventEmitter, ChangeDetectorRef, Output, Input, ChangeDetectionStrategy, Injectable, InjectionToken, input, ElementRef, computed, linkedSignal, effect, output, Injector, booleanAttribute, Optional, ENVIRONMENT_INITIALIZER } from '@angular/core';
|
|
3
3
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
4
4
|
import { HttpClient } from '@angular/common/http';
|
|
5
5
|
import { Router, ActivatedRoute, RouterLink } from '@angular/router';
|
|
@@ -7,7 +7,7 @@ import { MatSnackBar } from '@angular/material/snack-bar';
|
|
|
7
7
|
import { firstValueFrom, BehaviorSubject, Subscription } from 'rxjs';
|
|
8
8
|
import { take, filter } from 'rxjs/operators';
|
|
9
9
|
import * as i2$1 from '@praxisui/core';
|
|
10
|
-
import { PraxisBulkClientService, generateBulkIdempotencyKey, API_URL, SafeExpressionEvaluator, buildApiUrl, ASYNC_CONFIG_STORAGE, GlobalConfigService, CrudOperationResolutionService, LoggerService, SurfaceNavigationError, fillUndefined, SETTINGS_PANEL_DATA, PraxisI18nService, PraxisIconDirective, providePraxisI18nConfig, createDefaultTableConfig, PraxisWidgetEventBus, EnterpriseRuntimeContextService, GLOBAL_SURFACE_SERVICE, SurfaceOutletRegistryService, ComponentKeyService, ResourceDiscoveryService, ResourceActionOpenAdapterService, ResourceSurfaceOpenAdapterService, composeHeadersWithVersion, normalizeSurfaceOperationContext, translateUnavailableWorkflowMessage, isSurfaceNavigationError, translateSurfaceNavigationRejected, normalizeUnknownError, EmptyStateCardComponent, RESOURCE_DISCOVERY_I18N_CONFIG, SURFACE_DRAWER_REF, SURFACE_DRAWER_CONTENT_DATA, PraxisIconButtonComponent, PraxisResourceIdentityComponent,
|
|
10
|
+
import { PraxisBulkClientService, generateBulkIdempotencyKey, API_URL, SafeExpressionEvaluator, buildApiUrl, ASYNC_CONFIG_STORAGE, GlobalConfigService, CrudOperationResolutionService, LoggerService, SurfaceNavigationError, fillUndefined, GenericCrudService, SETTINGS_PANEL_DATA, PraxisI18nService, buildBaseColumnFromDef, PraxisIconDirective, providePraxisI18nConfig, SchemaNormalizerService, createDefaultTableConfig, PraxisWidgetEventBus, EnterpriseRuntimeContextService, GLOBAL_SURFACE_SERVICE, SurfaceOutletRegistryService, ComponentKeyService, ResourceDiscoveryService, ResourceActionOpenAdapterService, ResourceSurfaceOpenAdapterService, composeHeadersWithVersion, normalizeSurfaceOperationContext, translateUnavailableWorkflowMessage, isSurfaceNavigationError, translateSurfaceNavigationRejected, normalizeUnknownError, EmptyStateCardComponent, RESOURCE_DISCOVERY_I18N_CONFIG, SURFACE_DRAWER_REF, SURFACE_DRAWER_CONTENT_DATA, PraxisIconButtonComponent, PraxisResourceIdentityComponent, ComponentMetadataRegistry, PraxisCustomizationModeService } from '@praxisui/core';
|
|
11
11
|
export { createDynamicRecord, extractRecordId, isDynamicDataRecord, resolveDataField } from '@praxisui/core';
|
|
12
12
|
import * as i1 from '@angular/common';
|
|
13
13
|
import { CommonModule } from '@angular/common';
|
|
@@ -2804,6 +2804,12 @@ class PraxisKpiBandComponent {
|
|
|
2804
2804
|
? card.interactive
|
|
2805
2805
|
: !!(card.action || card.filter || card.route || isObserved);
|
|
2806
2806
|
const active = this.isCardFilterActive(card.filter, this.activeFilter);
|
|
2807
|
+
const rawVariant = card.variant;
|
|
2808
|
+
const mappedVariantTone = rawVariant === 'primary'
|
|
2809
|
+
? 'info'
|
|
2810
|
+
: rawVariant === 'default'
|
|
2811
|
+
? 'neutral'
|
|
2812
|
+
: rawVariant;
|
|
2807
2813
|
return {
|
|
2808
2814
|
raw: card,
|
|
2809
2815
|
id: card.id,
|
|
@@ -2812,7 +2818,7 @@ class PraxisKpiBandComponent {
|
|
|
2812
2818
|
displayCaption: rawCaption,
|
|
2813
2819
|
footnote: card.footnote,
|
|
2814
2820
|
icon: card.icon,
|
|
2815
|
-
tone: card.tone || 'neutral',
|
|
2821
|
+
tone: card.tone || mappedVariantTone || 'neutral',
|
|
2816
2822
|
interactive,
|
|
2817
2823
|
active,
|
|
2818
2824
|
className: card.className,
|
|
@@ -3697,6 +3703,13 @@ const DOCUMENT_KIND = 'praxis.crud.editor';
|
|
|
3697
3703
|
const DOCUMENT_VERSION = 1;
|
|
3698
3704
|
const CANONICAL_ACTIONS = ['create', 'view', 'edit', 'delete'];
|
|
3699
3705
|
const OPEN_MODES$1 = ['route', 'modal', 'drawer'];
|
|
3706
|
+
const KPI_VARIANTS$1 = [
|
|
3707
|
+
'default',
|
|
3708
|
+
'primary',
|
|
3709
|
+
'success',
|
|
3710
|
+
'warning',
|
|
3711
|
+
'danger',
|
|
3712
|
+
];
|
|
3700
3713
|
function createCrudAuthoringDocument(source) {
|
|
3701
3714
|
return normalizeCrudAuthoringDocument({
|
|
3702
3715
|
kind: DOCUMENT_KIND,
|
|
@@ -3770,11 +3783,63 @@ function validateCrudAuthoringDocument(doc, context) {
|
|
|
3770
3783
|
for (const action of metadata.actions || []) {
|
|
3771
3784
|
validateCrudAction(action, metadata, diagnostics);
|
|
3772
3785
|
}
|
|
3786
|
+
validateCrudKpiBand(metadata, diagnostics);
|
|
3787
|
+
validateCrudFilterBar(metadata, diagnostics);
|
|
3773
3788
|
return diagnostics;
|
|
3774
3789
|
}
|
|
3775
3790
|
function findCrudAction(actions, actionName) {
|
|
3776
3791
|
return (actions || []).find((action) => action?.action === actionName);
|
|
3777
3792
|
}
|
|
3793
|
+
function validateCrudKpiBand(metadata, diagnostics) {
|
|
3794
|
+
const kpiBand = metadata.kpiBand;
|
|
3795
|
+
if (!kpiBand || typeof kpiBand !== 'object' || kpiBand.enabled === false) {
|
|
3796
|
+
return;
|
|
3797
|
+
}
|
|
3798
|
+
const cards = Array.isArray(kpiBand.cards) ? kpiBand.cards : [];
|
|
3799
|
+
cards.forEach((card, index) => {
|
|
3800
|
+
if (!trimString(card?.label)) {
|
|
3801
|
+
diagnostics.push(errorDiagnostic('crud.metadata.kpiBand.cards.label.required', `KPI card #${index + 1} requires a label`, `metadata.kpiBand.cards.${index}.label`));
|
|
3802
|
+
}
|
|
3803
|
+
const val = card?.value;
|
|
3804
|
+
const hasValueExpr = !!trimString(card?.valueExpr);
|
|
3805
|
+
const isEmptyValue = !hasValueExpr &&
|
|
3806
|
+
(val === undefined ||
|
|
3807
|
+
val === null ||
|
|
3808
|
+
(typeof val === 'string' && !val.trim()) ||
|
|
3809
|
+
(typeof val === 'number' && !Number.isFinite(val)));
|
|
3810
|
+
if (isEmptyValue) {
|
|
3811
|
+
diagnostics.push(errorDiagnostic('crud.metadata.kpiBand.cards.value.required', `KPI card #${index + 1} requires a displayed value`, `metadata.kpiBand.cards.${index}.value`));
|
|
3812
|
+
}
|
|
3813
|
+
if (card?.filter !== undefined &&
|
|
3814
|
+
(!card.filter || typeof card.filter !== 'object' || Array.isArray(card.filter))) {
|
|
3815
|
+
diagnostics.push(errorDiagnostic('crud.metadata.kpiBand.cards.filter.invalid', `KPI card #${index + 1} filter must be a valid criteria object`, `metadata.kpiBand.cards.${index}.filter`));
|
|
3816
|
+
}
|
|
3817
|
+
});
|
|
3818
|
+
}
|
|
3819
|
+
function validateCrudFilterBar(metadata, diagnostics) {
|
|
3820
|
+
const filterBar = metadata.filterBar;
|
|
3821
|
+
if (!filterBar || typeof filterBar !== 'object') {
|
|
3822
|
+
return;
|
|
3823
|
+
}
|
|
3824
|
+
if (Array.isArray(filterBar.quickFilters)) {
|
|
3825
|
+
filterBar.quickFilters.forEach((qf, index) => {
|
|
3826
|
+
if (!trimString(qf?.id)) {
|
|
3827
|
+
diagnostics.push(errorDiagnostic('crud.metadata.filterBar.quickFilters.id.required', `Quick filter #${index + 1} requires an id`, `metadata.filterBar.quickFilters.${index}.id`));
|
|
3828
|
+
}
|
|
3829
|
+
if (!trimString(qf?.label)) {
|
|
3830
|
+
diagnostics.push(errorDiagnostic('crud.metadata.filterBar.quickFilters.label.required', `Quick filter #${index + 1} requires a label`, `metadata.filterBar.quickFilters.${index}.label`));
|
|
3831
|
+
}
|
|
3832
|
+
const filterVal = qf?.filter;
|
|
3833
|
+
const isInvalidFilter = filterVal === undefined ||
|
|
3834
|
+
filterVal === null ||
|
|
3835
|
+
(typeof filterVal !== 'string' &&
|
|
3836
|
+
(typeof filterVal !== 'object' || Array.isArray(filterVal)));
|
|
3837
|
+
if (isInvalidFilter) {
|
|
3838
|
+
diagnostics.push(errorDiagnostic('crud.metadata.filterBar.quickFilters.filter.invalid', `Quick filter #${index + 1} requires a valid filter condition`, `metadata.filterBar.quickFilters.${index}.filter`));
|
|
3839
|
+
}
|
|
3840
|
+
});
|
|
3841
|
+
}
|
|
3842
|
+
}
|
|
3778
3843
|
function validateCrudAction(action, metadata, diagnostics) {
|
|
3779
3844
|
const actionName = trimString(action?.action) || 'unknown';
|
|
3780
3845
|
const effectiveOpenMode = normalizeOpenMode(action?.openMode ?? metadata.defaults?.openMode) || 'route';
|
|
@@ -3843,8 +3908,84 @@ function normalizeCrudMetadata(metadata) {
|
|
|
3843
3908
|
.map((action) => normalizeCrudAction(action))
|
|
3844
3909
|
.sort(compareCrudActions);
|
|
3845
3910
|
}
|
|
3911
|
+
if (base.kpiBand && typeof base.kpiBand === 'object') {
|
|
3912
|
+
normalized.kpiBand = normalizeKpiBandConfig(base.kpiBand);
|
|
3913
|
+
}
|
|
3914
|
+
if (base.filterBar && typeof base.filterBar === 'object') {
|
|
3915
|
+
normalized.filterBar = normalizeFilterBarConfig(base.filterBar);
|
|
3916
|
+
}
|
|
3846
3917
|
return stripUndefinedDeep(normalized);
|
|
3847
3918
|
}
|
|
3919
|
+
function normalizeKpiBandConfig(value) {
|
|
3920
|
+
const cards = Array.isArray(value.cards)
|
|
3921
|
+
? value.cards.map((card) => normalizeKpiCardConfig(card))
|
|
3922
|
+
: [];
|
|
3923
|
+
return stripUndefinedDeep({
|
|
3924
|
+
...value,
|
|
3925
|
+
enabled: typeof value.enabled === 'boolean' ? value.enabled : undefined,
|
|
3926
|
+
cards,
|
|
3927
|
+
});
|
|
3928
|
+
}
|
|
3929
|
+
function normalizeKpiCardConfig(card) {
|
|
3930
|
+
const rawVariant = trimString(card?.variant);
|
|
3931
|
+
const variant = rawVariant && KPI_VARIANTS$1.includes(rawVariant)
|
|
3932
|
+
? rawVariant
|
|
3933
|
+
: undefined;
|
|
3934
|
+
const rawTone = trimString(card?.tone);
|
|
3935
|
+
const rawValue = card?.value;
|
|
3936
|
+
const normalizedValue = typeof rawValue === 'number'
|
|
3937
|
+
? rawValue
|
|
3938
|
+
: typeof rawValue === 'string'
|
|
3939
|
+
? rawValue.trim()
|
|
3940
|
+
: trimString(card?.valueExpr)
|
|
3941
|
+
? undefined
|
|
3942
|
+
: '';
|
|
3943
|
+
const filter = card?.filter !== undefined &&
|
|
3944
|
+
card.filter !== null &&
|
|
3945
|
+
typeof card.filter === 'object' &&
|
|
3946
|
+
!Array.isArray(card.filter)
|
|
3947
|
+
? cloneJson(card.filter)
|
|
3948
|
+
: undefined;
|
|
3949
|
+
return stripUndefinedDeep({
|
|
3950
|
+
...card,
|
|
3951
|
+
id: trimString(card?.id) || undefined,
|
|
3952
|
+
label: typeof card?.label === 'string' ? card.label.trim() : '',
|
|
3953
|
+
value: normalizedValue,
|
|
3954
|
+
caption: trimString(card?.caption) || undefined,
|
|
3955
|
+
icon: trimString(card?.icon) || undefined,
|
|
3956
|
+
variant,
|
|
3957
|
+
tone: rawTone || undefined,
|
|
3958
|
+
filter,
|
|
3959
|
+
});
|
|
3960
|
+
}
|
|
3961
|
+
function normalizeFilterBarConfig(value) {
|
|
3962
|
+
const inlineFields = Array.isArray(value.inlineFields)
|
|
3963
|
+
? Array.from(new Set(value.inlineFields
|
|
3964
|
+
.map((field) => trimString(field))
|
|
3965
|
+
.filter((field) => !!field)))
|
|
3966
|
+
: undefined;
|
|
3967
|
+
let quickFilters;
|
|
3968
|
+
if (value.quickFilters === false) {
|
|
3969
|
+
quickFilters = false;
|
|
3970
|
+
}
|
|
3971
|
+
else if (Array.isArray(value.quickFilters)) {
|
|
3972
|
+
quickFilters = value.quickFilters.map((qf) => stripUndefinedDeep({
|
|
3973
|
+
id: typeof qf?.id === 'string' ? qf.id.trim() : '',
|
|
3974
|
+
label: typeof qf?.label === 'string' ? qf.label.trim() : '',
|
|
3975
|
+
icon: trimString(qf?.icon) || undefined,
|
|
3976
|
+
filter: typeof qf?.filter === 'string'
|
|
3977
|
+
? qf.filter.trim()
|
|
3978
|
+
: qf?.filter && typeof qf.filter === 'object' && !Array.isArray(qf.filter)
|
|
3979
|
+
? cloneJson(qf.filter)
|
|
3980
|
+
: {},
|
|
3981
|
+
}));
|
|
3982
|
+
}
|
|
3983
|
+
return stripUndefinedDeep({
|
|
3984
|
+
inlineFields,
|
|
3985
|
+
quickFilters,
|
|
3986
|
+
showAdvanced: typeof value.showAdvanced === 'boolean' ? value.showAdvanced : undefined,
|
|
3987
|
+
});
|
|
3988
|
+
}
|
|
3848
3989
|
function normalizeCrudAction(action) {
|
|
3849
3990
|
return stripUndefinedDeep({
|
|
3850
3991
|
...action,
|
|
@@ -4348,6 +4489,90 @@ const PRAXIS_CRUD_AUTHORING_I18N_CONFIG = {
|
|
|
4348
4489
|
'crud.authoring.action.schemaReadyNote': 'Schema vinculado',
|
|
4349
4490
|
'crud.authoring.action.submitReadyNote': 'Contrato de submit pronto',
|
|
4350
4491
|
'crud.authoring.action.apiReadyNote': 'Mapeamento de API pronto',
|
|
4492
|
+
'crud.authoring.section.kpis': 'Faixa de KPIs',
|
|
4493
|
+
'crud.authoring.tab.kpis': 'KPIs',
|
|
4494
|
+
'crud.authoring.overview.kpis': 'Faixa de KPIs',
|
|
4495
|
+
'crud.authoring.overview.kpisDisabled': 'Desativada',
|
|
4496
|
+
'crud.authoring.overview.kpisEnabled': '{count} cards configurados',
|
|
4497
|
+
'crud.authoring.overview.kpisEmpty': 'Ativa · adicione cards',
|
|
4498
|
+
'crud.authoring.nextFocus.kpis': 'Próximo foco: faixa de KPIs',
|
|
4499
|
+
'crud.authoring.kpis.enabled': 'Exibir faixa de KPIs acima da listagem',
|
|
4500
|
+
'crud.authoring.kpis.sectionSummary': 'Configure indicadores rápidos de contexto operacional que também podem filtrar a listagem ao clicar.',
|
|
4501
|
+
'crud.authoring.kpis.addCard': 'Adicionar KPI',
|
|
4502
|
+
'crud.authoring.kpis.removeCard': 'Remover KPI',
|
|
4503
|
+
'crud.authoring.kpis.moveUp': 'Mover para cima',
|
|
4504
|
+
'crud.authoring.kpis.moveDown': 'Mover para baixo',
|
|
4505
|
+
'crud.authoring.kpis.empty': 'Nenhum card de KPI configurado. Adicione indicadores para destacar métricas e atalhos de filtro.',
|
|
4506
|
+
'crud.authoring.kpis.cardTitle': 'Card #{index}',
|
|
4507
|
+
'crud.authoring.kpis.label': 'Rótulo',
|
|
4508
|
+
'crud.authoring.kpis.labelPlaceholder': 'ex.: Em andamento',
|
|
4509
|
+
'crud.authoring.kpis.value': 'Valor exibido',
|
|
4510
|
+
'crud.authoring.kpis.valuePlaceholder': 'ex.: 18',
|
|
4511
|
+
'crud.authoring.kpis.caption': 'Legenda / contexto',
|
|
4512
|
+
'crud.authoring.kpis.captionPlaceholder': 'ex.: Todos os setores',
|
|
4513
|
+
'crud.authoring.kpis.icon': 'Ícone (Material Icon)',
|
|
4514
|
+
'crud.authoring.kpis.iconPlaceholder': 'ex.: radar',
|
|
4515
|
+
'crud.authoring.kpis.variant': 'Tom semântico',
|
|
4516
|
+
'crud.authoring.kpis.variant.default': 'Neutro (Default)',
|
|
4517
|
+
'crud.authoring.kpis.variant.primary': 'Destaque (Primary)',
|
|
4518
|
+
'crud.authoring.kpis.variant.success': 'Sucesso (Success)',
|
|
4519
|
+
'crud.authoring.kpis.variant.warning': 'Atenção (Warning)',
|
|
4520
|
+
'crud.authoring.kpis.variant.danger': 'Crítico (Danger)',
|
|
4521
|
+
'crud.authoring.kpis.filterMode': 'Comportamento ao clicar (Filtro)',
|
|
4522
|
+
'crud.authoring.kpis.filterMode.none': 'Apenas informativo (sem filtro)',
|
|
4523
|
+
'crud.authoring.kpis.filterMode.clear': 'Limpar filtros (Exibir todos)',
|
|
4524
|
+
'crud.authoring.kpis.filterMode.condition': 'Aplicar critério de filtro',
|
|
4525
|
+
'crud.authoring.kpis.filterField': 'Campo do filtro',
|
|
4526
|
+
'crud.authoring.kpis.filterFieldPlaceholder': 'ex.: status',
|
|
4527
|
+
'crud.authoring.kpis.filterValue': 'Valor esperado',
|
|
4528
|
+
'crud.authoring.kpis.filterValuePlaceholder': 'ex.: EM_ANDAMENTO',
|
|
4529
|
+
'crud.authoring.kpis.filterJson': 'Critério de filtro avançado (JSON)',
|
|
4530
|
+
'crud.authoring.kpis.previewTitle': 'Prévia da faixa de KPIs',
|
|
4531
|
+
'crud.authoring.validation.kpiLabelRequired': 'Informe o rótulo do card de KPI #{index}.',
|
|
4532
|
+
'crud.authoring.validation.kpiValueRequired': 'Informe o valor exibido no card de KPI #{index}.',
|
|
4533
|
+
'crud.authoring.validation.kpiFilterInvalid': 'Informe um critério de filtro válido no card de KPI #{index}.',
|
|
4534
|
+
'crud.authoring.section.filters': 'Barra de Filtros',
|
|
4535
|
+
'crud.authoring.tab.filters': 'Filtros',
|
|
4536
|
+
'crud.authoring.overview.filters': 'Filtros',
|
|
4537
|
+
'crud.authoring.overview.filtersDefault': 'Descoberta automática do recurso',
|
|
4538
|
+
'crud.authoring.overview.filtersConfigured': '{inline} inline · {quick} rápidos',
|
|
4539
|
+
'crud.authoring.nextFocus.filters': 'Próximo foco: barra de filtros',
|
|
4540
|
+
'crud.authoring.filters.sectionSummary': 'Configure os campos sempre visíveis na barra de busca, os atalhos de filtro rápido (chips) e o acesso à gaveta de filtros avançados.',
|
|
4541
|
+
'crud.authoring.filters.showAdvanced': 'Permitir abertura de filtros avançados',
|
|
4542
|
+
'crud.authoring.filters.inlineGroup': 'Campos sempre visíveis (Inline Fields)',
|
|
4543
|
+
'crud.authoring.filters.inlineNote': 'Selecione campos do recurso ou adicione identificadores de campos que devem aparecer diretamente na barra de filtros.',
|
|
4544
|
+
'crud.authoring.filters.inlineFieldInput': 'Adicionar campo inline',
|
|
4545
|
+
'crud.authoring.filters.inlineFieldPlaceholder': 'ex.: status, prioridade',
|
|
4546
|
+
'crud.authoring.filters.inlineAdd': 'Adicionar campo',
|
|
4547
|
+
'crud.authoring.filters.inlineEmpty': 'Nenhum campo inline fixado. O runtime usará a configuração padrão do schema.',
|
|
4548
|
+
'crud.authoring.filters.availableFields': 'Sugestões de campos disponíveis',
|
|
4549
|
+
'crud.authoring.filters.quickGroup': 'Atalhos de filtro rápido (Quick Filters)',
|
|
4550
|
+
'crud.authoring.filters.quickMode': 'Modo dos filtros rápidos',
|
|
4551
|
+
'crud.authoring.filters.quickMode.inherit': 'Padrão da tabela / recurso',
|
|
4552
|
+
'crud.authoring.filters.quickMode.disabled': 'Ocultar filtros rápidos',
|
|
4553
|
+
'crud.authoring.filters.quickMode.custom': 'Personalizar chips de filtro rápido',
|
|
4554
|
+
'crud.authoring.filters.addQuickFilter': 'Adicionar filtro rápido',
|
|
4555
|
+
'crud.authoring.filters.removeQuickFilter': 'Remover filtro rápido',
|
|
4556
|
+
'crud.authoring.filters.quickEmpty': 'Nenhum filtro rápido configurado.',
|
|
4557
|
+
'crud.authoring.filters.quickId': 'Identificador',
|
|
4558
|
+
'crud.authoring.filters.quickLabel': 'Rótulo do chip',
|
|
4559
|
+
'crud.authoring.filters.quickIcon': 'Ícone (opcional)',
|
|
4560
|
+
'crud.authoring.filters.quickConditionMode': 'Tipo de condição',
|
|
4561
|
+
'crud.authoring.filters.quickConditionMode.clear': 'Todos (limpar filtro)',
|
|
4562
|
+
'crud.authoring.filters.quickConditionMode.fieldEquals': 'Campo igual a valor',
|
|
4563
|
+
'crud.authoring.filters.quickConditionMode.expression': 'Expressão / JSON customizado',
|
|
4564
|
+
'crud.authoring.filters.quickField': 'Campo',
|
|
4565
|
+
'crud.authoring.filters.quickValue': 'Valor',
|
|
4566
|
+
'crud.authoring.filters.quickExpression': 'Expressão ou JSON de filtro',
|
|
4567
|
+
'crud.authoring.filters.previewTitle': 'Resumo da barra de filtros',
|
|
4568
|
+
'crud.authoring.validation.quickFilterLabelRequired': 'Informe o rótulo do filtro rápido #{index}.',
|
|
4569
|
+
'crud.authoring.validation.quickFilterIdRequired': 'Informe o identificador do filtro rápido #{index}.',
|
|
4570
|
+
'crud.authoring.validation.quickFilterInvalid': 'Informe uma condição válida para o filtro rápido #{index}.',
|
|
4571
|
+
'crud.authoring.table.inheritedColumnsCount': '{count} colunas do recurso',
|
|
4572
|
+
'crud.authoring.table.inheritedColumnsPending': 'Colunas herdadas do recurso',
|
|
4573
|
+
'crud.authoring.table.inheritedColumnsNote': 'As colunas estão sendo derivadas automaticamente do contrato do recurso ({path}). Você pode reordenar, ocultar ou formatar colunas abaixo.',
|
|
4574
|
+
'crud.authoring.table.customColumnsNote': 'As colunas desta tabela possuem customizações explícitas salvas nos metadados do CRUD.',
|
|
4575
|
+
'crud.authoring.table.focusColumnsButton': 'Editar colunas do recurso',
|
|
4351
4576
|
},
|
|
4352
4577
|
'en-US': {
|
|
4353
4578
|
'crud.authoring.title': 'CRUD settings',
|
|
@@ -4581,6 +4806,90 @@ const PRAXIS_CRUD_AUTHORING_I18N_CONFIG = {
|
|
|
4581
4806
|
'crud.authoring.action.schemaReadyNote': 'Schema linked',
|
|
4582
4807
|
'crud.authoring.action.submitReadyNote': 'Submit contract ready',
|
|
4583
4808
|
'crud.authoring.action.apiReadyNote': 'API mapping ready',
|
|
4809
|
+
'crud.authoring.section.kpis': 'KPI Band',
|
|
4810
|
+
'crud.authoring.tab.kpis': 'KPIs',
|
|
4811
|
+
'crud.authoring.overview.kpis': 'KPI Band',
|
|
4812
|
+
'crud.authoring.overview.kpisDisabled': 'Disabled',
|
|
4813
|
+
'crud.authoring.overview.kpisEnabled': '{count} cards configured',
|
|
4814
|
+
'crud.authoring.overview.kpisEmpty': 'Enabled · add cards',
|
|
4815
|
+
'crud.authoring.nextFocus.kpis': 'Next focus: KPI band',
|
|
4816
|
+
'crud.authoring.kpis.enabled': 'Show KPI band above collection table',
|
|
4817
|
+
'crud.authoring.kpis.sectionSummary': 'Configure operational summary indicators that can also filter the collection when clicked.',
|
|
4818
|
+
'crud.authoring.kpis.addCard': 'Add KPI',
|
|
4819
|
+
'crud.authoring.kpis.removeCard': 'Remove KPI',
|
|
4820
|
+
'crud.authoring.kpis.moveUp': 'Move up',
|
|
4821
|
+
'crud.authoring.kpis.moveDown': 'Move down',
|
|
4822
|
+
'crud.authoring.kpis.empty': 'No KPI cards configured yet. Add indicators to highlight metrics and filter shortcuts.',
|
|
4823
|
+
'crud.authoring.kpis.cardTitle': 'Card #{index}',
|
|
4824
|
+
'crud.authoring.kpis.label': 'Label',
|
|
4825
|
+
'crud.authoring.kpis.labelPlaceholder': 'e.g. In progress',
|
|
4826
|
+
'crud.authoring.kpis.value': 'Displayed value',
|
|
4827
|
+
'crud.authoring.kpis.valuePlaceholder': 'e.g. 18',
|
|
4828
|
+
'crud.authoring.kpis.caption': 'Caption / context',
|
|
4829
|
+
'crud.authoring.kpis.captionPlaceholder': 'e.g. All sectors',
|
|
4830
|
+
'crud.authoring.kpis.icon': 'Icon (Material Icon)',
|
|
4831
|
+
'crud.authoring.kpis.iconPlaceholder': 'e.g. radar',
|
|
4832
|
+
'crud.authoring.kpis.variant': 'Semantic tone',
|
|
4833
|
+
'crud.authoring.kpis.variant.default': 'Neutral (Default)',
|
|
4834
|
+
'crud.authoring.kpis.variant.primary': 'Accent (Primary)',
|
|
4835
|
+
'crud.authoring.kpis.variant.success': 'Success',
|
|
4836
|
+
'crud.authoring.kpis.variant.warning': 'Warning',
|
|
4837
|
+
'crud.authoring.kpis.variant.danger': 'Danger',
|
|
4838
|
+
'crud.authoring.kpis.filterMode': 'Click behavior (Filter)',
|
|
4839
|
+
'crud.authoring.kpis.filterMode.none': 'Informational only (no filter)',
|
|
4840
|
+
'crud.authoring.kpis.filterMode.clear': 'Clear filters (Show all)',
|
|
4841
|
+
'crud.authoring.kpis.filterMode.condition': 'Apply filter condition',
|
|
4842
|
+
'crud.authoring.kpis.filterField': 'Filter field',
|
|
4843
|
+
'crud.authoring.kpis.filterFieldPlaceholder': 'e.g. status',
|
|
4844
|
+
'crud.authoring.kpis.filterValue': 'Expected value',
|
|
4845
|
+
'crud.authoring.kpis.filterValuePlaceholder': 'e.g. IN_PROGRESS',
|
|
4846
|
+
'crud.authoring.kpis.filterJson': 'Advanced filter criteria (JSON)',
|
|
4847
|
+
'crud.authoring.kpis.previewTitle': 'KPI band preview',
|
|
4848
|
+
'crud.authoring.validation.kpiLabelRequired': 'Provide a label for KPI card #{index}.',
|
|
4849
|
+
'crud.authoring.validation.kpiValueRequired': 'Provide a displayed value for KPI card #{index}.',
|
|
4850
|
+
'crud.authoring.validation.kpiFilterInvalid': 'Provide valid filter criteria for KPI card #{index}.',
|
|
4851
|
+
'crud.authoring.section.filters': 'Filter Bar',
|
|
4852
|
+
'crud.authoring.tab.filters': 'Filters',
|
|
4853
|
+
'crud.authoring.overview.filters': 'Filters',
|
|
4854
|
+
'crud.authoring.overview.filtersDefault': 'Automatic resource discovery',
|
|
4855
|
+
'crud.authoring.overview.filtersConfigured': '{inline} inline · {quick} quick',
|
|
4856
|
+
'crud.authoring.nextFocus.filters': 'Next focus: filter bar',
|
|
4857
|
+
'crud.authoring.filters.sectionSummary': 'Configure always-visible search fields, quick filter chips, and access to the advanced filters drawer.',
|
|
4858
|
+
'crud.authoring.filters.showAdvanced': 'Allow opening advanced filters',
|
|
4859
|
+
'crud.authoring.filters.inlineGroup': 'Always-visible fields (Inline Fields)',
|
|
4860
|
+
'crud.authoring.filters.inlineNote': 'Select resource fields or add field identifiers that should appear directly in the filter bar.',
|
|
4861
|
+
'crud.authoring.filters.inlineFieldInput': 'Add inline field',
|
|
4862
|
+
'crud.authoring.filters.inlineFieldPlaceholder': 'e.g. status, priority',
|
|
4863
|
+
'crud.authoring.filters.inlineAdd': 'Add field',
|
|
4864
|
+
'crud.authoring.filters.inlineEmpty': 'No pinned inline fields. Runtime will use the default schema configuration.',
|
|
4865
|
+
'crud.authoring.filters.availableFields': 'Available field suggestions',
|
|
4866
|
+
'crud.authoring.filters.quickGroup': 'Quick filter shortcuts (Quick Filters)',
|
|
4867
|
+
'crud.authoring.filters.quickMode': 'Quick filters mode',
|
|
4868
|
+
'crud.authoring.filters.quickMode.inherit': 'Table / resource default',
|
|
4869
|
+
'crud.authoring.filters.quickMode.disabled': 'Hide quick filters',
|
|
4870
|
+
'crud.authoring.filters.quickMode.custom': 'Customize quick filter chips',
|
|
4871
|
+
'crud.authoring.filters.addQuickFilter': 'Add quick filter',
|
|
4872
|
+
'crud.authoring.filters.removeQuickFilter': 'Remove quick filter',
|
|
4873
|
+
'crud.authoring.filters.quickEmpty': 'No quick filters configured.',
|
|
4874
|
+
'crud.authoring.filters.quickId': 'Identifier',
|
|
4875
|
+
'crud.authoring.filters.quickLabel': 'Chip label',
|
|
4876
|
+
'crud.authoring.filters.quickIcon': 'Icon (optional)',
|
|
4877
|
+
'crud.authoring.filters.quickConditionMode': 'Condition type',
|
|
4878
|
+
'crud.authoring.filters.quickConditionMode.clear': 'All (clear filter)',
|
|
4879
|
+
'crud.authoring.filters.quickConditionMode.fieldEquals': 'Field equals value',
|
|
4880
|
+
'crud.authoring.filters.quickConditionMode.expression': 'Custom expression / JSON',
|
|
4881
|
+
'crud.authoring.filters.quickField': 'Field',
|
|
4882
|
+
'crud.authoring.filters.quickValue': 'Value',
|
|
4883
|
+
'crud.authoring.filters.quickExpression': 'Filter expression or JSON',
|
|
4884
|
+
'crud.authoring.filters.previewTitle': 'Filter bar summary',
|
|
4885
|
+
'crud.authoring.validation.quickFilterLabelRequired': 'Provide a label for quick filter #{index}.',
|
|
4886
|
+
'crud.authoring.validation.quickFilterIdRequired': 'Provide an identifier for quick filter #{index}.',
|
|
4887
|
+
'crud.authoring.validation.quickFilterInvalid': 'Provide a valid condition for quick filter #{index}.',
|
|
4888
|
+
'crud.authoring.table.inheritedColumnsCount': '{count} resource columns',
|
|
4889
|
+
'crud.authoring.table.inheritedColumnsPending': 'Columns inherited from resource',
|
|
4890
|
+
'crud.authoring.table.inheritedColumnsNote': 'Columns are automatically derived from the resource contract ({path}). You can reorder, hide, or format columns below.',
|
|
4891
|
+
'crud.authoring.table.customColumnsNote': 'This table has explicit column customizations saved in the CRUD metadata.',
|
|
4892
|
+
'crud.authoring.table.focusColumnsButton': 'Edit resource columns',
|
|
4584
4893
|
},
|
|
4585
4894
|
},
|
|
4586
4895
|
},
|
|
@@ -4590,21 +4899,48 @@ function translateCrudAuthoringText(i18n, key, fallback, params) {
|
|
|
4590
4899
|
}
|
|
4591
4900
|
|
|
4592
4901
|
const ACTION_KEYS = ['create', 'view', 'edit'];
|
|
4902
|
+
const SECTION_KEYS = [
|
|
4903
|
+
'connection',
|
|
4904
|
+
'defaults',
|
|
4905
|
+
'actions',
|
|
4906
|
+
'kpis',
|
|
4907
|
+
'filters',
|
|
4908
|
+
'table',
|
|
4909
|
+
];
|
|
4910
|
+
const SECTION_TAB_INDEX = {
|
|
4911
|
+
connection: 0,
|
|
4912
|
+
defaults: 1,
|
|
4913
|
+
actions: 2,
|
|
4914
|
+
kpis: 3,
|
|
4915
|
+
filters: 4,
|
|
4916
|
+
table: 5,
|
|
4917
|
+
json: 6,
|
|
4918
|
+
};
|
|
4593
4919
|
const OPEN_MODES = ['route', 'modal', 'drawer'];
|
|
4594
4920
|
const HEADER_VARIANTS = ['ghost', 'tonal', 'outlined'];
|
|
4595
4921
|
const SUBMIT_METHODS = ['post', 'put', 'patch'];
|
|
4596
4922
|
const MODAL_DENSITIES = ['default', 'compact'];
|
|
4597
4923
|
const BACK_STRATEGIES = ['auto', 'close', 'navigate'];
|
|
4598
4924
|
const PARAM_TARGETS = ['routeParam', 'query', 'input'];
|
|
4925
|
+
const KPI_VARIANTS = ['default', 'primary', 'success', 'warning', 'danger'];
|
|
4926
|
+
const KPI_FILTER_MODES = ['none', 'clear', 'condition'];
|
|
4927
|
+
const QUICK_FILTER_MODES = ['inherit', 'disabled', 'custom'];
|
|
4928
|
+
const QUICK_CONDITION_MODES = [
|
|
4929
|
+
'clear',
|
|
4930
|
+
'fieldEquals',
|
|
4931
|
+
'expression',
|
|
4932
|
+
];
|
|
4599
4933
|
class CrudMetadataEditorComponent {
|
|
4600
4934
|
documentInput = input(null, { ...(ngDevMode ? { debugName: "documentInput" } : /* istanbul ignore next */ {}), alias: 'document' });
|
|
4601
4935
|
metadataInput = input(null, { ...(ngDevMode ? { debugName: "metadataInput" } : /* istanbul ignore next */ {}), alias: 'metadata' });
|
|
4602
4936
|
crudIdInput = input(null, { ...(ngDevMode ? { debugName: "crudIdInput" } : /* istanbul ignore next */ {}), alias: 'crudId' });
|
|
4603
4937
|
readonlyInput = input(false, { ...(ngDevMode ? { debugName: "readonlyInput" } : /* istanbul ignore next */ {}), alias: 'readonly' });
|
|
4938
|
+
effectiveTableConfigInput = input(null, { ...(ngDevMode ? { debugName: "effectiveTableConfigInput" } : /* istanbul ignore next */ {}), alias: 'effectiveTableConfig' });
|
|
4604
4939
|
isDirty$ = new BehaviorSubject(false);
|
|
4605
4940
|
isValid$ = new BehaviorSubject(true);
|
|
4606
4941
|
isBusy$ = new BehaviorSubject(false);
|
|
4607
4942
|
actionKeys = ACTION_KEYS;
|
|
4943
|
+
sectionKeys = SECTION_KEYS;
|
|
4608
4944
|
healthBuckets = ['invalid', 'pending', 'ready'];
|
|
4609
4945
|
openModes = OPEN_MODES;
|
|
4610
4946
|
headerVariants = HEADER_VARIANTS;
|
|
@@ -4612,14 +4948,40 @@ class CrudMetadataEditorComponent {
|
|
|
4612
4948
|
modalDensities = MODAL_DENSITIES;
|
|
4613
4949
|
backStrategies = BACK_STRATEGIES;
|
|
4614
4950
|
paramTargets = PARAM_TARGETS;
|
|
4951
|
+
kpiVariants = KPI_VARIANTS;
|
|
4952
|
+
kpiFilterModes = KPI_FILTER_MODES;
|
|
4953
|
+
quickFilterModes = QUICK_FILTER_MODES;
|
|
4954
|
+
quickConditionModes = QUICK_CONDITION_MODES;
|
|
4955
|
+
selectedTabIndex = signal(0, ...(ngDevMode ? [{ debugName: "selectedTabIndex" }] : /* istanbul ignore next */ []));
|
|
4956
|
+
inlineFieldDraft = signal('', ...(ngDevMode ? [{ debugName: "inlineFieldDraft" }] : /* istanbul ignore next */ []));
|
|
4615
4957
|
initialValueDrafts = signal({}, ...(ngDevMode ? [{ debugName: "initialValueDrafts" }] : /* istanbul ignore next */ []));
|
|
4616
|
-
|
|
4958
|
+
kpiFilterJsonDrafts = signal({}, ...(ngDevMode ? [{ debugName: "kpiFilterJsonDrafts" }] : /* istanbul ignore next */ []));
|
|
4959
|
+
quickFilterExpressionDrafts = signal({}, ...(ngDevMode ? [{ debugName: "quickFilterExpressionDrafts" }] : /* istanbul ignore next */ []));
|
|
4960
|
+
schemaResolvedColumnsSignal = signal([], ...(ngDevMode ? [{ debugName: "schemaResolvedColumnsSignal" }] : /* istanbul ignore next */ []));
|
|
4961
|
+
schemaResolvedFilterFieldsSignal = signal([], ...(ngDevMode ? [{ debugName: "schemaResolvedFilterFieldsSignal" }] : /* istanbul ignore next */ []));
|
|
4962
|
+
destroyRef = inject(DestroyRef);
|
|
4963
|
+
hostRef = inject((ElementRef), { optional: true });
|
|
4964
|
+
crudService = inject(GenericCrudService, { optional: true });
|
|
4965
|
+
injectedData = inject(SETTINGS_PANEL_DATA, {
|
|
4966
|
+
optional: true,
|
|
4967
|
+
});
|
|
4617
4968
|
i18n = inject(PraxisI18nService);
|
|
4618
4969
|
currentDocument = signal(createCrudAuthoringDocument({}), ...(ngDevMode ? [{ debugName: "currentDocument" }] : /* istanbul ignore next */ []));
|
|
4619
4970
|
initialDocument = signal(createCrudAuthoringDocument({}), ...(ngDevMode ? [{ debugName: "initialDocument" }] : /* istanbul ignore next */ []));
|
|
4620
4971
|
lastExternalSignature = null;
|
|
4972
|
+
lastLoadedResourceSignature = '';
|
|
4973
|
+
schemaSub;
|
|
4974
|
+
schemaScopeSignature = null;
|
|
4975
|
+
schemaGeneration = 0;
|
|
4976
|
+
destroyed = false;
|
|
4621
4977
|
effectiveCrudId = computed(() => this.crudIdInput() || this.injectedData?.crudId || null, ...(ngDevMode ? [{ debugName: "effectiveCrudId" }] : /* istanbul ignore next */ []));
|
|
4622
4978
|
isReadonly = computed(() => !!(this.readonlyInput() || this.injectedData?.readonly), ...(ngDevMode ? [{ debugName: "isReadonly" }] : /* istanbul ignore next */ []));
|
|
4979
|
+
providedEffectiveTableConfig = computed(() => this.effectiveTableConfigInput() || this.injectedData?.effectiveTableConfig || null, ...(ngDevMode ? [{ debugName: "providedEffectiveTableConfig" }] : /* istanbul ignore next */ []));
|
|
4980
|
+
providedSnapshotOwner = linkedSignal({ ...(ngDevMode ? { debugName: "providedSnapshotOwner" } : /* istanbul ignore next */ {}), source: () => ({
|
|
4981
|
+
snapshot: this.providedEffectiveTableConfig(),
|
|
4982
|
+
resourceIdentity: this.resourceSchemaIdentity(parseLegacyOrCrudDocument(this.resolveExternalSeed()).metadata),
|
|
4983
|
+
}),
|
|
4984
|
+
computation: (source, previous) => previous && source.snapshot === previous.source.snapshot ? previous.value : source });
|
|
4623
4985
|
diagnostics = computed(() => validateCrudAuthoringDocument(this.currentDocument(), { requireCanonicalActions: true }), ...(ngDevMode ? [{ debugName: "diagnostics" }] : /* istanbul ignore next */ []));
|
|
4624
4986
|
errorCount = computed(() => this.diagnostics().filter((issue) => issue.level === 'error').length, ...(ngDevMode ? [{ debugName: "errorCount" }] : /* istanbul ignore next */ []));
|
|
4625
4987
|
warningCount = computed(() => this.diagnostics().filter((issue) => issue.level !== 'error').length, ...(ngDevMode ? [{ debugName: "warningCount" }] : /* istanbul ignore next */ []));
|
|
@@ -4634,6 +4996,113 @@ class CrudMetadataEditorComponent {
|
|
|
4634
4996
|
resourcePath = computed(() => this.currentDocument().metadata.resource?.path || '', ...(ngDevMode ? [{ debugName: "resourcePath" }] : /* istanbul ignore next */ []));
|
|
4635
4997
|
resourceIdField = computed(() => String(this.currentDocument().metadata.resource?.idField ?? ''), ...(ngDevMode ? [{ debugName: "resourceIdField" }] : /* istanbul ignore next */ []));
|
|
4636
4998
|
resourceEndpointKey = computed(() => String(this.currentDocument().metadata.resource?.endpointKey ?? ''), ...(ngDevMode ? [{ debugName: "resourceEndpointKey" }] : /* istanbul ignore next */ []));
|
|
4999
|
+
resourceChangedSinceInit = computed(() => this.resourceSchemaIdentity(this.initialDocument().metadata) !==
|
|
5000
|
+
this.resourceSchemaIdentity(this.currentDocument().metadata), ...(ngDevMode ? [{ debugName: "resourceChangedSinceInit" }] : /* istanbul ignore next */ []));
|
|
5001
|
+
authoredTableColumns = computed(() => {
|
|
5002
|
+
const raw = this.currentDocument().metadata.table?.columns;
|
|
5003
|
+
return Array.isArray(raw) ? raw : [];
|
|
5004
|
+
}, ...(ngDevMode ? [{ debugName: "authoredTableColumns" }] : /* istanbul ignore next */ []));
|
|
5005
|
+
resolvedInheritedColumns = computed(() => {
|
|
5006
|
+
const provided = this.providedSnapshotOwner();
|
|
5007
|
+
if (provided.resourceIdentity === this.resourceSchemaIdentity(this.currentDocument().metadata)) {
|
|
5008
|
+
const runtimeColumns = provided.snapshot?.columns;
|
|
5009
|
+
if (Array.isArray(runtimeColumns) && runtimeColumns.length > 0) {
|
|
5010
|
+
return runtimeColumns;
|
|
5011
|
+
}
|
|
5012
|
+
}
|
|
5013
|
+
const schemaColumns = this.schemaResolvedColumnsSignal();
|
|
5014
|
+
if (Array.isArray(schemaColumns) && schemaColumns.length > 0) {
|
|
5015
|
+
return schemaColumns;
|
|
5016
|
+
}
|
|
5017
|
+
return [];
|
|
5018
|
+
}, ...(ngDevMode ? [{ debugName: "resolvedInheritedColumns" }] : /* istanbul ignore next */ []));
|
|
5019
|
+
effectiveTableColumns = computed(() => {
|
|
5020
|
+
const authored = this.authoredTableColumns();
|
|
5021
|
+
return authored.length > 0 ? authored : this.resolvedInheritedColumns();
|
|
5022
|
+
}, ...(ngDevMode ? [{ debugName: "effectiveTableColumns" }] : /* istanbul ignore next */ []));
|
|
5023
|
+
isUsingInheritedResourceColumns = computed(() => {
|
|
5024
|
+
return (this.authoredTableColumns().length === 0 &&
|
|
5025
|
+
(this.resolvedInheritedColumns().length > 0 || !!this.resourcePath().trim()));
|
|
5026
|
+
}, ...(ngDevMode ? [{ debugName: "isUsingInheritedResourceColumns" }] : /* istanbul ignore next */ []));
|
|
5027
|
+
effectiveTableConfigForEditor = computed(() => {
|
|
5028
|
+
const base = this.tableConfig();
|
|
5029
|
+
if (base.columns.length > 0) {
|
|
5030
|
+
return base;
|
|
5031
|
+
}
|
|
5032
|
+
const inherited = this.resolvedInheritedColumns();
|
|
5033
|
+
if (inherited.length > 0) {
|
|
5034
|
+
return {
|
|
5035
|
+
...base,
|
|
5036
|
+
columns: cloneColumns(inherited),
|
|
5037
|
+
};
|
|
5038
|
+
}
|
|
5039
|
+
return base;
|
|
5040
|
+
}, ...(ngDevMode ? [{ debugName: "effectiveTableConfigForEditor" }] : /* istanbul ignore next */ []));
|
|
5041
|
+
availableResourceFieldOptions = computed(() => {
|
|
5042
|
+
const seen = new Set();
|
|
5043
|
+
const options = [];
|
|
5044
|
+
const registerField = (field, header) => {
|
|
5045
|
+
const normalized = String(field || '').trim();
|
|
5046
|
+
if (!normalized || seen.has(normalized)) {
|
|
5047
|
+
return;
|
|
5048
|
+
}
|
|
5049
|
+
seen.add(normalized);
|
|
5050
|
+
options.push({
|
|
5051
|
+
field: normalized,
|
|
5052
|
+
header: String(header || normalized).trim() || normalized,
|
|
5053
|
+
});
|
|
5054
|
+
};
|
|
5055
|
+
for (const column of this.effectiveTableColumns()) {
|
|
5056
|
+
registerField(column.field, column.header);
|
|
5057
|
+
}
|
|
5058
|
+
for (const fieldItem of this.schemaResolvedFilterFieldsSignal()) {
|
|
5059
|
+
registerField(fieldItem.field, fieldItem.header);
|
|
5060
|
+
}
|
|
5061
|
+
return options;
|
|
5062
|
+
}, ...(ngDevMode ? [{ debugName: "availableResourceFieldOptions" }] : /* istanbul ignore next */ []));
|
|
5063
|
+
effectiveKpiBand = computed(() => {
|
|
5064
|
+
const meta = this.currentDocument().metadata;
|
|
5065
|
+
return (meta.kpiBand ||
|
|
5066
|
+
meta.table?.kpiBand ||
|
|
5067
|
+
meta.table?.behavior?.kpiBand ||
|
|
5068
|
+
null);
|
|
5069
|
+
}, ...(ngDevMode ? [{ debugName: "effectiveKpiBand" }] : /* istanbul ignore next */ []));
|
|
5070
|
+
kpiBandEnabled = computed(() => {
|
|
5071
|
+
const band = this.effectiveKpiBand();
|
|
5072
|
+
if (!band) {
|
|
5073
|
+
return false;
|
|
5074
|
+
}
|
|
5075
|
+
return band.enabled !== false;
|
|
5076
|
+
}, ...(ngDevMode ? [{ debugName: "kpiBandEnabled" }] : /* istanbul ignore next */ []));
|
|
5077
|
+
kpiCards = computed(() => {
|
|
5078
|
+
const cards = this.effectiveKpiBand()?.cards;
|
|
5079
|
+
return Array.isArray(cards) ? cards : [];
|
|
5080
|
+
}, ...(ngDevMode ? [{ debugName: "kpiCards" }] : /* istanbul ignore next */ []));
|
|
5081
|
+
kpiBandPreviewConfig = computed(() => ({
|
|
5082
|
+
...(this.effectiveKpiBand() || {}),
|
|
5083
|
+
enabled: true,
|
|
5084
|
+
cards: this.kpiCards(),
|
|
5085
|
+
}), ...(ngDevMode ? [{ debugName: "kpiBandPreviewConfig" }] : /* istanbul ignore next */ []));
|
|
5086
|
+
effectiveFilterBar = computed(() => this.currentDocument().metadata.filterBar || null, ...(ngDevMode ? [{ debugName: "effectiveFilterBar" }] : /* istanbul ignore next */ []));
|
|
5087
|
+
filterBarShowAdvanced = computed(() => this.effectiveFilterBar()?.showAdvanced !== false, ...(ngDevMode ? [{ debugName: "filterBarShowAdvanced" }] : /* istanbul ignore next */ []));
|
|
5088
|
+
filterBarInlineFields = computed(() => {
|
|
5089
|
+
const fields = this.effectiveFilterBar()?.inlineFields;
|
|
5090
|
+
return Array.isArray(fields) ? fields : [];
|
|
5091
|
+
}, ...(ngDevMode ? [{ debugName: "filterBarInlineFields" }] : /* istanbul ignore next */ []));
|
|
5092
|
+
filterBarQuickMode = computed(() => {
|
|
5093
|
+
const quickFilters = this.effectiveFilterBar()?.quickFilters;
|
|
5094
|
+
if (quickFilters === false) {
|
|
5095
|
+
return 'disabled';
|
|
5096
|
+
}
|
|
5097
|
+
if (Array.isArray(quickFilters)) {
|
|
5098
|
+
return 'custom';
|
|
5099
|
+
}
|
|
5100
|
+
return 'inherit';
|
|
5101
|
+
}, ...(ngDevMode ? [{ debugName: "filterBarQuickMode" }] : /* istanbul ignore next */ []));
|
|
5102
|
+
filterBarQuickFilters = computed(() => {
|
|
5103
|
+
const quickFilters = this.effectiveFilterBar()?.quickFilters;
|
|
5104
|
+
return Array.isArray(quickFilters) ? quickFilters : [];
|
|
5105
|
+
}, ...(ngDevMode ? [{ debugName: "filterBarQuickFilters" }] : /* istanbul ignore next */ []));
|
|
4637
5106
|
defaultsOpenMode = computed(() => this.currentDocument().metadata.defaults?.openMode || 'route', ...(ngDevMode ? [{ debugName: "defaultsOpenMode" }] : /* istanbul ignore next */ []));
|
|
4638
5107
|
headerShowBack = computed(() => !!this.currentDocument().metadata.defaults?.header?.showBack, ...(ngDevMode ? [{ debugName: "headerShowBack" }] : /* istanbul ignore next */ []));
|
|
4639
5108
|
headerBackLabel = computed(() => this.currentDocument().metadata.defaults?.header?.backLabel || '', ...(ngDevMode ? [{ debugName: "headerBackLabel" }] : /* istanbul ignore next */ []));
|
|
@@ -4652,12 +5121,12 @@ class CrudMetadataEditorComponent {
|
|
|
4652
5121
|
backReturnTo = computed(() => this.currentDocument().metadata.defaults?.back?.returnTo || '', ...(ngDevMode ? [{ debugName: "backReturnTo" }] : /* istanbul ignore next */ []));
|
|
4653
5122
|
backConfirmOnDirty = computed(() => !!this.currentDocument().metadata.defaults?.back?.confirmOnDirty, ...(ngDevMode ? [{ debugName: "backConfirmOnDirty" }] : /* istanbul ignore next */ []));
|
|
4654
5123
|
nextFocusSection = computed(() => {
|
|
4655
|
-
for (const section of
|
|
5124
|
+
for (const section of SECTION_KEYS) {
|
|
4656
5125
|
if (this.sectionStatus(section) === 'invalid') {
|
|
4657
5126
|
return section;
|
|
4658
5127
|
}
|
|
4659
5128
|
}
|
|
4660
|
-
for (const section of
|
|
5129
|
+
for (const section of SECTION_KEYS) {
|
|
4661
5130
|
if (this.sectionStatus(section) === 'pending') {
|
|
4662
5131
|
return section;
|
|
4663
5132
|
}
|
|
@@ -4666,24 +5135,21 @@ class CrudMetadataEditorComponent {
|
|
|
4666
5135
|
}, ...(ngDevMode ? [{ debugName: "nextFocusSection" }] : /* istanbul ignore next */ []));
|
|
4667
5136
|
nextFocusStatus = computed(() => this.sectionStatus(this.nextFocusSection()), ...(ngDevMode ? [{ debugName: "nextFocusStatus" }] : /* istanbul ignore next */ []));
|
|
4668
5137
|
visibleHealthBuckets = computed(() => {
|
|
4669
|
-
const attentionBuckets = ['invalid', 'pending']
|
|
4670
|
-
.filter((bucket) => this.hasHealthBucketContent(bucket));
|
|
5138
|
+
const attentionBuckets = ['invalid', 'pending'].filter((bucket) => this.hasHealthBucketContent(bucket));
|
|
4671
5139
|
if (attentionBuckets.length) {
|
|
4672
5140
|
return attentionBuckets;
|
|
4673
5141
|
}
|
|
4674
5142
|
return this.hasHealthBucketContent('ready') ? ['ready'] : [];
|
|
4675
5143
|
}, ...(ngDevMode ? [{ debugName: "visibleHealthBuckets" }] : /* istanbul ignore next */ []));
|
|
4676
5144
|
showHealthMap = computed(() => this.visibleHealthBuckets().length > 1, ...(ngDevMode ? [{ debugName: "showHealthMap" }] : /* istanbul ignore next */ []));
|
|
4677
|
-
groupedDiagnostics = computed(() =>
|
|
4678
|
-
.map((section) => {
|
|
5145
|
+
groupedDiagnostics = computed(() => SECTION_KEYS.map((section) => {
|
|
4679
5146
|
const issues = this.sectionDiagnostics(section);
|
|
4680
5147
|
return {
|
|
4681
5148
|
section,
|
|
4682
5149
|
issues,
|
|
4683
5150
|
hasError: issues.some((issue) => issue.level === 'error'),
|
|
4684
5151
|
};
|
|
4685
|
-
})
|
|
4686
|
-
.filter((group) => group.issues.length > 0), ...(ngDevMode ? [{ debugName: "groupedDiagnostics" }] : /* istanbul ignore next */ []));
|
|
5152
|
+
}).filter((group) => group.issues.length > 0), ...(ngDevMode ? [{ debugName: "groupedDiagnostics" }] : /* istanbul ignore next */ []));
|
|
4687
5153
|
constructor() {
|
|
4688
5154
|
effect(() => {
|
|
4689
5155
|
const seed = this.resolveExternalSeed();
|
|
@@ -4695,8 +5161,31 @@ class CrudMetadataEditorComponent {
|
|
|
4695
5161
|
const parsed = parseLegacyOrCrudDocument(seed);
|
|
4696
5162
|
this.initialDocument.set(parsed);
|
|
4697
5163
|
this.currentDocument.set(parsed);
|
|
5164
|
+
// Observe the supplied snapshot with its external resource, before local edits.
|
|
5165
|
+
this.providedSnapshotOwner();
|
|
5166
|
+
this.synchronizeSchemaScope();
|
|
5167
|
+
this.kpiFilterJsonDrafts.set({});
|
|
5168
|
+
this.quickFilterExpressionDrafts.set({});
|
|
4698
5169
|
this.syncState();
|
|
4699
5170
|
});
|
|
5171
|
+
effect(() => {
|
|
5172
|
+
const path = this.resourcePath().trim();
|
|
5173
|
+
const endpointKey = this.resourceEndpointKey().trim();
|
|
5174
|
+
const apiUrlEntry = this.currentDocument().metadata.resource?.apiUrlEntry ?? null;
|
|
5175
|
+
const signature = this.resourceSchemaIdentity(this.currentDocument().metadata);
|
|
5176
|
+
this.synchronizeSchemaScope();
|
|
5177
|
+
if (!path)
|
|
5178
|
+
return;
|
|
5179
|
+
if (signature === this.lastLoadedResourceSignature) {
|
|
5180
|
+
return;
|
|
5181
|
+
}
|
|
5182
|
+
this.lastLoadedResourceSignature = signature;
|
|
5183
|
+
this.loadResourceSchemaColumns(path, endpointKey || undefined, apiUrlEntry);
|
|
5184
|
+
});
|
|
5185
|
+
this.destroyRef.onDestroy(() => {
|
|
5186
|
+
this.destroyed = true;
|
|
5187
|
+
this.invalidateSchemaScope();
|
|
5188
|
+
});
|
|
4700
5189
|
}
|
|
4701
5190
|
getSettingsValue() {
|
|
4702
5191
|
return this.buildPayload();
|
|
@@ -4706,7 +5195,11 @@ class CrudMetadataEditorComponent {
|
|
|
4706
5195
|
}
|
|
4707
5196
|
reset() {
|
|
4708
5197
|
this.currentDocument.set(this.initialDocument());
|
|
5198
|
+
this.synchronizeSchemaScope();
|
|
4709
5199
|
this.initialValueDrafts.set({});
|
|
5200
|
+
this.kpiFilterJsonDrafts.set({});
|
|
5201
|
+
this.quickFilterExpressionDrafts.set({});
|
|
5202
|
+
this.inlineFieldDraft.set('');
|
|
4710
5203
|
this.syncState();
|
|
4711
5204
|
}
|
|
4712
5205
|
actionValue(actionName) {
|
|
@@ -4794,8 +5287,7 @@ class CrudMetadataEditorComponent {
|
|
|
4794
5287
|
const parts = [];
|
|
4795
5288
|
const params = this.actionParams(actionName);
|
|
4796
5289
|
if (params.length) {
|
|
4797
|
-
parts.push(this.tx('crud.authoring.action.paramsSummary', '{count} mappings')
|
|
4798
|
-
.replace('{count}', String(params.length)));
|
|
5290
|
+
parts.push(this.tx('crud.authoring.action.paramsSummary', '{count} mappings').replace('{count}', String(params.length)));
|
|
4799
5291
|
}
|
|
4800
5292
|
else {
|
|
4801
5293
|
parts.push(this.tx('crud.authoring.action.paramsNone', 'No param mappings yet'));
|
|
@@ -4846,8 +5338,7 @@ class CrudMetadataEditorComponent {
|
|
|
4846
5338
|
if (apiStatus !== 'ready') {
|
|
4847
5339
|
pending.push(this.tx('crud.authoring.action.apiGroup', 'API mapping'));
|
|
4848
5340
|
}
|
|
4849
|
-
const summary = this.tx('crud.authoring.action.advancedPending', 'Pending: {groups}')
|
|
4850
|
-
.replace('{groups}', pending.join(', '));
|
|
5341
|
+
const summary = this.tx('crud.authoring.action.advancedPending', 'Pending: {groups}').replace('{groups}', pending.join(', '));
|
|
4851
5342
|
return inputsSummary ? `${summary} · ${inputsSummary}` : summary;
|
|
4852
5343
|
}
|
|
4853
5344
|
actionPrimarySummary(actionName) {
|
|
@@ -4864,8 +5355,7 @@ class CrudMetadataEditorComponent {
|
|
|
4864
5355
|
invalid.push(this.tx('crud.authoring.action.schemaGroup', 'Schema contract'));
|
|
4865
5356
|
}
|
|
4866
5357
|
if (invalid.length) {
|
|
4867
|
-
return this.tx('crud.authoring.action.primaryInvalid', 'Resolve: {groups}')
|
|
4868
|
-
.replace('{groups}', invalid.join(', '));
|
|
5358
|
+
return this.tx('crud.authoring.action.primaryInvalid', 'Resolve: {groups}').replace('{groups}', invalid.join(', '));
|
|
4869
5359
|
}
|
|
4870
5360
|
const pending = [];
|
|
4871
5361
|
if (bindingStatus !== 'ready') {
|
|
@@ -4874,8 +5364,7 @@ class CrudMetadataEditorComponent {
|
|
|
4874
5364
|
if (schemaStatus !== 'ready') {
|
|
4875
5365
|
pending.push(this.tx('crud.authoring.action.schemaGroup', 'Schema contract'));
|
|
4876
5366
|
}
|
|
4877
|
-
return this.tx('crud.authoring.action.primaryPending', 'Next: {groups}')
|
|
4878
|
-
.replace('{groups}', pending.join(', '));
|
|
5367
|
+
return this.tx('crud.authoring.action.primaryPending', 'Next: {groups}').replace('{groups}', pending.join(', '));
|
|
4879
5368
|
}
|
|
4880
5369
|
actionBindingNote(actionName) {
|
|
4881
5370
|
if (this.actionGroupStatus(actionName, 'binding') === 'ready') {
|
|
@@ -4917,12 +5406,10 @@ class CrudMetadataEditorComponent {
|
|
|
4917
5406
|
}
|
|
4918
5407
|
const parts = [];
|
|
4919
5408
|
if (actionBack.strategy) {
|
|
4920
|
-
parts.push(this.tx('crud.authoring.action.backNoteStrategy', 'Strategy: {value}')
|
|
4921
|
-
.replace('{value}', this.backStrategyLabel(actionBack.strategy)));
|
|
5409
|
+
parts.push(this.tx('crud.authoring.action.backNoteStrategy', 'Strategy: {value}').replace('{value}', this.backStrategyLabel(actionBack.strategy)));
|
|
4922
5410
|
}
|
|
4923
5411
|
if (actionBack.returnTo) {
|
|
4924
|
-
parts.push(this.tx('crud.authoring.action.backNoteReturnTo', 'Return: {value}')
|
|
4925
|
-
.replace('{value}', actionBack.returnTo));
|
|
5412
|
+
parts.push(this.tx('crud.authoring.action.backNoteReturnTo', 'Return: {value}').replace('{value}', actionBack.returnTo));
|
|
4926
5413
|
}
|
|
4927
5414
|
if (actionBack.confirmOnDirty) {
|
|
4928
5415
|
parts.push(this.tx('crud.authoring.action.backNoteConfirmOnDirty', 'Dirty confirmation'));
|
|
@@ -4934,8 +5421,8 @@ class CrudMetadataEditorComponent {
|
|
|
4934
5421
|
const effectiveMode = (action.openMode || this.defaultsOpenMode());
|
|
4935
5422
|
const actionDiagnostics = this.actionDiagnostics(actionName);
|
|
4936
5423
|
if (group === 'binding') {
|
|
4937
|
-
const hasBindingError = actionDiagnostics.some((issue) =>
|
|
4938
|
-
|
|
5424
|
+
const hasBindingError = actionDiagnostics.some((issue) => issue.code === 'crud.metadata.actions.route.required' ||
|
|
5425
|
+
issue.code === 'crud.metadata.actions.formId.required');
|
|
4939
5426
|
if (hasBindingError) {
|
|
4940
5427
|
return 'invalid';
|
|
4941
5428
|
}
|
|
@@ -4974,26 +5461,22 @@ class CrudMetadataEditorComponent {
|
|
|
4974
5461
|
actionGroupsSummary(actionName) {
|
|
4975
5462
|
const invalidGroups = this.actionGroupLabelsByStatus(actionName, 'invalid');
|
|
4976
5463
|
if (invalidGroups.length) {
|
|
4977
|
-
return this.tx('crud.authoring.action.groupsInvalid', 'Invalid: {groups}')
|
|
4978
|
-
.replace('{groups}', invalidGroups.join(', '));
|
|
5464
|
+
return this.tx('crud.authoring.action.groupsInvalid', 'Invalid: {groups}').replace('{groups}', invalidGroups.join(', '));
|
|
4979
5465
|
}
|
|
4980
5466
|
const pendingGroups = this.actionGroupLabelsByStatus(actionName, 'pending');
|
|
4981
5467
|
if (pendingGroups.length) {
|
|
4982
|
-
return this.tx('crud.authoring.action.groupsPending', 'Pending: {groups}')
|
|
4983
|
-
.replace('{groups}', pendingGroups.join(', '));
|
|
5468
|
+
return this.tx('crud.authoring.action.groupsPending', 'Pending: {groups}').replace('{groups}', pendingGroups.join(', '));
|
|
4984
5469
|
}
|
|
4985
5470
|
return this.tx('crud.authoring.action.groupsReady', 'All groups ready');
|
|
4986
5471
|
}
|
|
4987
5472
|
actionEditorialSummary(actionName) {
|
|
4988
5473
|
const invalidGroups = this.actionGroupLabelsByStatus(actionName, 'invalid');
|
|
4989
5474
|
if (invalidGroups.length) {
|
|
4990
|
-
return this.tx('crud.authoring.action.editorialInvalid', 'Resolve: {groups}')
|
|
4991
|
-
.replace('{groups}', invalidGroups.join(', '));
|
|
5475
|
+
return this.tx('crud.authoring.action.editorialInvalid', 'Resolve: {groups}').replace('{groups}', invalidGroups.join(', '));
|
|
4992
5476
|
}
|
|
4993
5477
|
const pendingGroups = this.actionGroupLabelsByStatus(actionName, 'pending');
|
|
4994
5478
|
if (pendingGroups.length) {
|
|
4995
|
-
return this.tx('crud.authoring.action.editorialPending', 'Finish: {groups}')
|
|
4996
|
-
.replace('{groups}', pendingGroups.join(', '));
|
|
5479
|
+
return this.tx('crud.authoring.action.editorialPending', 'Finish: {groups}').replace('{groups}', pendingGroups.join(', '));
|
|
4997
5480
|
}
|
|
4998
5481
|
return this.tx('crud.authoring.action.editorialReady', 'Ready to use');
|
|
4999
5482
|
}
|
|
@@ -5001,8 +5484,8 @@ class CrudMetadataEditorComponent {
|
|
|
5001
5484
|
return this.actionStatus(actionName) !== 'ready';
|
|
5002
5485
|
}
|
|
5003
5486
|
actionAdvancedPanelExpanded(actionName) {
|
|
5004
|
-
return this.actionGroupStatus(actionName, 'submit') !== 'ready'
|
|
5005
|
-
|
|
5487
|
+
return (this.actionGroupStatus(actionName, 'submit') !== 'ready' ||
|
|
5488
|
+
this.actionGroupStatus(actionName, 'api') !== 'ready');
|
|
5006
5489
|
}
|
|
5007
5490
|
actionStatus(actionName) {
|
|
5008
5491
|
const diagnosticPaths = this.actionDiagnostics(actionName);
|
|
@@ -5050,8 +5533,7 @@ class CrudMetadataEditorComponent {
|
|
|
5050
5533
|
: this.tx('crud.authoring.overview.sourceRemote', 'Fonte remota'));
|
|
5051
5534
|
}
|
|
5052
5535
|
else if (Array.isArray(localRows)) {
|
|
5053
|
-
parts.push(this.tx('crud.authoring.overview.sourceLocal', 'Fonte local · {count} registros')
|
|
5054
|
-
.replace('{count}', String(localRows.length)));
|
|
5536
|
+
parts.push(this.tx('crud.authoring.overview.sourceLocal', 'Fonte local · {count} registros').replace('{count}', String(localRows.length)));
|
|
5055
5537
|
}
|
|
5056
5538
|
if (!parts.length) {
|
|
5057
5539
|
return this.tx('crud.authoring.overview.connectionDetailsPending', 'Identifier and endpoint details are still optional here.');
|
|
@@ -5068,12 +5550,10 @@ class CrudMetadataEditorComponent {
|
|
|
5068
5550
|
: this.tx('crud.authoring.overview.backHidden', 'Back hidden'),
|
|
5069
5551
|
];
|
|
5070
5552
|
if (this.modalDensity() !== 'default') {
|
|
5071
|
-
parts.push(this.tx('crud.authoring.overview.modalDensity', 'Modal: {value}')
|
|
5072
|
-
.replace('{value}', this.modalDensityLabel(this.modalDensity())));
|
|
5553
|
+
parts.push(this.tx('crud.authoring.overview.modalDensity', 'Modal: {value}').replace('{value}', this.modalDensityLabel(this.modalDensity())));
|
|
5073
5554
|
}
|
|
5074
5555
|
if (this.backStrategy() !== 'auto') {
|
|
5075
|
-
parts.push(this.tx('crud.authoring.overview.backStrategy', 'Back: {value}')
|
|
5076
|
-
.replace('{value}', this.backStrategyLabel(this.backStrategy())));
|
|
5556
|
+
parts.push(this.tx('crud.authoring.overview.backStrategy', 'Back: {value}').replace('{value}', this.backStrategyLabel(this.backStrategy())));
|
|
5077
5557
|
}
|
|
5078
5558
|
if (this.modalRememberLastState()) {
|
|
5079
5559
|
parts.push(this.tx('crud.authoring.overview.modalRememberState', 'State remembered'));
|
|
@@ -5091,8 +5571,7 @@ class CrudMetadataEditorComponent {
|
|
|
5091
5571
|
}
|
|
5092
5572
|
modalDefaultsNote() {
|
|
5093
5573
|
const parts = [
|
|
5094
|
-
this.tx('crud.authoring.defaults.modal.noteDensity', 'Density: {value}')
|
|
5095
|
-
.replace('{value}', this.modalDensityLabel(this.modalDensity())),
|
|
5574
|
+
this.tx('crud.authoring.defaults.modal.noteDensity', 'Density: {value}').replace('{value}', this.modalDensityLabel(this.modalDensity())),
|
|
5096
5575
|
];
|
|
5097
5576
|
if (!this.modalCanMaximize()) {
|
|
5098
5577
|
parts.push(this.tx('crud.authoring.defaults.modal.noteNoMaximize', 'Maximize hidden'));
|
|
@@ -5110,19 +5589,16 @@ class CrudMetadataEditorComponent {
|
|
|
5110
5589
|
parts.push(this.tx('crud.authoring.defaults.modal.noteBackdropLocked', 'Backdrop locked'));
|
|
5111
5590
|
}
|
|
5112
5591
|
if (this.modalFullscreenBreakpoint().trim()) {
|
|
5113
|
-
parts.push(this.tx('crud.authoring.defaults.modal.noteBreakpoint', 'Fullscreen at {value}px')
|
|
5114
|
-
.replace('{value}', this.modalFullscreenBreakpoint().trim()));
|
|
5592
|
+
parts.push(this.tx('crud.authoring.defaults.modal.noteBreakpoint', 'Fullscreen at {value}px').replace('{value}', this.modalFullscreenBreakpoint().trim()));
|
|
5115
5593
|
}
|
|
5116
5594
|
return parts.join(' · ');
|
|
5117
5595
|
}
|
|
5118
5596
|
backDefaultsNote() {
|
|
5119
5597
|
const parts = [
|
|
5120
|
-
this.tx('crud.authoring.defaults.back.noteStrategy', 'Strategy: {value}')
|
|
5121
|
-
.replace('{value}', this.backStrategyLabel(this.backStrategy())),
|
|
5598
|
+
this.tx('crud.authoring.defaults.back.noteStrategy', 'Strategy: {value}').replace('{value}', this.backStrategyLabel(this.backStrategy())),
|
|
5122
5599
|
];
|
|
5123
5600
|
if (this.backReturnTo().trim()) {
|
|
5124
|
-
parts.push(this.tx('crud.authoring.defaults.back.noteReturnTo', 'Return: {value}')
|
|
5125
|
-
.replace('{value}', this.backReturnTo().trim()));
|
|
5601
|
+
parts.push(this.tx('crud.authoring.defaults.back.noteReturnTo', 'Return: {value}').replace('{value}', this.backReturnTo().trim()));
|
|
5126
5602
|
}
|
|
5127
5603
|
if (this.backConfirmOnDirty()) {
|
|
5128
5604
|
parts.push(this.tx('crud.authoring.defaults.back.noteConfirmOnDirty', 'Dirty confirmation'));
|
|
@@ -5151,13 +5627,46 @@ class CrudMetadataEditorComponent {
|
|
|
5151
5627
|
.replace('{action}', this.actionLabel(focusedAction))
|
|
5152
5628
|
.replace('{summary}', this.actionFocusSummary(focusedAction));
|
|
5153
5629
|
}
|
|
5154
|
-
return this.tx('crud.authoring.overview.actionsTracked', '{count} tracked action blocks')
|
|
5155
|
-
|
|
5630
|
+
return this.tx('crud.authoring.overview.actionsTracked', '{count} tracked action blocks').replace('{count}', String(this.actionKeys.length));
|
|
5631
|
+
}
|
|
5632
|
+
kpisOverview() {
|
|
5633
|
+
if (!this.kpiBandEnabled()) {
|
|
5634
|
+
return this.tx('crud.authoring.overview.kpisDisabled', 'Disabled');
|
|
5635
|
+
}
|
|
5636
|
+
const count = this.kpiCards().length;
|
|
5637
|
+
if (count === 0) {
|
|
5638
|
+
return this.tx('crud.authoring.overview.kpisEmpty', 'Enabled · add cards');
|
|
5639
|
+
}
|
|
5640
|
+
return this.tx('crud.authoring.overview.kpisEnabled', '{count} cards configured').replace('{count}', String(count));
|
|
5641
|
+
}
|
|
5642
|
+
kpisOverviewNote() {
|
|
5643
|
+
if (!this.kpiBandEnabled()) {
|
|
5644
|
+
return this.tx('crud.authoring.kpis.sectionSummary', 'Configure operational summary indicators that can also filter the collection when clicked.');
|
|
5645
|
+
}
|
|
5646
|
+
const interactiveCount = this.kpiCards().filter((card) => card.filter !== undefined && card.filter !== null).length;
|
|
5647
|
+
return `${interactiveCount} / ${this.kpiCards().length} cards filterable`;
|
|
5648
|
+
}
|
|
5649
|
+
filtersOverview() {
|
|
5650
|
+
const inlineCount = this.filterBarInlineFields().length;
|
|
5651
|
+
const quickCount = this.filterBarQuickFilters().length;
|
|
5652
|
+
if (inlineCount === 0 && this.filterBarQuickMode() === 'inherit') {
|
|
5653
|
+
return this.tx('crud.authoring.overview.filtersDefault', 'Automatic resource discovery');
|
|
5654
|
+
}
|
|
5655
|
+
return this.tx('crud.authoring.overview.filtersConfigured', '{inline} inline · {quick} quick')
|
|
5656
|
+
.replace('{inline}', String(inlineCount))
|
|
5657
|
+
.replace('{quick}', String(quickCount));
|
|
5658
|
+
}
|
|
5659
|
+
filtersOverviewNote() {
|
|
5660
|
+
const modeLabel = this.quickFilterModeLabel(this.filterBarQuickMode());
|
|
5661
|
+
const advLabel = this.filterBarShowAdvanced()
|
|
5662
|
+
? this.tx('crud.authoring.filters.showAdvanced', 'Allow opening advanced filters')
|
|
5663
|
+
: 'Advanced off';
|
|
5664
|
+
return `${modeLabel} · ${advLabel}`;
|
|
5156
5665
|
}
|
|
5157
5666
|
tableOverview() {
|
|
5158
5667
|
const table = this.tableConfig();
|
|
5159
5668
|
const title = table.toolbar?.title;
|
|
5160
|
-
const count =
|
|
5669
|
+
const count = this.effectiveTableColumns().length;
|
|
5161
5670
|
if (title) {
|
|
5162
5671
|
return title;
|
|
5163
5672
|
}
|
|
@@ -5167,6 +5676,9 @@ class CrudMetadataEditorComponent {
|
|
|
5167
5676
|
const table = this.tableConfig();
|
|
5168
5677
|
const subtitle = table.toolbar?.subtitle;
|
|
5169
5678
|
const parts = [this.tableBehaviorSummary(), this.tableStatesSummary()];
|
|
5679
|
+
if (this.isUsingInheritedResourceColumns()) {
|
|
5680
|
+
parts.unshift(this.inheritedColumnsCountLabel());
|
|
5681
|
+
}
|
|
5170
5682
|
if (subtitle) {
|
|
5171
5683
|
parts.unshift(subtitle);
|
|
5172
5684
|
}
|
|
@@ -5174,7 +5686,7 @@ class CrudMetadataEditorComponent {
|
|
|
5174
5686
|
}
|
|
5175
5687
|
tableStructureSummary() {
|
|
5176
5688
|
const table = this.tableConfig();
|
|
5177
|
-
const count =
|
|
5689
|
+
const count = this.effectiveTableColumns().length;
|
|
5178
5690
|
const density = table.appearance?.density
|
|
5179
5691
|
? this.tx(`crud.authoring.table.density.${table.appearance.density}`, table.appearance.density)
|
|
5180
5692
|
: this.tx('crud.authoring.table.density.comfortable', 'Comfortable');
|
|
@@ -5191,8 +5703,7 @@ class CrudMetadataEditorComponent {
|
|
|
5191
5703
|
parts.push(this.tx('crud.authoring.table.paginationOff', 'Pagination off'));
|
|
5192
5704
|
}
|
|
5193
5705
|
else {
|
|
5194
|
-
parts.push(this.tx('crud.authoring.table.paginationSummary', 'Page size {value}')
|
|
5195
|
-
.replace('{value}', String(pagination?.pageSize || 10)));
|
|
5706
|
+
parts.push(this.tx('crud.authoring.table.paginationSummary', 'Page size {value}').replace('{value}', String(pagination?.pageSize || 10)));
|
|
5196
5707
|
}
|
|
5197
5708
|
if (sorting?.enabled === false) {
|
|
5198
5709
|
parts.push(this.tx('crud.authoring.table.sortingOff', 'Sorting off'));
|
|
@@ -5206,29 +5717,60 @@ class CrudMetadataEditorComponent {
|
|
|
5206
5717
|
}
|
|
5207
5718
|
tableStatesSummary() {
|
|
5208
5719
|
const states = this.tableConfig().messages?.states;
|
|
5209
|
-
const customized = [
|
|
5210
|
-
states?.loading,
|
|
5211
|
-
states?.empty,
|
|
5212
|
-
states?.noResults,
|
|
5213
|
-
states?.error,
|
|
5214
|
-
].filter((value) => !!String(value || '').trim()).length;
|
|
5720
|
+
const customized = [states?.loading, states?.empty, states?.noResults, states?.error].filter((value) => !!String(value || '').trim()).length;
|
|
5215
5721
|
if (!customized) {
|
|
5216
5722
|
return this.tx('crud.authoring.table.statesDefault', 'Default state copy');
|
|
5217
5723
|
}
|
|
5218
|
-
return this.tx('crud.authoring.table.statesCustom', '{count} custom states')
|
|
5219
|
-
.replace('{count}', String(customized));
|
|
5724
|
+
return this.tx('crud.authoring.table.statesCustom', '{count} custom states').replace('{count}', String(customized));
|
|
5220
5725
|
}
|
|
5221
5726
|
tableEditingFlowSummary() {
|
|
5222
5727
|
return this.tx('crud.authoring.table.flowSummary', 'Core presentation stays visible below. Pagination, sorting, state copy, and columns stay in advanced table panels.');
|
|
5223
5728
|
}
|
|
5729
|
+
inheritedColumnsCountLabel() {
|
|
5730
|
+
const count = this.resolvedInheritedColumns().length;
|
|
5731
|
+
if (count > 0) {
|
|
5732
|
+
return this.tx('crud.authoring.table.inheritedColumnsCount', '{count} resource columns').replace('{count}', String(count));
|
|
5733
|
+
}
|
|
5734
|
+
return this.tx('crud.authoring.table.inheritedColumnsPending', 'Columns inherited from resource');
|
|
5735
|
+
}
|
|
5736
|
+
inheritedColumnsNote() {
|
|
5737
|
+
return this.tx('crud.authoring.table.inheritedColumnsNote', 'Columns are automatically derived from the resource contract ({path}). You can reorder, hide, or format columns below.').replace('{path}', this.resourcePath() || 'resource');
|
|
5738
|
+
}
|
|
5739
|
+
focusTableColumnsAuthoring() {
|
|
5740
|
+
this.selectedTabIndex.set(SECTION_TAB_INDEX.table);
|
|
5741
|
+
const expandColumnsPanel = () => {
|
|
5742
|
+
const root = this.hostRef?.nativeElement;
|
|
5743
|
+
if (!root) {
|
|
5744
|
+
return;
|
|
5745
|
+
}
|
|
5746
|
+
const panel = root.querySelector('[data-testid="table-inline-editor-columns-panel"]');
|
|
5747
|
+
if (!panel) {
|
|
5748
|
+
return;
|
|
5749
|
+
}
|
|
5750
|
+
if (!panel.classList.contains('mat-expanded')) {
|
|
5751
|
+
const header = panel.querySelector('mat-expansion-panel-header');
|
|
5752
|
+
header?.click();
|
|
5753
|
+
}
|
|
5754
|
+
panel.scrollIntoView?.({ behavior: 'smooth', block: 'nearest' });
|
|
5755
|
+
};
|
|
5756
|
+
expandColumnsPanel();
|
|
5757
|
+
setTimeout(expandColumnsPanel, 0);
|
|
5758
|
+
}
|
|
5759
|
+
resetToInheritedColumns() {
|
|
5760
|
+
this.updateMetadata((metadata) => ({
|
|
5761
|
+
...metadata,
|
|
5762
|
+
table: {
|
|
5763
|
+
...(metadata.table || {}),
|
|
5764
|
+
columns: [],
|
|
5765
|
+
},
|
|
5766
|
+
}));
|
|
5767
|
+
}
|
|
5224
5768
|
validationOverview() {
|
|
5225
5769
|
if (this.errorCount() > 0) {
|
|
5226
|
-
return this.tx('crud.authoring.overview.validationErrors', '{count} errors')
|
|
5227
|
-
.replace('{count}', String(this.errorCount()));
|
|
5770
|
+
return this.tx('crud.authoring.overview.validationErrors', '{count} errors').replace('{count}', String(this.errorCount()));
|
|
5228
5771
|
}
|
|
5229
5772
|
if (this.warningCount() > 0) {
|
|
5230
|
-
return this.tx('crud.authoring.overview.validationWarnings', '{count} warnings')
|
|
5231
|
-
.replace('{count}', String(this.warningCount()));
|
|
5773
|
+
return this.tx('crud.authoring.overview.validationWarnings', '{count} warnings').replace('{count}', String(this.warningCount()));
|
|
5232
5774
|
}
|
|
5233
5775
|
return this.tx('crud.authoring.overview.validationClean', 'No blocking diagnostics');
|
|
5234
5776
|
}
|
|
@@ -5252,6 +5794,9 @@ class CrudMetadataEditorComponent {
|
|
|
5252
5794
|
if (diagnostics.some((issue) => issue.level === 'error')) {
|
|
5253
5795
|
return 'invalid';
|
|
5254
5796
|
}
|
|
5797
|
+
if (section === 'kpis' && Object.keys(this.kpiFilterJsonDrafts()).length > 0) {
|
|
5798
|
+
return 'invalid';
|
|
5799
|
+
}
|
|
5255
5800
|
if (diagnostics.some((issue) => issue.level === 'warning')) {
|
|
5256
5801
|
return 'pending';
|
|
5257
5802
|
}
|
|
@@ -5266,7 +5811,19 @@ class CrudMetadataEditorComponent {
|
|
|
5266
5811
|
? 'ready'
|
|
5267
5812
|
: 'pending';
|
|
5268
5813
|
}
|
|
5269
|
-
|
|
5814
|
+
if (section === 'kpis') {
|
|
5815
|
+
if (this.kpiBandEnabled() && this.kpiCards().length === 0) {
|
|
5816
|
+
return 'pending';
|
|
5817
|
+
}
|
|
5818
|
+
return 'ready';
|
|
5819
|
+
}
|
|
5820
|
+
if (section === 'filters') {
|
|
5821
|
+
if (this.filterBarQuickMode() === 'custom' && this.filterBarQuickFilters().length === 0) {
|
|
5822
|
+
return 'pending';
|
|
5823
|
+
}
|
|
5824
|
+
return 'ready';
|
|
5825
|
+
}
|
|
5826
|
+
const table = this.effectiveTableConfigForEditor();
|
|
5270
5827
|
const hasTitle = !!String(table.toolbar?.title || '').trim();
|
|
5271
5828
|
const hasColumns = (table.columns?.length || 0) > 0;
|
|
5272
5829
|
return hasTitle || hasColumns ? 'ready' : 'pending';
|
|
@@ -5318,6 +5875,12 @@ class CrudMetadataEditorComponent {
|
|
|
5318
5875
|
.replace('{total}', String(this.actionKeys.length))
|
|
5319
5876
|
.replace('{action}', focusedAction ? this.actionLabel(focusedAction) : this.actionLabel('create'));
|
|
5320
5877
|
}
|
|
5878
|
+
if (section === 'kpis') {
|
|
5879
|
+
return this.tx('crud.authoring.kpis.sectionSummary', 'Configure operational summary indicators that can also filter the collection when clicked.');
|
|
5880
|
+
}
|
|
5881
|
+
if (section === 'filters') {
|
|
5882
|
+
return this.tx('crud.authoring.filters.sectionSummary', 'Configure always-visible search fields, quick filter chips, and access to the advanced filter drawer.');
|
|
5883
|
+
}
|
|
5321
5884
|
return this.tx('crud.authoring.table.sectionSummary', '{structure}. {behavior}. {states}.')
|
|
5322
5885
|
.replace('{structure}', this.tableStructureSummary())
|
|
5323
5886
|
.replace('{behavior}', this.tableBehaviorSummary())
|
|
@@ -5368,14 +5931,14 @@ class CrudMetadataEditorComponent {
|
|
|
5368
5931
|
}[bucket];
|
|
5369
5932
|
}
|
|
5370
5933
|
healthBucketSummary(bucket) {
|
|
5371
|
-
const sections =
|
|
5372
|
-
.filter((section) => this.sectionStatus(section) === bucket);
|
|
5934
|
+
const sections = SECTION_KEYS.filter((section) => this.sectionStatus(section) === bucket);
|
|
5373
5935
|
if (!sections.length) {
|
|
5374
5936
|
return this.tx(`crud.authoring.health.${bucket}Empty`, 'None');
|
|
5375
5937
|
}
|
|
5376
5938
|
return sections.map((section) => this.sectionDisplayLabel(section)).join(' · ');
|
|
5377
5939
|
}
|
|
5378
5940
|
diagnosticsGroupStatusLabel(section, issues) {
|
|
5941
|
+
void section;
|
|
5379
5942
|
const hasError = issues.some((issue) => issue.level === 'error');
|
|
5380
5943
|
if (hasError) {
|
|
5381
5944
|
return this.tx('crud.authoring.diagnostics.errors', 'Errors');
|
|
@@ -5383,19 +5946,18 @@ class CrudMetadataEditorComponent {
|
|
|
5383
5946
|
return this.tx('crud.authoring.diagnostics.warnings', 'Warnings');
|
|
5384
5947
|
}
|
|
5385
5948
|
diagnosticsGroupSummary(section, issues) {
|
|
5949
|
+
void section;
|
|
5386
5950
|
const errorCount = issues.filter((issue) => issue.level === 'error').length;
|
|
5387
5951
|
const warningCount = issues.length - errorCount;
|
|
5388
5952
|
if (errorCount > 0) {
|
|
5389
|
-
return this.tx('crud.authoring.diagnostics.errorSummary', '{count} blocking diagnostics in this section.')
|
|
5390
|
-
.replace('{count}', String(errorCount));
|
|
5953
|
+
return this.tx('crud.authoring.diagnostics.errorSummary', '{count} blocking diagnostics in this section.').replace('{count}', String(errorCount));
|
|
5391
5954
|
}
|
|
5392
5955
|
const key = warningCount === 1
|
|
5393
5956
|
? 'crud.authoring.diagnostics.warningSummarySingle'
|
|
5394
5957
|
: 'crud.authoring.diagnostics.warningSummary';
|
|
5395
5958
|
return this.tx(key, warningCount === 1
|
|
5396
5959
|
? '1 diagnostic worth reviewing in this section.'
|
|
5397
|
-
: '{count} diagnostics worth reviewing in this section.')
|
|
5398
|
-
.replace('{count}', String(warningCount));
|
|
5960
|
+
: '{count} diagnostics worth reviewing in this section.').replace('{count}', String(warningCount));
|
|
5399
5961
|
}
|
|
5400
5962
|
modeLabel(mode) {
|
|
5401
5963
|
return this.tx(`crud.authoring.mode.${mode}`, mode);
|
|
@@ -5409,13 +5971,454 @@ class CrudMetadataEditorComponent {
|
|
|
5409
5971
|
backStrategyLabel(strategy) {
|
|
5410
5972
|
return this.tx(`crud.authoring.defaults.back.strategy.${strategy}`, strategy);
|
|
5411
5973
|
}
|
|
5974
|
+
kpiCardTitle(index, card) {
|
|
5975
|
+
const base = this.tx('crud.authoring.kpis.cardTitle', 'Card #{index}').replace('{index}', String(index + 1));
|
|
5976
|
+
const label = String(card?.label || '').trim();
|
|
5977
|
+
return label ? `${base} · ${label}` : base;
|
|
5978
|
+
}
|
|
5979
|
+
kpiVariantLabel(variant) {
|
|
5980
|
+
return this.tx(`crud.authoring.kpis.variant.${variant}`, variant);
|
|
5981
|
+
}
|
|
5982
|
+
kpiFilterModeLabel(mode) {
|
|
5983
|
+
return this.tx(`crud.authoring.kpis.filterMode.${mode}`, mode);
|
|
5984
|
+
}
|
|
5985
|
+
kpiCardVariant(card) {
|
|
5986
|
+
if (card?.variant && KPI_VARIANTS.includes(card.variant)) {
|
|
5987
|
+
return card.variant;
|
|
5988
|
+
}
|
|
5989
|
+
const tone = String(card?.tone || '').trim().toLowerCase();
|
|
5990
|
+
if (tone === 'info' || tone === 'primary') {
|
|
5991
|
+
return 'primary';
|
|
5992
|
+
}
|
|
5993
|
+
if (tone === 'success') {
|
|
5994
|
+
return 'success';
|
|
5995
|
+
}
|
|
5996
|
+
if (tone === 'warning') {
|
|
5997
|
+
return 'warning';
|
|
5998
|
+
}
|
|
5999
|
+
if (tone === 'error' || tone === 'danger') {
|
|
6000
|
+
return 'danger';
|
|
6001
|
+
}
|
|
6002
|
+
return 'default';
|
|
6003
|
+
}
|
|
6004
|
+
kpiCardFilterMode(index) {
|
|
6005
|
+
if (this.kpiFilterJsonDrafts()[index] !== undefined) {
|
|
6006
|
+
return 'condition';
|
|
6007
|
+
}
|
|
6008
|
+
const card = this.kpiCards()[index];
|
|
6009
|
+
if (!card || card.filter === undefined || card.filter === null) {
|
|
6010
|
+
return 'none';
|
|
6011
|
+
}
|
|
6012
|
+
if (typeof card.filter === 'object' &&
|
|
6013
|
+
!Array.isArray(card.filter) &&
|
|
6014
|
+
Object.keys(card.filter).length === 0) {
|
|
6015
|
+
return 'clear';
|
|
6016
|
+
}
|
|
6017
|
+
return 'condition';
|
|
6018
|
+
}
|
|
6019
|
+
kpiCardFilterField(index) {
|
|
6020
|
+
const filter = this.kpiCards()[index]?.filter;
|
|
6021
|
+
if (!filter || typeof filter !== 'object' || Array.isArray(filter)) {
|
|
6022
|
+
return '';
|
|
6023
|
+
}
|
|
6024
|
+
return Object.keys(filter)[0] || '';
|
|
6025
|
+
}
|
|
6026
|
+
kpiCardFilterValue(index) {
|
|
6027
|
+
const filter = this.kpiCards()[index]?.filter;
|
|
6028
|
+
if (!filter || typeof filter !== 'object' || Array.isArray(filter)) {
|
|
6029
|
+
return '';
|
|
6030
|
+
}
|
|
6031
|
+
const key = Object.keys(filter)[0];
|
|
6032
|
+
if (!key) {
|
|
6033
|
+
return '';
|
|
6034
|
+
}
|
|
6035
|
+
const val = filter[key];
|
|
6036
|
+
if (val === undefined || val === null) {
|
|
6037
|
+
return '';
|
|
6038
|
+
}
|
|
6039
|
+
return typeof val === 'object' ? JSON.stringify(val) : String(val);
|
|
6040
|
+
}
|
|
6041
|
+
kpiCardFilterJsonDraft(index) {
|
|
6042
|
+
const draft = this.kpiFilterJsonDrafts()[index];
|
|
6043
|
+
if (draft !== undefined) {
|
|
6044
|
+
return draft;
|
|
6045
|
+
}
|
|
6046
|
+
const filter = this.kpiCards()[index]?.filter;
|
|
6047
|
+
return filter && typeof filter === 'object' ? JSON.stringify(filter, null, 2) : '';
|
|
6048
|
+
}
|
|
6049
|
+
setKpiBandEnabled(enabled) {
|
|
6050
|
+
this.updateKpiBand((current) => ({
|
|
6051
|
+
...current,
|
|
6052
|
+
enabled: !!enabled,
|
|
6053
|
+
cards: [...(current.cards || [])],
|
|
6054
|
+
}));
|
|
6055
|
+
}
|
|
6056
|
+
addKpiCard() {
|
|
6057
|
+
this.updateKpiBand((current) => {
|
|
6058
|
+
const cards = [...(current.cards || [])];
|
|
6059
|
+
cards.push({
|
|
6060
|
+
id: `kpi-${cards.length + 1}`,
|
|
6061
|
+
label: '',
|
|
6062
|
+
value: '',
|
|
6063
|
+
variant: 'default',
|
|
6064
|
+
tone: 'neutral',
|
|
6065
|
+
});
|
|
6066
|
+
return {
|
|
6067
|
+
...current,
|
|
6068
|
+
enabled: true,
|
|
6069
|
+
cards,
|
|
6070
|
+
};
|
|
6071
|
+
});
|
|
6072
|
+
}
|
|
6073
|
+
removeKpiCard(index) {
|
|
6074
|
+
this.kpiFilterJsonDrafts.update((drafts) => reindexDraftMapAfterRemove(drafts, index));
|
|
6075
|
+
this.updateKpiBand((current) => {
|
|
6076
|
+
const cards = [...(current.cards || [])];
|
|
6077
|
+
cards.splice(index, 1);
|
|
6078
|
+
return {
|
|
6079
|
+
...current,
|
|
6080
|
+
cards,
|
|
6081
|
+
};
|
|
6082
|
+
});
|
|
6083
|
+
}
|
|
6084
|
+
moveKpiCard(index, delta) {
|
|
6085
|
+
const cards = this.kpiCards();
|
|
6086
|
+
const targetIndex = index + delta;
|
|
6087
|
+
if (targetIndex < 0 || targetIndex >= cards.length) {
|
|
6088
|
+
return;
|
|
6089
|
+
}
|
|
6090
|
+
this.kpiFilterJsonDrafts.update((drafts) => reindexDraftMapAfterSwap(drafts, index, targetIndex));
|
|
6091
|
+
this.updateKpiBand((current) => {
|
|
6092
|
+
const nextCards = [...(current.cards || [])];
|
|
6093
|
+
if (targetIndex < 0 || targetIndex >= nextCards.length) {
|
|
6094
|
+
return current;
|
|
6095
|
+
}
|
|
6096
|
+
const [moved] = nextCards.splice(index, 1);
|
|
6097
|
+
nextCards.splice(targetIndex, 0, moved);
|
|
6098
|
+
return {
|
|
6099
|
+
...current,
|
|
6100
|
+
cards: nextCards,
|
|
6101
|
+
};
|
|
6102
|
+
});
|
|
6103
|
+
}
|
|
6104
|
+
setKpiCardField(index, field, value) {
|
|
6105
|
+
this.patchKpiCard(index, (card) => ({
|
|
6106
|
+
...card,
|
|
6107
|
+
[field]: field === 'value'
|
|
6108
|
+
? typeof value === 'number'
|
|
6109
|
+
? value
|
|
6110
|
+
: String(value ?? '')
|
|
6111
|
+
: typeof value === 'string'
|
|
6112
|
+
? value
|
|
6113
|
+
: value ?? undefined,
|
|
6114
|
+
}));
|
|
6115
|
+
}
|
|
6116
|
+
setKpiCardVariant(index, variant) {
|
|
6117
|
+
const mappedTone = variant === 'primary'
|
|
6118
|
+
? 'info'
|
|
6119
|
+
: variant === 'default'
|
|
6120
|
+
? 'neutral'
|
|
6121
|
+
: variant;
|
|
6122
|
+
this.patchKpiCard(index, (card) => ({
|
|
6123
|
+
...card,
|
|
6124
|
+
variant: variant || 'default',
|
|
6125
|
+
tone: mappedTone,
|
|
6126
|
+
}));
|
|
6127
|
+
}
|
|
6128
|
+
setKpiCardFilterMode(index, mode) {
|
|
6129
|
+
this.clearKpiFilterJsonDraft(index);
|
|
6130
|
+
if (mode === 'none') {
|
|
6131
|
+
this.patchKpiCard(index, (card) => ({
|
|
6132
|
+
...card,
|
|
6133
|
+
filter: undefined,
|
|
6134
|
+
}));
|
|
6135
|
+
return;
|
|
6136
|
+
}
|
|
6137
|
+
if (mode === 'clear') {
|
|
6138
|
+
this.patchKpiCard(index, (card) => ({
|
|
6139
|
+
...card,
|
|
6140
|
+
filter: {},
|
|
6141
|
+
}));
|
|
6142
|
+
return;
|
|
6143
|
+
}
|
|
6144
|
+
const defaultField = this.availableResourceFieldOptions()[0]?.field || 'status';
|
|
6145
|
+
this.patchKpiCard(index, (card) => ({
|
|
6146
|
+
...card,
|
|
6147
|
+
filter: card.filter && typeof card.filter === 'object' && Object.keys(card.filter).length > 0
|
|
6148
|
+
? card.filter
|
|
6149
|
+
: { [defaultField]: '' },
|
|
6150
|
+
}));
|
|
6151
|
+
}
|
|
6152
|
+
setKpiCardFilterCondition(index, field, rawValue) {
|
|
6153
|
+
this.clearKpiFilterJsonDraft(index);
|
|
6154
|
+
const cleanField = String(field || '').trim();
|
|
6155
|
+
const coercedValue = coerceFilterScalarValue(rawValue);
|
|
6156
|
+
this.patchKpiCard(index, (card) => ({
|
|
6157
|
+
...card,
|
|
6158
|
+
filter: cleanField ? { [cleanField]: coercedValue } : { '': coercedValue },
|
|
6159
|
+
}));
|
|
6160
|
+
}
|
|
6161
|
+
setKpiCardFilterJson(index, raw) {
|
|
6162
|
+
const trimmed = String(raw || '').trim();
|
|
6163
|
+
if (!trimmed) {
|
|
6164
|
+
this.clearKpiFilterJsonDraft(index);
|
|
6165
|
+
this.patchKpiCard(index, (card) => ({
|
|
6166
|
+
...card,
|
|
6167
|
+
filter: undefined,
|
|
6168
|
+
}));
|
|
6169
|
+
return;
|
|
6170
|
+
}
|
|
6171
|
+
try {
|
|
6172
|
+
const parsed = JSON.parse(trimmed);
|
|
6173
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
6174
|
+
this.clearKpiFilterJsonDraft(index);
|
|
6175
|
+
this.patchKpiCard(index, (card) => ({
|
|
6176
|
+
...card,
|
|
6177
|
+
filter: parsed,
|
|
6178
|
+
}));
|
|
6179
|
+
return;
|
|
6180
|
+
}
|
|
6181
|
+
}
|
|
6182
|
+
catch {
|
|
6183
|
+
// Keep invalid draft in signal so sectionStatus('kpis') surfaces 'invalid'
|
|
6184
|
+
}
|
|
6185
|
+
this.kpiFilterJsonDrafts.update((drafts) => ({
|
|
6186
|
+
...drafts,
|
|
6187
|
+
[index]: raw,
|
|
6188
|
+
}));
|
|
6189
|
+
this.syncState();
|
|
6190
|
+
}
|
|
6191
|
+
quickFilterModeLabel(mode) {
|
|
6192
|
+
return this.tx(`crud.authoring.filters.quickMode.${mode}`, mode);
|
|
6193
|
+
}
|
|
6194
|
+
quickConditionModeLabel(mode) {
|
|
6195
|
+
return this.tx(`crud.authoring.filters.quickConditionMode.${mode}`, mode);
|
|
6196
|
+
}
|
|
6197
|
+
setFilterBarShowAdvanced(showAdvanced) {
|
|
6198
|
+
this.updateFilterBar((current) => ({
|
|
6199
|
+
...current,
|
|
6200
|
+
showAdvanced: !!showAdvanced,
|
|
6201
|
+
}));
|
|
6202
|
+
}
|
|
6203
|
+
addInlineFilterField(field) {
|
|
6204
|
+
const clean = String(field || '').trim();
|
|
6205
|
+
if (!clean) {
|
|
6206
|
+
return;
|
|
6207
|
+
}
|
|
6208
|
+
const existing = this.filterBarInlineFields();
|
|
6209
|
+
if (existing.includes(clean)) {
|
|
6210
|
+
return;
|
|
6211
|
+
}
|
|
6212
|
+
this.updateFilterBar((current) => ({
|
|
6213
|
+
...current,
|
|
6214
|
+
inlineFields: [...existing, clean],
|
|
6215
|
+
}));
|
|
6216
|
+
}
|
|
6217
|
+
addInlineFilterFieldFromDraft() {
|
|
6218
|
+
const draft = this.inlineFieldDraft().trim();
|
|
6219
|
+
if (!draft) {
|
|
6220
|
+
return;
|
|
6221
|
+
}
|
|
6222
|
+
for (const part of draft.split(',')) {
|
|
6223
|
+
this.addInlineFilterField(part);
|
|
6224
|
+
}
|
|
6225
|
+
this.inlineFieldDraft.set('');
|
|
6226
|
+
}
|
|
6227
|
+
removeInlineFilterField(field) {
|
|
6228
|
+
const next = this.filterBarInlineFields().filter((item) => item !== field);
|
|
6229
|
+
this.updateFilterBar((current) => ({
|
|
6230
|
+
...current,
|
|
6231
|
+
inlineFields: next,
|
|
6232
|
+
}));
|
|
6233
|
+
}
|
|
6234
|
+
toggleInlineFilterField(field) {
|
|
6235
|
+
if (this.filterBarInlineFields().includes(field)) {
|
|
6236
|
+
this.removeInlineFilterField(field);
|
|
6237
|
+
}
|
|
6238
|
+
else {
|
|
6239
|
+
this.addInlineFilterField(field);
|
|
6240
|
+
}
|
|
6241
|
+
}
|
|
6242
|
+
setFilterBarQuickMode(mode) {
|
|
6243
|
+
this.quickFilterExpressionDrafts.set({});
|
|
6244
|
+
if (mode === 'inherit') {
|
|
6245
|
+
this.updateFilterBar((current) => ({
|
|
6246
|
+
...current,
|
|
6247
|
+
quickFilters: undefined,
|
|
6248
|
+
}));
|
|
6249
|
+
return;
|
|
6250
|
+
}
|
|
6251
|
+
if (mode === 'disabled') {
|
|
6252
|
+
this.updateFilterBar((current) => ({
|
|
6253
|
+
...current,
|
|
6254
|
+
quickFilters: false,
|
|
6255
|
+
}));
|
|
6256
|
+
return;
|
|
6257
|
+
}
|
|
6258
|
+
const existing = this.filterBarQuickFilters();
|
|
6259
|
+
this.updateFilterBar((current) => ({
|
|
6260
|
+
...current,
|
|
6261
|
+
quickFilters: [...existing],
|
|
6262
|
+
}));
|
|
6263
|
+
}
|
|
6264
|
+
addQuickFilter() {
|
|
6265
|
+
const list = [...this.filterBarQuickFilters()];
|
|
6266
|
+
list.push({
|
|
6267
|
+
id: `qf-${list.length + 1}`,
|
|
6268
|
+
label: '',
|
|
6269
|
+
filter: {},
|
|
6270
|
+
});
|
|
6271
|
+
this.updateFilterBar((current) => ({
|
|
6272
|
+
...current,
|
|
6273
|
+
quickFilters: list,
|
|
6274
|
+
}));
|
|
6275
|
+
}
|
|
6276
|
+
removeQuickFilter(index) {
|
|
6277
|
+
this.quickFilterExpressionDrafts.update((drafts) => reindexDraftMapAfterRemove(drafts, index));
|
|
6278
|
+
const list = [...this.filterBarQuickFilters()];
|
|
6279
|
+
list.splice(index, 1);
|
|
6280
|
+
this.updateFilterBar((current) => ({
|
|
6281
|
+
...current,
|
|
6282
|
+
quickFilters: list,
|
|
6283
|
+
}));
|
|
6284
|
+
}
|
|
6285
|
+
moveQuickFilter(index, delta) {
|
|
6286
|
+
const list = [...this.filterBarQuickFilters()];
|
|
6287
|
+
const targetIndex = index + delta;
|
|
6288
|
+
if (targetIndex < 0 || targetIndex >= list.length) {
|
|
6289
|
+
return;
|
|
6290
|
+
}
|
|
6291
|
+
this.quickFilterExpressionDrafts.update((drafts) => reindexDraftMapAfterSwap(drafts, index, targetIndex));
|
|
6292
|
+
const [moved] = list.splice(index, 1);
|
|
6293
|
+
list.splice(targetIndex, 0, moved);
|
|
6294
|
+
this.updateFilterBar((current) => ({
|
|
6295
|
+
...current,
|
|
6296
|
+
quickFilters: list,
|
|
6297
|
+
}));
|
|
6298
|
+
}
|
|
6299
|
+
setQuickFilterField(index, field, value) {
|
|
6300
|
+
this.patchQuickFilter(index, (qf) => ({
|
|
6301
|
+
...qf,
|
|
6302
|
+
[field]: typeof value === 'string' ? value : String(value ?? ''),
|
|
6303
|
+
}));
|
|
6304
|
+
}
|
|
6305
|
+
quickFilterConditionMode(index) {
|
|
6306
|
+
const qf = this.filterBarQuickFilters()[index];
|
|
6307
|
+
if (!qf) {
|
|
6308
|
+
return 'clear';
|
|
6309
|
+
}
|
|
6310
|
+
if (this.quickFilterExpressionDrafts()[index] !== undefined || typeof qf.filter === 'string') {
|
|
6311
|
+
return 'expression';
|
|
6312
|
+
}
|
|
6313
|
+
if (qf.filter && typeof qf.filter === 'object' && !Array.isArray(qf.filter)) {
|
|
6314
|
+
const keys = Object.keys(qf.filter);
|
|
6315
|
+
if (keys.length === 0) {
|
|
6316
|
+
return 'clear';
|
|
6317
|
+
}
|
|
6318
|
+
if (keys.length === 1 &&
|
|
6319
|
+
(typeof qf.filter[keys[0]] !== 'object' || qf.filter[keys[0]] === null)) {
|
|
6320
|
+
return 'fieldEquals';
|
|
6321
|
+
}
|
|
6322
|
+
return 'expression';
|
|
6323
|
+
}
|
|
6324
|
+
return 'clear';
|
|
6325
|
+
}
|
|
6326
|
+
setQuickFilterConditionMode(index, mode) {
|
|
6327
|
+
this.quickFilterExpressionDrafts.update((drafts) => {
|
|
6328
|
+
const next = { ...drafts };
|
|
6329
|
+
delete next[index];
|
|
6330
|
+
return next;
|
|
6331
|
+
});
|
|
6332
|
+
if (mode === 'clear') {
|
|
6333
|
+
this.patchQuickFilter(index, (qf) => ({ ...qf, filter: {} }));
|
|
6334
|
+
return;
|
|
6335
|
+
}
|
|
6336
|
+
if (mode === 'fieldEquals') {
|
|
6337
|
+
const defaultField = this.availableResourceFieldOptions()[0]?.field || 'status';
|
|
6338
|
+
this.patchQuickFilter(index, (qf) => ({
|
|
6339
|
+
...qf,
|
|
6340
|
+
filter: { [defaultField]: '' },
|
|
6341
|
+
}));
|
|
6342
|
+
return;
|
|
6343
|
+
}
|
|
6344
|
+
this.patchQuickFilter(index, (qf) => ({
|
|
6345
|
+
...qf,
|
|
6346
|
+
filter: typeof qf.filter === 'string' ? qf.filter : '',
|
|
6347
|
+
}));
|
|
6348
|
+
}
|
|
6349
|
+
quickFilterConditionField(index) {
|
|
6350
|
+
const filter = this.filterBarQuickFilters()[index]?.filter;
|
|
6351
|
+
if (!filter || typeof filter !== 'object' || Array.isArray(filter)) {
|
|
6352
|
+
return '';
|
|
6353
|
+
}
|
|
6354
|
+
return Object.keys(filter)[0] || '';
|
|
6355
|
+
}
|
|
6356
|
+
quickFilterConditionValue(index) {
|
|
6357
|
+
const filter = this.filterBarQuickFilters()[index]?.filter;
|
|
6358
|
+
if (!filter || typeof filter !== 'object' || Array.isArray(filter)) {
|
|
6359
|
+
return '';
|
|
6360
|
+
}
|
|
6361
|
+
const key = Object.keys(filter)[0];
|
|
6362
|
+
if (!key) {
|
|
6363
|
+
return '';
|
|
6364
|
+
}
|
|
6365
|
+
const val = filter[key];
|
|
6366
|
+
return val === undefined || val === null ? '' : String(val);
|
|
6367
|
+
}
|
|
6368
|
+
setQuickFilterFieldEquals(index, field, rawValue) {
|
|
6369
|
+
const cleanField = String(field || '').trim();
|
|
6370
|
+
const coerced = coerceFilterScalarValue(rawValue);
|
|
6371
|
+
this.patchQuickFilter(index, (qf) => ({
|
|
6372
|
+
...qf,
|
|
6373
|
+
filter: cleanField ? { [cleanField]: coerced } : { '': coerced },
|
|
6374
|
+
}));
|
|
6375
|
+
}
|
|
6376
|
+
quickFilterExpressionDraft(index) {
|
|
6377
|
+
const draft = this.quickFilterExpressionDrafts()[index];
|
|
6378
|
+
if (draft !== undefined) {
|
|
6379
|
+
return draft;
|
|
6380
|
+
}
|
|
6381
|
+
const filter = this.filterBarQuickFilters()[index]?.filter;
|
|
6382
|
+
if (typeof filter === 'string') {
|
|
6383
|
+
return filter;
|
|
6384
|
+
}
|
|
6385
|
+
if (filter && typeof filter === 'object') {
|
|
6386
|
+
return JSON.stringify(filter);
|
|
6387
|
+
}
|
|
6388
|
+
return '';
|
|
6389
|
+
}
|
|
6390
|
+
setQuickFilterExpression(index, raw) {
|
|
6391
|
+
this.quickFilterExpressionDrafts.update((drafts) => ({
|
|
6392
|
+
...drafts,
|
|
6393
|
+
[index]: raw,
|
|
6394
|
+
}));
|
|
6395
|
+
const trimmed = String(raw || '').trim();
|
|
6396
|
+
if (trimmed.startsWith('{')) {
|
|
6397
|
+
try {
|
|
6398
|
+
const parsed = JSON.parse(trimmed);
|
|
6399
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
6400
|
+
this.patchQuickFilter(index, (qf) => ({
|
|
6401
|
+
...qf,
|
|
6402
|
+
filter: parsed,
|
|
6403
|
+
}));
|
|
6404
|
+
return;
|
|
6405
|
+
}
|
|
6406
|
+
}
|
|
6407
|
+
catch {
|
|
6408
|
+
// Fall through to string expression
|
|
6409
|
+
}
|
|
6410
|
+
}
|
|
6411
|
+
this.patchQuickFilter(index, (qf) => ({
|
|
6412
|
+
...qf,
|
|
6413
|
+
filter: trimmed,
|
|
6414
|
+
}));
|
|
6415
|
+
}
|
|
5412
6416
|
actionInputsSummaryBadge(actionName) {
|
|
5413
6417
|
const paramsCount = this.actionParams(actionName).length;
|
|
5414
6418
|
const hasInitialValue = !!this.actionValue(actionName).form?.initialValue;
|
|
5415
6419
|
const parts = [];
|
|
5416
6420
|
if (paramsCount > 0) {
|
|
5417
|
-
parts.push(this.tx('crud.authoring.action.paramsSummaryCompact', '{count} map')
|
|
5418
|
-
.replace('{count}', String(paramsCount)));
|
|
6421
|
+
parts.push(this.tx('crud.authoring.action.paramsSummaryCompact', '{count} map').replace('{count}', String(paramsCount)));
|
|
5419
6422
|
}
|
|
5420
6423
|
if (hasInitialValue) {
|
|
5421
6424
|
parts.push(this.tx('crud.authoring.action.initialValueReadyCompact', 'Seed'));
|
|
@@ -5436,14 +6439,21 @@ class CrudMetadataEditorComponent {
|
|
|
5436
6439
|
'crud.metadata.actions.route.required': 'crud.authoring.validation.routeRequired',
|
|
5437
6440
|
'crud.metadata.actions.formId.required': 'crud.authoring.validation.formIdRequired',
|
|
5438
6441
|
'crud.metadata.actions.form.submit-contract.invalid': 'crud.authoring.validation.submitContract',
|
|
6442
|
+
'crud.metadata.kpiBand.cards.label.required': 'crud.authoring.validation.kpiLabelRequired',
|
|
6443
|
+
'crud.metadata.kpiBand.cards.value.required': 'crud.authoring.validation.kpiValueRequired',
|
|
6444
|
+
'crud.metadata.kpiBand.cards.filter.invalid': 'crud.authoring.validation.kpiFilterInvalid',
|
|
6445
|
+
'crud.metadata.filterBar.quickFilters.id.required': 'crud.authoring.validation.quickFilterIdRequired',
|
|
6446
|
+
'crud.metadata.filterBar.quickFilters.label.required': 'crud.authoring.validation.quickFilterLabelRequired',
|
|
6447
|
+
'crud.metadata.filterBar.quickFilters.filter.invalid': 'crud.authoring.validation.quickFilterInvalid',
|
|
5439
6448
|
}[code];
|
|
5440
6449
|
if (key) {
|
|
5441
|
-
|
|
6450
|
+
const indexMatch = fallback.match(/#(\d+)/);
|
|
6451
|
+
const translated = this.tx(key, fallback);
|
|
6452
|
+
return indexMatch ? translated.replace('{index}', indexMatch[1]) : translated;
|
|
5442
6453
|
}
|
|
5443
6454
|
const canonicalActionMatch = code.match(/^crud\.metadata\.actions\.(create|view|edit|delete)\.missing$/);
|
|
5444
6455
|
if (canonicalActionMatch) {
|
|
5445
|
-
return this.tx('crud.authoring.validation.canonicalActionMissing', 'Canonical CRUD action {action} is not declared yet')
|
|
5446
|
-
.replace('{action}', this.actionLabel(canonicalActionMatch[1]));
|
|
6456
|
+
return this.tx('crud.authoring.validation.canonicalActionMissing', 'Canonical CRUD action {action} is not declared yet').replace('{action}', this.actionLabel(canonicalActionMatch[1]));
|
|
5447
6457
|
}
|
|
5448
6458
|
return fallback;
|
|
5449
6459
|
}
|
|
@@ -5478,9 +6488,7 @@ class CrudMetadataEditorComponent {
|
|
|
5478
6488
|
}));
|
|
5479
6489
|
}
|
|
5480
6490
|
setModalField(field, value) {
|
|
5481
|
-
const nextValue = field === 'fullscreenBreakpoint'
|
|
5482
|
-
? this.normalizeOptionalNumber(value)
|
|
5483
|
-
: value;
|
|
6491
|
+
const nextValue = field === 'fullscreenBreakpoint' ? this.normalizeOptionalNumber(value) : value;
|
|
5484
6492
|
this.updateMetadata((metadata) => ({
|
|
5485
6493
|
...metadata,
|
|
5486
6494
|
defaults: {
|
|
@@ -5611,16 +6619,158 @@ class CrudMetadataEditorComponent {
|
|
|
5611
6619
|
}));
|
|
5612
6620
|
}
|
|
5613
6621
|
setTableConfig(config) {
|
|
6622
|
+
const nextTable = cloneTableConfig(config);
|
|
6623
|
+
const currentAuthoredCols = this.authoredTableColumns();
|
|
6624
|
+
const inheritedCols = this.resolvedInheritedColumns();
|
|
6625
|
+
if (currentAuthoredCols.length === 0 &&
|
|
6626
|
+
inheritedCols.length > 0 &&
|
|
6627
|
+
areColumnsEquivalentToInherited(nextTable.columns, inheritedCols)) {
|
|
6628
|
+
nextTable.columns = [];
|
|
6629
|
+
}
|
|
6630
|
+
else if (Array.isArray(nextTable.columns) && nextTable.columns.length > 0) {
|
|
6631
|
+
nextTable.columns = sanitizePersistedColumns(nextTable.columns);
|
|
6632
|
+
}
|
|
5614
6633
|
this.updateMetadata((metadata) => ({
|
|
5615
6634
|
...metadata,
|
|
5616
|
-
table:
|
|
6635
|
+
table: nextTable,
|
|
5617
6636
|
}));
|
|
5618
6637
|
}
|
|
6638
|
+
updateKpiBand(updater) {
|
|
6639
|
+
this.updateMetadata((metadata) => {
|
|
6640
|
+
const base = this.effectiveKpiBand() || { enabled: false, cards: [] };
|
|
6641
|
+
const nextBand = updater(base);
|
|
6642
|
+
return {
|
|
6643
|
+
...metadata,
|
|
6644
|
+
kpiBand: nextBand,
|
|
6645
|
+
};
|
|
6646
|
+
});
|
|
6647
|
+
}
|
|
6648
|
+
patchKpiCard(index, patcher) {
|
|
6649
|
+
this.updateKpiBand((current) => {
|
|
6650
|
+
const cards = [...(current.cards || [])];
|
|
6651
|
+
const existing = cards[index] || { label: '', value: '' };
|
|
6652
|
+
cards[index] = patcher(existing);
|
|
6653
|
+
return {
|
|
6654
|
+
...current,
|
|
6655
|
+
cards,
|
|
6656
|
+
};
|
|
6657
|
+
});
|
|
6658
|
+
}
|
|
6659
|
+
clearKpiFilterJsonDraft(index) {
|
|
6660
|
+
if (this.kpiFilterJsonDrafts()[index] === undefined) {
|
|
6661
|
+
return;
|
|
6662
|
+
}
|
|
6663
|
+
this.kpiFilterJsonDrafts.update((drafts) => {
|
|
6664
|
+
const next = { ...drafts };
|
|
6665
|
+
delete next[index];
|
|
6666
|
+
return next;
|
|
6667
|
+
});
|
|
6668
|
+
}
|
|
6669
|
+
updateFilterBar(updater) {
|
|
6670
|
+
this.updateMetadata((metadata) => {
|
|
6671
|
+
const base = this.effectiveFilterBar() || {};
|
|
6672
|
+
const nextFilterBar = updater(base);
|
|
6673
|
+
return {
|
|
6674
|
+
...metadata,
|
|
6675
|
+
filterBar: nextFilterBar,
|
|
6676
|
+
};
|
|
6677
|
+
});
|
|
6678
|
+
}
|
|
6679
|
+
patchQuickFilter(index, patcher) {
|
|
6680
|
+
this.updateFilterBar((current) => {
|
|
6681
|
+
const list = Array.isArray(current.quickFilters) ? [...current.quickFilters] : [];
|
|
6682
|
+
const existing = list[index] || { id: '', label: '', filter: {} };
|
|
6683
|
+
list[index] = patcher(existing);
|
|
6684
|
+
return {
|
|
6685
|
+
...current,
|
|
6686
|
+
quickFilters: list,
|
|
6687
|
+
};
|
|
6688
|
+
});
|
|
6689
|
+
}
|
|
6690
|
+
resourceSchemaIdentity(metadata) {
|
|
6691
|
+
const resource = metadata.resource;
|
|
6692
|
+
return JSON.stringify([
|
|
6693
|
+
String(resource?.path ?? '').trim(),
|
|
6694
|
+
String(resource?.endpointKey ?? '').trim(),
|
|
6695
|
+
resource?.apiUrlEntry ?? null,
|
|
6696
|
+
]);
|
|
6697
|
+
}
|
|
6698
|
+
synchronizeSchemaScope() {
|
|
6699
|
+
const signature = this.resourceSchemaIdentity(this.currentDocument().metadata);
|
|
6700
|
+
if (signature === this.schemaScopeSignature)
|
|
6701
|
+
return;
|
|
6702
|
+
this.invalidateSchemaScope();
|
|
6703
|
+
this.schemaScopeSignature = signature;
|
|
6704
|
+
}
|
|
6705
|
+
invalidateSchemaScope() {
|
|
6706
|
+
++this.schemaGeneration;
|
|
6707
|
+
this.schemaSub?.unsubscribe();
|
|
6708
|
+
this.schemaSub = undefined;
|
|
6709
|
+
this.lastLoadedResourceSignature = '';
|
|
6710
|
+
this.schemaResolvedColumnsSignal.set([]);
|
|
6711
|
+
this.schemaResolvedFilterFieldsSignal.set([]);
|
|
6712
|
+
this.isBusy$.next(false);
|
|
6713
|
+
}
|
|
6714
|
+
loadResourceSchemaColumns(resourcePath, endpointKey, apiUrlEntry) {
|
|
6715
|
+
if (!this.crudService)
|
|
6716
|
+
return;
|
|
6717
|
+
const generation = this.schemaGeneration;
|
|
6718
|
+
const signature = this.resourceSchemaIdentity(this.currentDocument().metadata);
|
|
6719
|
+
const externalSignature = () => this.resourceSchemaIdentity(parseLegacyOrCrudDocument(this.resolveExternalSeed()).metadata);
|
|
6720
|
+
const requestExternalSignature = externalSignature();
|
|
6721
|
+
const isCurrent = () => {
|
|
6722
|
+
const external = externalSignature();
|
|
6723
|
+
return !this.destroyed && generation === this.schemaGeneration &&
|
|
6724
|
+
signature === this.resourceSchemaIdentity(this.currentDocument().metadata) &&
|
|
6725
|
+
// An editorial seed update or owner echo of the edited resource keeps this read.
|
|
6726
|
+
(external === requestExternalSignature || external === signature);
|
|
6727
|
+
};
|
|
6728
|
+
this.isBusy$.next(true);
|
|
6729
|
+
try {
|
|
6730
|
+
this.crudService.configure(resourcePath, {
|
|
6731
|
+
endpointKey: endpointKey,
|
|
6732
|
+
apiUrlEntry: apiUrlEntry ?? undefined,
|
|
6733
|
+
});
|
|
6734
|
+
this.schemaSub = this.crudService.getSchema().subscribe({
|
|
6735
|
+
next: (fields) => {
|
|
6736
|
+
if (!isCurrent())
|
|
6737
|
+
return;
|
|
6738
|
+
const resolved = Array.isArray(fields) ? fields : [];
|
|
6739
|
+
this.schemaResolvedColumnsSignal.set(resolved
|
|
6740
|
+
.filter((field) => !field.tableHidden && !field.hidden)
|
|
6741
|
+
.map((field) => buildBaseColumnFromDef(field)));
|
|
6742
|
+
this.schemaResolvedFilterFieldsSignal.set(resolved
|
|
6743
|
+
.filter((field) => !!String(field?.name || '').trim())
|
|
6744
|
+
.map((field) => ({
|
|
6745
|
+
field: String(field.name).trim(),
|
|
6746
|
+
header: String(field.label || field.name).trim(),
|
|
6747
|
+
})));
|
|
6748
|
+
this.isBusy$.next(false);
|
|
6749
|
+
},
|
|
6750
|
+
error: () => {
|
|
6751
|
+
if (!isCurrent())
|
|
6752
|
+
return;
|
|
6753
|
+
this.schemaResolvedColumnsSignal.set([]);
|
|
6754
|
+
this.schemaResolvedFilterFieldsSignal.set([]);
|
|
6755
|
+
this.isBusy$.next(false);
|
|
6756
|
+
},
|
|
6757
|
+
complete: () => { if (isCurrent())
|
|
6758
|
+
this.isBusy$.next(false); },
|
|
6759
|
+
});
|
|
6760
|
+
}
|
|
6761
|
+
catch {
|
|
6762
|
+
// An unavailable reader must not preserve another resource's inferred fields.
|
|
6763
|
+
if (isCurrent())
|
|
6764
|
+
this.isBusy$.next(false);
|
|
6765
|
+
}
|
|
6766
|
+
}
|
|
5619
6767
|
patchAction(actionName, patcher) {
|
|
5620
6768
|
this.updateMetadata((metadata) => {
|
|
5621
6769
|
const actions = [...(metadata.actions || [])];
|
|
5622
6770
|
const index = actions.findIndex((entry) => entry?.action === actionName);
|
|
5623
|
-
const current = index >= 0
|
|
6771
|
+
const current = index >= 0
|
|
6772
|
+
? actions[index]
|
|
6773
|
+
: { action: actionName, label: this.actionLabel(actionName) };
|
|
5624
6774
|
const next = patcher(current);
|
|
5625
6775
|
if (index >= 0) {
|
|
5626
6776
|
actions[index] = next;
|
|
@@ -5637,6 +6787,7 @@ class CrudMetadataEditorComponent {
|
|
|
5637
6787
|
metadata: updater(this.currentDocument().metadata),
|
|
5638
6788
|
});
|
|
5639
6789
|
this.currentDocument.set(next);
|
|
6790
|
+
this.synchronizeSchemaScope();
|
|
5640
6791
|
this.syncState();
|
|
5641
6792
|
}
|
|
5642
6793
|
buildPayload() {
|
|
@@ -5648,17 +6799,19 @@ class CrudMetadataEditorComponent {
|
|
|
5648
6799
|
};
|
|
5649
6800
|
}
|
|
5650
6801
|
resolveExternalSeed() {
|
|
5651
|
-
return this.documentInput()
|
|
5652
|
-
|
|
5653
|
-
|
|
5654
|
-
|
|
5655
|
-
|
|
6802
|
+
return (this.documentInput() ||
|
|
6803
|
+
this.metadataInput() ||
|
|
6804
|
+
this.injectedData?.document ||
|
|
6805
|
+
this.injectedData?.metadata ||
|
|
6806
|
+
null);
|
|
5656
6807
|
}
|
|
5657
6808
|
syncState() {
|
|
5658
6809
|
const currentSignature = JSON.stringify(serializeCrudAuthoringDocument(this.currentDocument()));
|
|
5659
6810
|
const initialSignature = JSON.stringify(serializeCrudAuthoringDocument(this.initialDocument()));
|
|
5660
|
-
this.
|
|
5661
|
-
this.
|
|
6811
|
+
const hasInvalidKpiJson = Object.keys(this.kpiFilterJsonDrafts()).length > 0;
|
|
6812
|
+
this.isDirty$.next(currentSignature !== initialSignature || hasInvalidKpiJson);
|
|
6813
|
+
const hasErrorDiagnostics = this.diagnostics().some((issue) => issue.level === 'error');
|
|
6814
|
+
this.isValid$.next(!hasErrorDiagnostics && !hasInvalidKpiJson);
|
|
5662
6815
|
}
|
|
5663
6816
|
tx(key, fallback) {
|
|
5664
6817
|
return translateCrudAuthoringText(this.i18n, key, fallback);
|
|
@@ -5672,21 +6825,32 @@ class CrudMetadataEditorComponent {
|
|
|
5672
6825
|
return Number.isFinite(parsed) ? parsed : undefined;
|
|
5673
6826
|
}
|
|
5674
6827
|
hasHealthBucketContent(bucket) {
|
|
5675
|
-
return
|
|
5676
|
-
.some((section) => this.sectionStatus(section) === bucket);
|
|
6828
|
+
return SECTION_KEYS.some((section) => this.sectionStatus(section) === bucket);
|
|
5677
6829
|
}
|
|
5678
6830
|
sectionDiagnostics(section) {
|
|
6831
|
+
if (section === 'table') {
|
|
6832
|
+
return this.diagnostics().filter((issue) => {
|
|
6833
|
+
const path = String(issue.path || '');
|
|
6834
|
+
return (path.includes('metadata.table') &&
|
|
6835
|
+
!path.includes('kpiBand') &&
|
|
6836
|
+
!path.includes('filterBar'));
|
|
6837
|
+
});
|
|
6838
|
+
}
|
|
5679
6839
|
const prefixes = {
|
|
5680
6840
|
connection: ['metadata.component', 'metadata.resource', 'metadata.data'],
|
|
5681
6841
|
defaults: ['metadata.defaults'],
|
|
5682
6842
|
actions: ['metadata.actions'],
|
|
5683
|
-
|
|
6843
|
+
kpis: ['metadata.kpiBand', 'metadata.table.kpiBand', 'metadata.table.behavior.kpiBand'],
|
|
6844
|
+
filters: [
|
|
6845
|
+
'metadata.filterBar',
|
|
6846
|
+
'metadata.table.filterBar',
|
|
6847
|
+
'metadata.table.behavior.filtering',
|
|
6848
|
+
],
|
|
5684
6849
|
}[section];
|
|
5685
6850
|
return this.diagnostics().filter((issue) => prefixes.some((prefix) => String(issue.path || '').includes(prefix)));
|
|
5686
6851
|
}
|
|
5687
6852
|
actionDiagnostics(actionName) {
|
|
5688
|
-
return this.diagnostics()
|
|
5689
|
-
.filter((issue) => String(issue.path || '').includes(`metadata.actions.${actionName}`));
|
|
6853
|
+
return this.diagnostics().filter((issue) => String(issue.path || '').includes(`metadata.actions.${actionName}`));
|
|
5690
6854
|
}
|
|
5691
6855
|
actionGroupLabelsByStatus(actionName, status) {
|
|
5692
6856
|
return ['binding', 'schema', 'submit', 'api']
|
|
@@ -5696,31 +6860,44 @@ class CrudMetadataEditorComponent {
|
|
|
5696
6860
|
actionFocusSummary(actionName) {
|
|
5697
6861
|
const invalidGroups = this.actionGroupLabelsByStatus(actionName, 'invalid');
|
|
5698
6862
|
if (invalidGroups.length) {
|
|
5699
|
-
return this.tx('crud.authoring.action.focusInvalid', '{count} blocking groups')
|
|
5700
|
-
.replace('{count}', String(invalidGroups.length));
|
|
6863
|
+
return this.tx('crud.authoring.action.focusInvalid', '{count} blocking groups').replace('{count}', String(invalidGroups.length));
|
|
5701
6864
|
}
|
|
5702
6865
|
const pendingGroups = this.actionGroupLabelsByStatus(actionName, 'pending');
|
|
5703
6866
|
if (pendingGroups.length) {
|
|
5704
|
-
return this.tx('crud.authoring.action.focusPending', '{count} groups still pending')
|
|
5705
|
-
.replace('{count}', String(pendingGroups.length));
|
|
6867
|
+
return this.tx('crud.authoring.action.focusPending', '{count} groups still pending').replace('{count}', String(pendingGroups.length));
|
|
5706
6868
|
}
|
|
5707
6869
|
return this.tx('crud.authoring.action.focusReady', 'Ready');
|
|
5708
6870
|
}
|
|
5709
6871
|
prioritizedActionKey() {
|
|
5710
|
-
return this.actionKeys.find((actionName) => this.actionStatus(actionName) === 'invalid')
|
|
5711
|
-
|
|
5712
|
-
|
|
6872
|
+
return (this.actionKeys.find((actionName) => this.actionStatus(actionName) === 'invalid') ||
|
|
6873
|
+
this.actionKeys.find((actionName) => this.actionStatus(actionName) === 'pending') ||
|
|
6874
|
+
null);
|
|
5713
6875
|
}
|
|
5714
6876
|
sectionDisplayLabel(section) {
|
|
5715
6877
|
return {
|
|
5716
6878
|
connection: this.tx('crud.authoring.section.connection', 'Connection'),
|
|
5717
6879
|
defaults: this.tx('crud.authoring.section.defaults', 'Open mode and header'),
|
|
5718
6880
|
actions: this.tx('crud.authoring.section.actions', 'Actions'),
|
|
6881
|
+
kpis: this.tx('crud.authoring.section.kpis', 'KPI Band'),
|
|
6882
|
+
filters: this.tx('crud.authoring.section.filters', 'Filter Bar'),
|
|
5719
6883
|
table: this.tx('crud.authoring.section.table', 'Table'),
|
|
5720
6884
|
}[section];
|
|
5721
6885
|
}
|
|
5722
6886
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CrudMetadataEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
5723
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: CrudMetadataEditorComponent, isStandalone: true, selector: "praxis-crud-metadata-editor", inputs: { documentInput: { classPropertyName: "documentInput", publicName: "document", isSignal: true, isRequired: false, transformFunction: null }, metadataInput: { classPropertyName: "metadataInput", publicName: "metadata", isSignal: true, isRequired: false, transformFunction: null }, crudIdInput: { classPropertyName: "crudIdInput", publicName: "crudId", isSignal: true, isRequired: false, transformFunction: null }, readonlyInput: { classPropertyName: "readonlyInput", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }
|
|
6887
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: CrudMetadataEditorComponent, isStandalone: true, selector: "praxis-crud-metadata-editor", inputs: { documentInput: { classPropertyName: "documentInput", publicName: "document", isSignal: true, isRequired: false, transformFunction: null }, metadataInput: { classPropertyName: "metadataInput", publicName: "metadata", isSignal: true, isRequired: false, transformFunction: null }, crudIdInput: { classPropertyName: "crudIdInput", publicName: "crudId", isSignal: true, isRequired: false, transformFunction: null }, readonlyInput: { classPropertyName: "readonlyInput", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, effectiveTableConfigInput: { classPropertyName: "effectiveTableConfigInput", publicName: "effectiveTableConfig", isSignal: true, isRequired: false, transformFunction: null } }, providers: [
|
|
6888
|
+
providePraxisI18nConfig(PRAXIS_CRUD_AUTHORING_I18N_CONFIG),
|
|
6889
|
+
{
|
|
6890
|
+
provide: GenericCrudService,
|
|
6891
|
+
useFactory: () => {
|
|
6892
|
+
const http = inject(HttpClient, { optional: true });
|
|
6893
|
+
const apiUrl = inject(API_URL, { optional: true });
|
|
6894
|
+
// Offline authoring has no transport; a configured editor owns its mutable reader.
|
|
6895
|
+
if (http === null || apiUrl === null)
|
|
6896
|
+
return null;
|
|
6897
|
+
return new GenericCrudService(http, inject(SchemaNormalizerService), inject(GlobalConfigService), apiUrl);
|
|
6898
|
+
},
|
|
6899
|
+
},
|
|
6900
|
+
], ngImport: i0, template: `
|
|
5724
6901
|
<section class="editor-shell" data-testid="crud-metadata-editor">
|
|
5725
6902
|
<header class="editor-header">
|
|
5726
6903
|
<div>
|
|
@@ -5746,6 +6923,16 @@ class CrudMetadataEditorComponent {
|
|
|
5746
6923
|
<strong class="editor-overview-value" data-testid="crud-editor-overview-actions">{{ actionsOverview() }}</strong>
|
|
5747
6924
|
<p class="editor-overview-note" data-testid="crud-editor-overview-actions-note">{{ actionsOverviewNote() }}</p>
|
|
5748
6925
|
</article>
|
|
6926
|
+
<article class="editor-overview-card">
|
|
6927
|
+
<span class="editor-overview-label">{{ tx('crud.authoring.overview.kpis', 'KPI Band') }}</span>
|
|
6928
|
+
<strong class="editor-overview-value" data-testid="crud-editor-overview-kpis">{{ kpisOverview() }}</strong>
|
|
6929
|
+
<p class="editor-overview-note" data-testid="crud-editor-overview-kpis-note">{{ kpisOverviewNote() }}</p>
|
|
6930
|
+
</article>
|
|
6931
|
+
<article class="editor-overview-card">
|
|
6932
|
+
<span class="editor-overview-label">{{ tx('crud.authoring.overview.filters', 'Filters') }}</span>
|
|
6933
|
+
<strong class="editor-overview-value" data-testid="crud-editor-overview-filters">{{ filtersOverview() }}</strong>
|
|
6934
|
+
<p class="editor-overview-note" data-testid="crud-editor-overview-filters-note">{{ filtersOverviewNote() }}</p>
|
|
6935
|
+
</article>
|
|
5749
6936
|
<article class="editor-overview-card">
|
|
5750
6937
|
<span class="editor-overview-label">{{ tx('crud.authoring.overview.table', 'Table') }}</span>
|
|
5751
6938
|
<strong class="editor-overview-value" data-testid="crud-editor-overview-table">{{ tableOverview() }}</strong>
|
|
@@ -5788,7 +6975,14 @@ class CrudMetadataEditorComponent {
|
|
|
5788
6975
|
</div>
|
|
5789
6976
|
</section>
|
|
5790
6977
|
|
|
5791
|
-
<mat-tab-group
|
|
6978
|
+
<mat-tab-group
|
|
6979
|
+
class="editor-tabs"
|
|
6980
|
+
data-testid="crud-editor-tabs"
|
|
6981
|
+
dynamicHeight
|
|
6982
|
+
animationDuration="0ms"
|
|
6983
|
+
[selectedIndex]="selectedTabIndex()"
|
|
6984
|
+
(selectedIndexChange)="selectedTabIndex.set($event)"
|
|
6985
|
+
>
|
|
5792
6986
|
<mat-tab>
|
|
5793
6987
|
<ng-template mat-tab-label>
|
|
5794
6988
|
<mat-icon [praxisIcon]="'link'"></mat-icon>
|
|
@@ -6191,6 +7385,520 @@ class CrudMetadataEditorComponent {
|
|
|
6191
7385
|
</div>
|
|
6192
7386
|
</mat-tab>
|
|
6193
7387
|
|
|
7388
|
+
<mat-tab>
|
|
7389
|
+
<ng-template mat-tab-label>
|
|
7390
|
+
<mat-icon [praxisIcon]="'analytics'"></mat-icon>
|
|
7391
|
+
<span data-testid="crud-editor-tab-kpis">{{ tx('crud.authoring.tab.kpis', 'KPIs') }}</span>
|
|
7392
|
+
<span
|
|
7393
|
+
class="tab-status-dot"
|
|
7394
|
+
[class.tab-status-dot--warn]="sectionStatus('kpis') === 'pending'"
|
|
7395
|
+
[class.tab-status-dot--error]="sectionStatus('kpis') === 'invalid'"
|
|
7396
|
+
[attr.title]="sectionStatusLabel('kpis')"
|
|
7397
|
+
></span>
|
|
7398
|
+
</ng-template>
|
|
7399
|
+
<div class="tab-content">
|
|
7400
|
+
<mat-card class="editor-card" data-testid="crud-editor-kpis-section">
|
|
7401
|
+
<div class="editor-section-header">
|
|
7402
|
+
<div>
|
|
7403
|
+
<h3>{{ tx('crud.authoring.section.kpis', 'KPI Band') }}</h3>
|
|
7404
|
+
<p class="editor-section-note">{{ sectionSummary('kpis') }}</p>
|
|
7405
|
+
</div>
|
|
7406
|
+
<span
|
|
7407
|
+
class="action-summary-chip"
|
|
7408
|
+
[class.action-summary-chip--error]="sectionStatus('kpis') === 'invalid'"
|
|
7409
|
+
[class.action-summary-chip--warn]="sectionStatus('kpis') === 'pending'"
|
|
7410
|
+
>
|
|
7411
|
+
{{ sectionStatusLabel('kpis') }}
|
|
7412
|
+
</span>
|
|
7413
|
+
</div>
|
|
7414
|
+
|
|
7415
|
+
<div class="editor-toolbar-row">
|
|
7416
|
+
<mat-slide-toggle
|
|
7417
|
+
data-testid="crud-kpi-band-enabled"
|
|
7418
|
+
[ngModel]="kpiBandEnabled()"
|
|
7419
|
+
(ngModelChange)="setKpiBandEnabled($event)"
|
|
7420
|
+
[disabled]="isReadonly()"
|
|
7421
|
+
>
|
|
7422
|
+
{{ tx('crud.authoring.kpis.enabled', 'Show KPI band above collection table') }}
|
|
7423
|
+
</mat-slide-toggle>
|
|
7424
|
+
<button
|
|
7425
|
+
mat-stroked-button
|
|
7426
|
+
type="button"
|
|
7427
|
+
data-testid="crud-kpi-add-card-btn"
|
|
7428
|
+
(click)="addKpiCard()"
|
|
7429
|
+
[disabled]="isReadonly()"
|
|
7430
|
+
>
|
|
7431
|
+
<mat-icon [praxisIcon]="'add'"></mat-icon>
|
|
7432
|
+
{{ tx('crud.authoring.kpis.addCard', 'Add KPI') }}
|
|
7433
|
+
</button>
|
|
7434
|
+
</div>
|
|
7435
|
+
|
|
7436
|
+
@if (kpiCards().length === 0) {
|
|
7437
|
+
<p class="editor-section-note" data-testid="crud-kpi-empty-state">
|
|
7438
|
+
{{ tx('crud.authoring.kpis.empty', 'No KPI cards configured yet.') }}
|
|
7439
|
+
</p>
|
|
7440
|
+
} @else {
|
|
7441
|
+
<div class="kpi-cards-list">
|
|
7442
|
+
@for (card of kpiCards(); track $index) {
|
|
7443
|
+
<section class="action-group" [attr.data-testid]="'crud-kpi-card-item-' + $index">
|
|
7444
|
+
<div class="editor-section-header">
|
|
7445
|
+
<strong>{{ kpiCardTitle($index, card) }}</strong>
|
|
7446
|
+
<div class="inline-actions-row">
|
|
7447
|
+
<button
|
|
7448
|
+
mat-stroked-button
|
|
7449
|
+
type="button"
|
|
7450
|
+
[attr.data-testid]="'crud-kpi-card-up-' + $index"
|
|
7451
|
+
(click)="moveKpiCard($index, -1)"
|
|
7452
|
+
[disabled]="isReadonly() || $index === 0"
|
|
7453
|
+
>
|
|
7454
|
+
{{ tx('crud.authoring.kpis.moveUp', 'Move up') }}
|
|
7455
|
+
</button>
|
|
7456
|
+
<button
|
|
7457
|
+
mat-stroked-button
|
|
7458
|
+
type="button"
|
|
7459
|
+
[attr.data-testid]="'crud-kpi-card-down-' + $index"
|
|
7460
|
+
(click)="moveKpiCard($index, 1)"
|
|
7461
|
+
[disabled]="isReadonly() || $index === kpiCards().length - 1"
|
|
7462
|
+
>
|
|
7463
|
+
{{ tx('crud.authoring.kpis.moveDown', 'Move down') }}
|
|
7464
|
+
</button>
|
|
7465
|
+
<button
|
|
7466
|
+
mat-stroked-button
|
|
7467
|
+
type="button"
|
|
7468
|
+
[attr.data-testid]="'crud-kpi-card-remove-' + $index"
|
|
7469
|
+
(click)="removeKpiCard($index)"
|
|
7470
|
+
[disabled]="isReadonly()"
|
|
7471
|
+
>
|
|
7472
|
+
{{ tx('crud.authoring.kpis.removeCard', 'Remove KPI') }}
|
|
7473
|
+
</button>
|
|
7474
|
+
</div>
|
|
7475
|
+
</div>
|
|
7476
|
+
|
|
7477
|
+
<div class="editor-grid">
|
|
7478
|
+
<mat-form-field appearance="outline">
|
|
7479
|
+
<mat-label>{{ tx('crud.authoring.kpis.label', 'Label') }}</mat-label>
|
|
7480
|
+
<input
|
|
7481
|
+
matInput
|
|
7482
|
+
[attr.data-testid]="'crud-kpi-card-label-' + $index"
|
|
7483
|
+
[ngModel]="card.label || ''"
|
|
7484
|
+
(ngModelChange)="setKpiCardField($index, 'label', $event)"
|
|
7485
|
+
[placeholder]="tx('crud.authoring.kpis.labelPlaceholder', 'e.g. In progress')"
|
|
7486
|
+
[disabled]="isReadonly()"
|
|
7487
|
+
/>
|
|
7488
|
+
</mat-form-field>
|
|
7489
|
+
|
|
7490
|
+
<mat-form-field appearance="outline">
|
|
7491
|
+
<mat-label>{{ tx('crud.authoring.kpis.value', 'Displayed value') }}</mat-label>
|
|
7492
|
+
<input
|
|
7493
|
+
matInput
|
|
7494
|
+
[attr.data-testid]="'crud-kpi-card-value-' + $index"
|
|
7495
|
+
[ngModel]="card.value ?? ''"
|
|
7496
|
+
(ngModelChange)="setKpiCardField($index, 'value', $event)"
|
|
7497
|
+
[placeholder]="tx('crud.authoring.kpis.valuePlaceholder', 'e.g. 18')"
|
|
7498
|
+
[disabled]="isReadonly()"
|
|
7499
|
+
/>
|
|
7500
|
+
</mat-form-field>
|
|
7501
|
+
|
|
7502
|
+
<mat-form-field appearance="outline">
|
|
7503
|
+
<mat-label>{{ tx('crud.authoring.kpis.caption', 'Caption / context') }}</mat-label>
|
|
7504
|
+
<input
|
|
7505
|
+
matInput
|
|
7506
|
+
[attr.data-testid]="'crud-kpi-card-caption-' + $index"
|
|
7507
|
+
[ngModel]="card.caption || ''"
|
|
7508
|
+
(ngModelChange)="setKpiCardField($index, 'caption', $event)"
|
|
7509
|
+
[placeholder]="tx('crud.authoring.kpis.captionPlaceholder', 'e.g. All sectors')"
|
|
7510
|
+
[disabled]="isReadonly()"
|
|
7511
|
+
/>
|
|
7512
|
+
</mat-form-field>
|
|
7513
|
+
|
|
7514
|
+
<mat-form-field appearance="outline">
|
|
7515
|
+
<mat-label>{{ tx('crud.authoring.kpis.icon', 'Icon (Material Icon)') }}</mat-label>
|
|
7516
|
+
<input
|
|
7517
|
+
matInput
|
|
7518
|
+
[attr.data-testid]="'crud-kpi-card-icon-' + $index"
|
|
7519
|
+
[ngModel]="card.icon || ''"
|
|
7520
|
+
(ngModelChange)="setKpiCardField($index, 'icon', $event)"
|
|
7521
|
+
[placeholder]="tx('crud.authoring.kpis.iconPlaceholder', 'e.g. radar')"
|
|
7522
|
+
[disabled]="isReadonly()"
|
|
7523
|
+
/>
|
|
7524
|
+
</mat-form-field>
|
|
7525
|
+
|
|
7526
|
+
<mat-form-field appearance="outline">
|
|
7527
|
+
<mat-label>{{ tx('crud.authoring.kpis.variant', 'Semantic tone') }}</mat-label>
|
|
7528
|
+
<mat-select
|
|
7529
|
+
[attr.data-testid]="'crud-kpi-card-variant-' + $index"
|
|
7530
|
+
[ngModel]="kpiCardVariant(card)"
|
|
7531
|
+
(ngModelChange)="setKpiCardVariant($index, $event)"
|
|
7532
|
+
[disabled]="isReadonly()"
|
|
7533
|
+
>
|
|
7534
|
+
@for (variant of kpiVariants; track variant) {
|
|
7535
|
+
<mat-option [value]="variant">{{ kpiVariantLabel(variant) }}</mat-option>
|
|
7536
|
+
}
|
|
7537
|
+
</mat-select>
|
|
7538
|
+
</mat-form-field>
|
|
7539
|
+
|
|
7540
|
+
<mat-form-field appearance="outline">
|
|
7541
|
+
<mat-label>{{ tx('crud.authoring.kpis.filterMode', 'Click behavior (Filter)') }}</mat-label>
|
|
7542
|
+
<mat-select
|
|
7543
|
+
[attr.data-testid]="'crud-kpi-card-filter-mode-' + $index"
|
|
7544
|
+
[ngModel]="kpiCardFilterMode($index)"
|
|
7545
|
+
(ngModelChange)="setKpiCardFilterMode($index, $event)"
|
|
7546
|
+
[disabled]="isReadonly()"
|
|
7547
|
+
>
|
|
7548
|
+
@for (mode of kpiFilterModes; track mode) {
|
|
7549
|
+
<mat-option [value]="mode">{{ kpiFilterModeLabel(mode) }}</mat-option>
|
|
7550
|
+
}
|
|
7551
|
+
</mat-select>
|
|
7552
|
+
</mat-form-field>
|
|
7553
|
+
</div>
|
|
7554
|
+
|
|
7555
|
+
@if (kpiCardFilterMode($index) === 'condition') {
|
|
7556
|
+
<div class="editor-grid">
|
|
7557
|
+
<mat-form-field appearance="outline">
|
|
7558
|
+
<mat-label>{{ tx('crud.authoring.kpis.filterField', 'Filter field') }}</mat-label>
|
|
7559
|
+
<input
|
|
7560
|
+
matInput
|
|
7561
|
+
[attr.data-testid]="'crud-kpi-card-filter-field-' + $index"
|
|
7562
|
+
[ngModel]="kpiCardFilterField($index)"
|
|
7563
|
+
(ngModelChange)="setKpiCardFilterCondition($index, $event, kpiCardFilterValue($index))"
|
|
7564
|
+
[placeholder]="tx('crud.authoring.kpis.filterFieldPlaceholder', 'e.g. status')"
|
|
7565
|
+
[disabled]="isReadonly()"
|
|
7566
|
+
/>
|
|
7567
|
+
</mat-form-field>
|
|
7568
|
+
|
|
7569
|
+
<mat-form-field appearance="outline">
|
|
7570
|
+
<mat-label>{{ tx('crud.authoring.kpis.filterValue', 'Expected value') }}</mat-label>
|
|
7571
|
+
<input
|
|
7572
|
+
matInput
|
|
7573
|
+
[attr.data-testid]="'crud-kpi-card-filter-value-' + $index"
|
|
7574
|
+
[ngModel]="kpiCardFilterValue($index)"
|
|
7575
|
+
(ngModelChange)="setKpiCardFilterCondition($index, kpiCardFilterField($index), $event)"
|
|
7576
|
+
[placeholder]="tx('crud.authoring.kpis.filterValuePlaceholder', 'e.g. IN_PROGRESS')"
|
|
7577
|
+
[disabled]="isReadonly()"
|
|
7578
|
+
/>
|
|
7579
|
+
</mat-form-field>
|
|
7580
|
+
</div>
|
|
7581
|
+
|
|
7582
|
+
<mat-form-field appearance="outline">
|
|
7583
|
+
<mat-label>{{ tx('crud.authoring.kpis.filterJson', 'Advanced filter criteria (JSON)') }}</mat-label>
|
|
7584
|
+
<textarea
|
|
7585
|
+
matInput
|
|
7586
|
+
rows="3"
|
|
7587
|
+
[attr.data-testid]="'crud-kpi-card-filter-json-' + $index"
|
|
7588
|
+
[ngModel]="kpiCardFilterJsonDraft($index)"
|
|
7589
|
+
(ngModelChange)="setKpiCardFilterJson($index, $event)"
|
|
7590
|
+
[disabled]="isReadonly()"
|
|
7591
|
+
></textarea>
|
|
7592
|
+
</mat-form-field>
|
|
7593
|
+
}
|
|
7594
|
+
</section>
|
|
7595
|
+
}
|
|
7596
|
+
</div>
|
|
7597
|
+
}
|
|
7598
|
+
|
|
7599
|
+
@if (kpiBandEnabled() && kpiCards().length > 0) {
|
|
7600
|
+
<section class="action-group" data-testid="crud-kpi-band-preview-section">
|
|
7601
|
+
<strong>{{ tx('crud.authoring.kpis.previewTitle', 'KPI band preview') }}</strong>
|
|
7602
|
+
<praxis-kpi-band
|
|
7603
|
+
data-testid="crud-kpi-band-preview"
|
|
7604
|
+
[config]="kpiBandPreviewConfig()"
|
|
7605
|
+
[isInteractiveHost]="false"
|
|
7606
|
+
/>
|
|
7607
|
+
</section>
|
|
7608
|
+
}
|
|
7609
|
+
</mat-card>
|
|
7610
|
+
</div>
|
|
7611
|
+
</mat-tab>
|
|
7612
|
+
|
|
7613
|
+
<mat-tab>
|
|
7614
|
+
<ng-template mat-tab-label>
|
|
7615
|
+
<mat-icon [praxisIcon]="'filter_alt'"></mat-icon>
|
|
7616
|
+
<span data-testid="crud-editor-tab-filters">{{ tx('crud.authoring.tab.filters', 'Filters') }}</span>
|
|
7617
|
+
<span
|
|
7618
|
+
class="tab-status-dot"
|
|
7619
|
+
[class.tab-status-dot--warn]="sectionStatus('filters') === 'pending'"
|
|
7620
|
+
[class.tab-status-dot--error]="sectionStatus('filters') === 'invalid'"
|
|
7621
|
+
[attr.title]="sectionStatusLabel('filters')"
|
|
7622
|
+
></span>
|
|
7623
|
+
</ng-template>
|
|
7624
|
+
<div class="tab-content">
|
|
7625
|
+
<mat-card class="editor-card" data-testid="crud-editor-filters-section">
|
|
7626
|
+
<div class="editor-section-header">
|
|
7627
|
+
<div>
|
|
7628
|
+
<h3>{{ tx('crud.authoring.section.filters', 'Filter Bar') }}</h3>
|
|
7629
|
+
<p class="editor-section-note">{{ sectionSummary('filters') }}</p>
|
|
7630
|
+
</div>
|
|
7631
|
+
<span
|
|
7632
|
+
class="action-summary-chip"
|
|
7633
|
+
[class.action-summary-chip--error]="sectionStatus('filters') === 'invalid'"
|
|
7634
|
+
[class.action-summary-chip--warn]="sectionStatus('filters') === 'pending'"
|
|
7635
|
+
>
|
|
7636
|
+
{{ sectionStatusLabel('filters') }}
|
|
7637
|
+
</span>
|
|
7638
|
+
</div>
|
|
7639
|
+
|
|
7640
|
+
<div class="editor-toggles">
|
|
7641
|
+
<mat-slide-toggle
|
|
7642
|
+
data-testid="crud-filterbar-show-advanced"
|
|
7643
|
+
[ngModel]="filterBarShowAdvanced()"
|
|
7644
|
+
(ngModelChange)="setFilterBarShowAdvanced($event)"
|
|
7645
|
+
[disabled]="isReadonly()"
|
|
7646
|
+
>
|
|
7647
|
+
{{ tx('crud.authoring.filters.showAdvanced', 'Allow opening advanced filters') }}
|
|
7648
|
+
</mat-slide-toggle>
|
|
7649
|
+
</div>
|
|
7650
|
+
|
|
7651
|
+
<section class="action-group" data-testid="crud-filterbar-inline-group">
|
|
7652
|
+
<div class="action-group__header">
|
|
7653
|
+
<strong>{{ tx('crud.authoring.filters.inlineGroup', 'Always-visible fields (Inline Fields)') }}</strong>
|
|
7654
|
+
<p class="editor-section-note">{{ tx('crud.authoring.filters.inlineNote', 'Select resource fields or add field identifiers to keep pinned in the filter bar.') }}</p>
|
|
7655
|
+
</div>
|
|
7656
|
+
|
|
7657
|
+
<div class="editor-toolbar-row">
|
|
7658
|
+
<mat-form-field appearance="outline" class="editor-inline-grow">
|
|
7659
|
+
<mat-label>{{ tx('crud.authoring.filters.inlineFieldInput', 'Add inline field') }}</mat-label>
|
|
7660
|
+
<input
|
|
7661
|
+
matInput
|
|
7662
|
+
data-testid="crud-filterbar-inline-field-input"
|
|
7663
|
+
[ngModel]="inlineFieldDraft()"
|
|
7664
|
+
(ngModelChange)="inlineFieldDraft.set($event)"
|
|
7665
|
+
(keydown.enter)="$event.preventDefault(); addInlineFilterFieldFromDraft()"
|
|
7666
|
+
[placeholder]="tx('crud.authoring.filters.inlineFieldPlaceholder', 'e.g. status, priority')"
|
|
7667
|
+
[disabled]="isReadonly()"
|
|
7668
|
+
/>
|
|
7669
|
+
</mat-form-field>
|
|
7670
|
+
<button
|
|
7671
|
+
mat-stroked-button
|
|
7672
|
+
type="button"
|
|
7673
|
+
data-testid="crud-filterbar-inline-field-add-btn"
|
|
7674
|
+
(click)="addInlineFilterFieldFromDraft()"
|
|
7675
|
+
[disabled]="isReadonly()"
|
|
7676
|
+
>
|
|
7677
|
+
{{ tx('crud.authoring.filters.inlineAdd', 'Add field') }}
|
|
7678
|
+
</button>
|
|
7679
|
+
</div>
|
|
7680
|
+
|
|
7681
|
+
@if (filterBarInlineFields().length === 0) {
|
|
7682
|
+
<p class="editor-section-note" data-testid="crud-filterbar-inline-empty">
|
|
7683
|
+
{{ tx('crud.authoring.filters.inlineEmpty', 'No inline fields pinned yet. Runtime will use the default schema configuration.') }}
|
|
7684
|
+
</p>
|
|
7685
|
+
} @else {
|
|
7686
|
+
<div class="action-summary-strip" data-testid="crud-filterbar-inline-chips">
|
|
7687
|
+
@for (field of filterBarInlineFields(); track field) {
|
|
7688
|
+
<span class="action-summary-chip inline-chip" [attr.data-testid]="'crud-filterbar-inline-chip-' + field">
|
|
7689
|
+
<span>{{ field }}</span>
|
|
7690
|
+
<button
|
|
7691
|
+
type="button"
|
|
7692
|
+
class="chip-remove-btn"
|
|
7693
|
+
[attr.data-testid]="'crud-filterbar-inline-remove-' + field"
|
|
7694
|
+
(click)="removeInlineFilterField(field)"
|
|
7695
|
+
[disabled]="isReadonly()"
|
|
7696
|
+
aria-label="Remove"
|
|
7697
|
+
>
|
|
7698
|
+
×
|
|
7699
|
+
</button>
|
|
7700
|
+
</span>
|
|
7701
|
+
}
|
|
7702
|
+
</div>
|
|
7703
|
+
}
|
|
7704
|
+
|
|
7705
|
+
@if (availableResourceFieldOptions().length > 0) {
|
|
7706
|
+
<div class="action-subgroup" data-testid="crud-filterbar-available-fields">
|
|
7707
|
+
<span class="editor-overview-label">{{ tx('crud.authoring.filters.availableFields', 'Available field suggestions') }}</span>
|
|
7708
|
+
<div class="action-summary-strip">
|
|
7709
|
+
@for (opt of availableResourceFieldOptions(); track opt.field) {
|
|
7710
|
+
<button
|
|
7711
|
+
mat-stroked-button
|
|
7712
|
+
type="button"
|
|
7713
|
+
[attr.data-testid]="'crud-filterbar-available-field-' + opt.field"
|
|
7714
|
+
(click)="toggleInlineFilterField(opt.field)"
|
|
7715
|
+
[disabled]="isReadonly()"
|
|
7716
|
+
>
|
|
7717
|
+
{{ opt.header }} ({{ opt.field }})
|
|
7718
|
+
</button>
|
|
7719
|
+
}
|
|
7720
|
+
</div>
|
|
7721
|
+
</div>
|
|
7722
|
+
}
|
|
7723
|
+
</section>
|
|
7724
|
+
|
|
7725
|
+
<section class="action-group" data-testid="crud-filterbar-quick-group">
|
|
7726
|
+
<div class="action-group__header">
|
|
7727
|
+
<strong>{{ tx('crud.authoring.filters.quickGroup', 'Quick filter shortcuts (Quick Filters)') }}</strong>
|
|
7728
|
+
</div>
|
|
7729
|
+
|
|
7730
|
+
<div class="editor-toolbar-row">
|
|
7731
|
+
<mat-form-field appearance="outline" class="editor-inline-grow">
|
|
7732
|
+
<mat-label>{{ tx('crud.authoring.filters.quickMode', 'Quick filters mode') }}</mat-label>
|
|
7733
|
+
<mat-select
|
|
7734
|
+
data-testid="crud-filterbar-quick-mode"
|
|
7735
|
+
[ngModel]="filterBarQuickMode()"
|
|
7736
|
+
(ngModelChange)="setFilterBarQuickMode($event)"
|
|
7737
|
+
[disabled]="isReadonly()"
|
|
7738
|
+
>
|
|
7739
|
+
@for (mode of quickFilterModes; track mode) {
|
|
7740
|
+
<mat-option [value]="mode">{{ quickFilterModeLabel(mode) }}</mat-option>
|
|
7741
|
+
}
|
|
7742
|
+
</mat-select>
|
|
7743
|
+
</mat-form-field>
|
|
7744
|
+
|
|
7745
|
+
@if (filterBarQuickMode() === 'custom') {
|
|
7746
|
+
<button
|
|
7747
|
+
mat-stroked-button
|
|
7748
|
+
type="button"
|
|
7749
|
+
data-testid="crud-filterbar-add-quick-btn"
|
|
7750
|
+
(click)="addQuickFilter()"
|
|
7751
|
+
[disabled]="isReadonly()"
|
|
7752
|
+
>
|
|
7753
|
+
<mat-icon [praxisIcon]="'add'"></mat-icon>
|
|
7754
|
+
{{ tx('crud.authoring.filters.addQuickFilter', 'Add quick filter') }}
|
|
7755
|
+
</button>
|
|
7756
|
+
}
|
|
7757
|
+
</div>
|
|
7758
|
+
|
|
7759
|
+
@if (filterBarQuickMode() === 'custom') {
|
|
7760
|
+
@if (filterBarQuickFilters().length === 0) {
|
|
7761
|
+
<p class="editor-section-note" data-testid="crud-filterbar-quick-empty">
|
|
7762
|
+
{{ tx('crud.authoring.filters.quickEmpty', 'No quick filters configured.') }}
|
|
7763
|
+
</p>
|
|
7764
|
+
} @else {
|
|
7765
|
+
<div class="kpi-cards-list">
|
|
7766
|
+
@for (qf of filterBarQuickFilters(); track $index) {
|
|
7767
|
+
<div class="action-subgroup" [attr.data-testid]="'crud-filterbar-quick-item-' + $index">
|
|
7768
|
+
<div class="editor-section-header">
|
|
7769
|
+
<strong>#{{ $index + 1 }} · {{ qf.label || qf.id || 'Chip' }}</strong>
|
|
7770
|
+
<div class="inline-actions-row">
|
|
7771
|
+
<button
|
|
7772
|
+
mat-stroked-button
|
|
7773
|
+
type="button"
|
|
7774
|
+
[attr.data-testid]="'crud-filterbar-quick-up-' + $index"
|
|
7775
|
+
(click)="moveQuickFilter($index, -1)"
|
|
7776
|
+
[disabled]="isReadonly() || $index === 0"
|
|
7777
|
+
>
|
|
7778
|
+
{{ tx('crud.authoring.kpis.moveUp', 'Move up') }}
|
|
7779
|
+
</button>
|
|
7780
|
+
<button
|
|
7781
|
+
mat-stroked-button
|
|
7782
|
+
type="button"
|
|
7783
|
+
[attr.data-testid]="'crud-filterbar-quick-down-' + $index"
|
|
7784
|
+
(click)="moveQuickFilter($index, 1)"
|
|
7785
|
+
[disabled]="isReadonly() || $index === filterBarQuickFilters().length - 1"
|
|
7786
|
+
>
|
|
7787
|
+
{{ tx('crud.authoring.kpis.moveDown', 'Move down') }}
|
|
7788
|
+
</button>
|
|
7789
|
+
<button
|
|
7790
|
+
mat-stroked-button
|
|
7791
|
+
type="button"
|
|
7792
|
+
[attr.data-testid]="'crud-filterbar-quick-remove-' + $index"
|
|
7793
|
+
(click)="removeQuickFilter($index)"
|
|
7794
|
+
[disabled]="isReadonly()"
|
|
7795
|
+
>
|
|
7796
|
+
{{ tx('crud.authoring.filters.removeQuickFilter', 'Remove quick filter') }}
|
|
7797
|
+
</button>
|
|
7798
|
+
</div>
|
|
7799
|
+
</div>
|
|
7800
|
+
|
|
7801
|
+
<div class="editor-grid">
|
|
7802
|
+
<mat-form-field appearance="outline">
|
|
7803
|
+
<mat-label>{{ tx('crud.authoring.filters.quickId', 'Identifier') }}</mat-label>
|
|
7804
|
+
<input
|
|
7805
|
+
matInput
|
|
7806
|
+
[attr.data-testid]="'crud-filterbar-quick-id-' + $index"
|
|
7807
|
+
[ngModel]="qf.id || ''"
|
|
7808
|
+
(ngModelChange)="setQuickFilterField($index, 'id', $event)"
|
|
7809
|
+
[disabled]="isReadonly()"
|
|
7810
|
+
/>
|
|
7811
|
+
</mat-form-field>
|
|
7812
|
+
|
|
7813
|
+
<mat-form-field appearance="outline">
|
|
7814
|
+
<mat-label>{{ tx('crud.authoring.filters.quickLabel', 'Chip label') }}</mat-label>
|
|
7815
|
+
<input
|
|
7816
|
+
matInput
|
|
7817
|
+
[attr.data-testid]="'crud-filterbar-quick-label-' + $index"
|
|
7818
|
+
[ngModel]="qf.label || ''"
|
|
7819
|
+
(ngModelChange)="setQuickFilterField($index, 'label', $event)"
|
|
7820
|
+
[disabled]="isReadonly()"
|
|
7821
|
+
/>
|
|
7822
|
+
</mat-form-field>
|
|
7823
|
+
|
|
7824
|
+
<mat-form-field appearance="outline">
|
|
7825
|
+
<mat-label>{{ tx('crud.authoring.filters.quickIcon', 'Icon (optional)') }}</mat-label>
|
|
7826
|
+
<input
|
|
7827
|
+
matInput
|
|
7828
|
+
[attr.data-testid]="'crud-filterbar-quick-icon-' + $index"
|
|
7829
|
+
[ngModel]="qf.icon || ''"
|
|
7830
|
+
(ngModelChange)="setQuickFilterField($index, 'icon', $event)"
|
|
7831
|
+
[disabled]="isReadonly()"
|
|
7832
|
+
/>
|
|
7833
|
+
</mat-form-field>
|
|
7834
|
+
|
|
7835
|
+
<mat-form-field appearance="outline">
|
|
7836
|
+
<mat-label>{{ tx('crud.authoring.filters.quickConditionMode', 'Condition type') }}</mat-label>
|
|
7837
|
+
<mat-select
|
|
7838
|
+
[attr.data-testid]="'crud-filterbar-quick-condition-mode-' + $index"
|
|
7839
|
+
[ngModel]="quickFilterConditionMode($index)"
|
|
7840
|
+
(ngModelChange)="setQuickFilterConditionMode($index, $event)"
|
|
7841
|
+
[disabled]="isReadonly()"
|
|
7842
|
+
>
|
|
7843
|
+
@for (cMode of quickConditionModes; track cMode) {
|
|
7844
|
+
<mat-option [value]="cMode">{{ quickConditionModeLabel(cMode) }}</mat-option>
|
|
7845
|
+
}
|
|
7846
|
+
</mat-select>
|
|
7847
|
+
</mat-form-field>
|
|
7848
|
+
</div>
|
|
7849
|
+
|
|
7850
|
+
@if (quickFilterConditionMode($index) === 'fieldEquals') {
|
|
7851
|
+
<div class="editor-grid">
|
|
7852
|
+
<mat-form-field appearance="outline">
|
|
7853
|
+
<mat-label>{{ tx('crud.authoring.filters.quickField', 'Field') }}</mat-label>
|
|
7854
|
+
<input
|
|
7855
|
+
matInput
|
|
7856
|
+
[attr.data-testid]="'crud-filterbar-quick-field-' + $index"
|
|
7857
|
+
[ngModel]="quickFilterConditionField($index)"
|
|
7858
|
+
(ngModelChange)="setQuickFilterFieldEquals($index, $event, quickFilterConditionValue($index))"
|
|
7859
|
+
[disabled]="isReadonly()"
|
|
7860
|
+
/>
|
|
7861
|
+
</mat-form-field>
|
|
7862
|
+
<mat-form-field appearance="outline">
|
|
7863
|
+
<mat-label>{{ tx('crud.authoring.filters.quickValue', 'Value') }}</mat-label>
|
|
7864
|
+
<input
|
|
7865
|
+
matInput
|
|
7866
|
+
[attr.data-testid]="'crud-filterbar-quick-value-' + $index"
|
|
7867
|
+
[ngModel]="quickFilterConditionValue($index)"
|
|
7868
|
+
(ngModelChange)="setQuickFilterFieldEquals($index, quickFilterConditionField($index), $event)"
|
|
7869
|
+
[disabled]="isReadonly()"
|
|
7870
|
+
/>
|
|
7871
|
+
</mat-form-field>
|
|
7872
|
+
</div>
|
|
7873
|
+
}
|
|
7874
|
+
|
|
7875
|
+
@if (quickFilterConditionMode($index) === 'expression') {
|
|
7876
|
+
<mat-form-field appearance="outline">
|
|
7877
|
+
<mat-label>{{ tx('crud.authoring.filters.quickExpression', 'Filter expression or JSON') }}</mat-label>
|
|
7878
|
+
<input
|
|
7879
|
+
matInput
|
|
7880
|
+
[attr.data-testid]="'crud-filterbar-quick-expression-' + $index"
|
|
7881
|
+
[ngModel]="quickFilterExpressionDraft($index)"
|
|
7882
|
+
(ngModelChange)="setQuickFilterExpression($index, $event)"
|
|
7883
|
+
[disabled]="isReadonly()"
|
|
7884
|
+
/>
|
|
7885
|
+
</mat-form-field>
|
|
7886
|
+
}
|
|
7887
|
+
</div>
|
|
7888
|
+
}
|
|
7889
|
+
</div>
|
|
7890
|
+
}
|
|
7891
|
+
}
|
|
7892
|
+
</section>
|
|
7893
|
+
|
|
7894
|
+
<section class="action-summary-strip" data-testid="crud-filterbar-preview">
|
|
7895
|
+
<span class="action-summary-chip">{{ filtersOverview() }}</span>
|
|
7896
|
+
<span class="action-summary-chip">{{ filtersOverviewNote() }}</span>
|
|
7897
|
+
</section>
|
|
7898
|
+
</mat-card>
|
|
7899
|
+
</div>
|
|
7900
|
+
</mat-tab>
|
|
7901
|
+
|
|
6194
7902
|
<mat-tab>
|
|
6195
7903
|
<ng-template mat-tab-label>
|
|
6196
7904
|
<mat-icon [praxisIcon]="'table_view'"></mat-icon>
|
|
@@ -6218,10 +7926,68 @@ class CrudMetadataEditorComponent {
|
|
|
6218
7926
|
<span class="action-summary-chip" data-testid="crud-editor-table-summary-behavior">{{ tableBehaviorSummary() }}</span>
|
|
6219
7927
|
<span class="action-summary-chip" data-testid="crud-editor-table-summary-states">{{ tableStatesSummary() }}</span>
|
|
6220
7928
|
</section>
|
|
7929
|
+
|
|
7930
|
+
@if (isUsingInheritedResourceColumns()) {
|
|
7931
|
+
<section class="inherited-columns-banner" data-testid="crud-table-column-inheritance-banner">
|
|
7932
|
+
<div class="inherited-columns-banner__copy">
|
|
7933
|
+
<div class="inherited-columns-banner__title-row">
|
|
7934
|
+
<strong>{{ tx('crud.authoring.table.inheritedColumnsPending', 'Columns inherited from resource') }}</strong>
|
|
7935
|
+
@if (resolvedInheritedColumns().length > 0) {
|
|
7936
|
+
<span class="action-summary-chip" data-testid="crud-table-inherited-columns-badge">
|
|
7937
|
+
{{ inheritedColumnsCountLabel() }}
|
|
7938
|
+
</span>
|
|
7939
|
+
}
|
|
7940
|
+
</div>
|
|
7941
|
+
<p class="editor-section-note" data-testid="crud-table-inherited-columns-note">
|
|
7942
|
+
{{ inheritedColumnsNote() }}
|
|
7943
|
+
</p>
|
|
7944
|
+
</div>
|
|
7945
|
+
<button
|
|
7946
|
+
mat-stroked-button
|
|
7947
|
+
type="button"
|
|
7948
|
+
data-testid="crud-table-focus-columns-btn"
|
|
7949
|
+
(click)="focusTableColumnsAuthoring()"
|
|
7950
|
+
[disabled]="isReadonly()"
|
|
7951
|
+
>
|
|
7952
|
+
<mat-icon [praxisIcon]="'view_column'"></mat-icon>
|
|
7953
|
+
{{ tx('crud.authoring.table.focusColumnsButton', 'Edit resource columns') }}
|
|
7954
|
+
</button>
|
|
7955
|
+
</section>
|
|
7956
|
+
} @else {
|
|
7957
|
+
<section class="inherited-columns-banner" data-testid="crud-table-column-custom-banner">
|
|
7958
|
+
<p class="editor-section-note">
|
|
7959
|
+
{{ tx('crud.authoring.table.customColumnsNote', 'The columns in this table have explicit customizations saved in the CRUD metadata.') }}
|
|
7960
|
+
</p>
|
|
7961
|
+
<div class="inline-actions-row">
|
|
7962
|
+
@if (resolvedInheritedColumns().length > 0) {
|
|
7963
|
+
<button
|
|
7964
|
+
mat-stroked-button
|
|
7965
|
+
type="button"
|
|
7966
|
+
data-testid="crud-table-reset-inherited-columns-btn"
|
|
7967
|
+
(click)="resetToInheritedColumns()"
|
|
7968
|
+
[disabled]="isReadonly()"
|
|
7969
|
+
>
|
|
7970
|
+
{{ tx('crud.authoring.table.inheritedColumnsPending', 'Columns inherited from resource') }}
|
|
7971
|
+
</button>
|
|
7972
|
+
}
|
|
7973
|
+
<button
|
|
7974
|
+
mat-stroked-button
|
|
7975
|
+
type="button"
|
|
7976
|
+
data-testid="crud-table-focus-columns-btn"
|
|
7977
|
+
(click)="focusTableColumnsAuthoring()"
|
|
7978
|
+
[disabled]="isReadonly()"
|
|
7979
|
+
>
|
|
7980
|
+
<mat-icon [praxisIcon]="'view_column'"></mat-icon>
|
|
7981
|
+
{{ tx('crud.authoring.table.focusColumnsButton', 'Edit resource columns') }}
|
|
7982
|
+
</button>
|
|
7983
|
+
</div>
|
|
7984
|
+
</section>
|
|
7985
|
+
}
|
|
7986
|
+
|
|
6221
7987
|
<p class="editor-section-note" data-testid="crud-editor-table-flow-note">{{ tableEditingFlowSummary() }}</p>
|
|
6222
7988
|
<praxis-table-inline-authoring-editor
|
|
6223
7989
|
data-testid="crud-editor-table-inline"
|
|
6224
|
-
[config]="
|
|
7990
|
+
[config]="effectiveTableConfigForEditor()"
|
|
6225
7991
|
[readonly]="isReadonly()"
|
|
6226
7992
|
(configChange)="setTableConfig($event)"
|
|
6227
7993
|
/>
|
|
@@ -6272,7 +8038,7 @@ class CrudMetadataEditorComponent {
|
|
|
6272
8038
|
</mat-tab>
|
|
6273
8039
|
</mat-tab-group>
|
|
6274
8040
|
</section>
|
|
6275
|
-
`, isInline: true, styles: [":host{display:block;min-width:0;color:var(--md-sys-color-on-surface,#1a1b20)}.editor-shell{display:grid;gap:16px}.editor-header{display:flex;justify-content:space-between;gap:16px;align-items:start}.editor-overview,.editor-health-map{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(180px,1fr))}.editor-health-card{display:grid;gap:4px;padding:14px;border-radius:18px;background:color-mix(in srgb,var(--md-sys-color-surface-container-high,#eef1f6) 92%,transparent)}.editor-health-card--warn{background:color-mix(in srgb,var(--md-sys-color-tertiary-container,#f8e08e) 78%,transparent)}.editor-health-card--error{background:color-mix(in srgb,var(--md-sys-color-error-container,#f9dedc) 82%,transparent);color:var(--md-sys-color-on-error-container,#410e0b)}.editor-focus{display:flex;gap:12px;align-items:start;padding:14px 16px;border-radius:18px;background:color-mix(in srgb,var(--md-sys-color-surface-container-high,#eef1f6) 92%,transparent)}.editor-focus--success{background:color-mix(in srgb,var(--md-sys-color-secondary-container,#d7e3ff) 70%,transparent)}.editor-focus-chip{padding:6px 10px;border-radius:999px;font-size:12px;background:color-mix(in srgb,var(--md-sys-color-secondary-container,#d7e3ff) 70%,transparent);white-space:nowrap}.editor-focus-chip--warn{background:color-mix(in srgb,var(--md-sys-color-tertiary-container,#f8e08e) 78%,transparent)}.editor-focus-chip--error{background:color-mix(in srgb,var(--md-sys-color-error-container,#f9dedc) 82%,transparent);color:var(--md-sys-color-on-error-container,#410e0b)}.editor-focus-copy{display:grid;gap:4px}.editor-focus-copy p{margin:0;color:var(--md-sys-color-on-surface-variant,#5a5d67)}.editor-tabs{display:flex;flex-direction:column;min-width:0}.editor-tabs .mat-mdc-tab{min-width:118px}.editor-tabs ::ng-deep .mdc-tab__content{gap:8px}.editor-tabs ::ng-deep .mat-mdc-tab-body-wrapper,.editor-tabs ::ng-deep .mat-mdc-tab-group-container{flex:1 1 auto;min-height:0}.editor-tabs ::ng-deep .mat-mdc-tab-body-content{overflow:hidden}.tab-status-dot{width:8px;height:8px;border-radius:999px;background:var(--md-sys-color-primary,#1263b4);flex:0 0 auto}.tab-status-dot--warn{background:var(--md-sys-color-tertiary,#7d5700)}.tab-status-dot--error{background:var(--md-sys-color-error,#b3261e)}.tab-content{display:grid;gap:14px;min-width:0;padding:12px 2px 4px;box-sizing:border-box}.editor-overview-card{display:grid;gap:4px;padding:14px;border-radius:18px;background:color-mix(in srgb,var(--md-sys-color-surface-container-high,#eef1f6) 92%,transparent)}.editor-overview-card--warn{background:color-mix(in srgb,var(--md-sys-color-tertiary-container,#f8e08e) 78%,transparent)}.editor-overview-card--error{background:color-mix(in srgb,var(--md-sys-color-error-container,#f9dedc) 82%,transparent);color:var(--md-sys-color-on-error-container,#410e0b)}.editor-overview-label{font-size:12px;color:var(--md-sys-color-on-surface-variant,#5a5d67)}.editor-overview-value{line-height:1.3;overflow-wrap:anywhere}.editor-overview-note{margin:0;font-size:12px;color:var(--md-sys-color-on-surface-variant,#5a5d67)}.editor-header h2,.editor-card h3{margin:0}.editor-header p{margin:6px 0 0;color:var(--md-sys-color-on-surface-variant,#5a5d67)}.editor-chip{padding:6px 10px;border-radius:999px;background:color-mix(in srgb,var(--md-sys-color-primary,#1263b4) 12%,transparent);font-size:12px}.editor-card{display:grid;gap:14px;padding:16px;border-radius:20px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline,#c5c7ce) 70%,transparent)}.editor-section-header{display:flex;justify-content:space-between;gap:12px;align-items:start}.editor-grid{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}.editor-grid--compact{grid-template-columns:repeat(auto-fit,minmax(160px,1fr))}.editor-toggles{display:grid;gap:10px;grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}.editor-actions,.action-grid{display:grid;gap:14px}.action-summary-strip{display:flex;flex-wrap:wrap;gap:8px}.action-summary-chip{padding:6px 10px;border-radius:999px;font-size:12px;background:color-mix(in srgb,var(--md-sys-color-secondary-container,#d7e3ff) 70%,transparent)}.action-summary-chip--warn{background:color-mix(in srgb,var(--md-sys-color-tertiary-container,#f8e08e) 78%,transparent)}.action-summary-chip--error{background:color-mix(in srgb,var(--md-sys-color-error-container,#f9dedc) 82%,transparent);color:var(--md-sys-color-on-error-container,#410e0b)}.action-group{display:grid;gap:10px;padding:12px;border-radius:16px;background:color-mix(in srgb,var(--md-sys-color-surface-container-high,#eef1f6) 88%,transparent)}.action-group--advanced{background:color-mix(in srgb,var(--md-sys-color-surface-container,#f5f7fb) 90%,transparent)}.action-group__header{display:grid;gap:4px}.action-subgroup{display:grid;gap:10px;padding:10px;border-radius:14px;background:color-mix(in srgb,var(--md-sys-color-surface-container-low,#fafbfd) 92%,transparent)}.action-subgroup__header{display:grid;gap:4px}.action-param-list{display:grid;gap:12px}.action-param-row{display:grid;gap:8px}.action-advanced-panel{border:1px solid color-mix(in srgb,var(--md-sys-color-outline,#c5c7ce) 65%,transparent);border-radius:16px}.editor-section-note{margin:0;color:var(--md-sys-color-on-surface-variant,#5a5d67)}.diagnostics-groups{display:grid;gap:12px}.diagnostics-group{display:grid;gap:10px;padding:12px;border-radius:16px;background:color-mix(in srgb,var(--md-sys-color-surface-container-high,#eef1f6) 92%,transparent)}.diagnostics{display:grid;gap:8px;margin:0;padding-left:18px}.diagnostics .error{color:var(--md-sys-color-error,#b3261e)}pre{margin:0;overflow:auto;max-height:320px;padding:12px;border-radius:14px;background:color-mix(in srgb,var(--md-sys-color-surface-container-high,#eef1f6) 92%,transparent)}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.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: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatCardModule }, { kind: "component", type: i3$1.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "ngmodule", type: MatExpansionModule }, { kind: "component", type: i4$3.MatExpansionPanel, selector: "mat-expansion-panel", inputs: ["hideToggle", "togglePosition"], outputs: ["afterExpand", "afterCollapse"], exportAs: ["matExpansionPanel"] }, { kind: "component", type: i4$3.MatExpansionPanelHeader, selector: "mat-expansion-panel-header", inputs: ["expandedHeight", "collapsedHeight", "tabIndex"] }, { kind: "directive", type: i4$3.MatExpansionPanelTitle, selector: "mat-panel-title" }, { kind: "directive", type: i4$3.MatExpansionPanelDescription, selector: "mat-panel-description" }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i6.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i6.MatLabel, selector: "mat-label" }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i4.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i7.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i6.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i6.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i8.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "ngmodule", type: MatTabsModule }, { kind: "directive", type: i9.MatTabLabel, selector: "[mat-tab-label], [matTabLabel]" }, { kind: "component", type: i9.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass", "id"], exportAs: ["matTab"] }, { kind: "component", type: i9.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "mat-align-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }, { kind: "component", type: PraxisTableInlineAuthoringEditorComponent, selector: "praxis-table-inline-authoring-editor", inputs: ["config", "readonly"], outputs: ["configChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
8041
|
+
`, isInline: true, styles: [":host{display:block;min-width:0;color:var(--md-sys-color-on-surface,#1a1b20)}.editor-shell{display:grid;gap:16px}.editor-header{display:flex;justify-content:space-between;gap:16px;align-items:start}.editor-overview,.editor-health-map{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(180px,1fr))}.editor-health-card{display:grid;gap:4px;padding:14px;border-radius:18px;background:color-mix(in srgb,var(--md-sys-color-surface-container-high,#eef1f6) 92%,transparent)}.editor-health-card--warn{background:color-mix(in srgb,var(--md-sys-color-tertiary-container,#f8e08e) 78%,transparent)}.editor-health-card--error{background:color-mix(in srgb,var(--md-sys-color-error-container,#f9dedc) 82%,transparent);color:var(--md-sys-color-on-error-container,#410e0b)}.editor-focus{display:flex;gap:12px;align-items:start;padding:14px 16px;border-radius:18px;background:color-mix(in srgb,var(--md-sys-color-surface-container-high,#eef1f6) 92%,transparent)}.editor-focus--success{background:color-mix(in srgb,var(--md-sys-color-secondary-container,#d7e3ff) 70%,transparent)}.editor-focus-chip{padding:6px 10px;border-radius:999px;font-size:12px;background:color-mix(in srgb,var(--md-sys-color-secondary-container,#d7e3ff) 70%,transparent);white-space:nowrap}.editor-focus-chip--warn{background:color-mix(in srgb,var(--md-sys-color-tertiary-container,#f8e08e) 78%,transparent)}.editor-focus-chip--error{background:color-mix(in srgb,var(--md-sys-color-error-container,#f9dedc) 82%,transparent);color:var(--md-sys-color-on-error-container,#410e0b)}.editor-focus-copy{display:grid;gap:4px}.editor-focus-copy p{margin:0;color:var(--md-sys-color-on-surface-variant,#5a5d67)}.editor-tabs{display:flex;flex-direction:column;min-width:0}.editor-tabs .mat-mdc-tab{min-width:118px}.editor-tabs ::ng-deep .mdc-tab__content{gap:8px}.editor-tabs ::ng-deep .mat-mdc-tab-body-wrapper,.editor-tabs ::ng-deep .mat-mdc-tab-group-container{flex:1 1 auto;min-height:0}.editor-tabs ::ng-deep .mat-mdc-tab-body-content{overflow:hidden}.tab-status-dot{width:8px;height:8px;border-radius:999px;background:var(--md-sys-color-primary,#1263b4);flex:0 0 auto}.tab-status-dot--warn{background:var(--md-sys-color-tertiary,#7d5700)}.tab-status-dot--error{background:var(--md-sys-color-error,#b3261e)}.tab-content{display:grid;gap:14px;min-width:0;padding:12px 2px 4px;box-sizing:border-box}.editor-overview-card{display:grid;gap:4px;padding:14px;border-radius:18px;background:color-mix(in srgb,var(--md-sys-color-surface-container-high,#eef1f6) 92%,transparent)}.editor-overview-card--warn{background:color-mix(in srgb,var(--md-sys-color-tertiary-container,#f8e08e) 78%,transparent)}.editor-overview-card--error{background:color-mix(in srgb,var(--md-sys-color-error-container,#f9dedc) 82%,transparent);color:var(--md-sys-color-on-error-container,#410e0b)}.editor-overview-label{font-size:12px;color:var(--md-sys-color-on-surface-variant,#5a5d67)}.editor-overview-value{line-height:1.3;overflow-wrap:anywhere}.editor-overview-note{margin:0;font-size:12px;color:var(--md-sys-color-on-surface-variant,#5a5d67)}.editor-header h2,.editor-card h3{margin:0}.editor-header p{margin:6px 0 0;color:var(--md-sys-color-on-surface-variant,#5a5d67)}.editor-chip{padding:6px 10px;border-radius:999px;background:color-mix(in srgb,var(--md-sys-color-primary,#1263b4) 12%,transparent);font-size:12px}.editor-card{display:grid;gap:14px;padding:16px;border-radius:20px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline,#c5c7ce) 70%,transparent)}.editor-section-header{display:flex;justify-content:space-between;gap:12px;align-items:start;flex-wrap:wrap}.editor-toolbar-row{display:flex;justify-content:space-between;gap:12px;align-items:center;flex-wrap:wrap}.editor-inline-grow{flex:1 1 240px}.inline-actions-row{display:flex;gap:8px;flex-wrap:wrap;align-items:center}.inline-chip{display:inline-flex;align-items:center;gap:6px}.chip-remove-btn{border:0;background:transparent;cursor:pointer;font-size:14px;line-height:1;padding:0 2px;color:inherit}.kpi-cards-list{display:grid;gap:12px}.inherited-columns-banner{display:flex;justify-content:space-between;gap:12px;align-items:center;flex-wrap:wrap;padding:12px 14px;border-radius:16px;background:color-mix(in srgb,var(--md-sys-color-secondary-container,#d7e3ff) 55%,transparent)}.inherited-columns-banner__copy{display:grid;gap:4px}.inherited-columns-banner__title-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.editor-grid{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}.editor-grid--compact{grid-template-columns:repeat(auto-fit,minmax(160px,1fr))}.editor-toggles{display:grid;gap:10px;grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}.editor-actions,.action-grid{display:grid;gap:14px}.action-summary-strip{display:flex;flex-wrap:wrap;gap:8px}.action-summary-chip{padding:6px 10px;border-radius:999px;font-size:12px;background:color-mix(in srgb,var(--md-sys-color-secondary-container,#d7e3ff) 70%,transparent)}.action-summary-chip--warn{background:color-mix(in srgb,var(--md-sys-color-tertiary-container,#f8e08e) 78%,transparent)}.action-summary-chip--error{background:color-mix(in srgb,var(--md-sys-color-error-container,#f9dedc) 82%,transparent);color:var(--md-sys-color-on-error-container,#410e0b)}.action-group{display:grid;gap:10px;padding:12px;border-radius:16px;background:color-mix(in srgb,var(--md-sys-color-surface-container-high,#eef1f6) 88%,transparent)}.action-group--advanced{background:color-mix(in srgb,var(--md-sys-color-surface-container,#f5f7fb) 90%,transparent)}.action-group__header{display:grid;gap:4px}.action-subgroup{display:grid;gap:10px;padding:10px;border-radius:14px;background:color-mix(in srgb,var(--md-sys-color-surface-container-low,#fafbfd) 92%,transparent)}.action-subgroup__header{display:grid;gap:4px}.action-param-list{display:grid;gap:12px}.action-param-row{display:grid;gap:8px}.action-advanced-panel{border:1px solid color-mix(in srgb,var(--md-sys-color-outline,#c5c7ce) 65%,transparent);border-radius:16px}.editor-section-note{margin:0;color:var(--md-sys-color-on-surface-variant,#5a5d67)}.diagnostics-groups{display:grid;gap:12px}.diagnostics-group{display:grid;gap:10px;padding:12px;border-radius:16px;background:color-mix(in srgb,var(--md-sys-color-surface-container-high,#eef1f6) 92%,transparent)}.diagnostics{display:grid;gap:8px;margin:0;padding-left:18px}.diagnostics .error{color:var(--md-sys-color-error,#b3261e)}pre{margin:0;overflow:auto;max-height:320px;padding:12px;border-radius:14px;background:color-mix(in srgb,var(--md-sys-color-surface-container-high,#eef1f6) 92%,transparent)}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.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: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatCardModule }, { kind: "component", type: i3$1.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "ngmodule", type: MatExpansionModule }, { kind: "component", type: i4$3.MatExpansionPanel, selector: "mat-expansion-panel", inputs: ["hideToggle", "togglePosition"], outputs: ["afterExpand", "afterCollapse"], exportAs: ["matExpansionPanel"] }, { kind: "component", type: i4$3.MatExpansionPanelHeader, selector: "mat-expansion-panel-header", inputs: ["expandedHeight", "collapsedHeight", "tabIndex"] }, { kind: "directive", type: i4$3.MatExpansionPanelTitle, selector: "mat-panel-title" }, { kind: "directive", type: i4$3.MatExpansionPanelDescription, selector: "mat-panel-description" }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i6.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i6.MatLabel, selector: "mat-label" }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i4.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i7.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i6.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i6.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i8.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "ngmodule", type: MatTabsModule }, { kind: "directive", type: i9.MatTabLabel, selector: "[mat-tab-label], [matTabLabel]" }, { kind: "component", type: i9.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass", "id"], exportAs: ["matTab"] }, { kind: "component", type: i9.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "mat-align-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }, { kind: "component", type: PraxisKpiBandComponent, selector: "praxis-kpi-band", inputs: ["config", "dataContext", "isInteractiveHost", "activeFilter"], outputs: ["cardClick", "statsLoaded", "statsError", "refreshClick"] }, { kind: "component", type: PraxisTableInlineAuthoringEditorComponent, selector: "praxis-table-inline-authoring-editor", inputs: ["config", "readonly"], outputs: ["configChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
6276
8042
|
}
|
|
6277
8043
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: CrudMetadataEditorComponent, decorators: [{
|
|
6278
8044
|
type: Component,
|
|
@@ -6288,8 +8054,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
6288
8054
|
MatSlideToggleModule,
|
|
6289
8055
|
MatTabsModule,
|
|
6290
8056
|
PraxisIconDirective,
|
|
6291
|
-
|
|
6292
|
-
|
|
8057
|
+
PraxisKpiBandComponent,
|
|
8058
|
+
PraxisTableInlineAuthoringEditorComponent,
|
|
8059
|
+
], providers: [
|
|
8060
|
+
providePraxisI18nConfig(PRAXIS_CRUD_AUTHORING_I18N_CONFIG),
|
|
8061
|
+
{
|
|
8062
|
+
provide: GenericCrudService,
|
|
8063
|
+
useFactory: () => {
|
|
8064
|
+
const http = inject(HttpClient, { optional: true });
|
|
8065
|
+
const apiUrl = inject(API_URL, { optional: true });
|
|
8066
|
+
// Offline authoring has no transport; a configured editor owns its mutable reader.
|
|
8067
|
+
if (http === null || apiUrl === null)
|
|
8068
|
+
return null;
|
|
8069
|
+
return new GenericCrudService(http, inject(SchemaNormalizerService), inject(GlobalConfigService), apiUrl);
|
|
8070
|
+
},
|
|
8071
|
+
},
|
|
8072
|
+
], template: `
|
|
6293
8073
|
<section class="editor-shell" data-testid="crud-metadata-editor">
|
|
6294
8074
|
<header class="editor-header">
|
|
6295
8075
|
<div>
|
|
@@ -6315,6 +8095,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
6315
8095
|
<strong class="editor-overview-value" data-testid="crud-editor-overview-actions">{{ actionsOverview() }}</strong>
|
|
6316
8096
|
<p class="editor-overview-note" data-testid="crud-editor-overview-actions-note">{{ actionsOverviewNote() }}</p>
|
|
6317
8097
|
</article>
|
|
8098
|
+
<article class="editor-overview-card">
|
|
8099
|
+
<span class="editor-overview-label">{{ tx('crud.authoring.overview.kpis', 'KPI Band') }}</span>
|
|
8100
|
+
<strong class="editor-overview-value" data-testid="crud-editor-overview-kpis">{{ kpisOverview() }}</strong>
|
|
8101
|
+
<p class="editor-overview-note" data-testid="crud-editor-overview-kpis-note">{{ kpisOverviewNote() }}</p>
|
|
8102
|
+
</article>
|
|
8103
|
+
<article class="editor-overview-card">
|
|
8104
|
+
<span class="editor-overview-label">{{ tx('crud.authoring.overview.filters', 'Filters') }}</span>
|
|
8105
|
+
<strong class="editor-overview-value" data-testid="crud-editor-overview-filters">{{ filtersOverview() }}</strong>
|
|
8106
|
+
<p class="editor-overview-note" data-testid="crud-editor-overview-filters-note">{{ filtersOverviewNote() }}</p>
|
|
8107
|
+
</article>
|
|
6318
8108
|
<article class="editor-overview-card">
|
|
6319
8109
|
<span class="editor-overview-label">{{ tx('crud.authoring.overview.table', 'Table') }}</span>
|
|
6320
8110
|
<strong class="editor-overview-value" data-testid="crud-editor-overview-table">{{ tableOverview() }}</strong>
|
|
@@ -6357,7 +8147,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
6357
8147
|
</div>
|
|
6358
8148
|
</section>
|
|
6359
8149
|
|
|
6360
|
-
<mat-tab-group
|
|
8150
|
+
<mat-tab-group
|
|
8151
|
+
class="editor-tabs"
|
|
8152
|
+
data-testid="crud-editor-tabs"
|
|
8153
|
+
dynamicHeight
|
|
8154
|
+
animationDuration="0ms"
|
|
8155
|
+
[selectedIndex]="selectedTabIndex()"
|
|
8156
|
+
(selectedIndexChange)="selectedTabIndex.set($event)"
|
|
8157
|
+
>
|
|
6361
8158
|
<mat-tab>
|
|
6362
8159
|
<ng-template mat-tab-label>
|
|
6363
8160
|
<mat-icon [praxisIcon]="'link'"></mat-icon>
|
|
@@ -6760,6 +8557,520 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
6760
8557
|
</div>
|
|
6761
8558
|
</mat-tab>
|
|
6762
8559
|
|
|
8560
|
+
<mat-tab>
|
|
8561
|
+
<ng-template mat-tab-label>
|
|
8562
|
+
<mat-icon [praxisIcon]="'analytics'"></mat-icon>
|
|
8563
|
+
<span data-testid="crud-editor-tab-kpis">{{ tx('crud.authoring.tab.kpis', 'KPIs') }}</span>
|
|
8564
|
+
<span
|
|
8565
|
+
class="tab-status-dot"
|
|
8566
|
+
[class.tab-status-dot--warn]="sectionStatus('kpis') === 'pending'"
|
|
8567
|
+
[class.tab-status-dot--error]="sectionStatus('kpis') === 'invalid'"
|
|
8568
|
+
[attr.title]="sectionStatusLabel('kpis')"
|
|
8569
|
+
></span>
|
|
8570
|
+
</ng-template>
|
|
8571
|
+
<div class="tab-content">
|
|
8572
|
+
<mat-card class="editor-card" data-testid="crud-editor-kpis-section">
|
|
8573
|
+
<div class="editor-section-header">
|
|
8574
|
+
<div>
|
|
8575
|
+
<h3>{{ tx('crud.authoring.section.kpis', 'KPI Band') }}</h3>
|
|
8576
|
+
<p class="editor-section-note">{{ sectionSummary('kpis') }}</p>
|
|
8577
|
+
</div>
|
|
8578
|
+
<span
|
|
8579
|
+
class="action-summary-chip"
|
|
8580
|
+
[class.action-summary-chip--error]="sectionStatus('kpis') === 'invalid'"
|
|
8581
|
+
[class.action-summary-chip--warn]="sectionStatus('kpis') === 'pending'"
|
|
8582
|
+
>
|
|
8583
|
+
{{ sectionStatusLabel('kpis') }}
|
|
8584
|
+
</span>
|
|
8585
|
+
</div>
|
|
8586
|
+
|
|
8587
|
+
<div class="editor-toolbar-row">
|
|
8588
|
+
<mat-slide-toggle
|
|
8589
|
+
data-testid="crud-kpi-band-enabled"
|
|
8590
|
+
[ngModel]="kpiBandEnabled()"
|
|
8591
|
+
(ngModelChange)="setKpiBandEnabled($event)"
|
|
8592
|
+
[disabled]="isReadonly()"
|
|
8593
|
+
>
|
|
8594
|
+
{{ tx('crud.authoring.kpis.enabled', 'Show KPI band above collection table') }}
|
|
8595
|
+
</mat-slide-toggle>
|
|
8596
|
+
<button
|
|
8597
|
+
mat-stroked-button
|
|
8598
|
+
type="button"
|
|
8599
|
+
data-testid="crud-kpi-add-card-btn"
|
|
8600
|
+
(click)="addKpiCard()"
|
|
8601
|
+
[disabled]="isReadonly()"
|
|
8602
|
+
>
|
|
8603
|
+
<mat-icon [praxisIcon]="'add'"></mat-icon>
|
|
8604
|
+
{{ tx('crud.authoring.kpis.addCard', 'Add KPI') }}
|
|
8605
|
+
</button>
|
|
8606
|
+
</div>
|
|
8607
|
+
|
|
8608
|
+
@if (kpiCards().length === 0) {
|
|
8609
|
+
<p class="editor-section-note" data-testid="crud-kpi-empty-state">
|
|
8610
|
+
{{ tx('crud.authoring.kpis.empty', 'No KPI cards configured yet.') }}
|
|
8611
|
+
</p>
|
|
8612
|
+
} @else {
|
|
8613
|
+
<div class="kpi-cards-list">
|
|
8614
|
+
@for (card of kpiCards(); track $index) {
|
|
8615
|
+
<section class="action-group" [attr.data-testid]="'crud-kpi-card-item-' + $index">
|
|
8616
|
+
<div class="editor-section-header">
|
|
8617
|
+
<strong>{{ kpiCardTitle($index, card) }}</strong>
|
|
8618
|
+
<div class="inline-actions-row">
|
|
8619
|
+
<button
|
|
8620
|
+
mat-stroked-button
|
|
8621
|
+
type="button"
|
|
8622
|
+
[attr.data-testid]="'crud-kpi-card-up-' + $index"
|
|
8623
|
+
(click)="moveKpiCard($index, -1)"
|
|
8624
|
+
[disabled]="isReadonly() || $index === 0"
|
|
8625
|
+
>
|
|
8626
|
+
{{ tx('crud.authoring.kpis.moveUp', 'Move up') }}
|
|
8627
|
+
</button>
|
|
8628
|
+
<button
|
|
8629
|
+
mat-stroked-button
|
|
8630
|
+
type="button"
|
|
8631
|
+
[attr.data-testid]="'crud-kpi-card-down-' + $index"
|
|
8632
|
+
(click)="moveKpiCard($index, 1)"
|
|
8633
|
+
[disabled]="isReadonly() || $index === kpiCards().length - 1"
|
|
8634
|
+
>
|
|
8635
|
+
{{ tx('crud.authoring.kpis.moveDown', 'Move down') }}
|
|
8636
|
+
</button>
|
|
8637
|
+
<button
|
|
8638
|
+
mat-stroked-button
|
|
8639
|
+
type="button"
|
|
8640
|
+
[attr.data-testid]="'crud-kpi-card-remove-' + $index"
|
|
8641
|
+
(click)="removeKpiCard($index)"
|
|
8642
|
+
[disabled]="isReadonly()"
|
|
8643
|
+
>
|
|
8644
|
+
{{ tx('crud.authoring.kpis.removeCard', 'Remove KPI') }}
|
|
8645
|
+
</button>
|
|
8646
|
+
</div>
|
|
8647
|
+
</div>
|
|
8648
|
+
|
|
8649
|
+
<div class="editor-grid">
|
|
8650
|
+
<mat-form-field appearance="outline">
|
|
8651
|
+
<mat-label>{{ tx('crud.authoring.kpis.label', 'Label') }}</mat-label>
|
|
8652
|
+
<input
|
|
8653
|
+
matInput
|
|
8654
|
+
[attr.data-testid]="'crud-kpi-card-label-' + $index"
|
|
8655
|
+
[ngModel]="card.label || ''"
|
|
8656
|
+
(ngModelChange)="setKpiCardField($index, 'label', $event)"
|
|
8657
|
+
[placeholder]="tx('crud.authoring.kpis.labelPlaceholder', 'e.g. In progress')"
|
|
8658
|
+
[disabled]="isReadonly()"
|
|
8659
|
+
/>
|
|
8660
|
+
</mat-form-field>
|
|
8661
|
+
|
|
8662
|
+
<mat-form-field appearance="outline">
|
|
8663
|
+
<mat-label>{{ tx('crud.authoring.kpis.value', 'Displayed value') }}</mat-label>
|
|
8664
|
+
<input
|
|
8665
|
+
matInput
|
|
8666
|
+
[attr.data-testid]="'crud-kpi-card-value-' + $index"
|
|
8667
|
+
[ngModel]="card.value ?? ''"
|
|
8668
|
+
(ngModelChange)="setKpiCardField($index, 'value', $event)"
|
|
8669
|
+
[placeholder]="tx('crud.authoring.kpis.valuePlaceholder', 'e.g. 18')"
|
|
8670
|
+
[disabled]="isReadonly()"
|
|
8671
|
+
/>
|
|
8672
|
+
</mat-form-field>
|
|
8673
|
+
|
|
8674
|
+
<mat-form-field appearance="outline">
|
|
8675
|
+
<mat-label>{{ tx('crud.authoring.kpis.caption', 'Caption / context') }}</mat-label>
|
|
8676
|
+
<input
|
|
8677
|
+
matInput
|
|
8678
|
+
[attr.data-testid]="'crud-kpi-card-caption-' + $index"
|
|
8679
|
+
[ngModel]="card.caption || ''"
|
|
8680
|
+
(ngModelChange)="setKpiCardField($index, 'caption', $event)"
|
|
8681
|
+
[placeholder]="tx('crud.authoring.kpis.captionPlaceholder', 'e.g. All sectors')"
|
|
8682
|
+
[disabled]="isReadonly()"
|
|
8683
|
+
/>
|
|
8684
|
+
</mat-form-field>
|
|
8685
|
+
|
|
8686
|
+
<mat-form-field appearance="outline">
|
|
8687
|
+
<mat-label>{{ tx('crud.authoring.kpis.icon', 'Icon (Material Icon)') }}</mat-label>
|
|
8688
|
+
<input
|
|
8689
|
+
matInput
|
|
8690
|
+
[attr.data-testid]="'crud-kpi-card-icon-' + $index"
|
|
8691
|
+
[ngModel]="card.icon || ''"
|
|
8692
|
+
(ngModelChange)="setKpiCardField($index, 'icon', $event)"
|
|
8693
|
+
[placeholder]="tx('crud.authoring.kpis.iconPlaceholder', 'e.g. radar')"
|
|
8694
|
+
[disabled]="isReadonly()"
|
|
8695
|
+
/>
|
|
8696
|
+
</mat-form-field>
|
|
8697
|
+
|
|
8698
|
+
<mat-form-field appearance="outline">
|
|
8699
|
+
<mat-label>{{ tx('crud.authoring.kpis.variant', 'Semantic tone') }}</mat-label>
|
|
8700
|
+
<mat-select
|
|
8701
|
+
[attr.data-testid]="'crud-kpi-card-variant-' + $index"
|
|
8702
|
+
[ngModel]="kpiCardVariant(card)"
|
|
8703
|
+
(ngModelChange)="setKpiCardVariant($index, $event)"
|
|
8704
|
+
[disabled]="isReadonly()"
|
|
8705
|
+
>
|
|
8706
|
+
@for (variant of kpiVariants; track variant) {
|
|
8707
|
+
<mat-option [value]="variant">{{ kpiVariantLabel(variant) }}</mat-option>
|
|
8708
|
+
}
|
|
8709
|
+
</mat-select>
|
|
8710
|
+
</mat-form-field>
|
|
8711
|
+
|
|
8712
|
+
<mat-form-field appearance="outline">
|
|
8713
|
+
<mat-label>{{ tx('crud.authoring.kpis.filterMode', 'Click behavior (Filter)') }}</mat-label>
|
|
8714
|
+
<mat-select
|
|
8715
|
+
[attr.data-testid]="'crud-kpi-card-filter-mode-' + $index"
|
|
8716
|
+
[ngModel]="kpiCardFilterMode($index)"
|
|
8717
|
+
(ngModelChange)="setKpiCardFilterMode($index, $event)"
|
|
8718
|
+
[disabled]="isReadonly()"
|
|
8719
|
+
>
|
|
8720
|
+
@for (mode of kpiFilterModes; track mode) {
|
|
8721
|
+
<mat-option [value]="mode">{{ kpiFilterModeLabel(mode) }}</mat-option>
|
|
8722
|
+
}
|
|
8723
|
+
</mat-select>
|
|
8724
|
+
</mat-form-field>
|
|
8725
|
+
</div>
|
|
8726
|
+
|
|
8727
|
+
@if (kpiCardFilterMode($index) === 'condition') {
|
|
8728
|
+
<div class="editor-grid">
|
|
8729
|
+
<mat-form-field appearance="outline">
|
|
8730
|
+
<mat-label>{{ tx('crud.authoring.kpis.filterField', 'Filter field') }}</mat-label>
|
|
8731
|
+
<input
|
|
8732
|
+
matInput
|
|
8733
|
+
[attr.data-testid]="'crud-kpi-card-filter-field-' + $index"
|
|
8734
|
+
[ngModel]="kpiCardFilterField($index)"
|
|
8735
|
+
(ngModelChange)="setKpiCardFilterCondition($index, $event, kpiCardFilterValue($index))"
|
|
8736
|
+
[placeholder]="tx('crud.authoring.kpis.filterFieldPlaceholder', 'e.g. status')"
|
|
8737
|
+
[disabled]="isReadonly()"
|
|
8738
|
+
/>
|
|
8739
|
+
</mat-form-field>
|
|
8740
|
+
|
|
8741
|
+
<mat-form-field appearance="outline">
|
|
8742
|
+
<mat-label>{{ tx('crud.authoring.kpis.filterValue', 'Expected value') }}</mat-label>
|
|
8743
|
+
<input
|
|
8744
|
+
matInput
|
|
8745
|
+
[attr.data-testid]="'crud-kpi-card-filter-value-' + $index"
|
|
8746
|
+
[ngModel]="kpiCardFilterValue($index)"
|
|
8747
|
+
(ngModelChange)="setKpiCardFilterCondition($index, kpiCardFilterField($index), $event)"
|
|
8748
|
+
[placeholder]="tx('crud.authoring.kpis.filterValuePlaceholder', 'e.g. IN_PROGRESS')"
|
|
8749
|
+
[disabled]="isReadonly()"
|
|
8750
|
+
/>
|
|
8751
|
+
</mat-form-field>
|
|
8752
|
+
</div>
|
|
8753
|
+
|
|
8754
|
+
<mat-form-field appearance="outline">
|
|
8755
|
+
<mat-label>{{ tx('crud.authoring.kpis.filterJson', 'Advanced filter criteria (JSON)') }}</mat-label>
|
|
8756
|
+
<textarea
|
|
8757
|
+
matInput
|
|
8758
|
+
rows="3"
|
|
8759
|
+
[attr.data-testid]="'crud-kpi-card-filter-json-' + $index"
|
|
8760
|
+
[ngModel]="kpiCardFilterJsonDraft($index)"
|
|
8761
|
+
(ngModelChange)="setKpiCardFilterJson($index, $event)"
|
|
8762
|
+
[disabled]="isReadonly()"
|
|
8763
|
+
></textarea>
|
|
8764
|
+
</mat-form-field>
|
|
8765
|
+
}
|
|
8766
|
+
</section>
|
|
8767
|
+
}
|
|
8768
|
+
</div>
|
|
8769
|
+
}
|
|
8770
|
+
|
|
8771
|
+
@if (kpiBandEnabled() && kpiCards().length > 0) {
|
|
8772
|
+
<section class="action-group" data-testid="crud-kpi-band-preview-section">
|
|
8773
|
+
<strong>{{ tx('crud.authoring.kpis.previewTitle', 'KPI band preview') }}</strong>
|
|
8774
|
+
<praxis-kpi-band
|
|
8775
|
+
data-testid="crud-kpi-band-preview"
|
|
8776
|
+
[config]="kpiBandPreviewConfig()"
|
|
8777
|
+
[isInteractiveHost]="false"
|
|
8778
|
+
/>
|
|
8779
|
+
</section>
|
|
8780
|
+
}
|
|
8781
|
+
</mat-card>
|
|
8782
|
+
</div>
|
|
8783
|
+
</mat-tab>
|
|
8784
|
+
|
|
8785
|
+
<mat-tab>
|
|
8786
|
+
<ng-template mat-tab-label>
|
|
8787
|
+
<mat-icon [praxisIcon]="'filter_alt'"></mat-icon>
|
|
8788
|
+
<span data-testid="crud-editor-tab-filters">{{ tx('crud.authoring.tab.filters', 'Filters') }}</span>
|
|
8789
|
+
<span
|
|
8790
|
+
class="tab-status-dot"
|
|
8791
|
+
[class.tab-status-dot--warn]="sectionStatus('filters') === 'pending'"
|
|
8792
|
+
[class.tab-status-dot--error]="sectionStatus('filters') === 'invalid'"
|
|
8793
|
+
[attr.title]="sectionStatusLabel('filters')"
|
|
8794
|
+
></span>
|
|
8795
|
+
</ng-template>
|
|
8796
|
+
<div class="tab-content">
|
|
8797
|
+
<mat-card class="editor-card" data-testid="crud-editor-filters-section">
|
|
8798
|
+
<div class="editor-section-header">
|
|
8799
|
+
<div>
|
|
8800
|
+
<h3>{{ tx('crud.authoring.section.filters', 'Filter Bar') }}</h3>
|
|
8801
|
+
<p class="editor-section-note">{{ sectionSummary('filters') }}</p>
|
|
8802
|
+
</div>
|
|
8803
|
+
<span
|
|
8804
|
+
class="action-summary-chip"
|
|
8805
|
+
[class.action-summary-chip--error]="sectionStatus('filters') === 'invalid'"
|
|
8806
|
+
[class.action-summary-chip--warn]="sectionStatus('filters') === 'pending'"
|
|
8807
|
+
>
|
|
8808
|
+
{{ sectionStatusLabel('filters') }}
|
|
8809
|
+
</span>
|
|
8810
|
+
</div>
|
|
8811
|
+
|
|
8812
|
+
<div class="editor-toggles">
|
|
8813
|
+
<mat-slide-toggle
|
|
8814
|
+
data-testid="crud-filterbar-show-advanced"
|
|
8815
|
+
[ngModel]="filterBarShowAdvanced()"
|
|
8816
|
+
(ngModelChange)="setFilterBarShowAdvanced($event)"
|
|
8817
|
+
[disabled]="isReadonly()"
|
|
8818
|
+
>
|
|
8819
|
+
{{ tx('crud.authoring.filters.showAdvanced', 'Allow opening advanced filters') }}
|
|
8820
|
+
</mat-slide-toggle>
|
|
8821
|
+
</div>
|
|
8822
|
+
|
|
8823
|
+
<section class="action-group" data-testid="crud-filterbar-inline-group">
|
|
8824
|
+
<div class="action-group__header">
|
|
8825
|
+
<strong>{{ tx('crud.authoring.filters.inlineGroup', 'Always-visible fields (Inline Fields)') }}</strong>
|
|
8826
|
+
<p class="editor-section-note">{{ tx('crud.authoring.filters.inlineNote', 'Select resource fields or add field identifiers to keep pinned in the filter bar.') }}</p>
|
|
8827
|
+
</div>
|
|
8828
|
+
|
|
8829
|
+
<div class="editor-toolbar-row">
|
|
8830
|
+
<mat-form-field appearance="outline" class="editor-inline-grow">
|
|
8831
|
+
<mat-label>{{ tx('crud.authoring.filters.inlineFieldInput', 'Add inline field') }}</mat-label>
|
|
8832
|
+
<input
|
|
8833
|
+
matInput
|
|
8834
|
+
data-testid="crud-filterbar-inline-field-input"
|
|
8835
|
+
[ngModel]="inlineFieldDraft()"
|
|
8836
|
+
(ngModelChange)="inlineFieldDraft.set($event)"
|
|
8837
|
+
(keydown.enter)="$event.preventDefault(); addInlineFilterFieldFromDraft()"
|
|
8838
|
+
[placeholder]="tx('crud.authoring.filters.inlineFieldPlaceholder', 'e.g. status, priority')"
|
|
8839
|
+
[disabled]="isReadonly()"
|
|
8840
|
+
/>
|
|
8841
|
+
</mat-form-field>
|
|
8842
|
+
<button
|
|
8843
|
+
mat-stroked-button
|
|
8844
|
+
type="button"
|
|
8845
|
+
data-testid="crud-filterbar-inline-field-add-btn"
|
|
8846
|
+
(click)="addInlineFilterFieldFromDraft()"
|
|
8847
|
+
[disabled]="isReadonly()"
|
|
8848
|
+
>
|
|
8849
|
+
{{ tx('crud.authoring.filters.inlineAdd', 'Add field') }}
|
|
8850
|
+
</button>
|
|
8851
|
+
</div>
|
|
8852
|
+
|
|
8853
|
+
@if (filterBarInlineFields().length === 0) {
|
|
8854
|
+
<p class="editor-section-note" data-testid="crud-filterbar-inline-empty">
|
|
8855
|
+
{{ tx('crud.authoring.filters.inlineEmpty', 'No inline fields pinned yet. Runtime will use the default schema configuration.') }}
|
|
8856
|
+
</p>
|
|
8857
|
+
} @else {
|
|
8858
|
+
<div class="action-summary-strip" data-testid="crud-filterbar-inline-chips">
|
|
8859
|
+
@for (field of filterBarInlineFields(); track field) {
|
|
8860
|
+
<span class="action-summary-chip inline-chip" [attr.data-testid]="'crud-filterbar-inline-chip-' + field">
|
|
8861
|
+
<span>{{ field }}</span>
|
|
8862
|
+
<button
|
|
8863
|
+
type="button"
|
|
8864
|
+
class="chip-remove-btn"
|
|
8865
|
+
[attr.data-testid]="'crud-filterbar-inline-remove-' + field"
|
|
8866
|
+
(click)="removeInlineFilterField(field)"
|
|
8867
|
+
[disabled]="isReadonly()"
|
|
8868
|
+
aria-label="Remove"
|
|
8869
|
+
>
|
|
8870
|
+
×
|
|
8871
|
+
</button>
|
|
8872
|
+
</span>
|
|
8873
|
+
}
|
|
8874
|
+
</div>
|
|
8875
|
+
}
|
|
8876
|
+
|
|
8877
|
+
@if (availableResourceFieldOptions().length > 0) {
|
|
8878
|
+
<div class="action-subgroup" data-testid="crud-filterbar-available-fields">
|
|
8879
|
+
<span class="editor-overview-label">{{ tx('crud.authoring.filters.availableFields', 'Available field suggestions') }}</span>
|
|
8880
|
+
<div class="action-summary-strip">
|
|
8881
|
+
@for (opt of availableResourceFieldOptions(); track opt.field) {
|
|
8882
|
+
<button
|
|
8883
|
+
mat-stroked-button
|
|
8884
|
+
type="button"
|
|
8885
|
+
[attr.data-testid]="'crud-filterbar-available-field-' + opt.field"
|
|
8886
|
+
(click)="toggleInlineFilterField(opt.field)"
|
|
8887
|
+
[disabled]="isReadonly()"
|
|
8888
|
+
>
|
|
8889
|
+
{{ opt.header }} ({{ opt.field }})
|
|
8890
|
+
</button>
|
|
8891
|
+
}
|
|
8892
|
+
</div>
|
|
8893
|
+
</div>
|
|
8894
|
+
}
|
|
8895
|
+
</section>
|
|
8896
|
+
|
|
8897
|
+
<section class="action-group" data-testid="crud-filterbar-quick-group">
|
|
8898
|
+
<div class="action-group__header">
|
|
8899
|
+
<strong>{{ tx('crud.authoring.filters.quickGroup', 'Quick filter shortcuts (Quick Filters)') }}</strong>
|
|
8900
|
+
</div>
|
|
8901
|
+
|
|
8902
|
+
<div class="editor-toolbar-row">
|
|
8903
|
+
<mat-form-field appearance="outline" class="editor-inline-grow">
|
|
8904
|
+
<mat-label>{{ tx('crud.authoring.filters.quickMode', 'Quick filters mode') }}</mat-label>
|
|
8905
|
+
<mat-select
|
|
8906
|
+
data-testid="crud-filterbar-quick-mode"
|
|
8907
|
+
[ngModel]="filterBarQuickMode()"
|
|
8908
|
+
(ngModelChange)="setFilterBarQuickMode($event)"
|
|
8909
|
+
[disabled]="isReadonly()"
|
|
8910
|
+
>
|
|
8911
|
+
@for (mode of quickFilterModes; track mode) {
|
|
8912
|
+
<mat-option [value]="mode">{{ quickFilterModeLabel(mode) }}</mat-option>
|
|
8913
|
+
}
|
|
8914
|
+
</mat-select>
|
|
8915
|
+
</mat-form-field>
|
|
8916
|
+
|
|
8917
|
+
@if (filterBarQuickMode() === 'custom') {
|
|
8918
|
+
<button
|
|
8919
|
+
mat-stroked-button
|
|
8920
|
+
type="button"
|
|
8921
|
+
data-testid="crud-filterbar-add-quick-btn"
|
|
8922
|
+
(click)="addQuickFilter()"
|
|
8923
|
+
[disabled]="isReadonly()"
|
|
8924
|
+
>
|
|
8925
|
+
<mat-icon [praxisIcon]="'add'"></mat-icon>
|
|
8926
|
+
{{ tx('crud.authoring.filters.addQuickFilter', 'Add quick filter') }}
|
|
8927
|
+
</button>
|
|
8928
|
+
}
|
|
8929
|
+
</div>
|
|
8930
|
+
|
|
8931
|
+
@if (filterBarQuickMode() === 'custom') {
|
|
8932
|
+
@if (filterBarQuickFilters().length === 0) {
|
|
8933
|
+
<p class="editor-section-note" data-testid="crud-filterbar-quick-empty">
|
|
8934
|
+
{{ tx('crud.authoring.filters.quickEmpty', 'No quick filters configured.') }}
|
|
8935
|
+
</p>
|
|
8936
|
+
} @else {
|
|
8937
|
+
<div class="kpi-cards-list">
|
|
8938
|
+
@for (qf of filterBarQuickFilters(); track $index) {
|
|
8939
|
+
<div class="action-subgroup" [attr.data-testid]="'crud-filterbar-quick-item-' + $index">
|
|
8940
|
+
<div class="editor-section-header">
|
|
8941
|
+
<strong>#{{ $index + 1 }} · {{ qf.label || qf.id || 'Chip' }}</strong>
|
|
8942
|
+
<div class="inline-actions-row">
|
|
8943
|
+
<button
|
|
8944
|
+
mat-stroked-button
|
|
8945
|
+
type="button"
|
|
8946
|
+
[attr.data-testid]="'crud-filterbar-quick-up-' + $index"
|
|
8947
|
+
(click)="moveQuickFilter($index, -1)"
|
|
8948
|
+
[disabled]="isReadonly() || $index === 0"
|
|
8949
|
+
>
|
|
8950
|
+
{{ tx('crud.authoring.kpis.moveUp', 'Move up') }}
|
|
8951
|
+
</button>
|
|
8952
|
+
<button
|
|
8953
|
+
mat-stroked-button
|
|
8954
|
+
type="button"
|
|
8955
|
+
[attr.data-testid]="'crud-filterbar-quick-down-' + $index"
|
|
8956
|
+
(click)="moveQuickFilter($index, 1)"
|
|
8957
|
+
[disabled]="isReadonly() || $index === filterBarQuickFilters().length - 1"
|
|
8958
|
+
>
|
|
8959
|
+
{{ tx('crud.authoring.kpis.moveDown', 'Move down') }}
|
|
8960
|
+
</button>
|
|
8961
|
+
<button
|
|
8962
|
+
mat-stroked-button
|
|
8963
|
+
type="button"
|
|
8964
|
+
[attr.data-testid]="'crud-filterbar-quick-remove-' + $index"
|
|
8965
|
+
(click)="removeQuickFilter($index)"
|
|
8966
|
+
[disabled]="isReadonly()"
|
|
8967
|
+
>
|
|
8968
|
+
{{ tx('crud.authoring.filters.removeQuickFilter', 'Remove quick filter') }}
|
|
8969
|
+
</button>
|
|
8970
|
+
</div>
|
|
8971
|
+
</div>
|
|
8972
|
+
|
|
8973
|
+
<div class="editor-grid">
|
|
8974
|
+
<mat-form-field appearance="outline">
|
|
8975
|
+
<mat-label>{{ tx('crud.authoring.filters.quickId', 'Identifier') }}</mat-label>
|
|
8976
|
+
<input
|
|
8977
|
+
matInput
|
|
8978
|
+
[attr.data-testid]="'crud-filterbar-quick-id-' + $index"
|
|
8979
|
+
[ngModel]="qf.id || ''"
|
|
8980
|
+
(ngModelChange)="setQuickFilterField($index, 'id', $event)"
|
|
8981
|
+
[disabled]="isReadonly()"
|
|
8982
|
+
/>
|
|
8983
|
+
</mat-form-field>
|
|
8984
|
+
|
|
8985
|
+
<mat-form-field appearance="outline">
|
|
8986
|
+
<mat-label>{{ tx('crud.authoring.filters.quickLabel', 'Chip label') }}</mat-label>
|
|
8987
|
+
<input
|
|
8988
|
+
matInput
|
|
8989
|
+
[attr.data-testid]="'crud-filterbar-quick-label-' + $index"
|
|
8990
|
+
[ngModel]="qf.label || ''"
|
|
8991
|
+
(ngModelChange)="setQuickFilterField($index, 'label', $event)"
|
|
8992
|
+
[disabled]="isReadonly()"
|
|
8993
|
+
/>
|
|
8994
|
+
</mat-form-field>
|
|
8995
|
+
|
|
8996
|
+
<mat-form-field appearance="outline">
|
|
8997
|
+
<mat-label>{{ tx('crud.authoring.filters.quickIcon', 'Icon (optional)') }}</mat-label>
|
|
8998
|
+
<input
|
|
8999
|
+
matInput
|
|
9000
|
+
[attr.data-testid]="'crud-filterbar-quick-icon-' + $index"
|
|
9001
|
+
[ngModel]="qf.icon || ''"
|
|
9002
|
+
(ngModelChange)="setQuickFilterField($index, 'icon', $event)"
|
|
9003
|
+
[disabled]="isReadonly()"
|
|
9004
|
+
/>
|
|
9005
|
+
</mat-form-field>
|
|
9006
|
+
|
|
9007
|
+
<mat-form-field appearance="outline">
|
|
9008
|
+
<mat-label>{{ tx('crud.authoring.filters.quickConditionMode', 'Condition type') }}</mat-label>
|
|
9009
|
+
<mat-select
|
|
9010
|
+
[attr.data-testid]="'crud-filterbar-quick-condition-mode-' + $index"
|
|
9011
|
+
[ngModel]="quickFilterConditionMode($index)"
|
|
9012
|
+
(ngModelChange)="setQuickFilterConditionMode($index, $event)"
|
|
9013
|
+
[disabled]="isReadonly()"
|
|
9014
|
+
>
|
|
9015
|
+
@for (cMode of quickConditionModes; track cMode) {
|
|
9016
|
+
<mat-option [value]="cMode">{{ quickConditionModeLabel(cMode) }}</mat-option>
|
|
9017
|
+
}
|
|
9018
|
+
</mat-select>
|
|
9019
|
+
</mat-form-field>
|
|
9020
|
+
</div>
|
|
9021
|
+
|
|
9022
|
+
@if (quickFilterConditionMode($index) === 'fieldEquals') {
|
|
9023
|
+
<div class="editor-grid">
|
|
9024
|
+
<mat-form-field appearance="outline">
|
|
9025
|
+
<mat-label>{{ tx('crud.authoring.filters.quickField', 'Field') }}</mat-label>
|
|
9026
|
+
<input
|
|
9027
|
+
matInput
|
|
9028
|
+
[attr.data-testid]="'crud-filterbar-quick-field-' + $index"
|
|
9029
|
+
[ngModel]="quickFilterConditionField($index)"
|
|
9030
|
+
(ngModelChange)="setQuickFilterFieldEquals($index, $event, quickFilterConditionValue($index))"
|
|
9031
|
+
[disabled]="isReadonly()"
|
|
9032
|
+
/>
|
|
9033
|
+
</mat-form-field>
|
|
9034
|
+
<mat-form-field appearance="outline">
|
|
9035
|
+
<mat-label>{{ tx('crud.authoring.filters.quickValue', 'Value') }}</mat-label>
|
|
9036
|
+
<input
|
|
9037
|
+
matInput
|
|
9038
|
+
[attr.data-testid]="'crud-filterbar-quick-value-' + $index"
|
|
9039
|
+
[ngModel]="quickFilterConditionValue($index)"
|
|
9040
|
+
(ngModelChange)="setQuickFilterFieldEquals($index, quickFilterConditionField($index), $event)"
|
|
9041
|
+
[disabled]="isReadonly()"
|
|
9042
|
+
/>
|
|
9043
|
+
</mat-form-field>
|
|
9044
|
+
</div>
|
|
9045
|
+
}
|
|
9046
|
+
|
|
9047
|
+
@if (quickFilterConditionMode($index) === 'expression') {
|
|
9048
|
+
<mat-form-field appearance="outline">
|
|
9049
|
+
<mat-label>{{ tx('crud.authoring.filters.quickExpression', 'Filter expression or JSON') }}</mat-label>
|
|
9050
|
+
<input
|
|
9051
|
+
matInput
|
|
9052
|
+
[attr.data-testid]="'crud-filterbar-quick-expression-' + $index"
|
|
9053
|
+
[ngModel]="quickFilterExpressionDraft($index)"
|
|
9054
|
+
(ngModelChange)="setQuickFilterExpression($index, $event)"
|
|
9055
|
+
[disabled]="isReadonly()"
|
|
9056
|
+
/>
|
|
9057
|
+
</mat-form-field>
|
|
9058
|
+
}
|
|
9059
|
+
</div>
|
|
9060
|
+
}
|
|
9061
|
+
</div>
|
|
9062
|
+
}
|
|
9063
|
+
}
|
|
9064
|
+
</section>
|
|
9065
|
+
|
|
9066
|
+
<section class="action-summary-strip" data-testid="crud-filterbar-preview">
|
|
9067
|
+
<span class="action-summary-chip">{{ filtersOverview() }}</span>
|
|
9068
|
+
<span class="action-summary-chip">{{ filtersOverviewNote() }}</span>
|
|
9069
|
+
</section>
|
|
9070
|
+
</mat-card>
|
|
9071
|
+
</div>
|
|
9072
|
+
</mat-tab>
|
|
9073
|
+
|
|
6763
9074
|
<mat-tab>
|
|
6764
9075
|
<ng-template mat-tab-label>
|
|
6765
9076
|
<mat-icon [praxisIcon]="'table_view'"></mat-icon>
|
|
@@ -6787,10 +9098,68 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
6787
9098
|
<span class="action-summary-chip" data-testid="crud-editor-table-summary-behavior">{{ tableBehaviorSummary() }}</span>
|
|
6788
9099
|
<span class="action-summary-chip" data-testid="crud-editor-table-summary-states">{{ tableStatesSummary() }}</span>
|
|
6789
9100
|
</section>
|
|
9101
|
+
|
|
9102
|
+
@if (isUsingInheritedResourceColumns()) {
|
|
9103
|
+
<section class="inherited-columns-banner" data-testid="crud-table-column-inheritance-banner">
|
|
9104
|
+
<div class="inherited-columns-banner__copy">
|
|
9105
|
+
<div class="inherited-columns-banner__title-row">
|
|
9106
|
+
<strong>{{ tx('crud.authoring.table.inheritedColumnsPending', 'Columns inherited from resource') }}</strong>
|
|
9107
|
+
@if (resolvedInheritedColumns().length > 0) {
|
|
9108
|
+
<span class="action-summary-chip" data-testid="crud-table-inherited-columns-badge">
|
|
9109
|
+
{{ inheritedColumnsCountLabel() }}
|
|
9110
|
+
</span>
|
|
9111
|
+
}
|
|
9112
|
+
</div>
|
|
9113
|
+
<p class="editor-section-note" data-testid="crud-table-inherited-columns-note">
|
|
9114
|
+
{{ inheritedColumnsNote() }}
|
|
9115
|
+
</p>
|
|
9116
|
+
</div>
|
|
9117
|
+
<button
|
|
9118
|
+
mat-stroked-button
|
|
9119
|
+
type="button"
|
|
9120
|
+
data-testid="crud-table-focus-columns-btn"
|
|
9121
|
+
(click)="focusTableColumnsAuthoring()"
|
|
9122
|
+
[disabled]="isReadonly()"
|
|
9123
|
+
>
|
|
9124
|
+
<mat-icon [praxisIcon]="'view_column'"></mat-icon>
|
|
9125
|
+
{{ tx('crud.authoring.table.focusColumnsButton', 'Edit resource columns') }}
|
|
9126
|
+
</button>
|
|
9127
|
+
</section>
|
|
9128
|
+
} @else {
|
|
9129
|
+
<section class="inherited-columns-banner" data-testid="crud-table-column-custom-banner">
|
|
9130
|
+
<p class="editor-section-note">
|
|
9131
|
+
{{ tx('crud.authoring.table.customColumnsNote', 'The columns in this table have explicit customizations saved in the CRUD metadata.') }}
|
|
9132
|
+
</p>
|
|
9133
|
+
<div class="inline-actions-row">
|
|
9134
|
+
@if (resolvedInheritedColumns().length > 0) {
|
|
9135
|
+
<button
|
|
9136
|
+
mat-stroked-button
|
|
9137
|
+
type="button"
|
|
9138
|
+
data-testid="crud-table-reset-inherited-columns-btn"
|
|
9139
|
+
(click)="resetToInheritedColumns()"
|
|
9140
|
+
[disabled]="isReadonly()"
|
|
9141
|
+
>
|
|
9142
|
+
{{ tx('crud.authoring.table.inheritedColumnsPending', 'Columns inherited from resource') }}
|
|
9143
|
+
</button>
|
|
9144
|
+
}
|
|
9145
|
+
<button
|
|
9146
|
+
mat-stroked-button
|
|
9147
|
+
type="button"
|
|
9148
|
+
data-testid="crud-table-focus-columns-btn"
|
|
9149
|
+
(click)="focusTableColumnsAuthoring()"
|
|
9150
|
+
[disabled]="isReadonly()"
|
|
9151
|
+
>
|
|
9152
|
+
<mat-icon [praxisIcon]="'view_column'"></mat-icon>
|
|
9153
|
+
{{ tx('crud.authoring.table.focusColumnsButton', 'Edit resource columns') }}
|
|
9154
|
+
</button>
|
|
9155
|
+
</div>
|
|
9156
|
+
</section>
|
|
9157
|
+
}
|
|
9158
|
+
|
|
6790
9159
|
<p class="editor-section-note" data-testid="crud-editor-table-flow-note">{{ tableEditingFlowSummary() }}</p>
|
|
6791
9160
|
<praxis-table-inline-authoring-editor
|
|
6792
9161
|
data-testid="crud-editor-table-inline"
|
|
6793
|
-
[config]="
|
|
9162
|
+
[config]="effectiveTableConfigForEditor()"
|
|
6794
9163
|
[readonly]="isReadonly()"
|
|
6795
9164
|
(configChange)="setTableConfig($event)"
|
|
6796
9165
|
/>
|
|
@@ -6841,11 +9210,73 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
6841
9210
|
</mat-tab>
|
|
6842
9211
|
</mat-tab-group>
|
|
6843
9212
|
</section>
|
|
6844
|
-
`, styles: [":host{display:block;min-width:0;color:var(--md-sys-color-on-surface,#1a1b20)}.editor-shell{display:grid;gap:16px}.editor-header{display:flex;justify-content:space-between;gap:16px;align-items:start}.editor-overview,.editor-health-map{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(180px,1fr))}.editor-health-card{display:grid;gap:4px;padding:14px;border-radius:18px;background:color-mix(in srgb,var(--md-sys-color-surface-container-high,#eef1f6) 92%,transparent)}.editor-health-card--warn{background:color-mix(in srgb,var(--md-sys-color-tertiary-container,#f8e08e) 78%,transparent)}.editor-health-card--error{background:color-mix(in srgb,var(--md-sys-color-error-container,#f9dedc) 82%,transparent);color:var(--md-sys-color-on-error-container,#410e0b)}.editor-focus{display:flex;gap:12px;align-items:start;padding:14px 16px;border-radius:18px;background:color-mix(in srgb,var(--md-sys-color-surface-container-high,#eef1f6) 92%,transparent)}.editor-focus--success{background:color-mix(in srgb,var(--md-sys-color-secondary-container,#d7e3ff) 70%,transparent)}.editor-focus-chip{padding:6px 10px;border-radius:999px;font-size:12px;background:color-mix(in srgb,var(--md-sys-color-secondary-container,#d7e3ff) 70%,transparent);white-space:nowrap}.editor-focus-chip--warn{background:color-mix(in srgb,var(--md-sys-color-tertiary-container,#f8e08e) 78%,transparent)}.editor-focus-chip--error{background:color-mix(in srgb,var(--md-sys-color-error-container,#f9dedc) 82%,transparent);color:var(--md-sys-color-on-error-container,#410e0b)}.editor-focus-copy{display:grid;gap:4px}.editor-focus-copy p{margin:0;color:var(--md-sys-color-on-surface-variant,#5a5d67)}.editor-tabs{display:flex;flex-direction:column;min-width:0}.editor-tabs .mat-mdc-tab{min-width:118px}.editor-tabs ::ng-deep .mdc-tab__content{gap:8px}.editor-tabs ::ng-deep .mat-mdc-tab-body-wrapper,.editor-tabs ::ng-deep .mat-mdc-tab-group-container{flex:1 1 auto;min-height:0}.editor-tabs ::ng-deep .mat-mdc-tab-body-content{overflow:hidden}.tab-status-dot{width:8px;height:8px;border-radius:999px;background:var(--md-sys-color-primary,#1263b4);flex:0 0 auto}.tab-status-dot--warn{background:var(--md-sys-color-tertiary,#7d5700)}.tab-status-dot--error{background:var(--md-sys-color-error,#b3261e)}.tab-content{display:grid;gap:14px;min-width:0;padding:12px 2px 4px;box-sizing:border-box}.editor-overview-card{display:grid;gap:4px;padding:14px;border-radius:18px;background:color-mix(in srgb,var(--md-sys-color-surface-container-high,#eef1f6) 92%,transparent)}.editor-overview-card--warn{background:color-mix(in srgb,var(--md-sys-color-tertiary-container,#f8e08e) 78%,transparent)}.editor-overview-card--error{background:color-mix(in srgb,var(--md-sys-color-error-container,#f9dedc) 82%,transparent);color:var(--md-sys-color-on-error-container,#410e0b)}.editor-overview-label{font-size:12px;color:var(--md-sys-color-on-surface-variant,#5a5d67)}.editor-overview-value{line-height:1.3;overflow-wrap:anywhere}.editor-overview-note{margin:0;font-size:12px;color:var(--md-sys-color-on-surface-variant,#5a5d67)}.editor-header h2,.editor-card h3{margin:0}.editor-header p{margin:6px 0 0;color:var(--md-sys-color-on-surface-variant,#5a5d67)}.editor-chip{padding:6px 10px;border-radius:999px;background:color-mix(in srgb,var(--md-sys-color-primary,#1263b4) 12%,transparent);font-size:12px}.editor-card{display:grid;gap:14px;padding:16px;border-radius:20px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline,#c5c7ce) 70%,transparent)}.editor-section-header{display:flex;justify-content:space-between;gap:12px;align-items:start}.editor-grid{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}.editor-grid--compact{grid-template-columns:repeat(auto-fit,minmax(160px,1fr))}.editor-toggles{display:grid;gap:10px;grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}.editor-actions,.action-grid{display:grid;gap:14px}.action-summary-strip{display:flex;flex-wrap:wrap;gap:8px}.action-summary-chip{padding:6px 10px;border-radius:999px;font-size:12px;background:color-mix(in srgb,var(--md-sys-color-secondary-container,#d7e3ff) 70%,transparent)}.action-summary-chip--warn{background:color-mix(in srgb,var(--md-sys-color-tertiary-container,#f8e08e) 78%,transparent)}.action-summary-chip--error{background:color-mix(in srgb,var(--md-sys-color-error-container,#f9dedc) 82%,transparent);color:var(--md-sys-color-on-error-container,#410e0b)}.action-group{display:grid;gap:10px;padding:12px;border-radius:16px;background:color-mix(in srgb,var(--md-sys-color-surface-container-high,#eef1f6) 88%,transparent)}.action-group--advanced{background:color-mix(in srgb,var(--md-sys-color-surface-container,#f5f7fb) 90%,transparent)}.action-group__header{display:grid;gap:4px}.action-subgroup{display:grid;gap:10px;padding:10px;border-radius:14px;background:color-mix(in srgb,var(--md-sys-color-surface-container-low,#fafbfd) 92%,transparent)}.action-subgroup__header{display:grid;gap:4px}.action-param-list{display:grid;gap:12px}.action-param-row{display:grid;gap:8px}.action-advanced-panel{border:1px solid color-mix(in srgb,var(--md-sys-color-outline,#c5c7ce) 65%,transparent);border-radius:16px}.editor-section-note{margin:0;color:var(--md-sys-color-on-surface-variant,#5a5d67)}.diagnostics-groups{display:grid;gap:12px}.diagnostics-group{display:grid;gap:10px;padding:12px;border-radius:16px;background:color-mix(in srgb,var(--md-sys-color-surface-container-high,#eef1f6) 92%,transparent)}.diagnostics{display:grid;gap:8px;margin:0;padding-left:18px}.diagnostics .error{color:var(--md-sys-color-error,#b3261e)}pre{margin:0;overflow:auto;max-height:320px;padding:12px;border-radius:14px;background:color-mix(in srgb,var(--md-sys-color-surface-container-high,#eef1f6) 92%,transparent)}\n"] }]
|
|
6845
|
-
}], ctorParameters: () => [], propDecorators: { documentInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "document", required: false }] }], metadataInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "metadata", required: false }] }], crudIdInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "crudId", required: false }] }], readonlyInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }] } });
|
|
9213
|
+
`, styles: [":host{display:block;min-width:0;color:var(--md-sys-color-on-surface,#1a1b20)}.editor-shell{display:grid;gap:16px}.editor-header{display:flex;justify-content:space-between;gap:16px;align-items:start}.editor-overview,.editor-health-map{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(180px,1fr))}.editor-health-card{display:grid;gap:4px;padding:14px;border-radius:18px;background:color-mix(in srgb,var(--md-sys-color-surface-container-high,#eef1f6) 92%,transparent)}.editor-health-card--warn{background:color-mix(in srgb,var(--md-sys-color-tertiary-container,#f8e08e) 78%,transparent)}.editor-health-card--error{background:color-mix(in srgb,var(--md-sys-color-error-container,#f9dedc) 82%,transparent);color:var(--md-sys-color-on-error-container,#410e0b)}.editor-focus{display:flex;gap:12px;align-items:start;padding:14px 16px;border-radius:18px;background:color-mix(in srgb,var(--md-sys-color-surface-container-high,#eef1f6) 92%,transparent)}.editor-focus--success{background:color-mix(in srgb,var(--md-sys-color-secondary-container,#d7e3ff) 70%,transparent)}.editor-focus-chip{padding:6px 10px;border-radius:999px;font-size:12px;background:color-mix(in srgb,var(--md-sys-color-secondary-container,#d7e3ff) 70%,transparent);white-space:nowrap}.editor-focus-chip--warn{background:color-mix(in srgb,var(--md-sys-color-tertiary-container,#f8e08e) 78%,transparent)}.editor-focus-chip--error{background:color-mix(in srgb,var(--md-sys-color-error-container,#f9dedc) 82%,transparent);color:var(--md-sys-color-on-error-container,#410e0b)}.editor-focus-copy{display:grid;gap:4px}.editor-focus-copy p{margin:0;color:var(--md-sys-color-on-surface-variant,#5a5d67)}.editor-tabs{display:flex;flex-direction:column;min-width:0}.editor-tabs .mat-mdc-tab{min-width:118px}.editor-tabs ::ng-deep .mdc-tab__content{gap:8px}.editor-tabs ::ng-deep .mat-mdc-tab-body-wrapper,.editor-tabs ::ng-deep .mat-mdc-tab-group-container{flex:1 1 auto;min-height:0}.editor-tabs ::ng-deep .mat-mdc-tab-body-content{overflow:hidden}.tab-status-dot{width:8px;height:8px;border-radius:999px;background:var(--md-sys-color-primary,#1263b4);flex:0 0 auto}.tab-status-dot--warn{background:var(--md-sys-color-tertiary,#7d5700)}.tab-status-dot--error{background:var(--md-sys-color-error,#b3261e)}.tab-content{display:grid;gap:14px;min-width:0;padding:12px 2px 4px;box-sizing:border-box}.editor-overview-card{display:grid;gap:4px;padding:14px;border-radius:18px;background:color-mix(in srgb,var(--md-sys-color-surface-container-high,#eef1f6) 92%,transparent)}.editor-overview-card--warn{background:color-mix(in srgb,var(--md-sys-color-tertiary-container,#f8e08e) 78%,transparent)}.editor-overview-card--error{background:color-mix(in srgb,var(--md-sys-color-error-container,#f9dedc) 82%,transparent);color:var(--md-sys-color-on-error-container,#410e0b)}.editor-overview-label{font-size:12px;color:var(--md-sys-color-on-surface-variant,#5a5d67)}.editor-overview-value{line-height:1.3;overflow-wrap:anywhere}.editor-overview-note{margin:0;font-size:12px;color:var(--md-sys-color-on-surface-variant,#5a5d67)}.editor-header h2,.editor-card h3{margin:0}.editor-header p{margin:6px 0 0;color:var(--md-sys-color-on-surface-variant,#5a5d67)}.editor-chip{padding:6px 10px;border-radius:999px;background:color-mix(in srgb,var(--md-sys-color-primary,#1263b4) 12%,transparent);font-size:12px}.editor-card{display:grid;gap:14px;padding:16px;border-radius:20px;border:1px solid color-mix(in srgb,var(--md-sys-color-outline,#c5c7ce) 70%,transparent)}.editor-section-header{display:flex;justify-content:space-between;gap:12px;align-items:start;flex-wrap:wrap}.editor-toolbar-row{display:flex;justify-content:space-between;gap:12px;align-items:center;flex-wrap:wrap}.editor-inline-grow{flex:1 1 240px}.inline-actions-row{display:flex;gap:8px;flex-wrap:wrap;align-items:center}.inline-chip{display:inline-flex;align-items:center;gap:6px}.chip-remove-btn{border:0;background:transparent;cursor:pointer;font-size:14px;line-height:1;padding:0 2px;color:inherit}.kpi-cards-list{display:grid;gap:12px}.inherited-columns-banner{display:flex;justify-content:space-between;gap:12px;align-items:center;flex-wrap:wrap;padding:12px 14px;border-radius:16px;background:color-mix(in srgb,var(--md-sys-color-secondary-container,#d7e3ff) 55%,transparent)}.inherited-columns-banner__copy{display:grid;gap:4px}.inherited-columns-banner__title-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.editor-grid{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}.editor-grid--compact{grid-template-columns:repeat(auto-fit,minmax(160px,1fr))}.editor-toggles{display:grid;gap:10px;grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}.editor-actions,.action-grid{display:grid;gap:14px}.action-summary-strip{display:flex;flex-wrap:wrap;gap:8px}.action-summary-chip{padding:6px 10px;border-radius:999px;font-size:12px;background:color-mix(in srgb,var(--md-sys-color-secondary-container,#d7e3ff) 70%,transparent)}.action-summary-chip--warn{background:color-mix(in srgb,var(--md-sys-color-tertiary-container,#f8e08e) 78%,transparent)}.action-summary-chip--error{background:color-mix(in srgb,var(--md-sys-color-error-container,#f9dedc) 82%,transparent);color:var(--md-sys-color-on-error-container,#410e0b)}.action-group{display:grid;gap:10px;padding:12px;border-radius:16px;background:color-mix(in srgb,var(--md-sys-color-surface-container-high,#eef1f6) 88%,transparent)}.action-group--advanced{background:color-mix(in srgb,var(--md-sys-color-surface-container,#f5f7fb) 90%,transparent)}.action-group__header{display:grid;gap:4px}.action-subgroup{display:grid;gap:10px;padding:10px;border-radius:14px;background:color-mix(in srgb,var(--md-sys-color-surface-container-low,#fafbfd) 92%,transparent)}.action-subgroup__header{display:grid;gap:4px}.action-param-list{display:grid;gap:12px}.action-param-row{display:grid;gap:8px}.action-advanced-panel{border:1px solid color-mix(in srgb,var(--md-sys-color-outline,#c5c7ce) 65%,transparent);border-radius:16px}.editor-section-note{margin:0;color:var(--md-sys-color-on-surface-variant,#5a5d67)}.diagnostics-groups{display:grid;gap:12px}.diagnostics-group{display:grid;gap:10px;padding:12px;border-radius:16px;background:color-mix(in srgb,var(--md-sys-color-surface-container-high,#eef1f6) 92%,transparent)}.diagnostics{display:grid;gap:8px;margin:0;padding-left:18px}.diagnostics .error{color:var(--md-sys-color-error,#b3261e)}pre{margin:0;overflow:auto;max-height:320px;padding:12px;border-radius:14px;background:color-mix(in srgb,var(--md-sys-color-surface-container-high,#eef1f6) 92%,transparent)}\n"] }]
|
|
9214
|
+
}], ctorParameters: () => [], propDecorators: { documentInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "document", required: false }] }], metadataInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "metadata", required: false }] }], crudIdInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "crudId", required: false }] }], readonlyInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], effectiveTableConfigInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "effectiveTableConfig", required: false }] }] } });
|
|
6846
9215
|
function cloneTableConfig(config) {
|
|
6847
9216
|
return JSON.parse(JSON.stringify(config || { columns: [] }));
|
|
6848
9217
|
}
|
|
9218
|
+
function cloneColumns(columns) {
|
|
9219
|
+
return JSON.parse(JSON.stringify(Array.isArray(columns) ? columns : []));
|
|
9220
|
+
}
|
|
9221
|
+
function sanitizePersistedColumns(columns) {
|
|
9222
|
+
return cloneColumns(columns).map((col) => {
|
|
9223
|
+
const copy = { ...col };
|
|
9224
|
+
delete copy['_isApiField'];
|
|
9225
|
+
delete copy['_originalApiType'];
|
|
9226
|
+
return copy;
|
|
9227
|
+
});
|
|
9228
|
+
}
|
|
9229
|
+
function areColumnsEquivalentToInherited(candidateColumns, inheritedColumns) {
|
|
9230
|
+
const left = sanitizePersistedColumns(candidateColumns);
|
|
9231
|
+
const right = sanitizePersistedColumns(inheritedColumns);
|
|
9232
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
9233
|
+
}
|
|
9234
|
+
function coerceFilterScalarValue(rawValue) {
|
|
9235
|
+
const trimmed = String(rawValue ?? '').trim();
|
|
9236
|
+
if (trimmed === 'true') {
|
|
9237
|
+
return true;
|
|
9238
|
+
}
|
|
9239
|
+
if (trimmed === 'false') {
|
|
9240
|
+
return false;
|
|
9241
|
+
}
|
|
9242
|
+
if (trimmed !== '' && /^-?\d+(\.\d+)?$/.test(trimmed)) {
|
|
9243
|
+
const num = Number(trimmed);
|
|
9244
|
+
if (Number.isFinite(num)) {
|
|
9245
|
+
return num;
|
|
9246
|
+
}
|
|
9247
|
+
}
|
|
9248
|
+
return rawValue;
|
|
9249
|
+
}
|
|
9250
|
+
function reindexDraftMapAfterRemove(drafts, removedIndex) {
|
|
9251
|
+
const next = {};
|
|
9252
|
+
for (const [key, value] of Object.entries(drafts)) {
|
|
9253
|
+
const numericKey = Number(key);
|
|
9254
|
+
if (!Number.isFinite(numericKey) || numericKey === removedIndex) {
|
|
9255
|
+
continue;
|
|
9256
|
+
}
|
|
9257
|
+
next[numericKey > removedIndex ? numericKey - 1 : numericKey] = value;
|
|
9258
|
+
}
|
|
9259
|
+
return next;
|
|
9260
|
+
}
|
|
9261
|
+
function reindexDraftMapAfterSwap(drafts, fromIndex, toIndex) {
|
|
9262
|
+
const next = {};
|
|
9263
|
+
for (const [key, value] of Object.entries(drafts)) {
|
|
9264
|
+
const numericKey = Number(key);
|
|
9265
|
+
if (!Number.isFinite(numericKey)) {
|
|
9266
|
+
continue;
|
|
9267
|
+
}
|
|
9268
|
+
if (numericKey === fromIndex) {
|
|
9269
|
+
next[toIndex] = value;
|
|
9270
|
+
}
|
|
9271
|
+
else if (numericKey === toIndex) {
|
|
9272
|
+
next[fromIndex] = value;
|
|
9273
|
+
}
|
|
9274
|
+
else {
|
|
9275
|
+
next[numericKey] = value;
|
|
9276
|
+
}
|
|
9277
|
+
}
|
|
9278
|
+
return next;
|
|
9279
|
+
}
|
|
6849
9280
|
|
|
6850
9281
|
function openCrudMetadataEditor(settings, opts) {
|
|
6851
9282
|
return settings.open({
|
|
@@ -6857,6 +9288,7 @@ function openCrudMetadataEditor(settings, opts) {
|
|
|
6857
9288
|
inputs: {
|
|
6858
9289
|
document: opts?.document ?? null,
|
|
6859
9290
|
metadata: opts?.metadata ?? null,
|
|
9291
|
+
effectiveTableConfig: opts?.effectiveTableConfig ?? null,
|
|
6860
9292
|
crudId: opts?.crudId ?? null,
|
|
6861
9293
|
readonly: opts?.readonly ?? false,
|
|
6862
9294
|
},
|
|
@@ -7026,9 +9458,14 @@ class PraxisCrudComponent {
|
|
|
7026
9458
|
onKpiCardClick(event) {
|
|
7027
9459
|
this.kpiCardClick.emit(event);
|
|
7028
9460
|
if (!this.kpiCardClick.observed && event.card.filter && typeof event.card.filter === 'object') {
|
|
9461
|
+
const cardFilter = event.card.filter;
|
|
9462
|
+
if (Object.keys(cardFilter).length === 0) {
|
|
9463
|
+
this.clearFilterCriteria();
|
|
9464
|
+
return;
|
|
9465
|
+
}
|
|
7029
9466
|
this.tableFilterCriteria = {
|
|
7030
9467
|
...this.resolveFilterCriteria(this.resolvedMetadata),
|
|
7031
|
-
...
|
|
9468
|
+
...cardFilter,
|
|
7032
9469
|
};
|
|
7033
9470
|
if (this.table) {
|
|
7034
9471
|
this.table.applyAdvancedFilterCriteria(this.tableFilterCriteria);
|
|
@@ -7874,6 +10311,32 @@ class PraxisCrudComponent {
|
|
|
7874
10311
|
return current;
|
|
7875
10312
|
}
|
|
7876
10313
|
}
|
|
10314
|
+
getMaterializedInputSnapshot(_inputNames) {
|
|
10315
|
+
return {
|
|
10316
|
+
metadata: this.cloneResolvedMetadataSnapshot(),
|
|
10317
|
+
effectiveTableConfig: this.buildMaterializedEffectiveTableConfigSnapshot(),
|
|
10318
|
+
crudId: this.crudId,
|
|
10319
|
+
resourcePath: this.resolveResourcePath(this.resolvedMetadata),
|
|
10320
|
+
};
|
|
10321
|
+
}
|
|
10322
|
+
cloneResolvedMetadataSnapshot() {
|
|
10323
|
+
try {
|
|
10324
|
+
return JSON.parse(JSON.stringify(this.resolvedMetadata || { component: 'praxis-crud', table: { columns: [] } }));
|
|
10325
|
+
}
|
|
10326
|
+
catch {
|
|
10327
|
+
return {
|
|
10328
|
+
...(this.resolvedMetadata || { component: 'praxis-crud', table: { columns: [] } }),
|
|
10329
|
+
};
|
|
10330
|
+
}
|
|
10331
|
+
}
|
|
10332
|
+
buildMaterializedEffectiveTableConfigSnapshot() {
|
|
10333
|
+
const runtimeTable = this.lastTableRuntimeConfigSnapshot ||
|
|
10334
|
+
this.getCurrentTableConfigSnapshot() ||
|
|
10335
|
+
this.effectiveTableConfig ||
|
|
10336
|
+
this.resolvedMetadata?.table ||
|
|
10337
|
+
createDefaultTableConfig();
|
|
10338
|
+
return this.cloneTableConfigSnapshot(runtimeTable);
|
|
10339
|
+
}
|
|
7877
10340
|
onTableConfigChange(config) {
|
|
7878
10341
|
if (this.isInspectOnly()) {
|
|
7879
10342
|
return;
|
|
@@ -8853,6 +11316,9 @@ class PraxisCrudComponent {
|
|
|
8853
11316
|
else if (fb.quickFilters === false) {
|
|
8854
11317
|
toolbar.filters.quickFilters = [];
|
|
8855
11318
|
}
|
|
11319
|
+
else if (fb.quickFilters === undefined) {
|
|
11320
|
+
delete toolbar.filters.quickFilters;
|
|
11321
|
+
}
|
|
8856
11322
|
if (Array.isArray(fb.inlineFields) && fb.inlineFields.length > 0) {
|
|
8857
11323
|
const behavior = (cfg.behavior = cfg.behavior || {});
|
|
8858
11324
|
const filtering = (behavior.filtering = behavior.filtering || {
|
|
@@ -8869,6 +11335,16 @@ class PraxisCrudComponent {
|
|
|
8869
11335
|
settings.showAdvanced = fb.showAdvanced;
|
|
8870
11336
|
}
|
|
8871
11337
|
}
|
|
11338
|
+
else if (Array.isArray(fb.inlineFields) && fb.inlineFields.length === 0) {
|
|
11339
|
+
const settings = cfg.behavior?.filtering?.advancedFilters?.settings;
|
|
11340
|
+
if (settings && Array.isArray(settings.alwaysVisibleFields)) {
|
|
11341
|
+
settings.alwaysVisibleFields = [];
|
|
11342
|
+
}
|
|
11343
|
+
}
|
|
11344
|
+
if (fb.showAdvanced !== undefined &&
|
|
11345
|
+
cfg.behavior?.filtering?.advancedFilters?.settings) {
|
|
11346
|
+
cfg.behavior.filtering.advancedFilters.settings.showAdvanced = fb.showAdvanced;
|
|
11347
|
+
}
|
|
8872
11348
|
changed = true;
|
|
8873
11349
|
}
|
|
8874
11350
|
else if (this.supportsFilterCapability(capabilities) && !cfg.toolbar?.filters && !cfg.behavior?.filtering?.advancedFilters) {
|
|
@@ -9183,17 +11659,37 @@ class PraxisCrudComponent {
|
|
|
9183
11659
|
|| createCrudAuthoringDocument({ metadata: this.resolvedMetadata });
|
|
9184
11660
|
const liveTableConfig = this.lastTableRuntimeConfigSnapshot
|
|
9185
11661
|
|| this.getCurrentTableConfigSnapshot();
|
|
9186
|
-
const
|
|
11662
|
+
const effectiveTableSnapshot = liveTableConfig
|
|
11663
|
+
? this.cloneTableConfigSnapshot(liveTableConfig)
|
|
11664
|
+
: null;
|
|
11665
|
+
const authoredColumns = Array.isArray(canonicalSeed.metadata?.table?.columns)
|
|
11666
|
+
? canonicalSeed.metadata.table.columns
|
|
11667
|
+
: [];
|
|
11668
|
+
const isInheritingColumns = authoredColumns.length === 0;
|
|
11669
|
+
const seed = effectiveTableSnapshot
|
|
9187
11670
|
? createCrudAuthoringDocument({
|
|
9188
11671
|
metadata: {
|
|
9189
11672
|
...canonicalSeed.metadata,
|
|
9190
|
-
table:
|
|
11673
|
+
table: {
|
|
11674
|
+
...effectiveTableSnapshot,
|
|
11675
|
+
columns: isInheritingColumns
|
|
11676
|
+
? []
|
|
11677
|
+
: JSON.parse(JSON.stringify(Array.isArray(effectiveTableSnapshot.columns)
|
|
11678
|
+
? effectiveTableSnapshot.columns
|
|
11679
|
+
: authoredColumns)),
|
|
11680
|
+
...(canonicalSeed.metadata?.table?.columnProjection
|
|
11681
|
+
? {
|
|
11682
|
+
columnProjection: JSON.parse(JSON.stringify(canonicalSeed.metadata.table.columnProjection)),
|
|
11683
|
+
}
|
|
11684
|
+
: {}),
|
|
11685
|
+
},
|
|
9191
11686
|
},
|
|
9192
11687
|
})
|
|
9193
11688
|
: canonicalSeed;
|
|
9194
11689
|
const ref = openCrudMetadataEditor(this.settingsPanel, {
|
|
9195
11690
|
crudId: this.crudId,
|
|
9196
11691
|
document: seed,
|
|
11692
|
+
effectiveTableConfig: effectiveTableSnapshot,
|
|
9197
11693
|
title: this.tx('crud.authoring.title', 'Configurações do CRUD'),
|
|
9198
11694
|
titleIcon: 'table_chart',
|
|
9199
11695
|
});
|
|
@@ -10812,6 +13308,7 @@ var dynamicFormDialogHost_component = /*#__PURE__*/Object.freeze({
|
|
|
10812
13308
|
|
|
10813
13309
|
class PraxisCrudWidgetConfigEditor {
|
|
10814
13310
|
inputs = null;
|
|
13311
|
+
context = null;
|
|
10815
13312
|
widgetKey;
|
|
10816
13313
|
crudEditor;
|
|
10817
13314
|
isDirty$ = new BehaviorSubject(false);
|
|
@@ -10832,6 +13329,34 @@ class PraxisCrudWidgetConfigEditor {
|
|
|
10832
13329
|
get metadata() {
|
|
10833
13330
|
return this.createDocumentFromInputs().metadata;
|
|
10834
13331
|
}
|
|
13332
|
+
get effectiveTableConfig() {
|
|
13333
|
+
const rawContext = this.context ??
|
|
13334
|
+
this.inputs?.context ??
|
|
13335
|
+
null;
|
|
13336
|
+
if (!rawContext || typeof rawContext !== 'object') {
|
|
13337
|
+
return null;
|
|
13338
|
+
}
|
|
13339
|
+
const directTable = rawContext['effectiveTableConfig'];
|
|
13340
|
+
if (directTable &&
|
|
13341
|
+
typeof directTable === 'object' &&
|
|
13342
|
+
!Array.isArray(directTable)) {
|
|
13343
|
+
return directTable;
|
|
13344
|
+
}
|
|
13345
|
+
const effectiveMeta = rawContext['effectiveMetadata'];
|
|
13346
|
+
if (effectiveMeta) {
|
|
13347
|
+
try {
|
|
13348
|
+
const parsed = parseLegacyOrCrudDocument(effectiveMeta);
|
|
13349
|
+
if (parsed.metadata?.table &&
|
|
13350
|
+
typeof parsed.metadata.table === 'object') {
|
|
13351
|
+
return parsed.metadata.table;
|
|
13352
|
+
}
|
|
13353
|
+
}
|
|
13354
|
+
catch {
|
|
13355
|
+
// Ignore invalid effectiveMetadata
|
|
13356
|
+
}
|
|
13357
|
+
}
|
|
13358
|
+
return null;
|
|
13359
|
+
}
|
|
10835
13360
|
get effectiveCrudId() {
|
|
10836
13361
|
return this.inputs?.crudId ?? this.widgetKey ?? null;
|
|
10837
13362
|
}
|
|
@@ -10846,12 +13371,13 @@ class PraxisCrudWidgetConfigEditor {
|
|
|
10846
13371
|
}
|
|
10847
13372
|
buildValue(rawPayload) {
|
|
10848
13373
|
const document = this.extractDocument(rawPayload);
|
|
13374
|
+
const sanitizedContext = this.sanitizePersistedContext(this.inputs?.context);
|
|
10849
13375
|
const inputs = {
|
|
10850
13376
|
...(this.inputs ?? {}),
|
|
10851
13377
|
metadata: document.metadata,
|
|
10852
13378
|
crudId: this.inputs?.crudId ?? this.widgetKey,
|
|
10853
13379
|
componentInstanceId: this.inputs?.componentInstanceId ?? this.widgetKey,
|
|
10854
|
-
context:
|
|
13380
|
+
context: sanitizedContext,
|
|
10855
13381
|
enableCustomization: this.inputs?.enableCustomization ?? false,
|
|
10856
13382
|
authoringCapability: this.inputs?.authoringCapability ?? null,
|
|
10857
13383
|
};
|
|
@@ -10859,6 +13385,19 @@ class PraxisCrudWidgetConfigEditor {
|
|
|
10859
13385
|
inputs,
|
|
10860
13386
|
};
|
|
10861
13387
|
}
|
|
13388
|
+
sanitizePersistedContext(context) {
|
|
13389
|
+
if (!context || typeof context !== 'object') {
|
|
13390
|
+
return undefined;
|
|
13391
|
+
}
|
|
13392
|
+
if (!('effectiveMetadata' in context) &&
|
|
13393
|
+
!('effectiveTableConfig' in context)) {
|
|
13394
|
+
return context;
|
|
13395
|
+
}
|
|
13396
|
+
const rest = { ...context };
|
|
13397
|
+
delete rest['effectiveMetadata'];
|
|
13398
|
+
delete rest['effectiveTableConfig'];
|
|
13399
|
+
return Object.keys(rest).length > 0 ? rest : undefined;
|
|
13400
|
+
}
|
|
10862
13401
|
createDocumentFromInputs() {
|
|
10863
13402
|
return parseLegacyOrCrudDocument(this.inputs?.metadata ?? {
|
|
10864
13403
|
component: 'praxis-crud',
|
|
@@ -10872,15 +13411,16 @@ class PraxisCrudWidgetConfigEditor {
|
|
|
10872
13411
|
serializeCrudAuthoringDocument(this.createDocumentFromInputs()));
|
|
10873
13412
|
}
|
|
10874
13413
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisCrudWidgetConfigEditor, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
10875
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.14", type: PraxisCrudWidgetConfigEditor, isStandalone: true, selector: "praxis-crud-widget-config-editor", inputs: { inputs: "inputs", widgetKey: "widgetKey" }, viewQueries: [{ propertyName: "crudEditor", first: true, predicate: ["crudEditor"], descendants: true }], ngImport: i0, template: `
|
|
13414
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.14", type: PraxisCrudWidgetConfigEditor, isStandalone: true, selector: "praxis-crud-widget-config-editor", inputs: { inputs: "inputs", context: "context", widgetKey: "widgetKey" }, viewQueries: [{ propertyName: "crudEditor", first: true, predicate: ["crudEditor"], descendants: true }], ngImport: i0, template: `
|
|
10876
13415
|
<section data-testid="crud-widget-config-editor">
|
|
10877
13416
|
<praxis-crud-metadata-editor
|
|
10878
13417
|
#crudEditor
|
|
10879
13418
|
[metadata]="metadata"
|
|
13419
|
+
[effectiveTableConfig]="effectiveTableConfig"
|
|
10880
13420
|
[crudId]="effectiveCrudId"
|
|
10881
13421
|
/>
|
|
10882
13422
|
</section>
|
|
10883
|
-
`, isInline: true, dependencies: [{ kind: "component", type: CrudMetadataEditorComponent, selector: "praxis-crud-metadata-editor", inputs: ["document", "metadata", "crudId", "readonly"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
13423
|
+
`, isInline: true, dependencies: [{ kind: "component", type: CrudMetadataEditorComponent, selector: "praxis-crud-metadata-editor", inputs: ["document", "metadata", "crudId", "readonly", "effectiveTableConfig"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
10884
13424
|
}
|
|
10885
13425
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisCrudWidgetConfigEditor, decorators: [{
|
|
10886
13426
|
type: Component,
|
|
@@ -10893,6 +13433,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
10893
13433
|
<praxis-crud-metadata-editor
|
|
10894
13434
|
#crudEditor
|
|
10895
13435
|
[metadata]="metadata"
|
|
13436
|
+
[effectiveTableConfig]="effectiveTableConfig"
|
|
10896
13437
|
[crudId]="effectiveCrudId"
|
|
10897
13438
|
/>
|
|
10898
13439
|
</section>
|
|
@@ -10901,6 +13442,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
10901
13442
|
}]
|
|
10902
13443
|
}], propDecorators: { inputs: [{
|
|
10903
13444
|
type: Input
|
|
13445
|
+
}], context: [{
|
|
13446
|
+
type: Input
|
|
10904
13447
|
}], widgetKey: [{
|
|
10905
13448
|
type: Input
|
|
10906
13449
|
}], crudEditor: [{
|
|
@@ -11156,6 +13699,12 @@ const PRAXIS_CRUD_COMPONENT_METADATA = {
|
|
|
11156
13699
|
configEditor: {
|
|
11157
13700
|
component: PraxisCrudWidgetConfigEditor,
|
|
11158
13701
|
title: 'Configurar CRUD',
|
|
13702
|
+
contextResolver: ({ materializedInputs }) => ({
|
|
13703
|
+
context: {
|
|
13704
|
+
effectiveMetadata: materializedInputs?.['metadata'],
|
|
13705
|
+
effectiveTableConfig: materializedInputs?.['effectiveTableConfig'],
|
|
13706
|
+
},
|
|
13707
|
+
}),
|
|
11159
13708
|
},
|
|
11160
13709
|
inputs: [
|
|
11161
13710
|
{
|
|
@@ -11685,12 +14234,64 @@ const surfaceConfigureSchema = {
|
|
|
11685
14234
|
back: { type: 'object' },
|
|
11686
14235
|
},
|
|
11687
14236
|
};
|
|
14237
|
+
const kpiBandSchema = {
|
|
14238
|
+
type: 'object',
|
|
14239
|
+
properties: {
|
|
14240
|
+
enabled: { type: 'boolean' },
|
|
14241
|
+
cards: {
|
|
14242
|
+
type: 'array',
|
|
14243
|
+
items: {
|
|
14244
|
+
type: 'object',
|
|
14245
|
+
required: ['label', 'value'],
|
|
14246
|
+
properties: {
|
|
14247
|
+
id: { type: 'string' },
|
|
14248
|
+
label: { type: 'string' },
|
|
14249
|
+
value: { type: ['string', 'number'] },
|
|
14250
|
+
caption: { type: 'string' },
|
|
14251
|
+
icon: { type: 'string' },
|
|
14252
|
+
variant: { enum: ['default', 'primary', 'success', 'warning', 'danger'] },
|
|
14253
|
+
filter: { type: 'object' },
|
|
14254
|
+
},
|
|
14255
|
+
},
|
|
14256
|
+
},
|
|
14257
|
+
},
|
|
14258
|
+
};
|
|
14259
|
+
const filterBarSchema = {
|
|
14260
|
+
type: 'object',
|
|
14261
|
+
properties: {
|
|
14262
|
+
inlineFields: {
|
|
14263
|
+
type: 'array',
|
|
14264
|
+
items: { type: 'string' },
|
|
14265
|
+
},
|
|
14266
|
+
quickFilters: {
|
|
14267
|
+
oneOf: [
|
|
14268
|
+
{ const: false },
|
|
14269
|
+
{
|
|
14270
|
+
type: 'array',
|
|
14271
|
+
items: {
|
|
14272
|
+
type: 'object',
|
|
14273
|
+
required: ['id', 'label', 'filter'],
|
|
14274
|
+
properties: {
|
|
14275
|
+
id: { type: 'string' },
|
|
14276
|
+
label: { type: 'string' },
|
|
14277
|
+
icon: { type: 'string' },
|
|
14278
|
+
filter: { type: ['string', 'object'] },
|
|
14279
|
+
},
|
|
14280
|
+
},
|
|
14281
|
+
},
|
|
14282
|
+
],
|
|
14283
|
+
},
|
|
14284
|
+
showAdvanced: { type: 'boolean' },
|
|
14285
|
+
},
|
|
14286
|
+
};
|
|
11688
14287
|
const listSurfaceSchema = {
|
|
11689
14288
|
type: 'object',
|
|
11690
14289
|
properties: {
|
|
11691
14290
|
tablePatch: { type: 'object' },
|
|
11692
14291
|
queryContext: { type: 'object' },
|
|
11693
14292
|
filterCriteria: { type: 'object' },
|
|
14293
|
+
kpiBand: kpiBandSchema,
|
|
14294
|
+
filterBar: filterBarSchema,
|
|
11694
14295
|
},
|
|
11695
14296
|
};
|
|
11696
14297
|
const deleteBehaviorSchema = {
|
|
@@ -11866,18 +14467,58 @@ const PRAXIS_CRUD_AUTHORING_MANIFEST = {
|
|
|
11866
14467
|
target: { kind: 'listSurface', resolver: 'crud-list-surface', ambiguityPolicy: 'fail', required: true },
|
|
11867
14468
|
inputSchema: listSurfaceSchema,
|
|
11868
14469
|
effects: [{ kind: 'compile-domain-patch', handler: 'crud-list-surface-configure', handlerContract: {
|
|
11869
|
-
reads: ['CrudMetadata.table', 'CrudMetadata.queryContext', 'PRAXIS_TABLE_AUTHORING_MANIFEST', 'CRUD_AI_CAPABILITIES'],
|
|
11870
|
-
writes: ['CrudMetadata.table', 'CrudMetadata.queryContext', 'CrudMetadata.filterCriteria'],
|
|
14470
|
+
reads: ['CrudMetadata.table', 'CrudMetadata.queryContext', 'CrudMetadata.kpiBand', 'CrudMetadata.filterBar', 'PRAXIS_TABLE_AUTHORING_MANIFEST', 'CRUD_AI_CAPABILITIES'],
|
|
14471
|
+
writes: ['CrudMetadata.table', 'CrudMetadata.queryContext', 'CrudMetadata.filterCriteria', 'CrudMetadata.kpiBand', 'CrudMetadata.filterBar'],
|
|
11871
14472
|
identityKeys: ['crudId'],
|
|
11872
14473
|
inputSchema: listSurfaceSchema,
|
|
11873
14474
|
failureModes: ['table-operation-not-delegated', 'table-patch-not-supported', 'query-context-invalid'],
|
|
11874
|
-
description: 'Configures CRUD-owned list orchestration while routing table semantics to the praxis-table manifest.',
|
|
14475
|
+
description: 'Configures CRUD-owned list orchestration (including KPI band and filter bar shortcuts) while routing table semantics to the praxis-table manifest.',
|
|
11875
14476
|
} }],
|
|
11876
14477
|
validators: ['table-child-operation-delegated', 'query-context-valid', 'filter-criteria-bridge-valid', 'crud-context-stable'],
|
|
11877
|
-
affectedPaths: ['table', 'queryContext', 'filterCriteria'],
|
|
14478
|
+
affectedPaths: ['table', 'queryContext', 'filterCriteria', 'kpiBand', 'filterBar'],
|
|
11878
14479
|
submissionImpact: 'config-only',
|
|
11879
14480
|
preconditions: ['crud-metadata-loaded', 'praxis-table-manifest-available'],
|
|
11880
14481
|
},
|
|
14482
|
+
{
|
|
14483
|
+
operationId: 'list.kpiBand.configure',
|
|
14484
|
+
title: 'Configure CRUD KPI band',
|
|
14485
|
+
scope: 'interaction',
|
|
14486
|
+
targetKind: 'listSurface',
|
|
14487
|
+
target: { kind: 'listSurface', resolver: 'crud-list-surface', ambiguityPolicy: 'fail', required: true },
|
|
14488
|
+
inputSchema: kpiBandSchema,
|
|
14489
|
+
effects: [{ kind: 'compile-domain-patch', handler: 'crud-kpi-band-configure', handlerContract: {
|
|
14490
|
+
reads: ['CrudMetadata.kpiBand', 'CrudMetadata.resource'],
|
|
14491
|
+
writes: ['CrudMetadata.kpiBand'],
|
|
14492
|
+
identityKeys: ['crudId'],
|
|
14493
|
+
inputSchema: kpiBandSchema,
|
|
14494
|
+
failureModes: ['kpi-card-label-missing', 'kpi-card-value-missing', 'kpi-card-filter-invalid'],
|
|
14495
|
+
description: 'Configures the CRUD KPI summary band and optional click-to-filter card criteria.',
|
|
14496
|
+
} }],
|
|
14497
|
+
validators: ['crud-context-stable'],
|
|
14498
|
+
affectedPaths: ['kpiBand'],
|
|
14499
|
+
submissionImpact: 'config-only',
|
|
14500
|
+
preconditions: ['crud-metadata-loaded'],
|
|
14501
|
+
},
|
|
14502
|
+
{
|
|
14503
|
+
operationId: 'list.filterBar.configure',
|
|
14504
|
+
title: 'Configure CRUD filter bar',
|
|
14505
|
+
scope: 'interaction',
|
|
14506
|
+
targetKind: 'listSurface',
|
|
14507
|
+
target: { kind: 'listSurface', resolver: 'crud-list-surface', ambiguityPolicy: 'fail', required: true },
|
|
14508
|
+
inputSchema: filterBarSchema,
|
|
14509
|
+
effects: [{ kind: 'compile-domain-patch', handler: 'crud-filter-bar-configure', handlerContract: {
|
|
14510
|
+
reads: ['CrudMetadata.filterBar', 'CrudMetadata.resource'],
|
|
14511
|
+
writes: ['CrudMetadata.filterBar'],
|
|
14512
|
+
identityKeys: ['crudId'],
|
|
14513
|
+
inputSchema: filterBarSchema,
|
|
14514
|
+
failureModes: ['quick-filter-id-missing', 'quick-filter-label-missing', 'quick-filter-condition-invalid'],
|
|
14515
|
+
description: 'Configures CRUD filterBar shorthand (inlineFields, quickFilters, showAdvanced) expanded into the internal table runtime.',
|
|
14516
|
+
} }],
|
|
14517
|
+
validators: ['crud-context-stable'],
|
|
14518
|
+
affectedPaths: ['filterBar'],
|
|
14519
|
+
submissionImpact: 'config-only',
|
|
14520
|
+
preconditions: ['crud-metadata-loaded'],
|
|
14521
|
+
},
|
|
11881
14522
|
{
|
|
11882
14523
|
operationId: 'surface.create.configure',
|
|
11883
14524
|
title: 'Configure create surface',
|