@mintplayer/ng-spark 0.0.8 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { signal, InjectionToken, Injectable, inject, Pipe, input, computed, ChangeDetectionStrategy, Component, model, output, effect } from '@angular/core';
|
|
2
|
+
import { signal, InjectionToken, Injectable, inject, NgZone, Pipe, input, computed, ChangeDetectionStrategy, Component, model, output, effect, DestroyRef } from '@angular/core';
|
|
3
3
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
|
4
|
-
import { firstValueFrom } from 'rxjs';
|
|
4
|
+
import { firstValueFrom, Observable } from 'rxjs';
|
|
5
5
|
import * as i1 from '@angular/common';
|
|
6
6
|
import { CommonModule, NgTemplateOutlet, NgComponentOutlet } from '@angular/common';
|
|
7
7
|
import * as i2 from '@angular/forms';
|
|
@@ -26,6 +26,7 @@ import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
|
|
26
26
|
import { BsAlertComponent } from '@mintplayer/ng-bootstrap/alert';
|
|
27
27
|
import { BsContainerComponent } from '@mintplayer/ng-bootstrap/container';
|
|
28
28
|
import { BsButtonGroupComponent } from '@mintplayer/ng-bootstrap/button-group';
|
|
29
|
+
import { VirtualDatatableDataSource, BsVirtualDatatableComponent, BsVirtualRowTemplateDirective } from '@mintplayer/ng-bootstrap/virtual-datatable';
|
|
29
30
|
|
|
30
31
|
/** Global reactive language state — shared across library boundaries via globalThis */
|
|
31
32
|
const currentLanguage = (globalThis.__sparkCurrentLanguage ??= signal('en'));
|
|
@@ -123,21 +124,26 @@ class SparkService {
|
|
|
123
124
|
const queries = await this.getQueries();
|
|
124
125
|
return queries.find(q => q.name === name);
|
|
125
126
|
}
|
|
126
|
-
async executeQuery(queryId,
|
|
127
|
+
async executeQuery(queryId, options) {
|
|
127
128
|
let params = new HttpParams();
|
|
128
|
-
if (
|
|
129
|
-
params = params.set('
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
129
|
+
if (options?.sortColumns?.length) {
|
|
130
|
+
params = params.set('sortColumns', options.sortColumns.map(c => `${c.property}:${c.direction === 'descending' ? 'desc' : 'asc'}`).join(','));
|
|
131
|
+
}
|
|
132
|
+
if (options?.parentId)
|
|
133
|
+
params = params.set('parentId', options.parentId);
|
|
134
|
+
if (options?.parentType)
|
|
135
|
+
params = params.set('parentType', options.parentType);
|
|
136
|
+
if (options?.skip != null)
|
|
137
|
+
params = params.set('skip', options.skip);
|
|
138
|
+
if (options?.take != null)
|
|
139
|
+
params = params.set('take', options.take);
|
|
140
|
+
if (options?.search)
|
|
141
|
+
params = params.set('search', options.search);
|
|
136
142
|
return firstValueFrom(this.http.get(`${this.baseUrl}/queries/${encodeURIComponent(queryId)}/execute`, { params }));
|
|
137
143
|
}
|
|
138
144
|
async executeQueryByName(queryName) {
|
|
139
145
|
const query = await this.getQueryByName(queryName);
|
|
140
|
-
return query ? this.executeQuery(query.id) : [];
|
|
146
|
+
return query ? this.executeQuery(query.id) : { data: [], totalRecords: 0, skip: 0, take: 50 };
|
|
141
147
|
}
|
|
142
148
|
// Program Units
|
|
143
149
|
async getProgramUnits() {
|
|
@@ -232,6 +238,92 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
|
|
|
232
238
|
args: [{ providedIn: 'root' }]
|
|
233
239
|
}] });
|
|
234
240
|
|
|
241
|
+
class SparkStreamingService {
|
|
242
|
+
config = inject(SPARK_CONFIG, { optional: true });
|
|
243
|
+
baseUrl = this.config?.baseUrl ?? '/spark';
|
|
244
|
+
ngZone = inject(NgZone);
|
|
245
|
+
connectToStreamingQuery(queryId) {
|
|
246
|
+
return new Observable(subscriber => {
|
|
247
|
+
let ws = null;
|
|
248
|
+
let retryCount = 0;
|
|
249
|
+
let retryTimeout = null;
|
|
250
|
+
let closed = false;
|
|
251
|
+
const maxRetries = 10;
|
|
252
|
+
const maxDelay = 30000;
|
|
253
|
+
const connect = () => {
|
|
254
|
+
if (closed)
|
|
255
|
+
return;
|
|
256
|
+
const url = this.buildWebSocketUrl(queryId);
|
|
257
|
+
ws = new WebSocket(url);
|
|
258
|
+
ws.onopen = () => {
|
|
259
|
+
retryCount = 0; // Reset on successful connect
|
|
260
|
+
};
|
|
261
|
+
ws.onmessage = (event) => {
|
|
262
|
+
try {
|
|
263
|
+
const message = JSON.parse(event.data);
|
|
264
|
+
this.ngZone.run(() => subscriber.next(message));
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
// Ignore malformed messages
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
ws.onerror = () => {
|
|
271
|
+
// Error will trigger onclose
|
|
272
|
+
};
|
|
273
|
+
ws.onclose = (event) => {
|
|
274
|
+
if (closed)
|
|
275
|
+
return;
|
|
276
|
+
// Don't reconnect on normal closure
|
|
277
|
+
if (event.code === 1000) {
|
|
278
|
+
this.ngZone.run(() => subscriber.complete());
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
// Reconnect with exponential backoff
|
|
282
|
+
if (retryCount < maxRetries) {
|
|
283
|
+
const delay = Math.min(1000 * Math.pow(2, retryCount), maxDelay);
|
|
284
|
+
retryCount++;
|
|
285
|
+
retryTimeout = setTimeout(() => connect(), delay);
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
this.ngZone.run(() => subscriber.error(new Error('WebSocket connection failed after maximum retries')));
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
};
|
|
292
|
+
connect();
|
|
293
|
+
// Teardown: close WebSocket on unsubscribe
|
|
294
|
+
return () => {
|
|
295
|
+
closed = true;
|
|
296
|
+
if (retryTimeout) {
|
|
297
|
+
clearTimeout(retryTimeout);
|
|
298
|
+
}
|
|
299
|
+
if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) {
|
|
300
|
+
ws.close(1000, 'Client unsubscribed');
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
buildWebSocketUrl(queryId) {
|
|
306
|
+
const encodedId = encodeURIComponent(queryId);
|
|
307
|
+
const path = `${this.baseUrl}/queries/${encodedId}/stream`;
|
|
308
|
+
// If baseUrl is absolute (starts with http/https), replace protocol
|
|
309
|
+
if (this.baseUrl.startsWith('http://')) {
|
|
310
|
+
return path.replace(/^http:\/\//, 'ws://');
|
|
311
|
+
}
|
|
312
|
+
if (this.baseUrl.startsWith('https://')) {
|
|
313
|
+
return path.replace(/^https:\/\//, 'wss://');
|
|
314
|
+
}
|
|
315
|
+
// Relative URL — construct from window.location
|
|
316
|
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
317
|
+
return `${protocol}//${window.location.host}${path}`;
|
|
318
|
+
}
|
|
319
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: SparkStreamingService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
320
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: SparkStreamingService, providedIn: 'root' });
|
|
321
|
+
}
|
|
322
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: SparkStreamingService, decorators: [{
|
|
323
|
+
type: Injectable,
|
|
324
|
+
args: [{ providedIn: 'root' }]
|
|
325
|
+
}] });
|
|
326
|
+
|
|
235
327
|
class SparkLanguageService {
|
|
236
328
|
http = inject(HttpClient);
|
|
237
329
|
config = inject(SPARK_CONFIG, { optional: true });
|
|
@@ -664,12 +756,11 @@ class SparkPoFormComponent {
|
|
|
664
756
|
referenceModalItems = signal([], ...(ngDevMode ? [{ debugName: "referenceModalItems" }] : []));
|
|
665
757
|
referenceModalEntityType = signal(null, ...(ngDevMode ? [{ debugName: "referenceModalEntityType" }] : []));
|
|
666
758
|
referenceModalPagination = signal(undefined, ...(ngDevMode ? [{ debugName: "referenceModalPagination" }] : []));
|
|
667
|
-
referenceModalSettings = new DatatableSettings({
|
|
759
|
+
referenceModalSettings = signal(new DatatableSettings({
|
|
668
760
|
perPage: { values: [10, 25, 50], selected: 10 },
|
|
669
761
|
page: { values: [1], selected: 1 },
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
});
|
|
762
|
+
sortColumns: []
|
|
763
|
+
}), ...(ngDevMode ? [{ debugName: "referenceModalSettings" }] : []));
|
|
673
764
|
referenceSearchTerm = '';
|
|
674
765
|
// Modal state for LookupReference selection (Modal display type)
|
|
675
766
|
editingLookupAttr = signal(null, ...(ngDevMode ? [{ debugName: "editingLookupAttr" }] : []));
|
|
@@ -745,8 +836,8 @@ class SparkPoFormComponent {
|
|
|
745
836
|
if (refAttrs.length === 0)
|
|
746
837
|
return;
|
|
747
838
|
const entries = await Promise.all(refAttrs.filter(a => a.query).map(async (attr) => {
|
|
748
|
-
const
|
|
749
|
-
return [attr.name,
|
|
839
|
+
const result = await this.sparkService.executeQueryByName(attr.query);
|
|
840
|
+
return [attr.name, result.data];
|
|
750
841
|
}));
|
|
751
842
|
this.referenceOptions.set(this.toRecord(entries));
|
|
752
843
|
}
|
|
@@ -766,8 +857,8 @@ class SparkPoFormComponent {
|
|
|
766
857
|
const refCols = asDetailType.attributes.filter(a => a.dataType === 'Reference' && a.query);
|
|
767
858
|
if (refCols.length > 0) {
|
|
768
859
|
const refEntries = await Promise.all(refCols.filter(c => c.query).map(async (col) => {
|
|
769
|
-
const
|
|
770
|
-
return [col.name,
|
|
860
|
+
const result = await this.sparkService.executeQueryByName(col.query);
|
|
861
|
+
return [col.name, result.data];
|
|
771
862
|
}));
|
|
772
863
|
this.asDetailReferenceOptions.update(prev => ({ ...prev, [attr.name]: this.toRecord(refEntries) }));
|
|
773
864
|
}
|
|
@@ -917,17 +1008,16 @@ class SparkPoFormComponent {
|
|
|
917
1008
|
this.referenceModalItems.set(this.getReferenceOptions(attr));
|
|
918
1009
|
const types = await this.sparkService.getEntityTypes();
|
|
919
1010
|
this.referenceModalEntityType.set(types.find(t => t.clrType === attr.referenceType) || null);
|
|
920
|
-
this.referenceModalSettings
|
|
1011
|
+
this.referenceModalSettings.set(new DatatableSettings({
|
|
921
1012
|
perPage: { values: [10, 25, 50], selected: 10 },
|
|
922
1013
|
page: { values: [1], selected: 1 },
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
});
|
|
1014
|
+
sortColumns: []
|
|
1015
|
+
}));
|
|
926
1016
|
this.applyReferenceFilter();
|
|
927
1017
|
this.showReferenceModal.set(true);
|
|
928
1018
|
}
|
|
929
1019
|
onReferenceSearchChange() {
|
|
930
|
-
this.referenceModalSettings.page.selected = 1;
|
|
1020
|
+
this.referenceModalSettings().page.selected = 1;
|
|
931
1021
|
this.applyReferenceFilter();
|
|
932
1022
|
}
|
|
933
1023
|
applyReferenceFilter() {
|
|
@@ -947,17 +1037,17 @@ class SparkPoFormComponent {
|
|
|
947
1037
|
});
|
|
948
1038
|
});
|
|
949
1039
|
}
|
|
950
|
-
const totalPages = Math.ceil(filteredItems.length / this.referenceModalSettings.perPage.selected) || 1;
|
|
1040
|
+
const totalPages = Math.ceil(filteredItems.length / this.referenceModalSettings().perPage.selected) || 1;
|
|
951
1041
|
this.referenceModalPagination.set({
|
|
952
1042
|
data: filteredItems,
|
|
953
1043
|
totalRecords: filteredItems.length,
|
|
954
1044
|
totalPages: totalPages,
|
|
955
|
-
perPage: this.referenceModalSettings.perPage.selected,
|
|
956
|
-
page: this.referenceModalSettings.page.selected
|
|
1045
|
+
perPage: this.referenceModalSettings().perPage.selected,
|
|
1046
|
+
page: this.referenceModalSettings().page.selected
|
|
957
1047
|
});
|
|
958
|
-
this.referenceModalSettings.page.values = Array.from({ length: totalPages }, (_, i) => i + 1);
|
|
959
|
-
if (this.referenceModalSettings.page.selected > totalPages) {
|
|
960
|
-
this.referenceModalSettings.page.selected = 1;
|
|
1048
|
+
this.referenceModalSettings().page.values = Array.from({ length: totalPages }, (_, i) => i + 1);
|
|
1049
|
+
if (this.referenceModalSettings().page.selected > totalPages) {
|
|
1050
|
+
this.referenceModalSettings().page.selected = 1;
|
|
961
1051
|
}
|
|
962
1052
|
}
|
|
963
1053
|
clearReferenceSearch() {
|
|
@@ -981,7 +1071,7 @@ class SparkPoFormComponent {
|
|
|
981
1071
|
this.referenceSearchTerm = '';
|
|
982
1072
|
}
|
|
983
1073
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: SparkPoFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
984
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: SparkPoFormComponent, isStandalone: true, selector: "spark-po-form", inputs: { entityType: { classPropertyName: "entityType", publicName: "entityType", isSignal: true, isRequired: false, transformFunction: null }, formData: { classPropertyName: "formData", publicName: "formData", isSignal: true, isRequired: false, transformFunction: null }, validationErrors: { classPropertyName: "validationErrors", publicName: "validationErrors", isSignal: true, isRequired: false, transformFunction: null }, showButtons: { classPropertyName: "showButtons", publicName: "showButtons", isSignal: true, isRequired: false, transformFunction: null }, isSaving: { classPropertyName: "isSaving", publicName: "isSaving", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { formData: "formDataChange", save: "save", cancel: "cancel" }, ngImport: i0, template: "<bs-form>\n @if (entityType()) {\n <bs-grid>\n <bs-tab-control>\n @for (tab of resolvedTabs(); track tab.id) {\n <bs-tab-page>\n <ng-template bsTabPageHeader>{{ tab.label | resolveTranslation:tab.name }}</ng-template>\n <ng-container *ngTemplateOutlet=\"tabContent; context: { $implicit: tab }\"></ng-container>\n </bs-tab-page>\n }\n </bs-tab-control>\n\n <ng-template #tabContent let-tab>\n @if (tab.id === '__default__') {\n @if (ungroupedAttributes().length > 0) {\n <bs-card class=\"d-block m-3\">\n <div class=\"p-3\">\n @for (attr of ungroupedAttributes(); track attr.id) {\n <ng-container *ngTemplateOutlet=\"attrField; context: { $implicit: attr }\"></ng-container>\n }\n </div>\n </bs-card>\n }\n }\n @for (group of groupsForTab(tab); track group.id) {\n @if (attrsForGroup(group); as groupAttrs) {\n @if (groupAttrs.length > 0) {\n <bs-card class=\"d-block m-3\">\n @if (group.label) {\n <bs-card-header>{{ group.label | resolveTranslation:group.name }}</bs-card-header>\n }\n <div class=\"p-3\">\n @for (attr of groupAttrs; track attr.id) {\n <ng-container *ngTemplateOutlet=\"attrField; context: { $implicit: attr }\"></ng-container>\n }\n </div>\n </bs-card>\n }\n }\n }\n </ng-template>\n\n <ng-template #attrField let-attr>\n <div bsRow class=\"mb-3\">\n <label [md]=\"4\" bsColFormLabel [for]=\"attr.name\">\n {{ attr.label | resolveTranslation:attr.name }}\n @if (attr.isRequired) {\n <span class=\"text-danger\">*</span>\n }\n </label>\n <div [md]=\"8\">\n @if (attr.dataType === 'boolean') {\n <bs-toggle-button\n [ngModel]=\"formData()[attr.name]\"\n (ngModelChange)=\"formData()[attr.name] = $event; onFieldChange()\">\n </bs-toggle-button>\n } @else if (attr.lookupReferenceType) {\n @if ((attr | lookupDisplayType:lookupReferenceOptions()) === ELookupDisplayType.Modal) {\n <bs-input-group>\n <input\n type=\"text\"\n [id]=\"attr.name\"\n [value]=\"attr | lookupDisplayValue:formData():lookupReferenceOptions()\"\n readonly\n [class.is-invalid]=\"hasError(attr.name)\">\n <button\n type=\"button\"\n bsInputGroupBtn\n [color]=\"colors.secondary\"\n (click)=\"openLookupSelector(attr)\">\n ...\n </button>\n </bs-input-group>\n } @else {\n <bs-select\n [ngModel]=\"formData()[attr.name]\"\n (ngModelChange)=\"formData()[attr.name] = $event; onFieldChange()\"\n [id]=\"attr.name\"\n [class.is-invalid]=\"hasError(attr.name)\">\n <option [ngValue]=\"null\">{{ 'selectPlaceholder' | t }}</option>\n @for (option of (attr | lookupOptions:lookupReferenceOptions()); track option.key) {\n <option [ngValue]=\"option.key\">\n {{ option.values | resolveTranslation:option.key }}\n </option>\n }\n </bs-select>\n }\n } @else if (attr.dataType === 'Reference') {\n <bs-input-group>\n <input\n type=\"text\"\n [id]=\"attr.name\"\n [value]=\"attr | referenceDisplayValue:formData():referenceOptions()\"\n readonly\n [class.is-invalid]=\"hasError(attr.name)\">\n <button\n type=\"button\"\n bsInputGroupBtn\n [color]=\"colors.secondary\"\n (click)=\"openReferenceSelector(attr)\">\n ...\n </button>\n </bs-input-group>\n } @else if (attr.dataType === 'AsDetail' && attr.isArray && attr.editMode === 'inline') {\n <bs-table [isResponsive]=\"true\" class=\"mb-1\">\n <thead>\n <tr>\n @for (col of (attr | asDetailColumns:asDetailTypes()); track col.name) {\n <th>{{ col.label | resolveTranslation:col.name }}</th>\n }\n <th style=\"width: 50px\"></th>\n </tr>\n </thead>\n <tbody>\n @for (row of formData()[attr.name] || []; track $index) {\n <tr>\n @for (col of (attr | asDetailColumns:asDetailTypes()); track col.name) {\n <td>\n @if (col.dataType === 'boolean') {\n <bs-toggle-button\n [(ngModel)]=\"row[col.name]\"\n (ngModelChange)=\"onFieldChange()\">\n </bs-toggle-button>\n } @else if (col.dataType === 'Reference' && col.query) {\n <bs-select\n [(ngModel)]=\"row[col.name]\"\n (ngModelChange)=\"onFieldChange()\">\n <option [ngValue]=\"null\">{{ 'selectPlaceholder' | t }}</option>\n @for (option of (attr | inlineRefOptions:col:asDetailReferenceOptions()); track option.id) {\n <option [ngValue]=\"option.id\">\n {{ option.breadcrumb || option.name || option.id }}\n </option>\n }\n </bs-select>\n } @else {\n <input\n [type]=\"col.dataType | inputType\"\n [(ngModel)]=\"row[col.name]\"\n [required]=\"col.isRequired\"\n [step]=\"col.dataType === 'decimal' ? '0.01' : '1'\"\n (ngModelChange)=\"onFieldChange()\">\n }\n </td>\n }\n <td class=\"text-nowrap\">\n @if (attr | canDeleteDetailRow:asDetailPermissions()) {\n <button type=\"button\" [color]=\"colors.secondary\" (click)=\"removeArrayItem(attr, $index)\">\n <spark-icon name=\"trash\" />\n </button>\n }\n </td>\n </tr>\n } @empty {\n <tr>\n <td [attr.colspan]=\"(attr | asDetailColumns:asDetailTypes()).length + 1\" class=\"text-center text-muted\">\n {{ 'noItemsFound' | t }}\n </td>\n </tr>\n }\n </tbody>\n </bs-table>\n @if (attr | canCreateDetailRow:asDetailPermissions()) {\n <button type=\"button\" [color]=\"colors.primary\" class=\"w-100 rounded-0\" (click)=\"addInlineRow(attr)\">\n <spark-icon name=\"plus\" /> {{ 'add' | t }}\n </button>\n }\n } @else if (attr.dataType === 'AsDetail' && attr.isArray) {\n <bs-table [isResponsive]=\"true\" class=\"mb-1\">\n <thead>\n <tr>\n @for (col of (attr | asDetailColumns:asDetailTypes()); track col.name) {\n <th>{{ col.label | resolveTranslation:col.name }}</th>\n }\n <th style=\"width: 80px\"></th>\n </tr>\n </thead>\n <tbody>\n @for (row of formData()[attr.name] || []; track $index) {\n <tr>\n @for (col of (attr | asDetailColumns:asDetailTypes()); track col.name) {\n <td>{{ row | asDetailCellValue:attr:col:asDetailReferenceOptions() }}</td>\n }\n <td class=\"text-nowrap\">\n <button type=\"button\" class=\"btn btn-sm btn-outline-secondary me-1\" (click)=\"editArrayItem(attr, $index)\">\n <spark-icon name=\"pencil\" />\n </button>\n @if (attr | canDeleteDetailRow:asDetailPermissions()) {\n <button type=\"button\" class=\"btn btn-sm btn-outline-danger\" (click)=\"removeArrayItem(attr, $index)\">\n <spark-icon name=\"trash\" />\n </button>\n }\n </td>\n </tr>\n } @empty {\n <tr>\n <td [attr.colspan]=\"(attr | asDetailColumns:asDetailTypes()).length + 1\" class=\"text-center text-muted\">\n {{ 'noItemsFound' | t }}\n </td>\n </tr>\n }\n </tbody>\n </bs-table>\n @if (attr | canCreateDetailRow:asDetailPermissions()) {\n <button type=\"button\" [color]=\"colors.primary\" class=\"w-100 rounded-0\" (click)=\"addArrayItem(attr)\">\n <spark-icon name=\"plus\" /> {{ 'add' | t }}\n </button>\n }\n } @else if (attr.dataType === 'AsDetail') {\n <bs-input-group>\n <input\n type=\"text\"\n [id]=\"attr.name\"\n [value]=\"attr | asDetailDisplayValue:formData():asDetailTypes()\"\n readonly\n [class.is-invalid]=\"hasError(attr.name)\">\n <button\n type=\"button\"\n bsInputGroupBtn\n [color]=\"colors.secondary\"\n (click)=\"openAsDetailEditor(attr)\">\n <spark-icon name=\"pencil\" />\n </button>\n </bs-input-group>\n } @else if (getEditRendererComponent(attr); as editComp) {\n <ng-container *ngComponentOutlet=\"editComp; inputs: getEditRendererInputs(attr)\"></ng-container>\n } @else {\n <input\n [type]=\"attr.dataType | inputType\"\n [id]=\"attr.name\"\n [ngModel]=\"formData()[attr.name]\"\n (ngModelChange)=\"formData()[attr.name] = $event; onFieldChange()\"\n [name]=\"attr.name\"\n [required]=\"attr.isRequired\"\n [step]=\"attr.dataType === 'decimal' ? '0.01' : '1'\"\n [class.is-invalid]=\"hasError(attr.name)\">\n }\n @if (attr.name | errorForAttribute:validationErrors(); as errorMsg) {\n <div class=\"invalid-feedback d-block\">\n {{ errorMsg }}\n </div>\n }\n </div>\n </div>\n </ng-template>\n\n @if (showButtons()) {\n <div bsRow class=\"mt-4\">\n <div [md]=\"4\"></div>\n <div [md]=\"8\" class=\"d-flex justify-content-end gap-2\">\n <button type=\"button\" [color]=\"colors.secondary\" (click)=\"onCancel()\" [disabled]=\"isSaving()\">{{ 'cancel' | t }}</button>\n <button type=\"submit\" [color]=\"colors.primary\" [disabled]=\"isSaving()\" (click)=\"onSave()\">\n @if (isSaving()) {\n <bs-spinner class=\"me-1\" />\n }\n {{ 'save' | t }}\n </button>\n </div>\n </div>\n }\n </bs-grid>\n}\n\n<!-- Modal for editing AsDetail objects -->\n<bs-modal [isOpen]=\"showAsDetailModal()\" (isOpenChange)=\"!$event && closeAsDetailModal()\">\n <div *bsModal>\n <div bsModalHeader>\n <h5 class=\"modal-title\">{{ 'edit' | t }} {{ editingAsDetailAttr()?.label | resolveTranslation:editingAsDetailAttr()?.name }}</h5>\n </div>\n\n @if (editingAsDetailAttr(); as attr) {\n <div bsModalBody>\n <spark-po-form\n [entityType]=\"attr | asDetailType:asDetailTypes()\"\n [(formData)]=\"asDetailFormData\">\n </spark-po-form>\n </div>\n }\n\n <div bsModalFooter>\n <button type=\"button\" [color]=\"colors.secondary\" (click)=\"closeAsDetailModal()\">{{ 'cancel' | t }}</button>\n <button type=\"button\" [color]=\"colors.primary\" (click)=\"saveAsDetailObject()\">{{ 'save' | t }}</button>\n </div>\n </div>\n</bs-modal>\n\n<!-- Modal for selecting Reference items -->\n<bs-modal [isOpen]=\"showReferenceModal()\" (isOpenChange)=\"!$event && closeReferenceModal()\">\n <div *bsModal class=\"reference-modal\">\n <div bsModalHeader>\n <h5 class=\"modal-title\">{{ 'select' | t }} {{ editingReferenceAttr()?.label | resolveTranslation:editingReferenceAttr()?.name }}</h5>\n </div>\n\n <div bsModalBody>\n @if (referenceModalEntityType()) {\n <!-- Search box -->\n <bs-grid>\n <div bsRow class=\"mb-3\">\n <div [md]=\"6\">\n <bs-input-group>\n <span class=\"input-group-text\">\n <spark-icon name=\"search\" />\n </span>\n <input\n type=\"text\"\n [placeholder]=\"'search' | t\"\n [(ngModel)]=\"referenceSearchTerm\"\n (ngModelChange)=\"onReferenceSearchChange()\">\n @if (referenceSearchTerm) {\n <button\n type=\"button\"\n class=\"btn btn-outline-secondary\"\n (click)=\"clearReferenceSearch()\">\n <spark-icon name=\"x-lg\" />\n </button>\n }\n </bs-input-group>\n </div>\n <div [md]=\"6\" class=\"text-end\">\n @if (referenceSearchTerm && referenceModalPagination()) {\n <span class=\"text-muted\">\n {{ referenceModalPagination()!.totalRecords }} {{ referenceModalPagination()!.totalRecords === 1 ? ('resultFound' | t) : ('resultsFound' | t) }}\n </span>\n }\n </div>\n </div>\n </bs-grid>\n\n <bs-datatable [(settings)]=\"referenceModalSettings\" (settingsChange)=\"applyReferenceFilter()\">\n @for (attr of referenceVisibleAttributes(); track attr.id) {\n <div *bsDatatableColumn=\"attr.name; sortable: true\">\n {{ attr.label | resolveTranslation:attr.name }}\n </div>\n }\n\n <tr *bsRowTemplate=\"let item of referenceModalPagination()\" (click)=\"selectReferenceItem(item)\" style=\"cursor: pointer;\">\n @for (attr of referenceVisibleAttributes(); track attr.id) {\n <td>{{ item | referenceAttrValue:attr.name }}</td>\n }\n </tr>\n </bs-datatable>\n } @else {\n <div class=\"text-center p-3\">\n <bs-spinner />\n </div>\n }\n </div>\n\n <div bsModalFooter>\n <button type=\"button\" [color]=\"colors.secondary\" (click)=\"closeReferenceModal()\">{{ 'cancel' | t }}</button>\n </div>\n </div>\n</bs-modal>\n\n<!-- Modal for selecting LookupReference items -->\n<bs-modal [isOpen]=\"showLookupModal()\" (isOpenChange)=\"!$event && closeLookupModal()\">\n <div *bsModal>\n <div bsModalHeader>\n <h5 class=\"modal-title\">{{ 'select' | t }} {{ editingLookupAttr()?.label | resolveTranslation:editingLookupAttr()?.name }}</h5>\n </div>\n\n <div bsModalBody>\n <!-- Search box -->\n <bs-grid>\n <div bsRow class=\"mb-3\">\n <div [col]>\n <bs-input-group>\n <span class=\"input-group-text\">\n <spark-icon name=\"search\" />\n </span>\n <input\n type=\"text\"\n [placeholder]=\"'search' | t\"\n [ngModel]=\"lookupSearchTerm()\"\n (ngModelChange)=\"lookupSearchTerm.set($event)\">\n @if (lookupSearchTerm()) {\n <button\n type=\"button\"\n class=\"btn btn-outline-secondary\"\n (click)=\"lookupSearchTerm.set('')\">\n <spark-icon name=\"x-lg\" />\n </button>\n }\n </bs-input-group>\n </div>\n </div>\n </bs-grid>\n\n <!-- List of items -->\n <bs-table [striped]=\"true\" [hover]=\"true\">\n <tbody>\n @for (item of filteredLookupItems(); track item.key) {\n <tr\n [class.table-primary]=\"formData()[editingLookupAttr()?.name ?? ''] === item.key\"\n (click)=\"selectLookupItem(item)\"\n style=\"cursor: pointer;\">\n <td>{{ item.values | resolveTranslation:item.key }}</td>\n </tr>\n } @empty {\n <tr>\n <td class=\"text-center text-muted\">{{ 'noItemsFound' | t }}</td>\n </tr>\n }\n </tbody>\n </bs-table>\n </div>\n\n <div bsModalFooter>\n <button type=\"button\" [color]=\"colors.secondary\" (click)=\"closeLookupModal()\">{{ 'cancel' | t }}</button>\n </div>\n </div>\n</bs-modal>\n</bs-form>\n", dependencies: [{ kind: "component", type: SparkPoFormComponent, selector: "spark-po-form", inputs: ["entityType", "formData", "validationErrors", "showButtons", "isSaving"], outputs: ["formDataChange", "save", "cancel"] }, { kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { 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.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: BsCardComponent, selector: "bs-card", inputs: ["rounded"] }, { kind: "component", type: BsCardHeaderComponent, selector: "bs-card-header", inputs: ["noPadding"] }, { kind: "component", type: BsFormComponent, selector: "bs-form", inputs: ["action", "method"], outputs: ["submitted"] }, { kind: "directive", type: BsFormControlDirective, selector: "bs-form input:not(.no-form-control), bs-form textarea:not(.no-form-control)" }, { kind: "component", type: BsGridComponent, selector: "bs-grid", inputs: ["stopFullWidthAt"] }, { kind: "directive", type: BsGridRowDirective, selector: "[bsRow]" }, { kind: "directive", type: BsGridColumnDirective, selector: "[xxs],[xs],[sm],[md],[lg],[xl],[xxl]", inputs: ["xxs", "xs", "sm", "md", "lg", "xl", "xxl"] }, { kind: "directive", type: BsGridColDirective, selector: "[col]", inputs: ["col"] }, { kind: "directive", type: BsColFormLabelDirective, selector: "[bsColFormLabel]" }, { kind: "directive", type: BsButtonTypeDirective, selector: "button[color],input[type=\"button\"][color],input[type=\"submit\"][color],a[color]", inputs: ["color"] }, { kind: "component", type: BsInputGroupComponent, selector: "bs-input-group" }, { kind: "component", type: BsSelectComponent, selector: "bs-select", inputs: ["identifier", "size", "multiple", "numberVisible", "disabled"] }, { kind: "directive", type: BsSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "component", type: BsModalHostComponent, selector: "bs-modal", inputs: ["isOpen", "closeOnEscape"], outputs: ["isOpenChange"] }, { kind: "directive", type: BsModalDirective, selector: "[bsModal]" }, { kind: "directive", type: BsModalHeaderDirective, selector: "[bsModalHeader]" }, { kind: "directive", type: BsModalBodyDirective, selector: "[bsModalBody]" }, { kind: "directive", type: BsModalFooterDirective, selector: "[bsModalFooter]" }, { kind: "component", type: BsDatatableComponent, selector: "bs-datatable", inputs: ["settings", "data"], outputs: ["settingsChange", "dataChange"] }, { kind: "directive", type: BsDatatableColumnDirective, selector: "[bsDatatableColumn]", inputs: ["bsDatatableColumn", "bsDatatableColumnSortable"] }, { kind: "directive", type: BsRowTemplateDirective, selector: "[bsRowTemplate]", inputs: ["bsRowTemplateOf"] }, { kind: "component", type: BsTableComponent, selector: "bs-table", inputs: ["isResponsive", "striped", "hover"] }, { kind: "component", type: BsToggleButtonComponent, selector: "bs-toggle-button", inputs: ["type", "isToggled", "name", "value", "group"], outputs: ["isToggledChange"] }, { kind: "component", type: BsSpinnerComponent, selector: "bs-spinner", inputs: ["type", "color"] }, { kind: "component", type: BsTabControlComponent, selector: "bs-tab-control", inputs: ["border", "restrictDragging", "selectFirstTab", "tabsPosition", "allowDragDrop"] }, { kind: "component", type: BsTabPageComponent, selector: "bs-tab-page", inputs: ["disabled"] }, { kind: "directive", type: BsTabPageHeaderDirective, selector: "[bsTabPageHeader]" }, { kind: "component", type: SparkIconComponent, selector: "spark-icon", inputs: ["name"] }, { kind: "pipe", type: TranslateKeyPipe, name: "t" }, { kind: "pipe", type: ResolveTranslationPipe, name: "resolveTranslation" }, { kind: "pipe", type: InputTypePipe, name: "inputType" }, { kind: "pipe", type: LookupDisplayValuePipe, name: "lookupDisplayValue" }, { kind: "pipe", type: LookupDisplayTypePipe, name: "lookupDisplayType" }, { kind: "pipe", type: LookupOptionsPipe, name: "lookupOptions" }, { kind: "pipe", type: ReferenceDisplayValuePipe, name: "referenceDisplayValue" }, { kind: "pipe", type: AsDetailDisplayValuePipe, name: "asDetailDisplayValue" }, { kind: "pipe", type: AsDetailTypePipe, name: "asDetailType" }, { kind: "pipe", type: AsDetailColumnsPipe, name: "asDetailColumns" }, { kind: "pipe", type: AsDetailCellValuePipe, name: "asDetailCellValue" }, { kind: "pipe", type: CanCreateDetailRowPipe, name: "canCreateDetailRow" }, { kind: "pipe", type: CanDeleteDetailRowPipe, name: "canDeleteDetailRow" }, { kind: "pipe", type: InlineRefOptionsPipe, name: "inlineRefOptions" }, { kind: "pipe", type: ReferenceAttrValuePipe, name: "referenceAttrValue" }, { kind: "pipe", type: ErrorForAttributePipe, name: "errorForAttribute" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1074
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: SparkPoFormComponent, isStandalone: true, selector: "spark-po-form", inputs: { entityType: { classPropertyName: "entityType", publicName: "entityType", isSignal: true, isRequired: false, transformFunction: null }, formData: { classPropertyName: "formData", publicName: "formData", isSignal: true, isRequired: false, transformFunction: null }, validationErrors: { classPropertyName: "validationErrors", publicName: "validationErrors", isSignal: true, isRequired: false, transformFunction: null }, showButtons: { classPropertyName: "showButtons", publicName: "showButtons", isSignal: true, isRequired: false, transformFunction: null }, isSaving: { classPropertyName: "isSaving", publicName: "isSaving", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { formData: "formDataChange", save: "save", cancel: "cancel" }, ngImport: i0, template: "<bs-form>\n @if (entityType()) {\n <bs-grid>\n <bs-tab-control>\n @for (tab of resolvedTabs(); track tab.id) {\n <bs-tab-page>\n <ng-template bsTabPageHeader>{{ tab.label | resolveTranslation:tab.name }}</ng-template>\n <ng-container *ngTemplateOutlet=\"tabContent; context: { $implicit: tab }\"></ng-container>\n </bs-tab-page>\n }\n </bs-tab-control>\n\n <ng-template #tabContent let-tab>\n @if (tab.id === '__default__') {\n @if (ungroupedAttributes().length > 0) {\n <bs-card class=\"d-block m-3\">\n <div class=\"p-3\">\n @for (attr of ungroupedAttributes(); track attr.id) {\n <ng-container *ngTemplateOutlet=\"attrField; context: { $implicit: attr }\"></ng-container>\n }\n </div>\n </bs-card>\n }\n }\n @for (group of groupsForTab(tab); track group.id) {\n @if (attrsForGroup(group); as groupAttrs) {\n @if (groupAttrs.length > 0) {\n <bs-card class=\"d-block m-3\">\n @if (group.label) {\n <bs-card-header>{{ group.label | resolveTranslation:group.name }}</bs-card-header>\n }\n <div class=\"p-3\">\n @for (attr of groupAttrs; track attr.id) {\n <ng-container *ngTemplateOutlet=\"attrField; context: { $implicit: attr }\"></ng-container>\n }\n </div>\n </bs-card>\n }\n }\n }\n </ng-template>\n\n <ng-template #attrField let-attr>\n <div bsRow class=\"mb-3\">\n <label [md]=\"4\" bsColFormLabel [for]=\"attr.name\">\n {{ attr.label | resolveTranslation:attr.name }}\n @if (attr.isRequired) {\n <span class=\"text-danger\">*</span>\n }\n </label>\n <div [md]=\"8\">\n @if (attr.dataType === 'boolean') {\n <bs-toggle-button\n [ngModel]=\"formData()[attr.name]\"\n (ngModelChange)=\"formData()[attr.name] = $event; onFieldChange()\">\n </bs-toggle-button>\n } @else if (attr.lookupReferenceType) {\n @if ((attr | lookupDisplayType:lookupReferenceOptions()) === ELookupDisplayType.Modal) {\n <bs-input-group>\n <input\n type=\"text\"\n [id]=\"attr.name\"\n [value]=\"attr | lookupDisplayValue:formData():lookupReferenceOptions()\"\n readonly\n [class.is-invalid]=\"hasError(attr.name)\">\n <button\n type=\"button\"\n bsInputGroupBtn\n [color]=\"colors.secondary\"\n (click)=\"openLookupSelector(attr)\">\n ...\n </button>\n </bs-input-group>\n } @else {\n <bs-select\n [ngModel]=\"formData()[attr.name]\"\n (ngModelChange)=\"formData()[attr.name] = $event; onFieldChange()\"\n [id]=\"attr.name\"\n [class.is-invalid]=\"hasError(attr.name)\">\n <option [ngValue]=\"null\">{{ 'selectPlaceholder' | t }}</option>\n @for (option of (attr | lookupOptions:lookupReferenceOptions()); track option.key) {\n <option [ngValue]=\"option.key\">\n {{ option.values | resolveTranslation:option.key }}\n </option>\n }\n </bs-select>\n }\n } @else if (attr.dataType === 'Reference') {\n <bs-input-group>\n <input\n type=\"text\"\n [id]=\"attr.name\"\n [value]=\"attr | referenceDisplayValue:formData():referenceOptions()\"\n readonly\n [class.is-invalid]=\"hasError(attr.name)\">\n <button\n type=\"button\"\n bsInputGroupBtn\n [color]=\"colors.secondary\"\n (click)=\"openReferenceSelector(attr)\">\n ...\n </button>\n </bs-input-group>\n } @else if (attr.dataType === 'AsDetail' && attr.isArray && attr.editMode === 'inline') {\n <bs-table [isResponsive]=\"true\" class=\"mb-1\">\n <thead>\n <tr>\n @for (col of (attr | asDetailColumns:asDetailTypes()); track col.name) {\n <th>{{ col.label | resolveTranslation:col.name }}</th>\n }\n <th style=\"width: 50px\"></th>\n </tr>\n </thead>\n <tbody>\n @for (row of formData()[attr.name] || []; track $index) {\n <tr>\n @for (col of (attr | asDetailColumns:asDetailTypes()); track col.name) {\n <td>\n @if (col.dataType === 'boolean') {\n <bs-toggle-button\n [(ngModel)]=\"row[col.name]\"\n (ngModelChange)=\"onFieldChange()\">\n </bs-toggle-button>\n } @else if (col.dataType === 'Reference' && col.query) {\n <bs-select\n [(ngModel)]=\"row[col.name]\"\n (ngModelChange)=\"onFieldChange()\">\n <option [ngValue]=\"null\">{{ 'selectPlaceholder' | t }}</option>\n @for (option of (attr | inlineRefOptions:col:asDetailReferenceOptions()); track option.id) {\n <option [ngValue]=\"option.id\">\n {{ option.breadcrumb || option.name || option.id }}\n </option>\n }\n </bs-select>\n } @else {\n <input\n [type]=\"col.dataType | inputType\"\n [(ngModel)]=\"row[col.name]\"\n [required]=\"col.isRequired\"\n [step]=\"col.dataType === 'decimal' ? '0.01' : '1'\"\n (ngModelChange)=\"onFieldChange()\">\n }\n </td>\n }\n <td class=\"text-nowrap\">\n @if (attr | canDeleteDetailRow:asDetailPermissions()) {\n <button type=\"button\" [color]=\"colors.secondary\" (click)=\"removeArrayItem(attr, $index)\">\n <spark-icon name=\"trash\" />\n </button>\n }\n </td>\n </tr>\n } @empty {\n <tr>\n <td [attr.colspan]=\"(attr | asDetailColumns:asDetailTypes()).length + 1\" class=\"text-center text-muted\">\n {{ 'noItemsFound' | t }}\n </td>\n </tr>\n }\n </tbody>\n </bs-table>\n @if (attr | canCreateDetailRow:asDetailPermissions()) {\n <button type=\"button\" [color]=\"colors.primary\" class=\"w-100 rounded-0\" (click)=\"addInlineRow(attr)\">\n <spark-icon name=\"plus\" /> {{ 'add' | t }}\n </button>\n }\n } @else if (attr.dataType === 'AsDetail' && attr.isArray) {\n <bs-table [isResponsive]=\"true\" class=\"mb-1\">\n <thead>\n <tr>\n @for (col of (attr | asDetailColumns:asDetailTypes()); track col.name) {\n <th>{{ col.label | resolveTranslation:col.name }}</th>\n }\n <th style=\"width: 80px\"></th>\n </tr>\n </thead>\n <tbody>\n @for (row of formData()[attr.name] || []; track $index) {\n <tr>\n @for (col of (attr | asDetailColumns:asDetailTypes()); track col.name) {\n <td>{{ row | asDetailCellValue:attr:col:asDetailReferenceOptions() }}</td>\n }\n <td class=\"text-nowrap\">\n <button type=\"button\" class=\"btn btn-sm btn-outline-secondary me-1\" (click)=\"editArrayItem(attr, $index)\">\n <spark-icon name=\"pencil\" />\n </button>\n @if (attr | canDeleteDetailRow:asDetailPermissions()) {\n <button type=\"button\" class=\"btn btn-sm btn-outline-danger\" (click)=\"removeArrayItem(attr, $index)\">\n <spark-icon name=\"trash\" />\n </button>\n }\n </td>\n </tr>\n } @empty {\n <tr>\n <td [attr.colspan]=\"(attr | asDetailColumns:asDetailTypes()).length + 1\" class=\"text-center text-muted\">\n {{ 'noItemsFound' | t }}\n </td>\n </tr>\n }\n </tbody>\n </bs-table>\n @if (attr | canCreateDetailRow:asDetailPermissions()) {\n <button type=\"button\" [color]=\"colors.primary\" class=\"w-100 rounded-0\" (click)=\"addArrayItem(attr)\">\n <spark-icon name=\"plus\" /> {{ 'add' | t }}\n </button>\n }\n } @else if (attr.dataType === 'AsDetail') {\n <bs-input-group>\n <input\n type=\"text\"\n [id]=\"attr.name\"\n [value]=\"attr | asDetailDisplayValue:formData():asDetailTypes()\"\n readonly\n [class.is-invalid]=\"hasError(attr.name)\">\n <button\n type=\"button\"\n bsInputGroupBtn\n [color]=\"colors.secondary\"\n (click)=\"openAsDetailEditor(attr)\">\n <spark-icon name=\"pencil\" />\n </button>\n </bs-input-group>\n } @else if (getEditRendererComponent(attr); as editComp) {\n <ng-container *ngComponentOutlet=\"editComp; inputs: getEditRendererInputs(attr)\"></ng-container>\n } @else {\n <input\n [type]=\"attr.dataType | inputType\"\n [id]=\"attr.name\"\n [ngModel]=\"formData()[attr.name]\"\n (ngModelChange)=\"formData()[attr.name] = $event; onFieldChange()\"\n [name]=\"attr.name\"\n [required]=\"attr.isRequired\"\n [step]=\"attr.dataType === 'decimal' ? '0.01' : '1'\"\n [class.is-invalid]=\"hasError(attr.name)\">\n }\n @if (attr.name | errorForAttribute:validationErrors(); as errorMsg) {\n <div class=\"invalid-feedback d-block\">\n {{ errorMsg }}\n </div>\n }\n </div>\n </div>\n </ng-template>\n\n @if (showButtons()) {\n <div bsRow class=\"mt-4\">\n <div [md]=\"4\"></div>\n <div [md]=\"8\" class=\"d-flex justify-content-end gap-2\">\n <button type=\"button\" [color]=\"colors.secondary\" (click)=\"onCancel()\" [disabled]=\"isSaving()\">{{ 'cancel' | t }}</button>\n <button type=\"submit\" [color]=\"colors.primary\" [disabled]=\"isSaving()\" (click)=\"onSave()\">\n @if (isSaving()) {\n <bs-spinner class=\"me-1\" />\n }\n {{ 'save' | t }}\n </button>\n </div>\n </div>\n }\n </bs-grid>\n}\n\n<!-- Modal for editing AsDetail objects -->\n<bs-modal [isOpen]=\"showAsDetailModal()\" (isOpenChange)=\"!$event && closeAsDetailModal()\">\n <div *bsModal>\n <div bsModalHeader>\n <h5 class=\"modal-title\">{{ 'edit' | t }} {{ editingAsDetailAttr()?.label | resolveTranslation:editingAsDetailAttr()?.name }}</h5>\n </div>\n\n @if (editingAsDetailAttr(); as attr) {\n <div bsModalBody>\n <spark-po-form\n [entityType]=\"attr | asDetailType:asDetailTypes()\"\n [(formData)]=\"asDetailFormData\">\n </spark-po-form>\n </div>\n }\n\n <div bsModalFooter>\n <button type=\"button\" [color]=\"colors.secondary\" (click)=\"closeAsDetailModal()\">{{ 'cancel' | t }}</button>\n <button type=\"button\" [color]=\"colors.primary\" (click)=\"saveAsDetailObject()\">{{ 'save' | t }}</button>\n </div>\n </div>\n</bs-modal>\n\n<!-- Modal for selecting Reference items -->\n<bs-modal [isOpen]=\"showReferenceModal()\" (isOpenChange)=\"!$event && closeReferenceModal()\">\n <div *bsModal class=\"reference-modal\">\n <div bsModalHeader>\n <h5 class=\"modal-title\">{{ 'select' | t }} {{ editingReferenceAttr()?.label | resolveTranslation:editingReferenceAttr()?.name }}</h5>\n </div>\n\n <div bsModalBody>\n @if (referenceModalEntityType()) {\n <!-- Search box -->\n <bs-grid>\n <div bsRow class=\"mb-3\">\n <div [md]=\"6\">\n <bs-input-group>\n <span class=\"input-group-text\">\n <spark-icon name=\"search\" />\n </span>\n <input\n type=\"text\"\n [placeholder]=\"'search' | t\"\n [(ngModel)]=\"referenceSearchTerm\"\n (ngModelChange)=\"onReferenceSearchChange()\">\n @if (referenceSearchTerm) {\n <button\n type=\"button\"\n class=\"btn btn-outline-secondary\"\n (click)=\"clearReferenceSearch()\">\n <spark-icon name=\"x-lg\" />\n </button>\n }\n </bs-input-group>\n </div>\n <div [md]=\"6\" class=\"text-end\">\n @if (referenceSearchTerm && referenceModalPagination()) {\n <span class=\"text-muted\">\n {{ referenceModalPagination()!.totalRecords }} {{ referenceModalPagination()!.totalRecords === 1 ? ('resultFound' | t) : ('resultsFound' | t) }}\n </span>\n }\n </div>\n </div>\n </bs-grid>\n\n <bs-datatable [(settings)]=\"referenceModalSettings\" (settingsChange)=\"applyReferenceFilter()\">\n @for (attr of referenceVisibleAttributes(); track attr.id) {\n <div *bsDatatableColumn=\"attr.name; sortable: true\">\n {{ attr.label | resolveTranslation:attr.name }}\n </div>\n }\n\n <tr *bsRowTemplate=\"let item of referenceModalPagination()\" (click)=\"selectReferenceItem(item)\" style=\"cursor: pointer;\">\n @for (attr of referenceVisibleAttributes(); track attr.id) {\n <td>{{ item | referenceAttrValue:attr.name }}</td>\n }\n </tr>\n </bs-datatable>\n } @else {\n <div class=\"text-center p-3\">\n <bs-spinner />\n </div>\n }\n </div>\n\n <div bsModalFooter>\n <button type=\"button\" [color]=\"colors.secondary\" (click)=\"closeReferenceModal()\">{{ 'cancel' | t }}</button>\n </div>\n </div>\n</bs-modal>\n\n<!-- Modal for selecting LookupReference items -->\n<bs-modal [isOpen]=\"showLookupModal()\" (isOpenChange)=\"!$event && closeLookupModal()\">\n <div *bsModal>\n <div bsModalHeader>\n <h5 class=\"modal-title\">{{ 'select' | t }} {{ editingLookupAttr()?.label | resolveTranslation:editingLookupAttr()?.name }}</h5>\n </div>\n\n <div bsModalBody>\n <!-- Search box -->\n <bs-grid>\n <div bsRow class=\"mb-3\">\n <div [col]>\n <bs-input-group>\n <span class=\"input-group-text\">\n <spark-icon name=\"search\" />\n </span>\n <input\n type=\"text\"\n [placeholder]=\"'search' | t\"\n [ngModel]=\"lookupSearchTerm()\"\n (ngModelChange)=\"lookupSearchTerm.set($event)\">\n @if (lookupSearchTerm()) {\n <button\n type=\"button\"\n class=\"btn btn-outline-secondary\"\n (click)=\"lookupSearchTerm.set('')\">\n <spark-icon name=\"x-lg\" />\n </button>\n }\n </bs-input-group>\n </div>\n </div>\n </bs-grid>\n\n <!-- List of items -->\n <bs-table [striped]=\"true\" [hover]=\"true\">\n <tbody>\n @for (item of filteredLookupItems(); track item.key) {\n <tr\n [class.table-primary]=\"formData()[editingLookupAttr()?.name ?? ''] === item.key\"\n (click)=\"selectLookupItem(item)\"\n style=\"cursor: pointer;\">\n <td>{{ item.values | resolveTranslation:item.key }}</td>\n </tr>\n } @empty {\n <tr>\n <td class=\"text-center text-muted\">{{ 'noItemsFound' | t }}</td>\n </tr>\n }\n </tbody>\n </bs-table>\n </div>\n\n <div bsModalFooter>\n <button type=\"button\" [color]=\"colors.secondary\" (click)=\"closeLookupModal()\">{{ 'cancel' | t }}</button>\n </div>\n </div>\n</bs-modal>\n</bs-form>\n", dependencies: [{ kind: "component", type: SparkPoFormComponent, selector: "spark-po-form", inputs: ["entityType", "formData", "validationErrors", "showButtons", "isSaving"], outputs: ["formDataChange", "save", "cancel"] }, { kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { 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.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: BsCardComponent, selector: "bs-card", inputs: ["rounded"] }, { kind: "component", type: BsCardHeaderComponent, selector: "bs-card-header", inputs: ["noPadding"] }, { kind: "component", type: BsFormComponent, selector: "bs-form", inputs: ["action", "method"], outputs: ["submitted"] }, { kind: "directive", type: BsFormControlDirective, selector: "bs-form input:not(.no-form-control), bs-form textarea:not(.no-form-control)" }, { kind: "component", type: BsGridComponent, selector: "bs-grid", inputs: ["stopFullWidthAt"] }, { kind: "directive", type: BsGridRowDirective, selector: "[bsRow]" }, { kind: "directive", type: BsGridColumnDirective, selector: "[xxs],[xs],[sm],[md],[lg],[xl],[xxl]", inputs: ["xxs", "xs", "sm", "md", "lg", "xl", "xxl"] }, { kind: "directive", type: BsGridColDirective, selector: "[col]", inputs: ["col"] }, { kind: "directive", type: BsColFormLabelDirective, selector: "[bsColFormLabel]" }, { kind: "directive", type: BsButtonTypeDirective, selector: "button[color],input[type=\"button\"][color],input[type=\"submit\"][color],a[color]", inputs: ["color"] }, { kind: "component", type: BsInputGroupComponent, selector: "bs-input-group" }, { kind: "component", type: BsSelectComponent, selector: "bs-select", inputs: ["identifier", "size", "multiple", "numberVisible", "disabled"] }, { kind: "directive", type: BsSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "component", type: BsModalHostComponent, selector: "bs-modal", inputs: ["isOpen", "closeOnEscape"], outputs: ["isOpenChange"] }, { kind: "directive", type: BsModalDirective, selector: "[bsModal]" }, { kind: "directive", type: BsModalHeaderDirective, selector: "[bsModalHeader]" }, { kind: "directive", type: BsModalBodyDirective, selector: "[bsModalBody]" }, { kind: "directive", type: BsModalFooterDirective, selector: "[bsModalFooter]" }, { kind: "component", type: BsDatatableComponent, selector: "bs-datatable", inputs: ["data"], outputs: ["dataChange"] }, { kind: "directive", type: BsDatatableColumnDirective, selector: "[bsDatatableColumn]", inputs: ["bsDatatableColumn", "bsDatatableColumnSortable"] }, { kind: "directive", type: BsRowTemplateDirective, selector: "[bsRowTemplate]", inputs: ["bsRowTemplateOf"] }, { kind: "component", type: BsTableComponent, selector: "bs-table", inputs: ["isResponsive", "striped", "hover"] }, { kind: "component", type: BsToggleButtonComponent, selector: "bs-toggle-button", inputs: ["type", "isToggled", "name", "value", "group"], outputs: ["isToggledChange"] }, { kind: "component", type: BsSpinnerComponent, selector: "bs-spinner", inputs: ["type", "color"] }, { kind: "component", type: BsTabControlComponent, selector: "bs-tab-control", inputs: ["border", "restrictDragging", "selectFirstTab", "tabsPosition", "allowDragDrop"] }, { kind: "component", type: BsTabPageComponent, selector: "bs-tab-page", inputs: ["disabled"] }, { kind: "directive", type: BsTabPageHeaderDirective, selector: "[bsTabPageHeader]" }, { kind: "component", type: SparkIconComponent, selector: "spark-icon", inputs: ["name"] }, { kind: "pipe", type: TranslateKeyPipe, name: "t" }, { kind: "pipe", type: ResolveTranslationPipe, name: "resolveTranslation" }, { kind: "pipe", type: InputTypePipe, name: "inputType" }, { kind: "pipe", type: LookupDisplayValuePipe, name: "lookupDisplayValue" }, { kind: "pipe", type: LookupDisplayTypePipe, name: "lookupDisplayType" }, { kind: "pipe", type: LookupOptionsPipe, name: "lookupOptions" }, { kind: "pipe", type: ReferenceDisplayValuePipe, name: "referenceDisplayValue" }, { kind: "pipe", type: AsDetailDisplayValuePipe, name: "asDetailDisplayValue" }, { kind: "pipe", type: AsDetailTypePipe, name: "asDetailType" }, { kind: "pipe", type: AsDetailColumnsPipe, name: "asDetailColumns" }, { kind: "pipe", type: AsDetailCellValuePipe, name: "asDetailCellValue" }, { kind: "pipe", type: CanCreateDetailRowPipe, name: "canCreateDetailRow" }, { kind: "pipe", type: CanDeleteDetailRowPipe, name: "canDeleteDetailRow" }, { kind: "pipe", type: InlineRefOptionsPipe, name: "inlineRefOptions" }, { kind: "pipe", type: ReferenceAttrValuePipe, name: "referenceAttrValue" }, { kind: "pipe", type: ErrorForAttributePipe, name: "errorForAttribute" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
985
1075
|
}
|
|
986
1076
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: SparkPoFormComponent, decorators: [{
|
|
987
1077
|
type: Component,
|
|
@@ -1334,10 +1424,19 @@ class SparkSubQueryComponent {
|
|
|
1334
1424
|
query = signal(null, ...(ngDevMode ? [{ debugName: "query" }] : []));
|
|
1335
1425
|
entityType = signal(null, ...(ngDevMode ? [{ debugName: "entityType" }] : []));
|
|
1336
1426
|
allEntityTypes = signal([], ...(ngDevMode ? [{ debugName: "allEntityTypes" }] : []));
|
|
1337
|
-
|
|
1427
|
+
paginationData = signal(undefined, ...(ngDevMode ? [{ debugName: "paginationData" }] : []));
|
|
1338
1428
|
lookupReferenceOptions = signal({}, ...(ngDevMode ? [{ debugName: "lookupReferenceOptions" }] : []));
|
|
1339
1429
|
loading = signal(true, ...(ngDevMode ? [{ debugName: "loading" }] : []));
|
|
1340
1430
|
canRead = signal(false, ...(ngDevMode ? [{ debugName: "canRead" }] : []));
|
|
1431
|
+
settings = signal(new DatatableSettings({
|
|
1432
|
+
perPage: { values: [10, 25, 50], selected: 10 },
|
|
1433
|
+
page: { values: [1], selected: 1 },
|
|
1434
|
+
sortColumns: []
|
|
1435
|
+
}), ...(ngDevMode ? [{ debugName: "settings" }] : []));
|
|
1436
|
+
virtualDataSource = signal(null, ...(ngDevMode ? [{ debugName: "virtualDataSource" }] : []));
|
|
1437
|
+
virtualSettings = signal(new DatatableSettings({
|
|
1438
|
+
sortColumns: []
|
|
1439
|
+
}), ...(ngDevMode ? [{ debugName: "virtualSettings" }] : []));
|
|
1341
1440
|
visibleAttributes = computed(() => {
|
|
1342
1441
|
return this.entityType()?.attributes
|
|
1343
1442
|
.filter(a => a.isVisible && hasShowedOnFlag(a.showedOn, ShowedOn.Query))
|
|
@@ -1362,6 +1461,10 @@ class SparkSubQueryComponent {
|
|
|
1362
1461
|
]);
|
|
1363
1462
|
this.query.set(resolvedQuery);
|
|
1364
1463
|
this.allEntityTypes.set(entityTypes);
|
|
1464
|
+
const initialSortColumns = (resolvedQuery.sortColumns || []).map(sc => ({
|
|
1465
|
+
property: sc.property,
|
|
1466
|
+
direction: sc.direction === 'desc' ? 'descending' : 'ascending'
|
|
1467
|
+
}));
|
|
1365
1468
|
// Resolve entity type from query's entityType field
|
|
1366
1469
|
if (resolvedQuery.entityType) {
|
|
1367
1470
|
const et = entityTypes.find(t => t.name === resolvedQuery.entityType || t.alias === resolvedQuery.entityType?.toLowerCase());
|
|
@@ -1371,19 +1474,67 @@ class SparkSubQueryComponent {
|
|
|
1371
1474
|
this.canRead.set(permissions.canRead);
|
|
1372
1475
|
}
|
|
1373
1476
|
}
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1477
|
+
if (resolvedQuery.renderMode === 'VirtualScrolling') {
|
|
1478
|
+
this.virtualSettings.set(new DatatableSettings({ sortColumns: initialSortColumns }));
|
|
1479
|
+
this.virtualDataSource.set(new VirtualDatatableDataSource((skip, take) => this.sparkService.executeQuery(resolvedQuery.id, {
|
|
1480
|
+
sortColumns: this.virtualSettings().sortColumns,
|
|
1481
|
+
skip, take,
|
|
1482
|
+
parentId, parentType,
|
|
1483
|
+
}).then(r => ({ data: r.data, totalRecords: r.totalRecords })), 50));
|
|
1484
|
+
}
|
|
1485
|
+
else {
|
|
1486
|
+
this.settings.set(new DatatableSettings({
|
|
1487
|
+
perPage: { values: [10, 25, 50], selected: 10 },
|
|
1488
|
+
page: { values: [1], selected: 1 },
|
|
1489
|
+
sortColumns: initialSortColumns
|
|
1490
|
+
}));
|
|
1491
|
+
await this.loadPage(resolvedQuery.id, parentId, parentType);
|
|
1492
|
+
}
|
|
1378
1493
|
this.loadLookupReferenceOptions();
|
|
1379
1494
|
}
|
|
1380
1495
|
catch {
|
|
1381
|
-
this.
|
|
1496
|
+
this.paginationData.set(undefined);
|
|
1382
1497
|
}
|
|
1383
1498
|
finally {
|
|
1384
1499
|
this.loading.set(false);
|
|
1385
1500
|
}
|
|
1386
1501
|
}
|
|
1502
|
+
async loadPage(queryId, parentId, parentType) {
|
|
1503
|
+
const s = this.settings();
|
|
1504
|
+
const result = await this.sparkService.executeQuery(queryId, {
|
|
1505
|
+
sortColumns: s.sortColumns,
|
|
1506
|
+
skip: (s.page.selected - 1) * s.perPage.selected,
|
|
1507
|
+
take: s.perPage.selected,
|
|
1508
|
+
parentId, parentType,
|
|
1509
|
+
});
|
|
1510
|
+
const totalPages = Math.ceil(result.totalRecords / s.perPage.selected) || 1;
|
|
1511
|
+
this.paginationData.set({
|
|
1512
|
+
data: result.data,
|
|
1513
|
+
totalRecords: result.totalRecords,
|
|
1514
|
+
totalPages,
|
|
1515
|
+
perPage: s.perPage.selected,
|
|
1516
|
+
page: s.page.selected,
|
|
1517
|
+
});
|
|
1518
|
+
s.page.values = Array.from({ length: totalPages }, (_, i) => i + 1);
|
|
1519
|
+
}
|
|
1520
|
+
onSettingsChange() {
|
|
1521
|
+
const q = this.query();
|
|
1522
|
+
if (!q)
|
|
1523
|
+
return;
|
|
1524
|
+
if (q.renderMode === 'VirtualScrolling') {
|
|
1525
|
+
this.virtualDataSource()?.reset();
|
|
1526
|
+
const pId = this.parentId();
|
|
1527
|
+
const pType = this.parentType();
|
|
1528
|
+
this.virtualDataSource.set(new VirtualDatatableDataSource((skip, take) => this.sparkService.executeQuery(q.id, {
|
|
1529
|
+
sortColumns: this.virtualSettings().sortColumns,
|
|
1530
|
+
skip, take,
|
|
1531
|
+
parentId: pId, parentType: pType,
|
|
1532
|
+
}).then(r => ({ data: r.data, totalRecords: r.totalRecords })), 50));
|
|
1533
|
+
}
|
|
1534
|
+
else {
|
|
1535
|
+
this.loadPage(q.id, this.parentId(), this.parentType());
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1387
1538
|
async loadLookupReferenceOptions() {
|
|
1388
1539
|
const lookupAttrs = this.visibleAttributes().filter(a => a.lookupReferenceType);
|
|
1389
1540
|
if (lookupAttrs.length === 0)
|
|
@@ -1409,11 +1560,11 @@ class SparkSubQueryComponent {
|
|
|
1409
1560
|
};
|
|
1410
1561
|
}
|
|
1411
1562
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: SparkSubQueryComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1412
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: SparkSubQueryComponent, isStandalone: true, selector: "spark-sub-query", inputs: { queryId: { classPropertyName: "queryId", publicName: "queryId", isSignal: true, isRequired: true, transformFunction: null }, parentId: { classPropertyName: "parentId", publicName: "parentId", isSignal: true, isRequired: true, transformFunction: null }, parentType: { classPropertyName: "parentType", publicName: "parentType", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: "@if (query(); as q) {\n <bs-card style=\"display: block; margin: 1rem 0;\">\n <bs-card-header>{{ (q.description | resolveTranslation) || q.name }}</bs-card-header>\n @if (loading()) {\n <div class=\"text-center p-3\">\n <bs-spinner />\n </div>\n } @else if (
|
|
1563
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: SparkSubQueryComponent, isStandalone: true, selector: "spark-sub-query", inputs: { queryId: { classPropertyName: "queryId", publicName: "queryId", isSignal: true, isRequired: true, transformFunction: null }, parentId: { classPropertyName: "parentId", publicName: "parentId", isSignal: true, isRequired: true, transformFunction: null }, parentType: { classPropertyName: "parentType", publicName: "parentType", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: "@if (query(); as q) {\n <bs-card style=\"display: block; margin: 1rem 0;\">\n <bs-card-header>{{ (q.description | resolveTranslation) || q.name }}</bs-card-header>\n @if (loading()) {\n <div class=\"text-center p-3\">\n <bs-spinner />\n </div>\n } @else if (q.renderMode === 'VirtualScrolling') {\n @if (virtualDataSource(); as vds) {\n <div class=\"p-3 d-flex flex-column\" style=\"height: 400px;\">\n <bs-virtual-datatable class=\"flex-grow-1 overflow-hidden\"\n [(settings)]=\"virtualSettings\"\n [dataSource]=\"vds\"\n [isResponsive]=\"true\"\n (settingsChange)=\"onSettingsChange()\">\n @for (attr of visibleAttributes(); track attr.id) {\n <div *bsDatatableColumn=\"attr.name; sortable: true\">\n {{ (attr.label | resolveTranslation) || attr.name }}\n </div>\n }\n\n <ng-template bsVirtualRowTemplate let-item>\n @for (attr of visibleAttributes(); track attr.id; let first = $first) {\n <td>\n @if (item) {\n @if (first && canRead()) {\n <a [routerLink]=\"['/po', entityType()!.alias || entityType()!.id, item.id]\">\n <ng-container *ngTemplateOutlet=\"cellContent; context: { $implicit: item, attr: attr }\"></ng-container>\n </a>\n } @else {\n <ng-container *ngTemplateOutlet=\"cellContent; context: { $implicit: item, attr: attr }\"></ng-container>\n }\n } @else {\n \n }\n </td>\n }\n </ng-template>\n </bs-virtual-datatable>\n </div>\n }\n } @else {\n <div class=\"p-3\">\n @if (!paginationData() || paginationData()!.totalRecords === 0) {\n <div class=\"text-center text-muted\">\n {{ 'noItemsFound' | t }}\n </div>\n } @else {\n <bs-datatable [(settings)]=\"settings\" (settingsChange)=\"onSettingsChange()\">\n @for (attr of visibleAttributes(); track attr.id) {\n <div *bsDatatableColumn=\"attr.name; sortable: true\">\n {{ (attr.label | resolveTranslation) || attr.name }}\n </div>\n }\n\n <tr *bsRowTemplate=\"let item of paginationData()\">\n @for (attr of visibleAttributes(); track attr.id; let first = $first) {\n <td>\n @if (first && canRead()) {\n <a [routerLink]=\"['/po', entityType()!.alias || entityType()!.id, item.id]\">\n <ng-container *ngTemplateOutlet=\"cellContent; context: { $implicit: item, attr: attr }\"></ng-container>\n </a>\n } @else {\n <ng-container *ngTemplateOutlet=\"cellContent; context: { $implicit: item, attr: attr }\"></ng-container>\n }\n </td>\n }\n </tr>\n </bs-datatable>\n }\n </div>\n }\n </bs-card>\n}\n\n<ng-template #cellContent let-item let-attr=\"attr\">\n @if (getColumnRendererComponent(attr); as rendererType) {\n <ng-container *ngComponentOutlet=\"rendererType; inputs: getColumnRendererInputs(item, attr)\"></ng-container>\n } @else if (attr.dataType === 'boolean') {\n <input type=\"checkbox\"\n [checked]=\"(attr.name | attributeValue:item:entityType():lookupReferenceOptions():allEntityTypes()) === true\"\n disabled\n onclick=\"return false;\">\n } @else if (attr.dataType === 'color') {\n @let colorVal = (attr.name | attributeValue:item:entityType():lookupReferenceOptions():allEntityTypes());\n @if (colorVal) {\n <span class=\"d-inline-block align-middle border rounded\" [style.background-color]=\"colorVal\" style=\"width: 1.5em; height: 1.5em;\"></span>\n }\n } @else {\n {{ (attr.name | attributeValue:item:entityType():lookupReferenceOptions():allEntityTypes()) }}\n }\n</ng-template>\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: RouterModule }, { kind: "directive", type: i2$1.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "component", type: BsCardComponent, selector: "bs-card", inputs: ["rounded"] }, { kind: "component", type: BsCardHeaderComponent, selector: "bs-card-header", inputs: ["noPadding"] }, { kind: "component", type: BsDatatableComponent, selector: "bs-datatable", inputs: ["data"], outputs: ["dataChange"] }, { kind: "directive", type: BsDatatableColumnDirective, selector: "[bsDatatableColumn]", inputs: ["bsDatatableColumn", "bsDatatableColumnSortable"] }, { kind: "directive", type: BsRowTemplateDirective, selector: "[bsRowTemplate]", inputs: ["bsRowTemplateOf"] }, { kind: "component", type: BsVirtualDatatableComponent, selector: "bs-virtual-datatable", inputs: ["dataSource", "isResponsive", "itemSize"] }, { kind: "directive", type: BsVirtualRowTemplateDirective, selector: "[bsVirtualRowTemplate]" }, { kind: "component", type: BsSpinnerComponent, selector: "bs-spinner", inputs: ["type", "color"] }, { kind: "pipe", type: ResolveTranslationPipe, name: "resolveTranslation" }, { kind: "pipe", type: TranslateKeyPipe, name: "t" }, { kind: "pipe", type: AttributeValuePipe, name: "attributeValue" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1413
1564
|
}
|
|
1414
1565
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: SparkSubQueryComponent, decorators: [{
|
|
1415
1566
|
type: Component,
|
|
1416
|
-
args: [{ selector: 'spark-sub-query', imports: [CommonModule, NgComponentOutlet, RouterModule, BsCardComponent, BsCardHeaderComponent, BsTableComponent, BsSpinnerComponent, ResolveTranslationPipe, TranslateKeyPipe, AttributeValuePipe], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (query(); as q) {\n <bs-card style=\"display: block; margin: 1rem 0;\">\n <bs-card-header>{{ (q.description | resolveTranslation) || q.name }}</bs-card-header>\n @if (loading()) {\n <div class=\"text-center p-3\">\n <bs-spinner />\n </div>\n } @else if (
|
|
1567
|
+
args: [{ selector: 'spark-sub-query', imports: [CommonModule, NgComponentOutlet, RouterModule, BsCardComponent, BsCardHeaderComponent, BsDatatableComponent, BsDatatableColumnDirective, BsRowTemplateDirective, BsVirtualDatatableComponent, BsVirtualRowTemplateDirective, BsTableComponent, BsSpinnerComponent, ResolveTranslationPipe, TranslateKeyPipe, AttributeValuePipe], changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (query(); as q) {\n <bs-card style=\"display: block; margin: 1rem 0;\">\n <bs-card-header>{{ (q.description | resolveTranslation) || q.name }}</bs-card-header>\n @if (loading()) {\n <div class=\"text-center p-3\">\n <bs-spinner />\n </div>\n } @else if (q.renderMode === 'VirtualScrolling') {\n @if (virtualDataSource(); as vds) {\n <div class=\"p-3 d-flex flex-column\" style=\"height: 400px;\">\n <bs-virtual-datatable class=\"flex-grow-1 overflow-hidden\"\n [(settings)]=\"virtualSettings\"\n [dataSource]=\"vds\"\n [isResponsive]=\"true\"\n (settingsChange)=\"onSettingsChange()\">\n @for (attr of visibleAttributes(); track attr.id) {\n <div *bsDatatableColumn=\"attr.name; sortable: true\">\n {{ (attr.label | resolveTranslation) || attr.name }}\n </div>\n }\n\n <ng-template bsVirtualRowTemplate let-item>\n @for (attr of visibleAttributes(); track attr.id; let first = $first) {\n <td>\n @if (item) {\n @if (first && canRead()) {\n <a [routerLink]=\"['/po', entityType()!.alias || entityType()!.id, item.id]\">\n <ng-container *ngTemplateOutlet=\"cellContent; context: { $implicit: item, attr: attr }\"></ng-container>\n </a>\n } @else {\n <ng-container *ngTemplateOutlet=\"cellContent; context: { $implicit: item, attr: attr }\"></ng-container>\n }\n } @else {\n \n }\n </td>\n }\n </ng-template>\n </bs-virtual-datatable>\n </div>\n }\n } @else {\n <div class=\"p-3\">\n @if (!paginationData() || paginationData()!.totalRecords === 0) {\n <div class=\"text-center text-muted\">\n {{ 'noItemsFound' | t }}\n </div>\n } @else {\n <bs-datatable [(settings)]=\"settings\" (settingsChange)=\"onSettingsChange()\">\n @for (attr of visibleAttributes(); track attr.id) {\n <div *bsDatatableColumn=\"attr.name; sortable: true\">\n {{ (attr.label | resolveTranslation) || attr.name }}\n </div>\n }\n\n <tr *bsRowTemplate=\"let item of paginationData()\">\n @for (attr of visibleAttributes(); track attr.id; let first = $first) {\n <td>\n @if (first && canRead()) {\n <a [routerLink]=\"['/po', entityType()!.alias || entityType()!.id, item.id]\">\n <ng-container *ngTemplateOutlet=\"cellContent; context: { $implicit: item, attr: attr }\"></ng-container>\n </a>\n } @else {\n <ng-container *ngTemplateOutlet=\"cellContent; context: { $implicit: item, attr: attr }\"></ng-container>\n }\n </td>\n }\n </tr>\n </bs-datatable>\n }\n </div>\n }\n </bs-card>\n}\n\n<ng-template #cellContent let-item let-attr=\"attr\">\n @if (getColumnRendererComponent(attr); as rendererType) {\n <ng-container *ngComponentOutlet=\"rendererType; inputs: getColumnRendererInputs(item, attr)\"></ng-container>\n } @else if (attr.dataType === 'boolean') {\n <input type=\"checkbox\"\n [checked]=\"(attr.name | attributeValue:item:entityType():lookupReferenceOptions():allEntityTypes()) === true\"\n disabled\n onclick=\"return false;\">\n } @else if (attr.dataType === 'color') {\n @let colorVal = (attr.name | attributeValue:item:entityType():lookupReferenceOptions():allEntityTypes());\n @if (colorVal) {\n <span class=\"d-inline-block align-middle border rounded\" [style.background-color]=\"colorVal\" style=\"width: 1.5em; height: 1.5em;\"></span>\n }\n } @else {\n {{ (attr.name | attributeValue:item:entityType():lookupReferenceOptions():allEntityTypes()) }}\n }\n</ng-template>\n" }]
|
|
1417
1568
|
}], ctorParameters: () => [], propDecorators: { queryId: [{ type: i0.Input, args: [{ isSignal: true, alias: "queryId", required: true }] }], parentId: [{ type: i0.Input, args: [{ isSignal: true, alias: "parentId", required: true }] }], parentType: [{ type: i0.Input, args: [{ isSignal: true, alias: "parentType", required: true }] }] } });
|
|
1418
1569
|
|
|
1419
1570
|
class SparkPoDetailComponent {
|
|
@@ -1546,8 +1697,8 @@ class SparkPoDetailComponent {
|
|
|
1546
1697
|
const refCols = asDetailType.attributes.filter(a => a.dataType === 'Reference' && a.query);
|
|
1547
1698
|
if (refCols.length > 0) {
|
|
1548
1699
|
const refEntries = await Promise.all(refCols.map(async (col) => {
|
|
1549
|
-
const
|
|
1550
|
-
return [col.name,
|
|
1700
|
+
const result = await this.sparkService.executeQueryByName(col.query);
|
|
1701
|
+
return [col.name, result.data];
|
|
1551
1702
|
}));
|
|
1552
1703
|
this.asDetailReferenceOptions.update(prev => ({
|
|
1553
1704
|
...prev,
|
|
@@ -1608,7 +1759,9 @@ class SparkQueryListComponent {
|
|
|
1608
1759
|
route = inject(ActivatedRoute);
|
|
1609
1760
|
router = inject(Router);
|
|
1610
1761
|
sparkService = inject(SparkService);
|
|
1762
|
+
streamingService = inject(SparkStreamingService);
|
|
1611
1763
|
rendererRegistry = inject(SPARK_ATTRIBUTE_RENDERERS);
|
|
1764
|
+
destroyRef = inject(DestroyRef);
|
|
1612
1765
|
extraActionsTemplate = input(null, ...(ngDevMode ? [{ debugName: "extraActionsTemplate" }] : []));
|
|
1613
1766
|
rowClicked = output();
|
|
1614
1767
|
createClicked = output();
|
|
@@ -1617,22 +1770,27 @@ class SparkQueryListComponent {
|
|
|
1617
1770
|
query = signal(null, ...(ngDevMode ? [{ debugName: "query" }] : []));
|
|
1618
1771
|
entityType = signal(null, ...(ngDevMode ? [{ debugName: "entityType" }] : []));
|
|
1619
1772
|
allEntityTypes = signal([], ...(ngDevMode ? [{ debugName: "allEntityTypes" }] : []));
|
|
1620
|
-
allItems = signal([], ...(ngDevMode ? [{ debugName: "allItems" }] : []));
|
|
1621
1773
|
lookupReferenceOptions = signal({}, ...(ngDevMode ? [{ debugName: "lookupReferenceOptions" }] : []));
|
|
1622
1774
|
paginationData = signal(undefined, ...(ngDevMode ? [{ debugName: "paginationData" }] : []));
|
|
1623
1775
|
searchTerm = '';
|
|
1624
1776
|
canRead = signal(false, ...(ngDevMode ? [{ debugName: "canRead" }] : []));
|
|
1625
1777
|
canCreate = signal(false, ...(ngDevMode ? [{ debugName: "canCreate" }] : []));
|
|
1626
|
-
|
|
1778
|
+
isStreaming = signal(false, ...(ngDevMode ? [{ debugName: "isStreaming" }] : []));
|
|
1779
|
+
streamingSub = null;
|
|
1780
|
+
allItems = signal([], ...(ngDevMode ? [{ debugName: "allItems" }] : []));
|
|
1781
|
+
filteredItems = [];
|
|
1782
|
+
settings = signal(new DatatableSettings({
|
|
1627
1783
|
perPage: { values: [10, 25, 50], selected: 10 },
|
|
1628
1784
|
page: { values: [1], selected: 1 },
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
});
|
|
1632
|
-
|
|
1633
|
-
|
|
1785
|
+
sortColumns: []
|
|
1786
|
+
}), ...(ngDevMode ? [{ debugName: "settings" }] : []));
|
|
1787
|
+
virtualDataSource = signal(null, ...(ngDevMode ? [{ debugName: "virtualDataSource" }] : []));
|
|
1788
|
+
virtualSettings = signal(new DatatableSettings({
|
|
1789
|
+
sortColumns: []
|
|
1790
|
+
}), ...(ngDevMode ? [{ debugName: "virtualSettings" }] : []));
|
|
1634
1791
|
constructor() {
|
|
1635
1792
|
this.route.paramMap.pipe(takeUntilDestroyed()).subscribe(params => this.onParamsChange(params));
|
|
1793
|
+
this.destroyRef.onDestroy(() => this.disconnectStreaming());
|
|
1636
1794
|
}
|
|
1637
1795
|
async onParamsChange(params) {
|
|
1638
1796
|
const queryId = params.get('queryId');
|
|
@@ -1672,17 +1830,36 @@ class SparkQueryListComponent {
|
|
|
1672
1830
|
if (resolvedEntityType) {
|
|
1673
1831
|
this.entityType.set(resolvedEntityType);
|
|
1674
1832
|
this.allEntityTypes.set(resolvedEntityTypes);
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1833
|
+
const initialSortColumns = (resolvedQuery?.sortColumns || []).map(sc => ({
|
|
1834
|
+
property: sc.property,
|
|
1835
|
+
direction: sc.direction === 'desc' ? 'descending' : 'ascending'
|
|
1836
|
+
}));
|
|
1837
|
+
if (resolvedQuery?.renderMode === 'VirtualScrolling') {
|
|
1838
|
+
this.virtualSettings.set(new DatatableSettings({ sortColumns: initialSortColumns }));
|
|
1839
|
+
if (resolvedQuery?.isStreamingQuery) {
|
|
1840
|
+
// Streaming + VirtualScrolling: create a stable client-side data source, then connect WebSocket
|
|
1841
|
+
this.virtualDataSource.set(new VirtualDatatableDataSource((skip, take) => Promise.resolve({
|
|
1842
|
+
data: this.filteredItems.slice(skip, skip + take),
|
|
1843
|
+
totalRecords: this.filteredItems.length
|
|
1844
|
+
}), 50));
|
|
1845
|
+
this.connectStreaming(resolvedQuery.id);
|
|
1846
|
+
}
|
|
1847
|
+
else {
|
|
1848
|
+
this.initVirtualDataSource();
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
else {
|
|
1852
|
+
this.settings.set(new DatatableSettings({
|
|
1853
|
+
perPage: { values: [10, 25, 50], selected: 10 },
|
|
1854
|
+
page: { values: [1], selected: 1 },
|
|
1855
|
+
sortColumns: initialSortColumns
|
|
1856
|
+
}));
|
|
1857
|
+
await this.loadItems();
|
|
1858
|
+
}
|
|
1681
1859
|
this.loadLookupReferenceOptions();
|
|
1682
1860
|
const permissions = await this.sparkService.getPermissions(resolvedEntityType.id);
|
|
1683
1861
|
this.canRead.set(permissions.canRead);
|
|
1684
1862
|
this.canCreate.set(permissions.canCreate);
|
|
1685
|
-
await this.loadItems();
|
|
1686
1863
|
}
|
|
1687
1864
|
}
|
|
1688
1865
|
async resolveEntityTypeForQuery(query) {
|
|
@@ -1724,74 +1901,84 @@ class SparkQueryListComponent {
|
|
|
1724
1901
|
}
|
|
1725
1902
|
return plural;
|
|
1726
1903
|
}
|
|
1904
|
+
initVirtualDataSource() {
|
|
1905
|
+
const currentQuery = this.query();
|
|
1906
|
+
if (!currentQuery)
|
|
1907
|
+
return;
|
|
1908
|
+
this.virtualDataSource.set(new VirtualDatatableDataSource((skip, take) => this.sparkService.executeQuery(currentQuery.id, {
|
|
1909
|
+
sortColumns: this.virtualSettings().sortColumns,
|
|
1910
|
+
skip, take,
|
|
1911
|
+
search: this.searchTerm || undefined,
|
|
1912
|
+
}).then(r => ({ data: r.data, totalRecords: r.totalRecords })), 50));
|
|
1913
|
+
}
|
|
1727
1914
|
async loadItems() {
|
|
1728
1915
|
const currentQuery = this.query();
|
|
1729
1916
|
if (!currentQuery)
|
|
1730
1917
|
return;
|
|
1918
|
+
// Streaming queries use WebSocket instead of HTTP
|
|
1919
|
+
if (currentQuery.isStreamingQuery) {
|
|
1920
|
+
this.connectStreaming(currentQuery.id);
|
|
1921
|
+
return;
|
|
1922
|
+
}
|
|
1923
|
+
// Non-streaming: disconnect any previous streaming connection
|
|
1924
|
+
this.disconnectStreaming();
|
|
1731
1925
|
try {
|
|
1732
|
-
const
|
|
1733
|
-
const
|
|
1926
|
+
const s = this.settings();
|
|
1927
|
+
const result = await this.sparkService.executeQuery(currentQuery.id, {
|
|
1928
|
+
sortColumns: s.sortColumns,
|
|
1929
|
+
skip: (s.page.selected - 1) * s.perPage.selected,
|
|
1930
|
+
take: s.perPage.selected,
|
|
1931
|
+
search: this.searchTerm || undefined,
|
|
1932
|
+
});
|
|
1734
1933
|
this.errorMessage.set(null);
|
|
1735
|
-
|
|
1736
|
-
this.
|
|
1737
|
-
|
|
1738
|
-
|
|
1934
|
+
const totalPages = Math.ceil(result.totalRecords / s.perPage.selected) || 1;
|
|
1935
|
+
this.paginationData.set({
|
|
1936
|
+
data: result.data,
|
|
1937
|
+
totalRecords: result.totalRecords,
|
|
1938
|
+
totalPages: totalPages,
|
|
1939
|
+
perPage: s.perPage.selected,
|
|
1940
|
+
page: s.page.selected
|
|
1941
|
+
});
|
|
1942
|
+
s.page.values = Array.from({ length: totalPages }, (_, i) => i + 1);
|
|
1739
1943
|
}
|
|
1740
1944
|
catch (e) {
|
|
1741
1945
|
this.errorMessage.set(e.error?.error || e.message || 'An unexpected error occurred');
|
|
1742
|
-
this.
|
|
1743
|
-
this.applyFilter();
|
|
1946
|
+
this.paginationData.set(undefined);
|
|
1744
1947
|
}
|
|
1745
1948
|
}
|
|
1746
1949
|
onSettingsChange() {
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1950
|
+
if (this.isStreaming()) {
|
|
1951
|
+
// Streaming: sort/filter is client-side only
|
|
1952
|
+
this.applyFilter();
|
|
1953
|
+
return;
|
|
1954
|
+
}
|
|
1955
|
+
if (this.query()?.renderMode === 'VirtualScrolling') {
|
|
1956
|
+
this.virtualDataSource()?.reset();
|
|
1957
|
+
this.initVirtualDataSource();
|
|
1752
1958
|
}
|
|
1753
1959
|
else {
|
|
1754
|
-
this.
|
|
1960
|
+
this.loadItems();
|
|
1755
1961
|
}
|
|
1756
1962
|
}
|
|
1757
1963
|
onSearchChange() {
|
|
1758
|
-
this.
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
applyFilter() {
|
|
1762
|
-
let filteredItems = this.allItems();
|
|
1763
|
-
if (this.searchTerm.trim()) {
|
|
1764
|
-
const term = this.searchTerm.toLowerCase().trim();
|
|
1765
|
-
filteredItems = filteredItems.filter(item => {
|
|
1766
|
-
if (item.name?.toLowerCase().includes(term))
|
|
1767
|
-
return true;
|
|
1768
|
-
if (item.breadcrumb?.toLowerCase().includes(term))
|
|
1769
|
-
return true;
|
|
1770
|
-
return item.attributes.some(attr => {
|
|
1771
|
-
const value = attr.breadcrumb || attr.value;
|
|
1772
|
-
if (value == null)
|
|
1773
|
-
return false;
|
|
1774
|
-
return String(value).toLowerCase().includes(term);
|
|
1775
|
-
});
|
|
1776
|
-
});
|
|
1964
|
+
if (this.isStreaming()) {
|
|
1965
|
+
this.applyFilter();
|
|
1966
|
+
return;
|
|
1777
1967
|
}
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
});
|
|
1786
|
-
this.settings.page.values = Array.from({ length: totalPages }, (_, i) => i + 1);
|
|
1787
|
-
if (this.settings.page.selected > totalPages) {
|
|
1788
|
-
this.settings.page.selected = 1;
|
|
1968
|
+
if (this.query()?.renderMode === 'VirtualScrolling') {
|
|
1969
|
+
this.virtualDataSource()?.reset();
|
|
1970
|
+
this.initVirtualDataSource();
|
|
1971
|
+
}
|
|
1972
|
+
else {
|
|
1973
|
+
this.settings().page.selected = 1;
|
|
1974
|
+
this.loadItems();
|
|
1789
1975
|
}
|
|
1790
1976
|
}
|
|
1791
1977
|
clearSearch() {
|
|
1792
1978
|
this.searchTerm = '';
|
|
1793
1979
|
this.onSearchChange();
|
|
1794
1980
|
}
|
|
1981
|
+
isVirtualScrolling = computed(() => this.query()?.renderMode === 'VirtualScrolling', ...(ngDevMode ? [{ debugName: "isVirtualScrolling" }] : []));
|
|
1795
1982
|
visibleAttributes = computed(() => {
|
|
1796
1983
|
return this.entityType()?.attributes
|
|
1797
1984
|
.filter(a => a.isVisible && hasShowedOnFlag(a.showedOn, ShowedOn.Query))
|
|
@@ -1828,12 +2015,106 @@ class SparkQueryListComponent {
|
|
|
1828
2015
|
this.router.navigate(['/po', et.alias || et.id, 'new']);
|
|
1829
2016
|
}
|
|
1830
2017
|
}
|
|
2018
|
+
connectStreaming(queryId) {
|
|
2019
|
+
this.disconnectStreaming();
|
|
2020
|
+
this.isStreaming.set(true);
|
|
2021
|
+
this.streamingSub = this.streamingService.connectToStreamingQuery(queryId).subscribe({
|
|
2022
|
+
next: (message) => this.handleStreamingMessage(message),
|
|
2023
|
+
error: (err) => {
|
|
2024
|
+
this.errorMessage.set(err?.message || 'Streaming connection failed');
|
|
2025
|
+
this.isStreaming.set(false);
|
|
2026
|
+
},
|
|
2027
|
+
complete: () => {
|
|
2028
|
+
this.isStreaming.set(false);
|
|
2029
|
+
}
|
|
2030
|
+
});
|
|
2031
|
+
}
|
|
2032
|
+
disconnectStreaming() {
|
|
2033
|
+
if (this.streamingSub) {
|
|
2034
|
+
this.streamingSub.unsubscribe();
|
|
2035
|
+
this.streamingSub = null;
|
|
2036
|
+
}
|
|
2037
|
+
this.isStreaming.set(false);
|
|
2038
|
+
}
|
|
2039
|
+
handleStreamingMessage(message) {
|
|
2040
|
+
switch (message.type) {
|
|
2041
|
+
case 'snapshot':
|
|
2042
|
+
this.errorMessage.set(null);
|
|
2043
|
+
this.allItems.set(message.data);
|
|
2044
|
+
this.applyFilter();
|
|
2045
|
+
break;
|
|
2046
|
+
case 'patch':
|
|
2047
|
+
if (message.updated.length > 0) {
|
|
2048
|
+
const currentItems = this.allItems();
|
|
2049
|
+
const updatedItems = currentItems.map(item => {
|
|
2050
|
+
const patch = message.updated.find(u => u.id === item.id);
|
|
2051
|
+
if (!patch)
|
|
2052
|
+
return item;
|
|
2053
|
+
// Clone the item and update only changed attribute values
|
|
2054
|
+
const updatedAttributes = item.attributes.map(attr => {
|
|
2055
|
+
if (attr.name in patch.attributes) {
|
|
2056
|
+
return { ...attr, value: patch.attributes[attr.name] };
|
|
2057
|
+
}
|
|
2058
|
+
return attr;
|
|
2059
|
+
});
|
|
2060
|
+
return { ...item, attributes: updatedAttributes };
|
|
2061
|
+
});
|
|
2062
|
+
this.allItems.set(updatedItems);
|
|
2063
|
+
this.applyFilter();
|
|
2064
|
+
}
|
|
2065
|
+
break;
|
|
2066
|
+
case 'error':
|
|
2067
|
+
this.errorMessage.set(message.message);
|
|
2068
|
+
break;
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
applyFilter() {
|
|
2072
|
+
let items = this.allItems();
|
|
2073
|
+
// Apply search filter
|
|
2074
|
+
if (this.searchTerm) {
|
|
2075
|
+
const term = this.searchTerm.toLowerCase();
|
|
2076
|
+
items = items.filter(item => item.attributes.some(a => String(a.value ?? '').toLowerCase().includes(term)));
|
|
2077
|
+
}
|
|
2078
|
+
// Apply sorting
|
|
2079
|
+
const isVirtual = this.query()?.renderMode === 'VirtualScrolling';
|
|
2080
|
+
const sortCols = isVirtual ? this.virtualSettings().sortColumns : this.settings().sortColumns;
|
|
2081
|
+
if (sortCols.length > 0) {
|
|
2082
|
+
items = [...items].sort((a, b) => {
|
|
2083
|
+
for (const col of sortCols) {
|
|
2084
|
+
const aVal = a.attributes.find(attr => attr.name === col.property)?.value ?? '';
|
|
2085
|
+
const bVal = b.attributes.find(attr => attr.name === col.property)?.value ?? '';
|
|
2086
|
+
const cmp = String(aVal).localeCompare(String(bVal));
|
|
2087
|
+
if (cmp !== 0)
|
|
2088
|
+
return col.direction === 'descending' ? -cmp : cmp;
|
|
2089
|
+
}
|
|
2090
|
+
return 0;
|
|
2091
|
+
});
|
|
2092
|
+
}
|
|
2093
|
+
if (isVirtual) {
|
|
2094
|
+
// Update the mutable filtered items array.
|
|
2095
|
+
// The stable data source's fetchFn closure reads from this.filteredItems,
|
|
2096
|
+
// so clearing its cache and emitting empty triggers the CDK viewport to re-fetch.
|
|
2097
|
+
this.filteredItems = items;
|
|
2098
|
+
this.virtualDataSource()?.reset();
|
|
2099
|
+
}
|
|
2100
|
+
else {
|
|
2101
|
+
this.paginationData.set({
|
|
2102
|
+
data: items,
|
|
2103
|
+
totalRecords: items.length,
|
|
2104
|
+
totalPages: 1,
|
|
2105
|
+
perPage: items.length,
|
|
2106
|
+
page: 1
|
|
2107
|
+
});
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
1831
2110
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: SparkQueryListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1832
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: SparkQueryListComponent, isStandalone: true, selector: "spark-query-list", inputs: { extraActionsTemplate: { classPropertyName: "extraActionsTemplate", publicName: "extraActionsTemplate", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { rowClicked: "rowClicked", createClicked: "createClicked" }, ngImport: i0, template: "<bs-container>\n<div class=\"container-fluid\">\n <div class=\"d-flex justify-content-between align-items-center mb-4\">\n <h2>{{ (query()?.description | resolveTranslation) || query()?.name || ('loading' | t) }}</h2>\n <div class=\"d-flex gap-2\">\n @if (extraActionsTemplate(); as extraActionsTpl) {\n <ng-container *ngTemplateOutlet=\"extraActionsTpl\"></ng-container>\n }\n @if (canCreate()) {\n <button class=\"btn btn-primary\" (click)=\"onCreate()\">\n <spark-icon name=\"plus-lg\" /> {{ 'new' | t }}\n </button>\n }\n </div>\n </div>\n\n @if (entityType()) {\n <!-- Search box -->\n <bs-form>\n <bs-grid>\n <div bsRow class=\"mb-3\">\n <div [md]=\"4\">\n <bs-input-group>\n <span class=\"input-group-text\">\n <spark-icon name=\"search\" />\n </span>\n <input\n type=\"text\"\n [placeholder]=\"'search' | t\"\n [(ngModel)]=\"searchTerm\"\n (ngModelChange)=\"onSearchChange()\">\n @if (searchTerm) {\n <button\n type=\"button\"\n class=\"btn btn-outline-secondary\"\n (click)=\"clearSearch()\">\n <spark-icon name=\"x-lg\" />\n </button>\n }\n </bs-input-group>\n </div>\n <div [md]=\"8\" class=\"text-end\">\n @if (searchTerm && paginationData()) {\n <span class=\"text-muted\">\n {{ paginationData()!.totalRecords }} {{ paginationData()!.totalRecords === 1 ? ('resultFound' | t) : ('resultsFound' | t) }}\n </span>\n }\n </div>\n </div>\n </bs-grid>\n </bs-form>\n\n @if (errorMessage(); as err) {\n <bs-alert [type]=\"colors.danger\" class=\"mb-3\">\n {{ err }}\n </bs-alert>\n }\n\n <bs-datatable [(settings)]=\"settings\" (settingsChange)=\"onSettingsChange()\">\n @for (attr of visibleAttributes(); track attr.id) {\n <div *bsDatatableColumn=\"attr.name; sortable: true\">\n {{ (attr.label | resolveTranslation) || attr.name }}\n </div>\n }\n\n <tr *bsRowTemplate=\"let item of paginationData()\">\n @for (attr of visibleAttributes(); track attr.id; let first = $first) {\n <td>\n @if (first && canRead()) {\n <a [routerLink]=\"['/po', entityType()!.alias || entityType()!.id, item.id]\" (click)=\"rowClicked.emit(item)\">\n <ng-container *ngTemplateOutlet=\"cellContent; context: { $implicit: item, attr: attr }\"></ng-container>\n </a>\n } @else {\n <ng-container *ngTemplateOutlet=\"cellContent; context: { $implicit: item, attr: attr }\"></ng-container>\n }\n </td>\n }\n </tr>\n </bs-datatable>\n } @else {\n <div class=\"text-center p-5\">\n <bs-spinner />\n </div>\n }\n</div>\n</bs-container>\n\n<ng-template #cellContent let-item let-attr=\"attr\">\n @if (getColumnRendererComponent(attr); as rendererType) {\n <ng-container *ngComponentOutlet=\"rendererType; inputs: getColumnRendererInputs(item, attr)\"></ng-container>\n } @else if (attr.dataType === 'boolean') {\n <input type=\"checkbox\"\n [checked]=\"(attr.name | attributeValue:item:entityType():lookupReferenceOptions():allEntityTypes()) === true\"\n [indeterminate]=\"(attr.name | attributeValue:item:entityType():lookupReferenceOptions():allEntityTypes()) == null\"\n disabled\n onclick=\"return false;\">\n } @else if (attr.dataType === 'color') {\n @let colorVal = (attr.name | attributeValue:item:entityType():lookupReferenceOptions():allEntityTypes());\n @if (colorVal) {\n <span class=\"d-inline-block align-middle border rounded\" [style.background-color]=\"colorVal\" style=\"width: 1.5em; height: 1.5em;\"></span>\n }\n } @else {\n {{ (attr.name | attributeValue:item:entityType():lookupReferenceOptions():allEntityTypes()) }}\n }\n</ng-template>\n", styles: ["tr:hover{background-color:#0000000d}td input[type=checkbox]:disabled{opacity:1;pointer-events:none}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { 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: RouterModule }, { kind: "directive", type: i2$1.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "component", type: BsAlertComponent, selector: "bs-alert", inputs: ["type", "isVisible"], outputs: ["isVisibleChange", "afterOpenedOrClosed"] }, { kind: "component", type: BsContainerComponent, selector: "bs-container" }, { kind: "component", type: BsDatatableComponent, selector: "bs-datatable", inputs: ["settings", "data"], outputs: ["settingsChange", "dataChange"] }, { kind: "directive", type: BsDatatableColumnDirective, selector: "[bsDatatableColumn]", inputs: ["bsDatatableColumn", "bsDatatableColumnSortable"] }, { kind: "directive", type: BsRowTemplateDirective, selector: "[bsRowTemplate]", inputs: ["bsRowTemplateOf"] }, { kind: "component", type: BsFormComponent, selector: "bs-form", inputs: ["action", "method"], outputs: ["submitted"] }, { kind: "directive", type: BsFormControlDirective, selector: "bs-form input:not(.no-form-control), bs-form textarea:not(.no-form-control)" }, { kind: "component", type: BsGridComponent, selector: "bs-grid", inputs: ["stopFullWidthAt"] }, { kind: "directive", type: BsGridRowDirective, selector: "[bsRow]" }, { kind: "directive", type: BsGridColumnDirective, selector: "[xxs],[xs],[sm],[md],[lg],[xl],[xxl]", inputs: ["xxs", "xs", "sm", "md", "lg", "xl", "xxl"] }, { kind: "component", type: BsInputGroupComponent, selector: "bs-input-group" }, { kind: "component", type: BsSpinnerComponent, selector: "bs-spinner", inputs: ["type", "color"] }, { kind: "component", type: SparkIconComponent, selector: "spark-icon", inputs: ["name"] }, { kind: "pipe", type: ResolveTranslationPipe, name: "resolveTranslation" }, { kind: "pipe", type: TranslateKeyPipe, name: "t" }, { kind: "pipe", type: AttributeValuePipe, name: "attributeValue" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2111
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: SparkQueryListComponent, isStandalone: true, selector: "spark-query-list", inputs: { extraActionsTemplate: { classPropertyName: "extraActionsTemplate", publicName: "extraActionsTemplate", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { rowClicked: "rowClicked", createClicked: "createClicked" }, host: { properties: { "class.virtual-scrolling": "isVirtualScrolling()" } }, ngImport: i0, template: "<div class=\"d-flex flex-column h-100\">\n <!-- Title + Actions row -->\n <div class=\"d-flex justify-content-between align-items-center mb-4 flex-shrink-0\" [class.px-4]=\"isVirtualScrolling()\">\n <h2>\n {{ (query()?.description | resolveTranslation) || query()?.name || ('loading' | t) }}\n @if (isStreaming()) {\n <span class=\"badge bg-success ms-2\" style=\"font-size: 0.5em; vertical-align: middle;\">LIVE</span>\n }\n </h2>\n <div class=\"d-flex gap-2\">\n @if (extraActionsTemplate(); as extraActionsTpl) {\n <ng-container *ngTemplateOutlet=\"extraActionsTpl\"></ng-container>\n }\n @if (canCreate()) {\n <button class=\"btn btn-primary\" (click)=\"onCreate()\">\n <spark-icon name=\"plus-lg\" /> {{ 'new' | t }}\n </button>\n }\n </div>\n </div>\n\n @if (entityType()) {\n <!-- Search box -->\n <div class=\"flex-shrink-0\" [class.px-4]=\"isVirtualScrolling()\">\n <bs-form>\n <bs-grid>\n <div bsRow class=\"mb-3\">\n <div [md]=\"4\">\n <bs-input-group>\n <span class=\"input-group-text\">\n <spark-icon name=\"search\" />\n </span>\n <input\n type=\"text\"\n [placeholder]=\"'search' | t\"\n [(ngModel)]=\"searchTerm\"\n (ngModelChange)=\"onSearchChange()\">\n @if (searchTerm) {\n <button\n type=\"button\"\n class=\"btn btn-outline-secondary\"\n (click)=\"clearSearch()\">\n <spark-icon name=\"x-lg\" />\n </button>\n }\n </bs-input-group>\n </div>\n <div [md]=\"8\" class=\"text-end\">\n @if (searchTerm && paginationData()) {\n <span class=\"text-muted\">\n {{ paginationData()!.totalRecords }} {{ paginationData()!.totalRecords === 1 ? ('resultFound' | t) : ('resultsFound' | t) }}\n </span>\n }\n </div>\n </div>\n </bs-grid>\n </bs-form>\n\n @if (errorMessage(); as err) {\n <bs-alert [type]=\"colors.danger\" class=\"mb-3\">\n {{ err }}\n </bs-alert>\n }\n </div>\n\n @if (query()?.renderMode === 'VirtualScrolling') {\n @if (virtualDataSource(); as vds) {\n <bs-virtual-datatable class=\"flex-grow-1 overflow-hidden\"\n [(settings)]=\"virtualSettings\"\n [dataSource]=\"vds\"\n [isResponsive]=\"true\"\n (settingsChange)=\"onSettingsChange()\">\n @for (attr of visibleAttributes(); track attr.id) {\n <div *bsDatatableColumn=\"attr.name; sortable: true\">\n {{ (attr.label | resolveTranslation) || attr.name }}\n </div>\n }\n\n <ng-template bsVirtualRowTemplate let-item>\n @for (attr of visibleAttributes(); track attr.id; let first = $first) {\n <td>\n @if (item) {\n @if (first && canRead()) {\n <a [routerLink]=\"['/po', entityType()!.alias || entityType()!.id, item.id]\" (click)=\"rowClicked.emit(item)\">\n <ng-container *ngTemplateOutlet=\"cellContent; context: { $implicit: item, attr: attr }\"></ng-container>\n </a>\n } @else {\n <ng-container *ngTemplateOutlet=\"cellContent; context: { $implicit: item, attr: attr }\"></ng-container>\n }\n } @else {\n \n }\n </td>\n }\n </ng-template>\n </bs-virtual-datatable>\n }\n } @else {\n <bs-datatable class=\"flex-grow-1\" [(settings)]=\"settings\" (settingsChange)=\"onSettingsChange()\">\n @for (attr of visibleAttributes(); track attr.id) {\n <div *bsDatatableColumn=\"attr.name; sortable: true\">\n {{ (attr.label | resolveTranslation) || attr.name }}\n </div>\n }\n\n <tr *bsRowTemplate=\"let item of paginationData()\">\n @for (attr of visibleAttributes(); track attr.id; let first = $first) {\n <td>\n @if (first && canRead()) {\n <a [routerLink]=\"['/po', entityType()!.alias || entityType()!.id, item.id]\" (click)=\"rowClicked.emit(item)\">\n <ng-container *ngTemplateOutlet=\"cellContent; context: { $implicit: item, attr: attr }\"></ng-container>\n </a>\n } @else {\n <ng-container *ngTemplateOutlet=\"cellContent; context: { $implicit: item, attr: attr }\"></ng-container>\n }\n </td>\n }\n </tr>\n </bs-datatable>\n }\n } @else {\n <div class=\"text-center p-5\">\n <bs-spinner />\n </div>\n }\n</div>\n\n<ng-template #cellContent let-item let-attr=\"attr\">\n @if (getColumnRendererComponent(attr); as rendererType) {\n <ng-container *ngComponentOutlet=\"rendererType; inputs: getColumnRendererInputs(item, attr)\"></ng-container>\n } @else if (attr.dataType === 'boolean') {\n <input type=\"checkbox\"\n [checked]=\"(attr.name | attributeValue:item:entityType():lookupReferenceOptions():allEntityTypes()) === true\"\n [indeterminate]=\"(attr.name | attributeValue:item:entityType():lookupReferenceOptions():allEntityTypes()) == null\"\n disabled\n onclick=\"return false;\">\n } @else if (attr.dataType === 'color') {\n @let colorVal = (attr.name | attributeValue:item:entityType():lookupReferenceOptions():allEntityTypes());\n @if (colorVal) {\n <span class=\"d-inline-block align-middle border rounded\" [style.background-color]=\"colorVal\" style=\"width: 1.5em; height: 1.5em;\"></span>\n }\n } @else {\n {{ (attr.name | attributeValue:item:entityType():lookupReferenceOptions():allEntityTypes()) }}\n }\n</ng-template>\n", styles: [":host{display:flex;flex-direction:column;flex:1;min-height:0}:host.virtual-scrolling{margin:0 -1.5rem -1.5rem}tr:hover{background-color:#0000000d}td input[type=checkbox]:disabled{opacity:1;pointer-events:none}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { 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: RouterModule }, { kind: "directive", type: i2$1.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "component", type: BsAlertComponent, selector: "bs-alert", inputs: ["type", "isVisible"], outputs: ["isVisibleChange", "afterOpenedOrClosed"] }, { kind: "component", type: BsDatatableComponent, selector: "bs-datatable", inputs: ["data"], outputs: ["dataChange"] }, { kind: "directive", type: BsDatatableColumnDirective, selector: "[bsDatatableColumn]", inputs: ["bsDatatableColumn", "bsDatatableColumnSortable"] }, { kind: "directive", type: BsRowTemplateDirective, selector: "[bsRowTemplate]", inputs: ["bsRowTemplateOf"] }, { kind: "component", type: BsVirtualDatatableComponent, selector: "bs-virtual-datatable", inputs: ["dataSource", "isResponsive", "itemSize"] }, { kind: "directive", type: BsVirtualRowTemplateDirective, selector: "[bsVirtualRowTemplate]" }, { kind: "component", type: BsFormComponent, selector: "bs-form", inputs: ["action", "method"], outputs: ["submitted"] }, { kind: "directive", type: BsFormControlDirective, selector: "bs-form input:not(.no-form-control), bs-form textarea:not(.no-form-control)" }, { kind: "component", type: BsGridComponent, selector: "bs-grid", inputs: ["stopFullWidthAt"] }, { kind: "directive", type: BsGridRowDirective, selector: "[bsRow]" }, { kind: "directive", type: BsGridColumnDirective, selector: "[xxs],[xs],[sm],[md],[lg],[xl],[xxl]", inputs: ["xxs", "xs", "sm", "md", "lg", "xl", "xxl"] }, { kind: "component", type: BsInputGroupComponent, selector: "bs-input-group" }, { kind: "component", type: BsSpinnerComponent, selector: "bs-spinner", inputs: ["type", "color"] }, { kind: "component", type: SparkIconComponent, selector: "spark-icon", inputs: ["name"] }, { kind: "pipe", type: ResolveTranslationPipe, name: "resolveTranslation" }, { kind: "pipe", type: TranslateKeyPipe, name: "t" }, { kind: "pipe", type: AttributeValuePipe, name: "attributeValue" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1833
2112
|
}
|
|
1834
2113
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: SparkQueryListComponent, decorators: [{
|
|
1835
2114
|
type: Component,
|
|
1836
|
-
args: [{ selector: 'spark-query-list', imports: [CommonModule, NgTemplateOutlet, NgComponentOutlet, FormsModule, RouterModule, BsAlertComponent, BsContainerComponent, BsDatatableComponent, BsDatatableColumnDirective, BsRowTemplateDirective, BsFormComponent, BsFormControlDirective, BsGridComponent, BsGridRowDirective, BsGridColumnDirective, BsInputGroupComponent, BsSpinnerComponent, SparkIconComponent, ResolveTranslationPipe, TranslateKeyPipe, AttributeValuePipe], changeDetection: ChangeDetectionStrategy.OnPush,
|
|
2115
|
+
args: [{ selector: 'spark-query-list', imports: [CommonModule, NgTemplateOutlet, NgComponentOutlet, FormsModule, RouterModule, BsAlertComponent, BsContainerComponent, BsDatatableComponent, BsDatatableColumnDirective, BsRowTemplateDirective, BsVirtualDatatableComponent, BsVirtualRowTemplateDirective, BsFormComponent, BsFormControlDirective, BsGridComponent, BsGridRowDirective, BsGridColumnDirective, BsInputGroupComponent, BsSpinnerComponent, SparkIconComponent, ResolveTranslationPipe, TranslateKeyPipe, AttributeValuePipe], changeDetection: ChangeDetectionStrategy.OnPush, host: {
|
|
2116
|
+
'[class.virtual-scrolling]': 'isVirtualScrolling()'
|
|
2117
|
+
}, template: "<div class=\"d-flex flex-column h-100\">\n <!-- Title + Actions row -->\n <div class=\"d-flex justify-content-between align-items-center mb-4 flex-shrink-0\" [class.px-4]=\"isVirtualScrolling()\">\n <h2>\n {{ (query()?.description | resolveTranslation) || query()?.name || ('loading' | t) }}\n @if (isStreaming()) {\n <span class=\"badge bg-success ms-2\" style=\"font-size: 0.5em; vertical-align: middle;\">LIVE</span>\n }\n </h2>\n <div class=\"d-flex gap-2\">\n @if (extraActionsTemplate(); as extraActionsTpl) {\n <ng-container *ngTemplateOutlet=\"extraActionsTpl\"></ng-container>\n }\n @if (canCreate()) {\n <button class=\"btn btn-primary\" (click)=\"onCreate()\">\n <spark-icon name=\"plus-lg\" /> {{ 'new' | t }}\n </button>\n }\n </div>\n </div>\n\n @if (entityType()) {\n <!-- Search box -->\n <div class=\"flex-shrink-0\" [class.px-4]=\"isVirtualScrolling()\">\n <bs-form>\n <bs-grid>\n <div bsRow class=\"mb-3\">\n <div [md]=\"4\">\n <bs-input-group>\n <span class=\"input-group-text\">\n <spark-icon name=\"search\" />\n </span>\n <input\n type=\"text\"\n [placeholder]=\"'search' | t\"\n [(ngModel)]=\"searchTerm\"\n (ngModelChange)=\"onSearchChange()\">\n @if (searchTerm) {\n <button\n type=\"button\"\n class=\"btn btn-outline-secondary\"\n (click)=\"clearSearch()\">\n <spark-icon name=\"x-lg\" />\n </button>\n }\n </bs-input-group>\n </div>\n <div [md]=\"8\" class=\"text-end\">\n @if (searchTerm && paginationData()) {\n <span class=\"text-muted\">\n {{ paginationData()!.totalRecords }} {{ paginationData()!.totalRecords === 1 ? ('resultFound' | t) : ('resultsFound' | t) }}\n </span>\n }\n </div>\n </div>\n </bs-grid>\n </bs-form>\n\n @if (errorMessage(); as err) {\n <bs-alert [type]=\"colors.danger\" class=\"mb-3\">\n {{ err }}\n </bs-alert>\n }\n </div>\n\n @if (query()?.renderMode === 'VirtualScrolling') {\n @if (virtualDataSource(); as vds) {\n <bs-virtual-datatable class=\"flex-grow-1 overflow-hidden\"\n [(settings)]=\"virtualSettings\"\n [dataSource]=\"vds\"\n [isResponsive]=\"true\"\n (settingsChange)=\"onSettingsChange()\">\n @for (attr of visibleAttributes(); track attr.id) {\n <div *bsDatatableColumn=\"attr.name; sortable: true\">\n {{ (attr.label | resolveTranslation) || attr.name }}\n </div>\n }\n\n <ng-template bsVirtualRowTemplate let-item>\n @for (attr of visibleAttributes(); track attr.id; let first = $first) {\n <td>\n @if (item) {\n @if (first && canRead()) {\n <a [routerLink]=\"['/po', entityType()!.alias || entityType()!.id, item.id]\" (click)=\"rowClicked.emit(item)\">\n <ng-container *ngTemplateOutlet=\"cellContent; context: { $implicit: item, attr: attr }\"></ng-container>\n </a>\n } @else {\n <ng-container *ngTemplateOutlet=\"cellContent; context: { $implicit: item, attr: attr }\"></ng-container>\n }\n } @else {\n \n }\n </td>\n }\n </ng-template>\n </bs-virtual-datatable>\n }\n } @else {\n <bs-datatable class=\"flex-grow-1\" [(settings)]=\"settings\" (settingsChange)=\"onSettingsChange()\">\n @for (attr of visibleAttributes(); track attr.id) {\n <div *bsDatatableColumn=\"attr.name; sortable: true\">\n {{ (attr.label | resolveTranslation) || attr.name }}\n </div>\n }\n\n <tr *bsRowTemplate=\"let item of paginationData()\">\n @for (attr of visibleAttributes(); track attr.id; let first = $first) {\n <td>\n @if (first && canRead()) {\n <a [routerLink]=\"['/po', entityType()!.alias || entityType()!.id, item.id]\" (click)=\"rowClicked.emit(item)\">\n <ng-container *ngTemplateOutlet=\"cellContent; context: { $implicit: item, attr: attr }\"></ng-container>\n </a>\n } @else {\n <ng-container *ngTemplateOutlet=\"cellContent; context: { $implicit: item, attr: attr }\"></ng-container>\n }\n </td>\n }\n </tr>\n </bs-datatable>\n }\n } @else {\n <div class=\"text-center p-5\">\n <bs-spinner />\n </div>\n }\n</div>\n\n<ng-template #cellContent let-item let-attr=\"attr\">\n @if (getColumnRendererComponent(attr); as rendererType) {\n <ng-container *ngComponentOutlet=\"rendererType; inputs: getColumnRendererInputs(item, attr)\"></ng-container>\n } @else if (attr.dataType === 'boolean') {\n <input type=\"checkbox\"\n [checked]=\"(attr.name | attributeValue:item:entityType():lookupReferenceOptions():allEntityTypes()) === true\"\n [indeterminate]=\"(attr.name | attributeValue:item:entityType():lookupReferenceOptions():allEntityTypes()) == null\"\n disabled\n onclick=\"return false;\">\n } @else if (attr.dataType === 'color') {\n @let colorVal = (attr.name | attributeValue:item:entityType():lookupReferenceOptions():allEntityTypes());\n @if (colorVal) {\n <span class=\"d-inline-block align-middle border rounded\" [style.background-color]=\"colorVal\" style=\"width: 1.5em; height: 1.5em;\"></span>\n }\n } @else {\n {{ (attr.name | attributeValue:item:entityType():lookupReferenceOptions():allEntityTypes()) }}\n }\n</ng-template>\n", styles: [":host{display:flex;flex-direction:column;flex:1;min-height:0}:host.virtual-scrolling{margin:0 -1.5rem -1.5rem}tr:hover{background-color:#0000000d}td input[type=checkbox]:disabled{opacity:1;pointer-events:none}\n"] }]
|
|
1837
2118
|
}], ctorParameters: () => [], propDecorators: { extraActionsTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "extraActionsTemplate", required: false }] }], rowClicked: [{ type: i0.Output, args: ["rowClicked"] }], createClicked: [{ type: i0.Output, args: ["createClicked"] }] } });
|
|
1838
2119
|
|
|
1839
2120
|
var sparkQueryList_component = /*#__PURE__*/Object.freeze({
|
|
@@ -1984,5 +2265,5 @@ function provideSpark(config) {
|
|
|
1984
2265
|
* Generated bundle index. Do not edit.
|
|
1985
2266
|
*/
|
|
1986
2267
|
|
|
1987
|
-
export { ArrayValuePipe, AsDetailCellValuePipe, AsDetailColumnsPipe, AsDetailDisplayValuePipe, AsDetailTypePipe, AttributeValuePipe, CanCreateDetailRowPipe, CanDeleteDetailRowPipe, ELookupDisplayType, ErrorForAttributePipe, IconNamePipe, InlineRefOptionsPipe, InputTypePipe, LookupDisplayTypePipe, LookupDisplayValuePipe, LookupOptionsPipe, RawAttributeValuePipe, ReferenceAttrValuePipe, ReferenceDisplayValuePipe, ReferenceLinkRoutePipe, ResolveTranslationPipe, RetryActionService, RouterLinkPipe, SPARK_ATTRIBUTE_RENDERERS, SPARK_CONFIG, ShowedOn, SparkIconComponent, SparkIconRegistry, SparkLanguageService, SparkPoCreateComponent, SparkPoDetailComponent, SparkPoEditComponent, SparkPoFormComponent, SparkQueryListComponent, SparkRetryActionModalComponent, SparkService, SparkSubQueryComponent, TranslateKeyPipe, currentLanguage, defaultSparkConfig, hasShowedOnFlag, provideSpark, provideSparkAttributeRenderers, resolveTranslation, sparkRoutes };
|
|
2268
|
+
export { ArrayValuePipe, AsDetailCellValuePipe, AsDetailColumnsPipe, AsDetailDisplayValuePipe, AsDetailTypePipe, AttributeValuePipe, CanCreateDetailRowPipe, CanDeleteDetailRowPipe, ELookupDisplayType, ErrorForAttributePipe, IconNamePipe, InlineRefOptionsPipe, InputTypePipe, LookupDisplayTypePipe, LookupDisplayValuePipe, LookupOptionsPipe, RawAttributeValuePipe, ReferenceAttrValuePipe, ReferenceDisplayValuePipe, ReferenceLinkRoutePipe, ResolveTranslationPipe, RetryActionService, RouterLinkPipe, SPARK_ATTRIBUTE_RENDERERS, SPARK_CONFIG, ShowedOn, SparkIconComponent, SparkIconRegistry, SparkLanguageService, SparkPoCreateComponent, SparkPoDetailComponent, SparkPoEditComponent, SparkPoFormComponent, SparkQueryListComponent, SparkRetryActionModalComponent, SparkService, SparkStreamingService, SparkSubQueryComponent, TranslateKeyPipe, currentLanguage, defaultSparkConfig, hasShowedOnFlag, provideSpark, provideSparkAttributeRenderers, resolveTranslation, sparkRoutes };
|
|
1988
2269
|
//# sourceMappingURL=mintplayer-ng-spark.mjs.map
|