@praxisui/crud 10.0.0-rc.14 → 10.0.0-rc.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -344,6 +344,33 @@ The host does not need to call `HttpClient`, rebuild create schema URLs, or own
344
344
  a second launcher. Absence of the HATEOAS link keeps the operation unavailable;
345
345
  the runtime does not infer permission from labels, tags, or frontend flags.
346
346
 
347
+ ## Governed Two-Phase Bulk Operations
348
+
349
+ `PraxisCrudComponent` discovers governed two-phase bulk operations published by
350
+ `praxis-metadata-starter` on collection capabilities (`actions[].execution.bulk`
351
+ for `DOMAIN_COMMAND` and `operations['bulk-update'].bulk` for `UNIFORM_UPDATE`)
352
+ and materializes them in the contextual bulk selection toolbar:
353
+
354
+ 1. **Pre-evaluation gates**: validates `limits.maxTargets` before evaluation and
355
+ resolves `expectedVersion` (`ETag`) via `_links.self` only when
356
+ `preconditions.resourceVersion === 'REQUIRED'`.
357
+ 2. **Phase 1 — Immutable Proposal (`PraxisBulkOperationDialogComponent` / `PraxisBulkEditDialogComponent`)**:
358
+ hydrates command parameters (unwrapping `/properties/parameters` and OpenAPI
359
+ `$ref` definitions with a resource-and-action-scoped `formId`) or
360
+ `@BulkEditable` fields (`SET` / `CLEAR`), submits the evaluation request, and
361
+ renders target eligibility totals (`executable`, `blocked`) and per-item
362
+ diagnostics.
363
+ 3. **Phase 2 — Transactional Confirmation & Reconciliation**: confirms the
364
+ approved `proposalId` with an `Idempotency-Key`, polls asynchronous
365
+ executions until terminal state (`COMPLETED`, `COMPLETED_WITH_ERRORS`,
366
+ `CANCELLED`, `STOPPED`, `RECONCILIATION_REQUIRED`), aggregates all rejection
367
+ categories (`denied`, `invalid`, `conflict`, `notProcessed`, `unknown`),
368
+ clears table selection, refreshes the collection, and re-fetches capabilities
369
+ when `refresh.capabilities === true`.
370
+
371
+ Unavailable bulk operations (`availability.allowed === false`) remain visible
372
+ but disabled in the bulk toolbar with their canonical translated reason tooltip.
373
+
347
374
  ## Visual Authoring
348
375
 
349
376
  The package publishes the CRUD-specific authoring surface:
@@ -377,6 +404,7 @@ The manifest owns CRUD orchestration only. It delegates table, form, field, and
377
404
  Main exports:
378
405
 
379
406
  - `PraxisCrudComponent`
407
+ - `PraxisBulkOperationDialogComponent`, `PraxisBulkEditDialogComponent`
380
408
  - `CrudLauncherService`
381
409
  - `DynamicFormDialogHostComponent`
382
410
  - `CrudMetadata`, `CrudActionFormContract`, `CrudDefaults`, `CrudParamMapping`
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": "1.0.0",
3
- "generatedAt": "2026-10-14T11:31:33.532Z",
3
+ "generatedAt": "2026-10-14T16:04:24.084Z",
4
4
  "packageName": "@praxisui/crud",
5
- "packageVersion": "10.0.0-rc.14",
5
+ "packageVersion": "10.0.0-rc.15",
6
6
  "sourceRegistry": "praxis-component-registry-ingestion",
7
7
  "sourceRegistryVersion": "1.0.0",
8
8
  "componentCount": 2,
@@ -104,6 +104,15 @@ class PraxisBulkOperationDialogComponent {
104
104
  this.action.requestSchemaUrl ||
105
105
  null);
106
106
  }
