@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.
@@ -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, GenericCrudService, ComponentMetadataRegistry, PraxisCustomizationModeService } from '@praxisui/core';
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
- injectedData = inject(SETTINGS_PANEL_DATA, { optional: true });
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 ['connection', 'defaults', 'actions', 'table']) {
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 ['connection', 'defaults', 'actions', 'table']) {
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(() => ['connection', 'defaults', 'actions', 'table']
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) => (issue.code === 'crud.metadata.actions.route.required'
4938
- || issue.code === 'crud.metadata.actions.formId.required'));
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
- || this.actionGroupStatus(actionName, 'api') !== 'ready';
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
- .replace('{count}', String(this.actionKeys.length));
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 = table.columns?.length || 0;
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 = table.columns?.length || 0;
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
- const table = this.tableConfig();
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 = ['connection', 'defaults', 'actions', 'table']
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
- return this.tx(key, fallback);
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: cloneTableConfig(config),
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 ? actions[index] : { action: actionName, label: this.actionLabel(actionName) };
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
- || this.metadataInput()
5653
- || this.injectedData?.document
5654
- || this.injectedData?.metadata
5655
- || null;
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.isDirty$.next(currentSignature !== initialSignature);
5661
- this.isValid$.next(!this.diagnostics().some((issue) => issue.level === 'error'));
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 ['connection', 'defaults', 'actions', 'table']
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
- table: ['metadata.table'],
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
- || this.actionKeys.find((actionName) => this.actionStatus(actionName) === 'pending')
5712
- || null;
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 } }, providers: [providePraxisI18nConfig(PRAXIS_CRUD_AUTHORING_I18N_CONFIG)], ngImport: i0, template: `
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 class="editor-tabs" data-testid="crud-editor-tabs" dynamicHeight animationDuration="0ms">
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]="tableConfig()"
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
- PraxisTableInlineAuthoringEditorComponent
6292
- ], providers: [providePraxisI18nConfig(PRAXIS_CRUD_AUTHORING_I18N_CONFIG)], template: `
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 class="editor-tabs" data-testid="crud-editor-tabs" dynamicHeight animationDuration="0ms">
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]="tableConfig()"
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
- ...event.card.filter,
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 seed = liveTableConfig
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: this.cloneTableConfigSnapshot(liveTableConfig),
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: this.inputs?.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',