@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 +28 -0
- package/ai/component-registry.json +2 -2
- package/fesm2022/praxisui-crud.mjs +115 -21
- package/package.json +7 -7
- package/types/praxisui-crud.d.ts +3 -0
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-
|
|
3
|
+
"generatedAt": "2026-10-14T16:04:24.084Z",
|
|
4
4
|
"packageName": "@praxisui/crud",
|
|
5
|
-
"packageVersion": "10.0.0-rc.
|
|
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]="
|
|
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]="
|
|
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() &&
|
|
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
|
|
11944
|
+
...(base.table?.toolbar || {}),
|
|
11901
11945
|
filters: {
|
|
11902
|
-
|
|
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 &&
|
|
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) =>
|
|
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
|
-
|
|
12042
|
-
|
|
12043
|
-
|
|
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
|
-
|
|
12046
|
-
const trimmed =
|
|
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
|
-
|
|
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
|
-
|
|
12105
|
+
// fallback to key-value or query below
|
|
12053
12106
|
}
|
|
12054
12107
|
}
|
|
12055
|
-
|
|
12056
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
9
|
-
"@praxisui/table": "^10.0.0-rc.
|
|
10
|
-
"@praxisui/core": "^10.0.0-rc.
|
|
11
|
-
"@praxisui/dynamic-fields": "^10.0.0-rc.
|
|
12
|
-
"@praxisui/settings-panel": "^10.0.0-rc.
|
|
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.
|
|
17
|
+
"@praxisui/ai": "^10.0.0-rc.15",
|
|
18
18
|
"rxjs": "~7.8.0"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
package/types/praxisui-crud.d.ts
CHANGED
|
@@ -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;
|