107
+ getParametersFormId() {
108
+ const resourcePart = String(this.data.resourceKey || this.data.resourcePath || 'resource')
109
+ .trim()
110
+ .replace(/[^a-zA-Z0-9_-]+/g, '-');
111
+ const actionPart = String(this.action.id || 'action')
112
+ .trim()
113
+ .replace(/[^a-zA-Z0-9_-]+/g, '-');
114
+ return `bulk-params-${resourcePart}-${actionPart}`;
115
+ }
107
116
  onParametersChange(event) {
108
117
  const raw = event?.formData ?? event?.value ?? event;
109
118
  if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
@@ -474,7 +483,7 @@ class PraxisBulkOperationDialogComponent {
474
483
  <div class="praxis-bulk-dialog__section">
475
484
  <h3 class="praxis-bulk-dialog__section-title">Parâmetros da Operação</h3>
476
485
  <praxis-dynamic-form
477
- [formId]="'bulk-params-form'"
486
+ [formId]="getParametersFormId()"
478
487
  [schemaUrl]="parametersSchemaUrl()!"
479
488
  [initialValue]="parametersValue()"
480
489
  [resourcePath]="data.resourcePath"
@@ -791,7 +800,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
791
800
  <div class="praxis-bulk-dialog__section">
792
801
  <h3 class="praxis-bulk-dialog__section-title">Parâmetros da Operação</h3>
793
802
  <praxis-dynamic-form
794
- [formId]="'bulk-params-form'"
803
+ [formId]="getParametersFormId()"
795
804
  [schemaUrl]="parametersSchemaUrl()!"
796
805
  [initialValue]="parametersValue()"
797
806
  [resourcePath]="data.resourcePath"
@@ -8560,6 +8569,35 @@ class PraxisCrudComponent {
8560
8569
  toolbar.filters.enabled = true;
8561
8570
  changed = true;
8562
8571
  }
8572
+ const existingAdvancedFilters = cfg.behavior?.filtering?.advancedFilters;
8573
+ const hasConfiguredFilterFields = (existingAdvancedFilters?.settings?.alwaysVisibleFields?.length ?? 0) > 0 ||
8574
+ (existingAdvancedFilters?.settings?.selectedFieldIds?.length ?? 0) > 0;
8575
+ if (hasConfiguredFilterFields &&
8576
+ cfg.behavior?.filtering?.enabled !== false &&
8577
+ existingAdvancedFilters?.enabled !== false) {
8578
+ const filtering = cfg.behavior.filtering;
8579
+ if (filtering.enabled !== true) {
8580
+ filtering.enabled = true;
8581
+ changed = true;
8582
+ }
8583
+ if (existingAdvancedFilters.enabled !== true) {
8584
+ existingAdvancedFilters.enabled = true;
8585
+ changed = true;
8586
+ }
8587
+ const toolbar = ensureToolbar();
8588
+ if (toolbar.visible === undefined) {
8589
+ toolbar.visible = true;
8590
+ changed = true;
8591
+ }
8592
+ if (!toolbar.filters) {
8593
+ toolbar.filters = { enabled: true, showAdvancedButton: true };
8594
+ changed = true;
8595
+ }
8596
+ else if (toolbar.filters.enabled === undefined) {
8597
+ toolbar.filters.enabled = true;
8598
+ changed = true;
8599
+ }
8600
+ }
8563
8601
  const effectiveResourcePath = meta?.resource?.schemaPath?.trim() || this.resolveResourcePath(meta);
8564
8602
  if (effectiveResourcePath && cfg.behavior?.filtering?.advancedFilters) {
8565
8603
  const advancedFilters = cfg.behavior.filtering.advancedFilters;
@@ -11814,7 +11852,7 @@ class PraxisResourcePage {
11814
11852
  const fromRoute = this.routeScopeBarSignal();
11815
11853
  if (fromRoute)
11816
11854
  return fromRoute;
11817
- // Auto-promoção declarativa a partir de metadata.table.toolbar.filters.quickFilters
11855
+ // Auto-promoção declarativa a partir de metadata.table.toolbar.filters.quickFilters ou metadata.filterBar.quickFilters
11818
11856
  const metaSource = this.metadata || this.routeMetadataSignal();
11819
11857
  let meta = null;
11820
11858
  if (typeof metaSource === 'string' && metaSource.trim().startsWith('{')) {
@@ -11826,7 +11864,10 @@ class PraxisResourcePage {
11826
11864
  else if (metaSource && typeof metaSource === 'object') {
11827
11865
  meta = metaSource;
11828
11866
  }
11829
- const quickFilters = meta?.table?.toolbar?.filters?.quickFilters;
11867
+ const quickFilters = meta?.table?.toolbar?.filters?.quickFilters ??
11868
+ (Array.isArray(meta?.filterBar?.quickFilters)
11869
+ ? meta.filterBar.quickFilters
11870
+ : undefined);
11830
11871
  if (Array.isArray(quickFilters) && quickFilters.length > 0) {
11831
11872
  return {
11832
11873
  items: quickFilters.map((qf) => ({
@@ -11837,6 +11878,7 @@ class PraxisResourcePage {
11837
11878
  tone: qf.tone,
11838
11879
  badge: qf.badge,
11839
11880
  count: qf.count,
11881
+ isDefault: qf.isDefault,
11840
11882
  })),
11841
11883
  leadLabel: 'Filtro:',
11842
11884
  leadIcon: 'tune',
@@ -11893,13 +11935,19 @@ class PraxisResourcePage {
11893
11935
  }
11894
11936
  // Se o ScopeBar estiver ativo no cabeçalho (seja manual ou auto-promovido),
11895
11937
  // suprimir os botões redundantes de quickFilters na toolbar interna da tabela
11896
- if (this.effectiveScopeBar() && base.table?.toolbar?.filters) {
11938
+ if (this.effectiveScopeBar() &&
11939
+ (base.table?.toolbar?.filters || Array.isArray(base.filterBar?.quickFilters))) {
11940
+ const existingFilters = base.table?.toolbar?.filters;
11897
11941
  base.table = {
11898
- ...base.table,
11942
+ ...(base.table || { columns: [] }),
11899
11943
  toolbar: {
11900
- ...base.table.toolbar,
11944
+ ...(base.table?.toolbar || {}),
11901
11945
  filters: {
11902
- ...base.table.toolbar.filters,
11946
+ enabled: existingFilters?.enabled ?? true,
11947
+ showAdvancedButton: existingFilters?.showAdvancedButton ??
11948
+ base.filterBar?.showAdvanced ??
11949
+ true,
11950
+ ...(existingFilters || {}),
11903
11951
  showQuickFilters: false,
11904
11952
  },
11905
11953
  },
@@ -12026,10 +12074,10 @@ class PraxisResourcePage {
12026
12074
  }
12027
12075
  handleKpiCardClick(event) {
12028
12076
  const cardFilter = event.card.filter;
12029
- if (cardFilter && typeof cardFilter === 'object') {
12077
+ if (cardFilter !== undefined && cardFilter !== null) {
12030
12078
  const sb = this.effectiveScopeBar();
12031
12079
  if (sb && sb.items) {
12032
- const matched = sb.items.find((item) => JSON.stringify(item.filter) === JSON.stringify(cardFilter));
12080
+ const matched = sb.items.find((item) => this.areScopeFiltersEqual(item.filter, cardFilter));
12033
12081
  if (matched) {
12034
12082
  this.onScopeChange(matched);
12035
12083
  }
@@ -12038,25 +12086,71 @@ class PraxisResourcePage {
12038
12086
  this.kpiCardClick.emit(event);
12039
12087
  }
12040
12088
  applyItemFilter(item) {
12041
- let filterObj = {};
12042
- if (item.filter && typeof item.filter === 'object') {
12043
- filterObj = item.filter;
12089
+ this.activeScopeFilter.set(this.normalizeScopeFilterExpression(item.filter));
12090
+ }
12091
+ normalizeScopeFilterExpression(filter) {
12092
+ if (filter && typeof filter === 'object' && !Array.isArray(filter)) {
12093
+ return { ...filter };
12044
12094
  }
12045
- else if (typeof item.filter === 'string') {
12046
- const trimmed = item.filter.trim();
12047
- if (trimmed.startsWith('{')) {
12095
+ if (typeof filter === 'string' && filter.trim().length > 0) {
12096
+ const trimmed = filter.trim();
12097
+ if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
12048
12098
  try {
12049
- filterObj = JSON.parse(trimmed);
12099
+ const parsed = JSON.parse(trimmed);
12100
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
12101
+ return parsed;
12102
+ }
12050
12103
  }
12051
12104
  catch {
12052
- filterObj = { query: trimmed };
12105
+ // fallback to key-value or query below
12053
12106
  }
12054
12107
  }
12055
- else if (trimmed) {
12056
- filterObj = { query: trimmed };
12108
+ if (trimmed.includes('=')) {
12109
+ const result = {};
12110
+ const pairs = trimmed.split('&');
12111
+ for (const pair of pairs) {
12112
+ const eqIdx = pair.indexOf('=');
12113
+ if (eqIdx > 0) {
12114
+ const key = pair.substring(0, eqIdx).trim();
12115
+ const rawVal = pair.substring(eqIdx + 1).trim();
12116
+ const isQuoted = (rawVal.startsWith("'") && rawVal.endsWith("'")) ||
12117
+ (rawVal.startsWith('"') && rawVal.endsWith('"'));
12118
+ const val = isQuoted ? rawVal.slice(1, -1) : rawVal;
12119
+ if (!isQuoted && val.toLowerCase() === 'true') {
12120
+ result[key] = true;
12121
+ }
12122
+ else if (!isQuoted && val.toLowerCase() === 'false') {
12123
+ result[key] = false;
12124
+ }
12125
+ else if (!isQuoted && /^-?\d+$/.test(val)) {
12126
+ result[key] = parseInt(val, 10);
12127
+ }
12128
+ else if (!isQuoted && /^-?\d+\.\d+$/.test(val)) {
12129
+ result[key] = parseFloat(val);
12130
+ }
12131
+ else {
12132
+ result[key] = val;
12133
+ }
12134
+ }
12135
+ }
12136
+ if (Object.keys(result).length > 0) {
12137
+ return result;
12138
+ }
12057
12139
  }
12140
+ return { query: trimmed };
12141
+ }
12142
+ return {};
12143
+ }
12144
+ areScopeFiltersEqual(a, b) {
12145
+ const normA = this.normalizeScopeFilterExpression(a);
12146
+ const normB = this.normalizeScopeFilterExpression(b);
12147
+ const keysA = Object.keys(normA).sort();
12148
+ const keysB = Object.keys(normB).sort();
12149
+ if (keysA.length === 0 || keysA.length !== keysB.length) {
12150
+ return false;
12058
12151
  }
12059
- this.activeScopeFilter.set(filterObj);
12152
+ return keysA.every((key, idx) => key === keysB[idx] &&
12153
+ JSON.stringify(normA[key]) === JSON.stringify(normB[key]));
12060
12154
  }
12061
12155
  extractRouteData(data) {
12062
12156
  if ('metadata' in data) {
package/package.json CHANGED
@@ -1,20 +1,20 @@
1
1
  {
2
2
  "name": "@praxisui/crud",
3
- "version": "10.0.0-rc.14",
3
+ "version": "10.0.0-rc.15",
4
4
  "description": "CRUD building blocks for Praxis UI: integrates dynamic forms and tables with unified configuration and services.",
5
5
  "peerDependencies": {
6
6
  "@angular/common": "^21.0.0",
7
7
  "@angular/core": "^21.0.0",
8
- "@praxisui/dynamic-form": "^10.0.0-rc.14",
9
- "@praxisui/table": "^10.0.0-rc.14",
10
- "@praxisui/core": "^10.0.0-rc.14",
11
- "@praxisui/dynamic-fields": "^10.0.0-rc.14",
12
- "@praxisui/settings-panel": "^10.0.0-rc.14",
8
+ "@praxisui/dynamic-form": "^10.0.0-rc.15",
9
+ "@praxisui/table": "^10.0.0-rc.15",
10
+ "@praxisui/core": "^10.0.0-rc.15",
11
+ "@praxisui/dynamic-fields": "^10.0.0-rc.15",
12
+ "@praxisui/settings-panel": "^10.0.0-rc.15",
13
13
  "@angular/cdk": "^21.0.0",
14
14
  "@angular/forms": "^21.0.0",
15
15
  "@angular/material": "^21.0.0",
16
16
  "@angular/router": "^21.0.0",
17
- "@praxisui/ai": "^10.0.0-rc.14",
17
+ "@praxisui/ai": "^10.0.0-rc.15",
18
18
  "rxjs": "~7.8.0"
19
19
  },
20
20
  "dependencies": {
@@ -1135,6 +1135,8 @@ declare class PraxisResourcePage implements OnInit, OnChanges, OnDestroy {
1135
1135
  data: any;
1136
1136
  }): void;
1137
1137
  private applyItemFilter;
1138
+ private normalizeScopeFilterExpression;
1139
+ private areScopeFiltersEqual;
1138
1140
  private extractRouteData;
1139
1141
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<PraxisResourcePage, never>;
1140
1142
  static ɵcmp: _angular_core.ɵɵComponentDeclaration<PraxisResourcePage, "praxis-resource-page", never, { "resource": { "alias": "resource"; "required": false; }; "resourcePath": { "alias": "resourcePath"; "required": false; }; "crudId": { "alias": "crudId"; "required": false; }; "metadata": { "alias": "metadata"; "required": false; }; "title": { "alias": "title"; "required": false; }; "subtitle": { "alias": "subtitle"; "required": false; }; "domain": { "alias": "domain"; "required": false; }; "scopeBar": { "alias": "scopeBar"; "required": false; }; "scopeItems": { "alias": "scopeItems"; "required": false; }; "defaultScopeId": { "alias": "defaultScopeId"; "required": false; }; "enableCustomization": { "alias": "enableCustomization"; "required": false; }; "tableConfigPersistenceStrategy": { "alias": "tableConfigPersistenceStrategy"; "required": false; }; "context": { "alias": "context"; "required": false; }; "actions": { "alias": "actions"; "required": false; }; "filterCriteria": { "alias": "filterCriteria"; "required": false; }; }, { "rowClick": "rowClick"; "rowAction": "rowAction"; "rowDoubleClick": "rowDoubleClick"; "selectionChange": "selectionChange"; "drawerOpen": "drawerOpen"; "drawerClose": "drawerClose"; "afterOpen": "afterOpen"; "afterClose": "afterClose"; "kpiCardClick": "kpiCardClick"; "afterSave": "afterSave"; "afterDelete": "afterDelete"; "error": "error"; }, never, ["[page-header-actions]"], true, never>;
@@ -1178,6 +1180,7 @@ declare class PraxisBulkOperationDialogComponent implements OnInit {
1178
1180
  ngOnInit(): void;
1179
1181
  hasParametersSchema(): boolean;
1180
1182
  parametersSchemaUrl(): string | null;
1183
+ getParametersFormId(): string;
1181
1184
  onParametersChange(event: any): void;
1182
1185
  isBlockedByAtomicity(): boolean;
1183
1186
  isConfirmDisabled(): boolean;